View | Details | Raw Unified | Return to bug 30719
Collapse All | Expand All

(-)a/Koha/Illbatch.pm (+196 lines)
Line 0 Link Here
1
package Koha::Illbatch;
2
3
# Copyright PTFS Europe 2022
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
use Koha::Database;
22
use Koha::Illrequest::Logger;
23
use Koha::IllbatchStatus;
24
use JSON qw( to_json );
25
use base qw(Koha::Object);
26
27
=head1 NAME
28
29
Koha::Illbatch - Koha Illbatch Object class
30
31
=head2 Class methods
32
33
=head3 status
34
35
    my $status = Koha::Illbatch->status;
36
37
Return the status object associated with this batch
38
39
=cut
40
41
sub status {
42
    my ( $self ) = @_;
43
    return Koha::IllbatchStatus->_new_from_dbic(
44
        scalar $self->_result->statuscode
45
    );
46
}
47
48
=head3 patron
49
50
    my $patron = Koha::Illbatch->patron;
51
52
Return the patron object associated with this batch
53
54
=cut
55
56
sub patron {
57
    my ( $self ) = @_;
58
    return Koha::Patron->_new_from_dbic(
59
        scalar $self->_result->borrowernumber
60
    );
61
}
62
63
=head3 branch
64
65
    my $branch = Koha::Illbatch->branch;
66
67
Return the branch object associated with this batch
68
69
=cut
70
71
sub branch {
72
    my ( $self ) = @_;
73
    return Koha::Library->_new_from_dbic(
74
        scalar $self->_result->branchcode
75
    );
76
}
77
78
=head3 requests_count
79
80
    my $requests_count = Koha::Illbatch->requests_count;
81
82
Return the number of requests associated with this batch
83
84
=cut
85
86
sub requests_count {
87
    my ( $self ) = @_;
88
    return Koha::Illrequests->search({
89
        batch_id => $self->id
90
    })->count;
91
}
92
93
=head3 create_and_log
94
95
    $batch->create_and_log;
96
97
Log batch creation following storage
98
99
=cut
100
101
sub create_and_log {
102
    my ( $self ) = @_;
103
104
    $self->store;
105
106
    my $logger = Koha::Illrequest::Logger->new;
107
108
    $logger->log_something({
109
        modulename   => 'ILL',
110
        actionname  => 'batch_create',
111
        objectnumber => $self->id,
112
        infos        => to_json({})
113
    });
114
}
115
116
=head3 update_and_log
117
118
    $batch->update_and_log;
119
120
Log batch update following storage
121
122
=cut
123
124
sub update_and_log {
125
    my ( $self, $params ) = @_;
126
127
    my $before = {
128
        name       => $self->name,
129
        branchcode => $self->branchcode
130
    };
131
132
    $self->set( $params );
133
    my $update = $self->store;
134
135
    my $after = {
136
        name       => $self->name,
137
        branchcode => $self->branchcode
138
    };
139
140
    my $logger = Koha::Illrequest::Logger->new;
141
142
    $logger->log_something({
143
        modulename   => 'ILL',
144
        actionname  => 'batch_update',
145
        objectnumber => $self->id,
146
        infos        => to_json({
147
            before => $before,
148
            after  => $after
149
        })
150
    });
151
}
152
153
=head3 delete_and_log
154
155
    $batch->delete_and_log;
156
157
Log batch delete
158
159
=cut
160
161
sub delete_and_log {
162
    my ( $self ) = @_;
163
164
    my $logger = Koha::Illrequest::Logger->new;
165
166
    $logger->log_something({
167
        modulename   => 'ILL',
168
        actionname  => 'batch_delete',
169
        objectnumber => $self->id,
170
        infos        => to_json({})
171
    });
172
173
    $self->delete;
174
}
175
176
=head2 Internal methods
177
178
=head3 _type
179
180
    my $type = Koha::Illbatch->_type;
181
182
Return this object's type
183
184
=cut
185
186
sub _type {
187
    return 'Illbatch';
188
}
189
190
=head1 AUTHOR
191
192
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
193
194
=cut
195
196
1;
(-)a/Koha/Illbatches.pm (+61 lines)
Line 0 Link Here
1
package Koha::Illbatches;
2
3
# Copyright PTFS Europe 2022
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
use Koha::Database;
22
use Koha::Illbatch;
23
use base qw(Koha::Objects);
24
25
=head1 NAME
26
27
Koha::Illbatches - Koha Illbatches Object class
28
29
=head2 Internal methods
30
31
=head3 _type
32
33
    my $type = Koha::Illbatches->_type;
34
35
Return this object's type
36
37
=cut
38
39
sub _type {
40
    return 'Illbatch';
41
}
42
43
=head3 object_class
44
45
    my $class = Koha::Illbatches->object_class;
46
47
Return this object's class name
48
49
=cut
50
51
sub object_class {
52
    return 'Koha::Illbatch';
53
}
54
55
=head1 AUTHOR
56
57
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
58
59
=cut
60
61
1;
(-)a/Koha/REST/V1/Illbatches.pm (+256 lines)
Line 0 Link Here
1
package Koha::REST::V1::Illbatches;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Mojo::Base 'Mojolicious::Controller';
21
22
use Koha::Illbatches;
23
use Koha::IllbatchStatuses;
24
use Koha::Illrequests;
25
26
use Try::Tiny qw( catch try );
27
28
=head1 NAME
29
30
Koha::REST::V1::Illbatches
31
32
=head2 Operations
33
34
=head3 list
35
36
Return a list of available ILL batches
37
38
=cut
39
40
sub list {
41
    my $c = shift->openapi->valid_input;
42
43
    #FIXME: This should be $c->objects-search
44
    my @batches = Koha::Illbatches->search()->as_list;
45
46
    #FIXME: Below should be coming from $c->objects accessors
47
    # Get all patrons associated with all our batches
48
    # in one go
49
    my $patrons = {};
50
    foreach my $batch(@batches) {
51
        my $patron_id = $batch->borrowernumber;
52
        $patrons->{$patron_id} = 1
53
    };
54
    my @patron_ids = keys %{$patrons};
55
    my $patron_results = Koha::Patrons->search({
56
        borrowernumber => { -in => \@patron_ids }
57
    });
58
59
    # Get all branches associated with all our batches
60
    # in one go
61
    my $branches = {};
62
    foreach my $batch(@batches) {
63
        my $branch_id = $batch->branchcode;
64
        $branches->{$branch_id} = 1
65
    };
66
    my @branchcodes = keys %{$branches};
67
    my $branch_results = Koha::Libraries->search({
68
        branchcode => { -in => \@branchcodes }
69
    });
70
71
    # Get all batch statuses associated with all our batches
72
    # in one go
73
    my $statuses = {};
74
    foreach my $batch(@batches) {
75
        my $code = $batch->statuscode;
76
        $statuses->{$code} = 1
77
    };
78
    my @statuscodes = keys %{$statuses};
79
    my $status_results = Koha::IllbatchStatuses->search({
80
        code => { -in => \@statuscodes }
81
    });
82
83
    # Populate the response
84
    my @to_return = ();
85
    foreach my $it_batch(@batches) {
86
        my $patron = $patron_results->find({ borrowernumber => $it_batch->borrowernumber});
87
        my $branch = $branch_results->find({ branchcode => $it_batch->branchcode });
88
        my $status = $status_results->find({ code => $it_batch->statuscode });
89
        push @to_return, {
90
            %{$it_batch->unblessed},
91
            patron         => $patron,
92
            branch         => $branch,
93
            status         => $status,
94
            requests_count => $it_batch->requests_count
95
        };
96
    }
97
98
    return $c->render( status => 200, openapi => \@to_return );
99
}
100
101
=head3 get
102
103
Get one batch
104
105
=cut
106
107
sub get {
108
    my $c = shift->openapi->valid_input;
109
110
    my $batchid = $c->validation->param('illbatch_id');
111
112
    my $batch = Koha::Illbatches->find($batchid);
113
114
    if (not defined $batch) {
115
        return $c->render(
116
            status => 404,
117
            openapi => { error => "ILL batch not found" }
118
        );
119
    }
120
121
    return $c->render(
122
        status => 200,
123
        openapi => {
124
            %{$batch->unblessed},
125
            patron         => $batch->patron->unblessed,
126
            branch         => $batch->branch->unblessed,
127
            status         => $batch->status->unblessed,
128
            requests_count => $batch->requests_count
129
        }
130
    );
131
}
132
133
=head3 add
134
135
Add a new batch
136
137
=cut
138
139
sub add {
140
    my $c = shift->openapi->valid_input or return;
141
142
    my $body = $c->validation->param('body');
143
144
    # We receive cardnumber, so we need to look up the corresponding
145
    # borrowernumber
146
    my $patron = Koha::Patrons->find({ cardnumber => $body->{cardnumber} });
147
148
    if ( not defined $patron ) {
149
        return $c->render(
150
            status  => 404,
151
            openapi => { error => "Patron with cardnumber " . $body->{cardnumber} . " not found" }
152
        );
153
    }
154
155
    delete $body->{cardnumber};
156
    $body->{borrowernumber} = $patron->borrowernumber;
157
158
    return try {
159
        my $batch = Koha::Illbatch->new( $body );
160
        $batch->create_and_log;
161
        $c->res->headers->location( $c->req->url->to_string . '/' . $batch->id );
162
163
        my $ret = {
164
            %{$batch->unblessed},
165
            patron           => $batch->patron->unblessed,
166
            branch           => $batch->branch->unblessed,
167
            status           => $batch->status->unblessed,
168
            requests_count   => 0
169
        };
170
171
        return $c->render(
172
            status  => 201,
173
            openapi => $ret
174
        );
175
    }
176
    catch {
177
        if ( blessed $_ ) {
178
            if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
179
                return $c->render(
180
                    status  => 409,
181
                    openapi => { error => "A batch named " . $body->{name} . " already exists" }
182
                );
183
            }
184
        }
185
        $c->unhandled_exception($_);
186
    };
187
}
188
189
=head3 update
190
191
Update a batch
192
193
=cut
194
195
sub update {
196
    my $c = shift->openapi->valid_input or return;
197
198
    my $batch = Koha::Illbatches->find( $c->validation->param('illbatch_id') );
199
200
    if ( not defined $batch ) {
201
        return $c->render(
202
            status  => 404,
203
            openapi => { error => "ILL batch not found" }
204
        );
205
    }
206
207
    my $params = $c->req->json;
208
    delete $params->{cardnumber};
209
210
    return try {
211
        $batch->update_and_log( $params );
212
213
        my $ret = {
214
            %{$batch->unblessed},
215
            patron         => $batch->patron->unblessed,
216
            branch         => $batch->branch->unblessed,
217
            status         => $batch->status->unblessed,
218
            requests_count => $batch->requests_count
219
        };
220
221
        return $c->render(
222
            status  => 200,
223
            openapi => $ret
224
        );
225
    }
226
    catch {
227
        $c->unhandled_exception($_);
228
    };
229
}
230
231
=head3 delete
232
233
Delete a batch
234
235
=cut
236
237
sub delete {
238
239
    my $c = shift->openapi->valid_input or return;
240
241
    my $batch = Koha::Illbatches->find( $c->validation->param( 'illbatch_id' ) );
242
243
    if ( not defined $batch ) {
244
        return $c->render( status => 404, openapi => { error => "ILL batch not found" } );
245
    }
246
247
    return try {
248
        $batch->delete_and_log;
249
        return $c->render( status => 204, openapi => '');
250
    }
251
    catch {
252
        $c->unhandled_exception($_);
253
    };
254
}
255
256
1;
(-)a/admin/columns_settings.yml (+2 lines)
Lines 825-830 modules: Link Here
825
        columns:
825
        columns:
826
            -
826
            -
827
              columnname: ill_request_id
827
              columnname: ill_request_id
828
            -
829
              columnname: batch
828
            -
830
            -
829
              columnname: metadata_author
831
              columnname: metadata_author
830
            -
832
            -
(-)a/api/v1/swagger/definitions/illbatch.yaml (+48 lines)
Line 0 Link Here
1
---
2
type: object
3
properties:
4
  id:
5
    type: string
6
    description: Internal ILL batch identifier
7
  name:
8
    type: string
9
    description: Name of the ILL batch
10
  backend:
11
    type: string
12
    description: Backend name
13
  cardnumber:
14
    type: string
15
    description: Card number of the patron of the ILL batch
16
  borrowernumber:
17
    type: string
18
    description: Borrower number of the patron of the ILL batch
19
  branchcode:
20
    type: string
21
    description: Branch code of the branch of the ILL batch
22
  patron:
23
    type:
24
      - object
25
      - "null"
26
    description: The patron associated with the batch
27
  branch:
28
    type:
29
      - object
30
      - "null"
31
    description: The branch associated with the batch
32
  statuscode:
33
    type: string
34
    description: Code of the status of the ILL batch
35
  status:
36
    type:
37
      - object
38
      - "null"
39
    description: The status associated with the batch
40
  requests_count:
41
    type: string
42
    description: The number of requests in this batch
43
additionalProperties: false
44
required:
45
  - name
46
  - backend
47
  - branchcode
48
  - statuscode
(-)a/api/v1/swagger/definitions/illbatches.yaml (+5 lines)
Line 0 Link Here
1
---
2
type: array
3
items:
4
  $ref: "illbatch.yaml"
5
additionalProperties: false
(-)a/api/v1/swagger/paths/illbatches.yaml (+236 lines)
Line 0 Link Here
1
---
2
/illbatches:
3
  get:
4
    x-mojo-to: Illbatches#list
5
    operationId: listIllbatches
6
    tags:
7
      - illbatches
8
    summary: List ILL batches
9
    parameters: []
10
    produces:
11
      - application/json
12
    responses:
13
      "200":
14
        description: A list of ILL batches
15
        schema:
16
          $ref: "../swagger.yaml#/definitions/illbatches"
17
      "401":
18
        description: Authentication required
19
        schema:
20
          $ref: "../swagger.yaml#/definitions/error"
21
      "403":
22
        description: Access forbidden
23
        schema:
24
          $ref: "../swagger.yaml#/definitions/error"
25
      "404":
26
        description: ILL batches not found
27
        schema:
28
          $ref: "../swagger.yaml#/definitions/error"
29
      "500":
30
        description: |
31
          Internal server error. Possible `error_code` attribute values:
32
33
          * `internal_server_error`
34
        schema:
35
          $ref: "../swagger.yaml#/definitions/error"
36
      "503":
37
        description: Under maintenance
38
        schema:
39
          $ref: "../swagger.yaml#/definitions/error"
40
    x-koha-authorization:
41
      permissions:
42
        ill: "1"
43
  post:
44
    x-mojo-to: Illbatches#add
45
    operationId: addIllbatch
46
    tags:
47
      - illbatches
48
    summary: Add ILL batch
49
    parameters:
50
      - name: body
51
        in: body
52
        description: A JSON object containing informations about the new batch
53
        required: true
54
        schema:
55
          $ref: "../swagger.yaml#/definitions/illbatch"
56
    produces:
57
      - application/json
58
    responses:
59
      "201":
60
        description: Batch added
61
        schema:
62
          $ref: "../swagger.yaml#/definitions/illbatch"
63
      "400":
64
        description: Bad request
65
        schema:
66
          $ref: "../swagger.yaml#/definitions/error"
67
      "401":
68
        description: Authentication required
69
        schema:
70
          $ref: "../swagger.yaml#/definitions/error"
71
      "403":
72
        description: Access forbidden
73
        schema:
74
          $ref: "../swagger.yaml#/definitions/error"
75
      "404":
76
        description: Patron with given cardnumber not found
77
        schema:
78
          $ref: "../swagger.yaml#/definitions/error"
79
      "409":
80
        description: Conflict in creating resource
81
        schema:
82
          $ref: "../swagger.yaml#/definitions/error"
83
      "500":
84
        description: |
85
          Internal server error. Possible `error_code` attribute values:
86
87
          * `internal_server_error`
88
        schema:
89
          $ref: "../swagger.yaml#/definitions/error"
90
      "503":
91
        description: Under maintenance
92
        schema:
93
          $ref: "../swagger.yaml#/definitions/error"
94
    x-koha-authorization:
95
      permissions:
96
        ill: "1"
97
"/illbatches/{illbatch_id}":
98
  get:
99
    x-mojo-to: Illbatches#get
100
    operationId: getIllbatches
101
    tags:
102
      - illbatches
103
    summary: Get ILL batch
104
    parameters:
105
      - name: illbatch_id
106
        in: path
107
        description: ILL batch id/name/contents
108
        required: true
109
        type: string
110
    produces:
111
      - application/json
112
    responses:
113
      "200":
114
        description: An ILL batch
115
        schema:
116
          $ref: "../swagger.yaml#/definitions/illbatch"
117
      "401":
118
        description: Authentication required
119
        schema:
120
          $ref: "../swagger.yaml#/definitions/error"
121
      "403":
122
        description: Access forbidden
123
        schema:
124
          $ref: "../swagger.yaml#/definitions/error"
125
      "404":
126
        description: ILL batch not found
127
        schema:
128
          $ref: "../swagger.yaml#/definitions/error"
129
      "500":
130
        description: |
131
          Internal server error. Possible `error_code` attribute values:
132
133
          * `internal_server_error`
134
        schema:
135
          $ref: "../swagger.yaml#/definitions/error"
136
      "503":
137
        description: Under maintenance
138
        schema:
139
          $ref: "../swagger.yaml#/definitions/error"
140
    x-koha-authorization:
141
      permissions:
142
        ill: "1"
143
  put:
144
    x-mojo-to: Illbatches#update
145
    operationId: updateIllBatch
146
    tags:
147
      - illbatches
148
    summary: Update batch
149
    parameters:
150
      - $ref: "../swagger.yaml#/parameters/illbatch_id_pp"
151
      - name: body
152
        in: body
153
        description: A JSON object containing information on the batch
154
        required: true
155
        schema:
156
          $ref: "../swagger.yaml#/definitions/illbatch"
157
    consumes:
158
      - application/json
159
    produces:
160
      - application/json
161
    responses:
162
      "200":
163
        description: An ILL batch
164
        schema:
165
          $ref: "../swagger.yaml#/definitions/illbatch"
166
      "400":
167
        description: Bad request
168
        schema:
169
          $ref: "../swagger.yaml#/definitions/error"
170
      "401":
171
        description: Authentication required
172
        schema:
173
          $ref: "../swagger.yaml#/definitions/error"
174
      "403":
175
        description: Access forbidden
176
        schema:
177
          $ref: "../swagger.yaml#/definitions/error"
178
      "404":
179
        description: ILL batch not found
180
        schema:
181
          $ref: "../swagger.yaml#/definitions/error"
182
      "500":
183
        description: |
184
          Internal server error. Possible `error_code` attribute values:
185
186
          * `internal_server_error`
187
        schema:
188
          $ref: "../swagger.yaml#/definitions/error"
189
      "503":
190
        description: Under maintenance
191
        schema:
192
          $ref: "../swagger.yaml#/definitions/error"
193
    x-koha-authorization:
194
      permissions:
195
        ill: "1"
196
  delete:
197
    x-mojo-to: Illbatches#delete
198
    operationId: deleteBatch
199
    tags:
200
      - illbatches
201
    summary: Delete ILL batch
202
    parameters:
203
      - $ref: "../swagger.yaml#/parameters/illbatch_id_pp"
204
    produces:
205
      - application/json
206
    responses:
207
      "204":
208
        description: ILL batch deleted
209
        schema:
210
          type: string
211
      "401":
212
        description: Authentication required
213
        schema:
214
          $ref: "../swagger.yaml#/definitions/error"
215
      "403":
216
        description: Access forbidden
217
        schema:
218
          $ref: "../swagger.yaml#/definitions/error"
219
      "404":
220
        description: ILL batch not found
221
        schema:
222
          $ref: "../swagger.yaml#/definitions/error"
223
      "500":
224
        description: |
225
          Internal server error. Possible `error_code` attribute values:
226
227
          * `internal_server_error`
228
        schema:
229
          $ref: "../swagger.yaml#/definitions/error"
230
      "503":
231
        description: Under maintenance
232
        schema:
233
          $ref: "../swagger.yaml#/definitions/error"
234
    x-koha-authorization:
235
      permissions:
236
        ill: "1"
(-)a/ill/ill-requests.pl (-5 / +100 lines)
Lines 27-36 use Koha::Notice::Templates; Link Here
27
use Koha::AuthorisedValues;
27
use Koha::AuthorisedValues;
28
use Koha::Illcomment;
28
use Koha::Illcomment;
29
use Koha::Illrequests;
29
use Koha::Illrequests;
30
use Koha::Illbatches;
30
use Koha::Illrequest::Workflow::Availability;
31
use Koha::Illrequest::Workflow::Availability;
31
use Koha::Illrequest::Workflow::TypeDisclaimer;
32
use Koha::Illrequest::Workflow::TypeDisclaimer;
32
use Koha::Libraries;
33
use Koha::Libraries;
33
use Koha::Token;
34
use Koha::Token;
35
use Koha::Plugins;
34
36
35
use Try::Tiny qw( catch try );
37
use Try::Tiny qw( catch try );
36
use URI::Escape qw( uri_escape_utf8 );
38
use URI::Escape qw( uri_escape_utf8 );
Lines 66-75 my $has_branch = $cfg->has_branch; Link Here
66
my $backends_available = ( scalar @{$backends} > 0 );
68
my $backends_available = ( scalar @{$backends} > 0 );
67
$template->param(
69
$template->param(
68
    backends_available => $backends_available,
70
    backends_available => $backends_available,
69
    has_branch         => $has_branch
71
    has_branch         => $has_branch,
72
    have_batch         => have_batch_backends($backends)
70
);
73
);
71
74
72
if ( $backends_available ) {
75
if ( $backends_available ) {
76
    # Establish what metadata enrichment plugins we have available
77
    my $enrichment_services = get_metadata_enrichment();
78
    if (scalar @{$enrichment_services} > 0) {
79
        $template->param(
80
            metadata_enrichment_services => encode_json($enrichment_services)
81
        );
82
    }
83
    # Establish whether we have any availability services that can provide availability
84
    # for the batch identifier types we support
85
    my $batch_availability_services = get_ill_availability($enrichment_services);
86
    if (scalar @{$batch_availability_services} > 0) {
87
        $template->param(
88
            batch_availability_services => encode_json($batch_availability_services)
89
        );
90
    }
91
73
    if ( $op eq 'illview' ) {
92
    if ( $op eq 'illview' ) {
74
        # View the details of an ILL
93
        # View the details of an ILL
75
        my $request = Koha::Illrequests->find($params->{illrequest_id});
94
        my $request = Koha::Illrequests->find($params->{illrequest_id});
Lines 136-143 if ( $backends_available ) { Link Here
136
            }
155
            }
137
156
138
            $template->param(
157
            $template->param(
139
                whole   => $backend_result,
158
                whole     => $backend_result,
140
                request => $request
159
                request   => $request
141
            );
160
            );
142
            handle_commit_maybe($backend_result, $request);
161
            handle_commit_maybe($backend_result, $request);
143
        }
162
        }
Lines 203-208 if ( $backends_available ) { Link Here
203
        # We simulate the API for backend requests for uniformity.
222
        # We simulate the API for backend requests for uniformity.
204
        # So, init:
223
        # So, init:
205
        my $request = Koha::Illrequests->find($params->{illrequest_id});
224
        my $request = Koha::Illrequests->find($params->{illrequest_id});
225
        my $batches = Koha::Illbatches->search(undef, {
226
            order_by => { -asc => 'name' }
227
        });
206
        if ( !$params->{stage} ) {
228
        if ( !$params->{stage} ) {
207
            my $backend_result = {
229
            my $backend_result = {
208
                error   => 0,
230
                error   => 0,
Lines 215-227 if ( $backends_available ) { Link Here
215
            };
237
            };
216
            $template->param(
238
            $template->param(
217
                whole          => $backend_result,
239
                whole          => $backend_result,
218
                request        => $request
240
                request        => $request,
241
                batches        => $batches
219
            );
242
            );
220
        } else {
243
        } else {
221
            # Commit:
244
            # Commit:
222
            # Save the changes
245
            # Save the changes
223
            $request->borrowernumber($params->{borrowernumber});
246
            $request->borrowernumber($params->{borrowernumber});
224
            $request->biblio_id($params->{biblio_id});
247
            $request->biblio_id($params->{biblio_id});
248
            $request->batch_id($params->{batch_id});
225
            $request->branchcode($params->{branchcode});
249
            $request->branchcode($params->{branchcode});
226
            $request->price_paid($params->{price_paid});
250
            $request->price_paid($params->{price_paid});
227
            $request->notesopac($params->{notesopac});
251
            $request->notesopac($params->{notesopac});
Lines 353-359 if ( $backends_available ) { Link Here
353
    } elsif ( $op eq 'illlist') {
377
    } elsif ( $op eq 'illlist') {
354
378
355
        # If we receive a pre-filter, make it available to the template
379
        # If we receive a pre-filter, make it available to the template
356
        my $possible_filters = ['borrowernumber'];
380
        my $possible_filters = ['borrowernumber', 'batch_id'];
357
        my $active_filters = {};
381
        my $active_filters = {};
358
        foreach my $filter(@{$possible_filters}) {
382
        foreach my $filter(@{$possible_filters}) {
359
            if ($params->{$filter}) {
383
            if ($params->{$filter}) {
Lines 372-377 if ( $backends_available ) { Link Here
372
        $template->param(
396
        $template->param(
373
            prefilters => join("&", @tpl_arr)
397
            prefilters => join("&", @tpl_arr)
374
        );
398
        );
399
400
        if ($active_filters->{batch_id}) {
401
            my $batch_id = $active_filters->{batch_id};
402
            if ($batch_id) {
403
                my $batch = Koha::Illbatches->find($batch_id);
404
                $template->param(
405
                    batch => $batch
406
                );
407
            }
408
        }
409
375
    } elsif ( $op eq "save_comment" ) {
410
    } elsif ( $op eq "save_comment" ) {
376
        die "Wrong CSRF token" unless Koha::Token->new->check_csrf({
411
        die "Wrong CSRF token" unless Koha::Token->new->check_csrf({
377
           session_id => scalar $cgi->cookie('CGISESSID'),
412
           session_id => scalar $cgi->cookie('CGISESSID'),
Lines 406-411 if ( $backends_available ) { Link Here
406
            scalar $params->{illrequest_id} . $append
441
            scalar $params->{illrequest_id} . $append
407
        );
442
        );
408
        exit;
443
        exit;
444
    } elsif ( $op eq "batch_list" ) {
445
        # Do not remove, it prevents us falling through to the 'else'
446
    } elsif ( $op eq "batch_create" ) {
447
        # Do not remove, it prevents us falling through to the 'else'
409
    } else {
448
    } else {
410
        my $request = Koha::Illrequests->find($params->{illrequest_id});
449
        my $request = Koha::Illrequests->find($params->{illrequest_id});
411
        my $backend_result = $request->custom_capability($op, $params);
450
        my $backend_result = $request->custom_capability($op, $params);
Lines 463-465 sub redirect_to_list { Link Here
463
    print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl');
502
    print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl');
464
    exit;
503
    exit;
465
}
504
}
505
506
# Do any of the available backends provide batch requesting
507
sub have_batch_backends {
508
    my ( $backends ) = @_;
509
510
    my @have_batch = ();
511
512
    foreach my $backend(@{$backends}) {
513
        my $can_batch = can_batch($backend);
514
        if ($can_batch) {
515
            push @have_batch, $backend;
516
        }
517
    }
518
    return \@have_batch;
519
}
520
521
# Does a given backend provide batch requests
522
# FIXME: This should be moved to Koha::Illbackend
523
sub can_batch {
524
    my ( $backend ) = @_;
525
    my $request = Koha::Illrequest->new->load_backend( $backend );
526
    return $request->_backend_capability( 'provides_batch_requests' );
527
}
528
529
# Get available metadata enrichment plugins
530
sub get_metadata_enrichment {
531
    my @candidates = Koha::Plugins->new()->GetPlugins({
532
        method => 'provides_api'
533
    });
534
    my @services = ();
535
    foreach my $plugin(@candidates) {
536
        my $supported = $plugin->provides_api();
537
        if ($supported->{type} eq 'search') {
538
            push @services, $supported;
539
        }
540
    }
541
    return \@services;
542
}
543
544
# Get ILL availability plugins that can help us with the batch identifier types
545
# we support
546
sub get_ill_availability {
547
    my ( $services ) = @_;
548
549
    my $id_types = {};
550
    foreach my $service(@{$services}) {
551
        foreach my $id_supported(keys %{$service->{identifiers_supported}}) {
552
            $id_types->{$id_supported} = 1;
553
        }
554
    }
555
556
    my $availability = Koha::Illrequest::Workflow::Availability->new($id_types);
557
    return $availability->get_services({
558
        ui_context => 'staff'
559
    });
560
}
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+106 lines)
Lines 3747-3752 input.renew { Link Here
3747
}
3747
}
3748
3748
3749
#interlibraryloans {
3749
#interlibraryloans {
3750
3751
    .ill-toolbar {
3752
        display: flex;
3753
    }
3754
3755
    #ill-batch {
3756
        flex-grow: 1;
3757
        display: flex;
3758
        justify-content: flex-end;
3759
        gap: 5px;
3760
    }
3761
3762
    #ill-batch-requests {
3763
        .action-buttons {
3764
            display: flex;
3765
            gap: 5px;
3766
            justify-content: center;
3767
        }
3768
    }
3769
3770
    #ill-batch-modal {
3771
        .modal-footer {
3772
            display: flex;
3773
            & > * {
3774
                flex: 1;
3775
            }
3776
            #lhs {
3777
                text-align: left;
3778
            }
3779
        }
3780
        #create-progress {
3781
            margin-top: 17px;
3782
        }
3783
        .fetch-failed {
3784
            background-color: rgba(255,0,0,0.1);
3785
            & > * {
3786
                background-color: inherit;
3787
            }
3788
        }
3789
        .progress {
3790
            margin-bottom: 0;
3791
            margin-top: 17px;
3792
        }
3793
        #create-requests {
3794
            display: flex;
3795
            justify-content: flex-end;
3796
        }
3797
        .action-column {
3798
            text-align: center;
3799
            & > * {
3800
                margin-left: 5px;
3801
            }
3802
            & > *:first-child {
3803
                margin-left: 0;
3804
            }
3805
        }
3806
        .metadata-row:not(:first-child) {
3807
            margin-top: 0.5em;
3808
        }
3809
        .metadata-label {
3810
            font-weight: 600;
3811
        }
3812
        .more-less {
3813
            text-align: right;
3814
            margin: 2px 0;
3815
        }
3816
3817
    }
3818
3819
    #batch-form {
3820
        legend {
3821
            margin-bottom: 2em;
3822
        }
3823
        textarea {
3824
            width: 100%;
3825
            min-height: 100px;
3826
            padding: 5px;
3827
            resize: vertical;
3828
        }
3829
        #new-batch-form {
3830
            display: flex;
3831
            gap: 20px;
3832
        }
3833
        li#process-button {
3834
            display: flex;
3835
            justify-content: flex-end;
3836
        }
3837
        #textarea-metadata {
3838
            padding: 0 15px;
3839
            display: flex;
3840
            justify-content: space-between;
3841
        }
3842
        #textarea-errors {
3843
            display: flex;
3844
            flex-direction: column;
3845
            gap: 10px;
3846
            padding: 20px 15px 10px
3847
        }
3848
        .batch-modal-actions {
3849
            text-align: center;
3850
        }
3851
        fieldset {
3852
            border: 2px solid #b9d8d9;
3853
        }
3854
    }
3855
3750
    #dataPreviewLabel {
3856
    #dataPreviewLabel {
3751
        margin: .3em 0;
3857
        margin: .3em 0;
3752
    }
3858
    }
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc (+38 lines)
Line 0 Link Here
1
<!-- ill-batch-table-strings.inc -->
2
<script>
3
    var ill_batch_add = _("Add new batch");
4
    var ill_batch_update = _("Update batch");
5
    var ill_batch_none = _("None");
6
    var ill_batch_retrieving_metadata = _("Retrieving metadata");
7
    var ill_batch_api_fail = _("Unable to retrieve batch details");
8
    var ill_batch_statuses_api_fail = _("Unable to retrieve batch statuses");
9
    var ill_batch_api_request_fail = _("Unable to create local request");
10
    var ill_batch_requests_api_fail = _("Unable to retrieve batch requests");
11
    var ill_batch_unknown = _("Unknown");
12
    var ill_batch_doi = _("DOI");
13
    var ill_batch_pmid = _("PubMed ID");
14
    var ill_populate_waiting = _("Retrieving...");
15
    var ill_populate_failed = _("Failed to retrieve");
16
    var ill_button_remove = _("Remove");
17
    var ill_batch_create_api_fail = _("Unable to create batch request");
18
    var ill_batch_update_api_fail = _("Unable to updatecreate batch request");
19
    var ill_batch_item_remove = _("Are you sure you want to remove this item from the batch");
20
    var ill_batch_create_cancel_button = _("Close");
21
    var ill_batch_metadata_more = _("More");
22
    var ill_batch_metadata_less = _("Less");
23
    var ill_batch_available_via = _("Available via");
24
    var ill_batch_metadata = {
25
        'doi': _("DOI"),
26
        'pmid': _("PubMed ID"),
27
        'issn': _("ISSN"),
28
        'title': _("Title"),
29
        'year': _("Year"),
30
        'issue': _("Issue"),
31
        'pages': _("Pages"),
32
        'publisher': _("Publisher"),
33
        'article_title': _("Article title"),
34
        'article_author': _("Article author"),
35
        'volume': _("Volume")
36
    };
37
</script>
38
<!-- / ill-batch-table-strings.inc -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc (+97 lines)
Line 0 Link Here
1
<div id="ill-batch-details" style="display:none"></div>
2
<div id="ill-batch-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ill-batch-modal-label" aria-hidden="true">
3
    <div class="modal-dialog modal-lg">
4
        <div class="modal-content">
5
            <div class="modal-header">
6
                <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
7
                <h3 id="ill-batch-modal-label"></h3>
8
            </div>
9
            <div class="modal-body">
10
                <div id="batch-form">
11
                    <form id="new-batch-form">
12
                        <fieldset class="rows">
13
                            <legend id="legend">Batch details</legend>
14
                            <ol>
15
                                <li id="batch_name">
16
                                    <label class="required" for="name">Batch name:</label>
17
                                    <input type="text" autocomplete="off" name="name" id="name" type="text"
18
                                    value="" />
19
                                </li>
20
                                <li id="batch_patron">
21
                                    <label class="required" for="batchcardnumber">Card number, username or surname:</label>
22
                                    <input type="text" autocomplete="off" name="batchcardnumber" id="batchcardnumber" type="text" value="" />
23
                                    <span id="patron_link"></span>
24
                                </li>
25
                                <li id="batch_branch">
26
                                    <label class="required" for="branchcode">Library:</label>
27
                                    <select id="branchcode" name="branchcode">
28
                                        [% PROCESS options_for_libraries libraries => Branches.all( selected => branchcode ) %]
29
                                    </select>
30
                                </li>
31
                                <li id="batch_statuscode" style="display:none">
32
                                    <label class="required" for="statuscode">Status:</label>
33
                                    <select id="statuscode" name="statuscode"></select>
34
                                </li>
35
                            </ol>
36
                        </fieldset>
37
                        <fieldset id="add_batch_items" class="rows" style="display:none">
38
                            <legend id="legend">Add batch items</legend>
39
                            <div id="textarea-metadata">
40
                                <div id="supported">Supported identifiers: <span id="supported_identifiers"></span></div>
41
                                <div id="row_count">Row count: <span id="row_count_value"></span></div>
42
                            </div>
43
                            <div id="textarea-errors" style="display:none" class="error">
44
                                <div id="duplicates" style="display:none">The following duplicates were found, these have been de-duplicated: <span id="dupelist"></span></div>
45
                                <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>
46
                            </div>
47
                            <ol>
48
                                <li>
49
                                    <textarea id="identifiers_input" placeholder="Enter identifiers, one per line"></textarea>
50
                                </li>
51
                                <li id="process-button">
52
                                    <button id="process_button" disabled aria-disabled="true" type="button">Process identifiers</button>
53
                                </li>
54
                            </ol>
55
                        </fieldset>
56
                    </form>
57
                    <div id="create-progress" class="alert alert-info" role="alert" style="display:none">
58
                        <span id="progress-label"><strong></strong></span> -
59
                        Items processed: <span id="processed_count">0</span> out of <span id="processed_total">0</span>.
60
                        Items failed: <span id="processed_failed">0</span>.
61
                        <div class="progress">
62
                            <div id="processed_progress_bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 2em;">
63
                                0%
64
                            </div>
65
                        </div>
66
                    </div>
67
                    <table id="identifier-table" style="display:none">
68
                        <thead>
69
                            <tr id="identifier-table-header">
70
                                <th scope="col">Identifier type</th>
71
                                <th scope="col">Identifier value</th>
72
                                <th scope="col">Metadata</th>
73
                                <th scope="col">Request ID</th>
74
                                <th scope="col">Request Status</th>
75
                                <th scope="col"></th>
76
                            </tr>
77
                        </thead>
78
                        <tbody id="identifier-table-body">
79
                        </tbody>
80
                    </table>
81
                </div>
82
                <div id="create-requests" style="display:none">
83
                    <button id="create-requests-button" type="button" class="btn btn-primary" aria-label="Add items to batch">Add items to batch</button>
84
                </div>
85
            </div>
86
            <div class="modal-footer">
87
                <div id="lhs">
88
                    <button class="btn btn-default" data-dismiss="modal" aria-hidden="false">Cancel</button>
89
                </div>
90
                <div id="rhs">
91
                    <button id="button_create_batch" class="btn btn-default" aria-hidden="true" disabled>Continue</button>
92
                    <button id="button_finish" disabled type="button" class="btn btn-default" aria-hidden="true">Finish and view batch</button>
93
                </div>
94
            </div>
95
        </div>
96
    </div>
97
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc (+9 lines)
Line 0 Link Here
1
<!-- ill-batch-table-strings.inc -->
2
<script>
3
    var ill_batch_borrower_details = _("View borrower details");
4
    var ill_batch_edit = _("Edit");
5
    var ill_batch_delete = _("Delete");
6
    var ill_batch_confirm_delete = _("Are you sure you want to delete this batch? All attached requests will be detached.");
7
    var ill_batch_delete_fail = _("Unable to delete batch");
8
</script>
9
<!-- / ill-batch-table-strings.inc -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc (+19 lines)
Line 0 Link Here
1
[% IF query_type == "batch_list" %]
2
<div>
3
    <table id="ill-batch-requests">
4
        <thead>
5
            <tr id="ill-batch-header">
6
                <th scope="col">Batch ID</th>
7
                <th scope="col">Name</th>
8
                <th scope="col">Number of requests</th>
9
                <th scope="col">Status</th>
10
                <th scope="col">Patron</th>
11
                <th scope="col">Branch</th>
12
                <th scope="col"></th>
13
            </tr>
14
        </thead>
15
        <tbody id="ill-batch-body">
16
        </tbody>
17
    </table>
18
</div>
19
[% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table-strings.inc (+1 lines)
Lines 1-5 Link Here
1
<!-- ill-list-table-strings.inc -->
1
<!-- ill-list-table-strings.inc -->
2
<script>
2
<script>
3
    var ill_borrower_details = _("View patron details");
3
    var ill_manage = _("Manage request");
4
    var ill_manage = _("Manage request");
4
    var ill_manage_select_backend_first = _("Select a backend first");
5
    var ill_manage_select_backend_first = _("Select a backend first");
5
    var ill_all_statuses = _("All statuses");
6
    var ill_all_statuses = _("All statuses");
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc (+1 lines)
Lines 2-7 Link Here
2
    <thead>
2
    <thead>
3
        <tr id="ill_requests_header">
3
        <tr id="ill_requests_header">
4
            <th scope="col">Request ID</th>
4
            <th scope="col">Request ID</th>
5
            <th scope="col">Batch</th>
5
            <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>
6
            <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>
6
            <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>
7
            <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>
7
            <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>
8
            <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>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc (-2 / +19 lines)
Lines 1-6 Link Here
1
[% USE Koha %]
1
[% USE Koha %]
2
[% IF Koha.Preference('ILLModule ') && CAN_user_ill %]
2
[% IF Koha.Preference('ILLModule') && CAN_user_ill %]
3
    <div id="toolbar" class="btn-toolbar">
3
    <div id="toolbar" class="btn-toolbar ill-toolbar">
4
        [% IF backends_available %]
4
        [% IF backends_available %]
5
          [% IF backends.size > 1 %]
5
          [% IF backends.size > 1 %]
6
            <div class="dropdown btn-group">
6
            <div class="dropdown btn-group">
Lines 32-36 Link Here
32
                <i class="fa fa-list"></i> List requests
32
                <i class="fa fa-list"></i> List requests
33
            </a>
33
            </a>
34
        [% END %]
34
        [% END %]
35
        [% IF have_batch.size > 0 && metadata_enrichment_services %]
36
        <div id="ill-batch">
37
            <div class="dropdown btn-group">
38
                <button class="btn btn-default dropdown-toggle" type="button" id="ill-batch-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
39
                    <i class="fa fa-plus"></i> New ILL batch request <span class="caret"></span>
40
                </button>
41
                <ul class="dropdown-menu" aria-labelledby="ill-batch-backend-dropdown">
42
                    [% FOREACH backend IN have_batch %]
43
                        <li><a href="#" role="button" onclick="window.openBatchModal(null, '[% backend | html %]')">[% backend | html %]</a></li>
44
                    [% END %]
45
                </ul>
46
            </div>
47
            <a class="btn btn-default" type="button" href="/cgi-bin/koha/ill/ill-requests.pl?method=batch_list" %]">
48
                <i class="fa fa-tasks"></i> Batch requests</span>
49
            </a>
50
        </div>
51
        [% END %]
35
    </div>
52
    </div>
36
[% END %]
53
[% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt (-1 / +51 lines)
Lines 126-131 Link Here
126
            <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>
126
            <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>
127
        [% ELSE %]
127
        [% ELSE %]
128
                [% INCLUDE 'ill-toolbar.inc' %]
128
                [% INCLUDE 'ill-toolbar.inc' %]
129
                [% INCLUDE 'ill-batch-modal.inc' %]
129
130
130
                [% IF whole.error %]
131
                [% IF whole.error %]
131
                    <h1>Error performing operation</h1>
132
                    <h1>Error performing operation</h1>
Lines 439-444 Link Here
439
                                        [% END %]
440
                                        [% END %]
440
                                    </select>
441
                                    </select>
441
                                </li>
442
                                </li>
443
                                [% IF batches.count > 0 %]
444
                                <li class="batch">
445
                                    <label class="batch_label">Batch:</label>
446
                                    <select id="batch_id" name="batch_id">
447
                                        <option value="">
448
                                        [% FOREACH batch IN batches %]
449
                                            [% IF batch.id == request.batch_id %]
450
                                            <option value="[% batch.id | html %]" selected>
451
                                            [% ELSE %]
452
                                            <option value="[% batch.id | html %]">
453
                                            [% END %]
454
                                                [% batch.name | html %]
455
                                            </option>
456
                                        [% END %]
457
                                    </select>
458
                                </li>
459
                                [% END %]
442
                                <li class="updated">
460
                                <li class="updated">
443
                                    <label class="updated">Last updated:</label>
461
                                    <label class="updated">Last updated:</label>
444
                                    [% request.updated | $KohaDates  with_hours => 1 %]
462
                                    [% request.updated | $KohaDates  with_hours => 1 %]
Lines 653-658 Link Here
653
                                            [% END %]
671
                                            [% END %]
654
                                        [% END %]
672
                                        [% END %]
655
                                    </li>
673
                                    </li>
674
                                    [% IF request.batch > 0 %]
675
                                    <li class="batch">
676
                                        <span class="label batch">Batch:</span>
677
                                        <a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=[% request.batch.id | html %]">
678
                                        [% request.batch.name | html %]
679
                                        </a>
680
                                    </li>
681
                                    [% END %]
656
                                    <li class="updated">
682
                                    <li class="updated">
657
                                        <span class="label updated">Last updated:</span>
683
                                        <span class="label updated">Last updated:</span>
658
                                        [% request.updated | $KohaDates  with_hours => 1 %]
684
                                        [% request.updated | $KohaDates  with_hours => 1 %]
Lines 789-795 Link Here
789
815
790
                [% ELSIF query_type == 'illlist' %]
816
                [% ELSIF query_type == 'illlist' %]
791
                    <!-- illlist -->
817
                    <!-- illlist -->
792
                    <h1>View ILL requests</h1>
818
                    <h1>
819
                        View ILL requests
820
                        [% IF batch %]
821
                        for batch "[% batch.name | html %]"
822
                        [% END %]
823
                    </h1>
793
                    <div id="results" class="page-section">
824
                    <div id="results" class="page-section">
794
                        <h2>Details for all requests</h2>
825
                        <h2>Details for all requests</h2>
795
                         [% INCLUDE 'ill-list-table.inc' %]
826
                         [% INCLUDE 'ill-list-table.inc' %]
Lines 867-872 Link Here
867
                            </fieldset>
898
                            </fieldset>
868
                        </form>
899
                        </form>
869
                    </div>
900
                    </div>
901
                [% ELSIF query_type == 'batch_list' || query_type == 'batch_create' %]
902
                    [% INCLUDE 'ill-batch.inc' %]
870
                [% ELSE %]
903
                [% ELSE %]
871
                <!-- Custom Backend Action -->
904
                <!-- Custom Backend Action -->
872
                [% PROCESS $whole.template %]
905
                [% PROCESS $whole.template %]
Lines 886-891 Link Here
886
    [% INCLUDE 'columns_settings.inc' %]
919
    [% INCLUDE 'columns_settings.inc' %]
887
    [% INCLUDE 'calendar.inc' %]
920
    [% INCLUDE 'calendar.inc' %]
888
    [% INCLUDE 'select2.inc' %]
921
    [% INCLUDE 'select2.inc' %]
922
    [% IF metadata_enrichment_services %]
923
    <script>
924
        var metadata_enrichment_services = [% metadata_enrichment_services | $raw %];
925
    </script>
926
    <script>
927
        [% IF batch_availability_services %]
928
        var batch_availability_services = [% batch_availability_services | $raw %];
929
        [% ELSE %]
930
        var batch_availability_services = [];
931
        [% END %]
932
    </script>
933
    [% END %]
889
    <script>
934
    <script>
890
        var prefilters = '[% prefilters | $raw %]';
935
        var prefilters = '[% prefilters | $raw %]';
891
        // Set column settings
936
        // Set column settings
Lines 910-916 Link Here
910
        });
955
        });
911
    </script>
956
    </script>
912
    [% INCLUDE 'ill-list-table-strings.inc' %]
957
    [% INCLUDE 'ill-list-table-strings.inc' %]
958
    [% INCLUDE 'ill-batch-table-strings.inc' %]
959
    [% INCLUDE 'ill-batch-modal-strings.inc' %]
913
    [% Asset.js("js/ill-list-table.js") | $raw %]
960
    [% Asset.js("js/ill-list-table.js") | $raw %]
961
    [% Asset.js("js/ill-batch.js") | $raw %]
962
    [% Asset.js("js/ill-batch-table.js") | $raw %]
963
    [% Asset.js("js/ill-batch-modal.js") | $raw %]
914
    [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %]
964
    [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %]
915
        [% Asset.js("js/ill-availability.js") | $raw %]
965
        [% Asset.js("js/ill-availability.js") | $raw %]
916
    [% END %]
966
    [% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js (+1093 lines)
Line 0 Link Here
1
(function () {
2
    // Bail if there aren't any metadata enrichment plugins installed
3
    if (typeof metadata_enrichment_services === 'undefined') {
4
        console.log('No metadata enrichment plugins found.')
5
        return;
6
    }
7
8
    window.addEventListener('load', onload);
9
10
    // Delay between API requests
11
    var debounceDelay = 1000;
12
13
    // Elements we work frequently with
14
    var textarea = document.getElementById("identifiers_input");
15
    var nameInput = document.getElementById("name");
16
    var cardnumberInput = document.getElementById("batchcardnumber");
17
    var branchcodeSelect = document.getElementById("branchcode");
18
    var processButton = document.getElementById("process_button");
19
    var createButton = document.getElementById("button_create_batch");
20
    var finishButton = document.getElementById("button_finish");
21
    var batchItemsDisplay = document.getElementById("add_batch_items");
22
    var createProgressTotal = document.getElementById("processed_total");
23
    var createProgressCount = document.getElementById("processed_count");
24
    var createProgressFailed = document.getElementById("processed_failed");
25
    var createProgressBar = document.getElementById("processed_progress_bar");
26
    var identifierTable = document.getElementById('identifier-table');
27
    var createRequestsButton = document.getElementById('create-requests-button');
28
    var statusesSelect = document.getElementById('statuscode');
29
    var cancelButton = document.getElementById('lhs').querySelector('button');
30
    var cancelButtonOriginalText = cancelButton.innerHTML;
31
32
    // We need a data structure keyed on identifier type, which tells us how to parse that
33
    // identifier type and what services can get its metadata. We receive an array of
34
    // available services
35
    var supportedIdentifiers = {};
36
    metadata_enrichment_services.forEach(function (service) {
37
        // Iterate the identifiers that this service supports
38
        Object.keys(service.identifiers_supported).forEach(function (idType) {
39
            if (!supportedIdentifiers[idType]) {
40
                supportedIdentifiers[idType] = [];
41
            }
42
            supportedIdentifiers[idType].push(service);
43
        });
44
    });
45
46
    // An object for when we're creating a new batch
47
    var emptyBatch = {
48
        name: '',
49
        backend: null,
50
        cardnumber: '',
51
        branchcode: '',
52
        statuscode: 'NEW'
53
    };
54
55
    // The object that holds the batch we're working with
56
    // It's a proxy so we can update portions of the UI
57
    // upon changes
58
    var batch = new Proxy(
59
        { data: {} },
60
        {
61
            get: function (obj, prop) {
62
                return obj[prop];
63
            },
64
            set: function (obj, prop, value) {
65
                obj[prop] = value;
66
                manageBatchItemsDisplay();
67
                updateBatchInputs();
68
                disableCardnumberInput();
69
                displayPatronName();
70
                updateStatusesSelect();
71
            }
72
        }
73
    );
74
75
    // The object that holds the contents of the table
76
    // It's a proxy so we can make it automatically redraw the
77
    // table upon changes
78
    var tableContent = new Proxy(
79
        { data: [] },
80
        {
81
            get: function (obj, prop) {
82
                return obj[prop];
83
            },
84
            set: function (obj, prop, value) {
85
                obj[prop] = value;
86
                updateTable();
87
                updateRowCount();
88
                updateProcessTotals();
89
                checkAvailability();
90
            }
91
        }
92
    );
93
94
    // The object that holds the contents of the table
95
    // It's a proxy so we can update portions of the UI
96
    // upon changes
97
    var statuses = new Proxy(
98
        { data: [] },
99
        {
100
            get: function (obj, prop) {
101
                return obj[prop];
102
            },
103
            set: function (obj, prop, value) {
104
                obj[prop] = value;
105
                updateStatusesSelect();
106
            }
107
        }
108
    );
109
110
    var progressTotals = new Proxy(
111
        {
112
            data: {}
113
        },
114
        {
115
            get: function (obj, prop) {
116
                return obj[prop];
117
            },
118
            set: function (obj, prop, value) {
119
                obj[prop] = value;
120
                showCreateRequestsButton();
121
            }
122
        }
123
    );
124
125
    // Keep track of submission API calls that are in progress
126
    // so we don't duplicate them
127
    var submissionSent = {};
128
129
    // Keep track of availability API calls that are in progress
130
    // so we don't duplicate them
131
    var availabilitySent = {};
132
133
    // Are we updating an existing batch
134
    var isUpdate = false;
135
136
    // The datatable
137
    var table;
138
    var tableEl = document.getElementById('identifier-table');
139
140
    // The element that potentially holds the ID of the batch
141
    // we're working with
142
    var idEl = document.getElementById('ill-batch-details');
143
    var batchId = null;
144
    var backend = null;
145
146
    function onload() {
147
        $('#ill-batch-modal').on('shown.bs.modal', function () {
148
            init();
149
            patronAutocomplete();
150
            batchInputsEventListeners();
151
            createButtonEventListener();
152
            createRequestsButtonEventListener();
153
            moreLessEventListener();
154
            removeRowEventListener();
155
        });
156
        $('#ill-batch-modal').on('hidden.bs.modal', function () {
157
            // Reset our state when we close the modal
158
            // TODO: need to also reset progress bar and already processed identifiers
159
            delete idEl.dataset.batchId;
160
            delete idEl.dataset.backend;
161
            batchId = null;
162
            tableEl.style.display = 'none';
163
            tableContent.data = [];
164
            progressTotals.data = {
165
                total: 0,
166
                count: 0,
167
                failed: 0
168
            };
169
            textarea.value = '';
170
            batch.data = {};
171
            cancelButton.innerHTML = cancelButtonOriginalText;
172
            // Remove event listeners we created
173
            removeEventListeners();
174
        });
175
    };
176
177
    function init() {
178
        batchId = idEl.dataset.batchId;
179
        backend = idEl.dataset.backend;
180
        emptyBatch.backend = backend;
181
        progressTotals.data = {
182
            total: 0,
183
            count: 0,
184
            failed: 0
185
        };
186
        if (batchId) {
187
            fetchBatch();
188
            isUpdate = true;
189
            setModalHeading();
190
        } else {
191
            batch.data = emptyBatch;
192
            setModalHeading();
193
        }
194
        fetchStatuses();
195
        finishButtonEventListener();
196
        processButtonEventListener();
197
        identifierTextareaEventListener();
198
        displaySupportedIdentifiers();
199
        createButtonEventListener();
200
        updateRowCount();
201
    };
202
203
    function initPostCreate() {
204
        disableCreateButton();
205
        cancelButton.innerHTML = ill_batch_create_cancel_button;
206
    };
207
208
    function setFinishButton() {
209
        if (batch.data.patron) {
210
            finishButton.removeAttribute('disabled');
211
        }
212
    };
213
214
    function setModalHeading() {
215
        var heading = document.getElementById('ill-batch-modal-label');
216
        heading.textContent = isUpdate ? ill_batch_update : ill_batch_add;
217
    }
218
219
    // Identify items that have metadata and therefore can have a local request
220
    // created, and do so
221
    function requestRequestable() {
222
        createRequestsButton.setAttribute('disabled', true);
223
        createRequestsButton.setAttribute('aria-disabled', true);
224
        setFinishButton();
225
        var toCheck = tableContent.data;
226
        toCheck.forEach(function (row) {
227
            if (
228
                !row.requestId &&
229
                Object.keys(row.metadata).length > 0 &&
230
                !submissionSent[row.value]
231
            ) {
232
                submissionSent[row.value] = 1;
233
                makeLocalSubmission(row.value, row.metadata);
234
            }
235
        });
236
    };
237
238
    // Identify items that can have their availability checked, and do it
239
    function checkAvailability() {
240
        // Only proceed if we've got services that can check availability
241
        if (!batch_availability_services || batch_availability_services.length === 0) return;
242
        var toCheck = tableContent.data;
243
        toCheck.forEach(function (row) {
244
            if (
245
                !row.url &&
246
                Object.keys(row.metadata).length > 0 &&
247
                !availabilitySent[row.value]
248
            ) {
249
                availabilitySent[row.value] = 1;
250
                getAvailability(row.value, row.metadata);
251
            }
252
        });
253
    };
254
255
    // Check availability services for immediate availability, if found,
256
    // create a link in the table linking to the item
257
    function getAvailability(identifier, metadata) {
258
        // Prep the metadata for passing to the availability plugins
259
        let availability_object = {};
260
        if (metadata.issn) availability_object['issn'] = metadata.issn;
261
        if (metadata.doi) availability_object['doi'] = metadata.doi;
262
        if (metadata.pubmedid) availability_object['pubmedid'] = metadata.pubmedid;
263
        var prepped = encodeURIComponent(base64EncodeUnicode(JSON.stringify(availability_object)));
264
        for (i = 0; i < batch_availability_services.length; i++) {
265
            var service = batch_availability_services[i];
266
            window.doApiRequest(
267
                service.endpoint + prepped
268
            )
269
                .then(function (response) {
270
                    return response.json();
271
                })
272
                .then(function (data) {
273
                    if (data.results.search_results && data.results.search_results.length > 0) {
274
                        var result = data.results.search_results[0];
275
                        tableContent.data = tableContent.data.map(function (row) {
276
                            if (row.value === identifier) {
277
                                row.url = result.url;
278
                                row.availabilitySupplier = service.name;
279
                            }
280
                            return row;
281
                        });
282
                    }
283
                });
284
        }
285
    };
286
287
    // Help btoa with > 8 bit strings
288
    // Shamelessly grabbed from: https://www.base64encoder.io/javascript/
289
    function base64EncodeUnicode(str) {
290
        // First we escape the string using encodeURIComponent to get the UTF-8 encoding of the characters,
291
        // then we convert the percent encodings into raw bytes, and finally feed it to btoa() function.
292
        utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
293
                return String.fromCharCode('0x' + p1);
294
        });
295
296
        return btoa(utf8Bytes);
297
    };
298
299
    // Create a local submission and update our local state
300
    // upon success
301
    function makeLocalSubmission(identifier, metadata) {
302
303
        // Prepare extended_attributes in array format for POST
304
        var extended_attributes = [];
305
        for (const [key, value] of Object.entries(metadata)) {
306
            extended_attributes.push({"type":key, "value":value});
307
        }
308
309
        let date = new Date();
310
        var payload = {
311
            batch_id: batchId,
312
            ill_backend_id: batch.data.backend,
313
            patron_id: batch.data.patron.borrowernumber,
314
            library_id: batch.data.branchcode,
315
            //FIXME: requested_date and timestamp should not be required by the API, they are handled by the backend create_api method
316
            requested_date: $date_to_rfc3339(date.toString()),
317
            timestamp: date,
318
            extended_attributes: extended_attributes
319
        };
320
        window.doCreateSubmission(payload)
321
            .then(function (response) {
322
                return response.json();
323
            })
324
            .then(function (data) {
325
                tableContent.data = tableContent.data.map(function (row) {
326
                    if (row.value === identifier) {
327
                        row.requestId = data.ill_request_id;
328
                        row.requestStatus = data.status;
329
                    }
330
                    return row;
331
                });
332
            })
333
            .catch(function () {
334
                window.handleApiError(ill_batch_api_request_fail);
335
            });
336
    };
337
338
    function updateProcessTotals() {
339
        var init = {
340
            total: 0,
341
            count: 0,
342
            failed: 0
343
        };
344
        progressTotals.data = init;
345
        var toUpdate = progressTotals.data;
346
        tableContent.data.forEach(function (row) {
347
            toUpdate.total++;
348
            if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) {
349
                toUpdate.count++;
350
            }
351
            if (Object.keys(row.failed).length > 0) {
352
                toUpdate.failed++;
353
            }
354
        });
355
        createProgressTotal.innerHTML = toUpdate.total;
356
        createProgressCount.innerHTML = toUpdate.count;
357
        createProgressFailed.innerHTML = toUpdate.failed;
358
        var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100);
359
        createProgressBar.setAttribute('aria-valuenow', percentDone);
360
        createProgressBar.innerHTML = percentDone + '%';
361
        createProgressBar.style.width = percentDone + '%';
362
        progressTotals.data = toUpdate;
363
    };
364
365
    function displayPatronName() {
366
        var span = document.getElementById('patron_link');
367
        if (batch.data.patron) {
368
            var link = createPatronLink();
369
            span.appendChild(link);
370
        } else {
371
            if (span.children.length > 0) {
372
                span.removeChild(span.firstChild);
373
            }
374
        }
375
    };
376
377
    function updateStatusesSelect() {
378
        while (statusesSelect.options.length > 0) {
379
            statusesSelect.remove(0);
380
        }
381
        statuses.data.forEach(function (status) {
382
            var option = document.createElement('option')
383
            option.value = status.code;
384
            option.text = status.name;
385
            if (batch.data.id && batch.data.statuscode === status.code) {
386
                option.selected = true;
387
            }
388
            statusesSelect.add(option);
389
        });
390
        if (isUpdate) {
391
            statusesSelect.parentElement.style.display = 'block';
392
        }
393
    };
394
395
    function removeEventListeners() {
396
        textarea.removeEventListener('paste', processButtonState);
397
        textarea.removeEventListener('keyup', processButtonState);
398
        processButton.removeEventListener('click', processIdentifiers);
399
        nameInput.removeEventListener('keyup', createButtonState);
400
        cardnumberInput.removeEventListener('keyup', createButtonState);
401
        branchcodeSelect.removeEventListener('change', createButtonState);
402
        createButton.removeEventListener('click', createBatch);
403
        identifierTable.removeEventListener('click', toggleMetadata);
404
        identifierTable.removeEventListener('click', removeRow);
405
        createRequestsButton.remove('click', requestRequestable);
406
    };
407
408
    function finishButtonEventListener() {
409
        finishButton.addEventListener('click', doFinish);
410
    };
411
412
    function identifierTextareaEventListener() {
413
        textarea.addEventListener('paste', textareaUpdate);
414
        textarea.addEventListener('keyup', textareaUpdate);
415
    };
416
417
    function processButtonEventListener() {
418
        processButton.addEventListener('click', processIdentifiers);
419
    };
420
421
    function createRequestsButtonEventListener() {
422
        createRequestsButton.addEventListener('click', requestRequestable);
423
    };
424
425
    function createButtonEventListener() {
426
        createButton.addEventListener('click', createBatch);
427
    };
428
429
    function batchInputsEventListeners() {
430
        nameInput.addEventListener('keyup', createButtonState);
431
        cardnumberInput.addEventListener('keyup', createButtonState);
432
        branchcodeSelect.addEventListener('change', createButtonState);
433
    };
434
435
    function moreLessEventListener() {
436
        identifierTable.addEventListener('click', toggleMetadata);
437
    };
438
439
    function removeRowEventListener() {
440
        identifierTable.addEventListener('click', removeRow);
441
    };
442
443
    function textareaUpdate() {
444
        processButtonState();
445
        updateRowCount();
446
    };
447
448
    function processButtonState() {
449
        if (textarea.value.length > 0) {
450
            processButton.removeAttribute('disabled');
451
            processButton.removeAttribute('aria-disabled');
452
        } else {
453
            processButton.setAttribute('disabled', true);
454
            processButton.setAttribute('aria-disabled', true);
455
        }
456
    };
457
458
    function disableCardnumberInput() {
459
        if (batch.data.patron) {
460
            cardnumberInput.setAttribute('disabled', true);
461
            cardnumberInput.setAttribute('aria-disabled', true);
462
        } else {
463
            cardnumberInput.removeAttribute('disabled');
464
            cardnumberInput.removeAttribute('aria-disabled');
465
        }
466
    };
467
468
    function createButtonState() {
469
        if (
470
            nameInput.value.length > 0 &&
471
            cardnumberInput.value.length > 0 &&
472
            branchcodeSelect.selectedOptions.length === 1
473
        ) {
474
            createButton.removeAttribute('disabled');
475
            createButton.setAttribute('display', 'inline-block');
476
        } else {
477
            createButton.setAttribute('disabled', 1);
478
            createButton.setAttribute('display', 'none');
479
        }
480
    };
481
482
    function doFinish() {
483
        updateBatch()
484
            .then(function () {
485
                $('#ill-batch-modal').modal({ show: false });
486
                location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.id;
487
            });
488
    };
489
490
    // Get all batch statuses
491
    function fetchStatuses() {
492
        window.doApiRequest('/api/v1/illbatchstatuses')
493
            .then(function (response) {
494
                return response.json();
495
            })
496
            .then(function (jsoned) {
497
                statuses.data = jsoned;
498
            })
499
            .catch(function (e) {
500
                window.handleApiError(ill_batch_statuses_api_fail);
501
            });
502
    };
503
504
    // Get the batch
505
    function fetchBatch() {
506
        window.doBatchApiRequest("/" + batchId)
507
            .then(function (response) {
508
                return response.json();
509
            })
510
            .then(function (jsoned) {
511
                batch.data = {
512
                    id: jsoned.id,
513
                    name: jsoned.name,
514
                    backend: jsoned.backend,
515
                    cardnumber: jsoned.cardnumber,
516
                    branchcode: jsoned.branchcode,
517
                    statuscode: jsoned.statuscode
518
                }
519
                return jsoned;
520
            })
521
            .then(function (data) {
522
                batch.data = data;
523
            })
524
            .catch(function () {
525
                window.handleApiError(ill_batch_api_fail);
526
            });
527
    };
528
529
    function createBatch() {
530
        var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
531
        var selectedStatuscode = statusesSelect.selectedOptions[0].value;
532
        return doBatchApiRequest('', {
533
            method: 'POST',
534
            headers: {
535
                'Content-type': 'application/json'
536
            },
537
            body: JSON.stringify({
538
                name: nameInput.value,
539
                backend: backend,
540
                cardnumber: cardnumberInput.value,
541
                branchcode: selectedBranchcode,
542
                statuscode: selectedStatuscode
543
            })
544
        })
545
            .then(function (response) {
546
                if ( response.ok ) {
547
                    return response.json();
548
                }
549
                return Promise.reject(response);
550
            })
551
            .then(function (body) {
552
                batchId = body.id;
553
                batch.data = {
554
                    id: body.id,
555
                    name: body.name,
556
                    backend: body.backend,
557
                    cardnumber: body.patron.cardnumber,
558
                    branchcode: body.branchcode,
559
                    statuscode: body.statuscode,
560
                    patron: body.patron,
561
                    status: body.status
562
                };
563
                initPostCreate();
564
            })
565
            .catch(function (response) {
566
                response.json().then((json) => {
567
                    if( json.error ) {
568
                        handleApiError(json.error);
569
                    } else {
570
                        handleApiError(ill_batch_create_api_fail);
571
                    }
572
                })
573
            });
574
    };
575
576
    function updateBatch() {
577
        var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
578
        var selectedStatuscode = statusesSelect.selectedOptions[0].value;
579
        return doBatchApiRequest('/' + batch.data.id, {
580
            method: 'PUT',
581
            headers: {
582
                'Content-type': 'application/json'
583
            },
584
            body: JSON.stringify({
585
                name: nameInput.value,
586
                backend: batch.data.backend,
587
                cardnumber: batch.data.patron.cardnumber,
588
                branchcode: selectedBranchcode,
589
                statuscode: selectedStatuscode
590
            })
591
        })
592
            .catch(function () {
593
                handleApiError(ill_batch_update_api_fail);
594
            });
595
    };
596
597
    function displaySupportedIdentifiers() {
598
        var names = Object.keys(supportedIdentifiers).map(function (identifier) {
599
            return window['ill_batch_' + identifier];
600
        });
601
        var displayEl = document.getElementById('supported_identifiers');
602
        displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none;
603
    }
604
605
    function updateRowCount() {
606
        var textEl = document.getElementById('row_count_value');
607
        var val = textarea.value.trim();
608
        var cnt = 0;
609
        if (val.length > 0) {
610
            cnt = val.split(/\n/).length;
611
        }
612
        textEl.textContent = cnt;
613
    }
614
615
    function showProgress() {
616
        var el = document.getElementById('create-progress');
617
        el.style.display = 'block';
618
    }
619
620
    function showCreateRequestsButton() {
621
        var data = progressTotals.data;
622
        var el = document.getElementById('create-requests');
623
        el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none';
624
    }
625
626
    async function processIdentifiers() {
627
        var content = textarea.value;
628
        hideErrors();
629
        if (content.length === 0) return;
630
631
        disableProcessButton();
632
        var label = document.getElementById('progress-label').firstChild;
633
        label.innerHTML = ill_batch_retrieving_metadata;
634
        showProgress();
635
636
        // Errors encountered when processing
637
        var processErrors = {};
638
639
        // Prepare the content, including trimming each row
640
        var contentArr = content.split(/\n/);
641
        var trimmed = contentArr.map(function (row) {
642
            return row.trim();
643
        });
644
645
        var parsed = [];
646
647
        trimmed.forEach(function (identifier) {
648
            var match = identifyIdentifier(identifier);
649
            // If this identifier is not identifiable or
650
            // looks like more than one type, we can't be sure
651
            // what it is
652
            if (match.length != 1) {
653
                parsed.push({
654
                    type: 'unknown',
655
                    value: identifier
656
                });
657
            } else {
658
                parsed.push(match[0]);
659
            }
660
        });
661
662
        var unknownIdentifiers = parsed
663
            .filter(function (parse) {
664
                if (parse.type == 'unknown') {
665
                    return parse;
666
                }
667
            })
668
            .map(function (filtered) {
669
                return filtered.value;
670
            });
671
672
        if (unknownIdentifiers.length > 0) {
673
            processErrors.badidentifiers = {
674
                element: 'badids',
675
                values: unknownIdentifiers.join(', ')
676
            };
677
        };
678
679
        // Deduping
680
        var deduped = [];
681
        var dupes = {};
682
        parsed.forEach(function (row) {
683
            var value = row.value;
684
            var alreadyInDeduped = deduped.filter(function (d) {
685
                return d.value === value;
686
            });
687
            if (alreadyInDeduped.length > 0 && !dupes[value]) {
688
                dupes[value] = 1;
689
            } else if (alreadyInDeduped.length === 0) {
690
                row.metadata = {};
691
                row.failed = {};
692
                row.requestId = null;
693
                deduped.push(row);
694
            }
695
        });
696
        // Update duplicate error if dupes were found
697
        if (Object.keys(dupes).length > 0) {
698
            processErrors.duplicates = {
699
                element: 'dupelist',
700
                values: Object.keys(dupes).join(', ')
701
            };
702
        }
703
704
        // Display any errors
705
        displayErrors(processErrors);
706
707
        // Now build and display the table
708
        if (!table) {
709
            buildTable();
710
        }
711
712
        // We may be appending new values to an existing table,
713
        // in which case, ensure we don't create duplicates
714
        var tabIdentifiers = tableContent.data.map(function (tabId) {
715
            return tabId.value;
716
        });
717
        var notInTable = deduped.filter(function (ded) {
718
            if (!tabIdentifiers.includes(ded.value)) {
719
                return ded;
720
           }
721
        });
722
        if (notInTable.length > 0) {
723
            tableContent.data = tableContent.data.concat(notInTable);
724
        }
725
726
        // Populate metadata for those records that need it
727
        var newData = tableContent.data;
728
        for (var i = 0; i < tableContent.data.length; i++) {
729
            var row = tableContent.data[i];
730
            // Skip rows that don't need populating
731
            if (
732
                Object.keys(tableContent.data[i].metadata).length > 0 ||
733
                Object.keys(tableContent.data[i].failed).length > 0
734
            ) continue;
735
            var identifier = { type: row.type, value: row.value };
736
            try {
737
                var populated = await populateMetadata(identifier);
738
                row.metadata = populated.results.result || {};
739
            } catch (e) {
740
                row.failed = ill_populate_failed;
741
            }
742
            newData[i] = row;
743
            tableContent.data = newData;
744
        }
745
    }
746
747
    function disableProcessButton() {
748
        processButton.setAttribute('disabled', true);
749
        processButton.setAttribute('aria-disabled', true);
750
    }
751
752
    function disableCreateButton() {
753
        createButton.setAttribute('disabled', true);
754
        createButton.setAttribute('aria-disabled', true);
755
    }
756
757
    async function populateMetadata(identifier) {
758
        // All services that support this identifier type
759
        var services = supportedIdentifiers[identifier.type];
760
        // Check each service and use the first results we get, if any
761
        for (var i = 0; i < services.length; i++) {
762
            var service = services[i];
763
            var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value;
764
            var metadata = await getMetadata(endpoint);
765
            if (metadata.errors.length === 0) {
766
                var parsed = await parseMetadata(metadata, service);
767
                if (parsed.errors.length > 0) {
768
                    throw Error(metadata.errors.map(function (error) {
769
                        return error.message;
770
                    }).join(', '));
771
                }
772
                return parsed;
773
            }
774
        }
775
    };
776
777
    async function getMetadata(endpoint) {
778
        var response = await debounce(doApiRequest)(endpoint);
779
        return response.json();
780
    };
781
782
    async function parseMetadata(metadata, service) {
783
        var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint;
784
        var response = await doApiRequest(endpoint, {
785
            method: 'POST',
786
            headers: {
787
                'Content-type': 'application/json'
788
            },
789
            body: JSON.stringify(metadata)
790
        });
791
        return response.json();
792
    }
793
794
    // A render function for identifier type
795
    function createIdentifierType(data) {
796
        return window['ill_batch_' + data];
797
    };
798
799
    // Get an item's title
800
    function getTitle(meta) {
801
        if (meta.article_title && meta.article_title.length > 0) {
802
            return 'article_title';
803
            return {
804
                prop: 'article_title',
805
                value: meta.article_title
806
            };
807
        } else if (meta.title && meta.title.length > 0) {
808
            return 'title';
809
            return {
810
                prop: 'title',
811
                value: meta.title
812
            };
813
        }
814
    };
815
816
    // Create a metadata row
817
    function createMetadataRow(data, meta, prop) {
818
        if (!meta[prop]) return;
819
820
        var div = document.createElement('div');
821
        div.classList.add('metadata-row');
822
        var label = document.createElement('span');
823
        label.classList.add('metadata-label');
824
        label.innerText = ill_batch_metadata[prop] + ': ';
825
826
        // Add a link to the availability URL if appropriate
827
        var value;
828
        if (!data.url) {
829
            value = document.createElement('span');
830
        } else {
831
            value = document.createElement('a');
832
            value.setAttribute('href', data.url);
833
            value.setAttribute('target', '_blank');
834
            value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier);
835
        }
836
        value.classList.add('metadata-value');
837
        value.innerText = meta[prop];
838
        div.appendChild(label);
839
        div.appendChild(value);
840
841
        return div;
842
    }
843
844
    // A render function for displaying metadata
845
    function createMetadata(x, y, data) {
846
        // If the fetch failed
847
        if (data.failed.length > 0) {
848
            return data.failed;
849
        }
850
851
        // If we've not yet got any metadata back
852
        if (Object.keys(data.metadata).length === 0) {
853
            return ill_populate_waiting;
854
        }
855
856
        var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume'];
857
        var meta = data.metadata;
858
859
        var container = document.createElement('div');
860
        container.classList.add('metadata-container');
861
862
        // Create the title row
863
        var title = getTitle(meta);
864
        if (title) {
865
            // Remove the title element from the props
866
            // we're about to iterate
867
            core = core.filter(function (i) {
868
                return i !== title;
869
            });
870
            var titleRow = createMetadataRow(data, meta, title);
871
            container.appendChild(titleRow);
872
        }
873
874
        var remainder = document.createElement('div');
875
        remainder.classList.add('metadata-remainder');
876
        remainder.style.display = 'none';
877
        // Create the remaining rows
878
        core.sort().forEach(function (prop) {
879
            var div = createMetadataRow(data, meta, prop);
880
            if (div) {
881
                remainder.appendChild(div);
882
            }
883
        });
884
        container.appendChild(remainder);
885
886
        // Add a more/less toggle
887
        var firstField = container.firstChild;
888
        var moreLess = document.createElement('div');
889
        moreLess.classList.add('more-less');
890
        var moreLessLink = document.createElement('a');
891
        moreLessLink.setAttribute('href', '#');
892
        moreLessLink.classList.add('more-less-link');
893
        moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']';
894
        moreLess.appendChild(moreLessLink);
895
        firstField.appendChild(moreLess);
896
897
        return container.outerHTML;
898
    };
899
900
    function removeRow(ev) {
901
        if (ev.target.className.includes('remove-row')) {
902
            if (!confirm(ill_batch_item_remove)) return;
903
            // Find the parent row
904
            var ancestor = ev.target.closest('tr');
905
            var identifier = ancestor.querySelector('.identifier').innerText;
906
            tableContent.data = tableContent.data.filter(function (row) {
907
                return row.value !== identifier;
908
            });
909
        }
910
    }
911
912
    function toggleMetadata(ev) {
913
        if (ev.target.className === 'more-less-link') {
914
            // Find the element we need to show
915
            var ancestor = ev.target.closest('.metadata-container');
916
            var meta = ancestor.querySelector('.metadata-remainder');
917
918
            // Display or hide based on its current state
919
            var display = window.getComputedStyle(meta).display;
920
921
            meta.style.display = display === 'block' ? 'none' : 'block';
922
923
            // Update the More / Less text
924
            ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]';
925
        }
926
    }
927
928
    // A render function for the link to a request ID
929
    function createRequestId(x, y, data) {
930
        return data.requestId || '-';
931
    }
932
933
    function createRequestStatus(x, y, data) {
934
        return data.requestStatus || '-';
935
    }
936
937
    function buildTable(identifiers) {
938
        table = KohaTable('identifier-table', {
939
            processing: true,
940
            deferRender: true,
941
            ordering: false,
942
            paging: false,
943
            searching: false,
944
            autoWidth: false,
945
            columns: [
946
                {
947
                    data: 'type',
948
                    width: '13%',
949
                    render: createIdentifierType
950
                },
951
                {
952
                    data: 'value',
953
                    width: '25%',
954
                    className: 'identifier'
955
                },
956
                {
957
                    data: 'metadata',
958
                    render: createMetadata
959
                },
960
                {
961
                    data: 'requestId',
962
                    width: '6.5%',
963
                    render: createRequestId
964
                },
965
                {
966
                    data: 'requestStatus',
967
                    width: '6.5%',
968
                    render: createRequestStatus
969
                },
970
                {
971
                    width: '18%',
972
                    render: createActions,
973
                    className: 'action-column'
974
                }
975
            ],
976
            createdRow: function (row, data) {
977
                if (data.failed.length > 0) {
978
                    row.classList.add('fetch-failed');
979
                }
980
            }
981
        });
982
    }
983
984
    function createActions(x, y, data) {
985
        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>';
986
    }
987
988
    // Redraw the table
989
    function updateTable() {
990
        if (!table) return;
991
        tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none';
992
        tableEl.style.width = '100%';
993
        table.api()
994
            .clear()
995
            .rows.add(tableContent.data)
996
            .draw();
997
    };
998
999
    function identifyIdentifier(identifier) {
1000
        var matches = [];
1001
1002
        // Iterate our available services to see if any can identify this identifier
1003
        Object.keys(supportedIdentifiers).forEach(function (identifierType) {
1004
            // Since all the services supporting this identifier type should use the same
1005
            // regex to identify it, we can just use the first
1006
            var service = supportedIdentifiers[identifierType][0];
1007
            var regex = new RegExp(service.identifiers_supported[identifierType].regex);
1008
            var match = identifier.match(regex);
1009
            if (match && match.groups && match.groups.identifier) {
1010
                matches.push({
1011
                    type: identifierType,
1012
                    value: match.groups.identifier
1013
                });
1014
            }
1015
        });
1016
        return matches;
1017
    }
1018
1019
    function displayErrors(errors) {
1020
        var keys = Object.keys(errors);
1021
        if (keys.length > 0) {
1022
            keys.forEach(function (key) {
1023
                var el = document.getElementById(errors[key].element);
1024
                el.textContent = errors[key].values;
1025
                el.style.display = 'inline';
1026
                var container = document.getElementById(key);
1027
                container.style.display = 'block';
1028
            });
1029
            var el = document.getElementById('textarea-errors');
1030
            el.style.display = 'flex';
1031
        }
1032
    }
1033
1034
    function hideErrors() {
1035
        var dupelist = document.getElementById('dupelist');
1036
        var badids = document.getElementById('badids');
1037
        dupelist.textContent = '';
1038
        dupelist.parentElement.style.display = 'none';
1039
        badids.textContent = '';
1040
        badids.parentElement.style.display = 'none';
1041
        var tae = document.getElementById('textarea-errors');
1042
        tae.style.display = 'none';
1043
    }
1044
1045
    function manageBatchItemsDisplay() {
1046
        batchItemsDisplay.style.display = batch.data.id ? 'block' : 'none'
1047
    };
1048
1049
    function updateBatchInputs() {
1050
        nameInput.value = batch.data.name || '';
1051
        cardnumberInput.value = batch.data.cardnumber || '';
1052
        branchcodeSelect.value = batch.data.branchcode || '';
1053
    }
1054
1055
    function debounce(func) {
1056
        var timeout;
1057
        return function (...args) {
1058
            return new Promise(function (resolve) {
1059
                if (timeout) {
1060
                    clearTimeout(timeout);
1061
                }
1062
                timeout = setTimeout(function () {
1063
                    return resolve(func(...args));
1064
                }, debounceDelay);
1065
            });
1066
        }
1067
    }
1068
1069
    function patronAutocomplete() {
1070
        patron_autocomplete(
1071
            $('#batch-form #batchcardnumber'),
1072
            {
1073
              'on-select-callback': function( event, ui ) {
1074
                $("#batch-form #batchcardnumber").val( ui.item.cardnumber );
1075
                return false;
1076
              }
1077
            }
1078
          );
1079
    };
1080
1081
    function createPatronLink() {
1082
        if (!batch.data.patron) return;
1083
        var patron = batch.data.patron;
1084
        var a = document.createElement('a');
1085
        var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber;
1086
        var text = patron.surname + ' (' + patron.cardnumber + ')';
1087
        a.setAttribute('title', ill_borrower_details);
1088
        a.setAttribute('href', href);
1089
        a.textContent = text;
1090
        return a;
1091
    };
1092
1093
})();
(-)a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js (+216 lines)
Line 0 Link Here
1
(function () {
2
    var table;
3
    var batchesProxy;
4
5
    window.addEventListener('load', onload);
6
7
    function onload() {
8
        // Only proceed if appropriate
9
        if (!document.getElementById('ill-batch-requests')) return;
10
11
        // A proxy that will give us some element of reactivity to
12
        // changes in our list of batches
13
        batchesProxy = new Proxy(
14
            { data: [] },
15
            {
16
                get: function (obj, prop) {
17
                    return obj[prop];
18
                },
19
                set: function (obj, prop, value) {
20
                    obj[prop] = value;
21
                    updateTable();
22
                }
23
            }
24
        );
25
26
        // Initialise the Datatable, binding it to our proxy object
27
        table = initTable();
28
29
        // Do the initial data population
30
        window.doBatchApiRequest()
31
            .then(function (response) {
32
                return response.json();
33
            })
34
            .then(function (data) {
35
                batchesProxy.data = data;
36
            });
37
38
        // Clean up any event listeners we added
39
        window.addEventListener('beforeunload', removeEventListeners);
40
    };
41
42
    // Initialise the Datatable
43
    // FIXME: This should be a kohaTable not KohaTable
44
    var initTable = function () {
45
        return KohaTable("ill-batch-requests", {
46
            data: batchesProxy.data,
47
            columns: [
48
                {
49
                    data: 'id',
50
                    width: '10%'
51
                },
52
                {
53
                    data: 'name',
54
                    render: createName,
55
                    width: '30%'
56
                },
57
                {
58
                    data: 'requests_count',
59
                    width: '10%'
60
                },
61
                {
62
                    data: 'status',
63
                    render: createStatus,
64
                    width: '10%'
65
                },
66
                {
67
                    data: 'patron',
68
                    render: createPatronLink,
69
                    width: '10%'
70
                },
71
                {
72
                    data: 'branch',
73
                    render: createBranch,
74
                    width: '20%'
75
                },
76
                {
77
                    render: createActions,
78
                    width: '10%',
79
                    orderable: false
80
                }
81
            ],
82
            processing: true,
83
            deferRender: true,
84
            drawCallback: addEventListeners
85
        });
86
    }
87
88
    // A render function for branch name
89
    var createBranch = function (data) {
90
        return data.branchname;
91
    };
92
93
    // A render function for batch name
94
    var createName = function (x, y, data) {
95
        var a = document.createElement('a');
96
        a.setAttribute('href', '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + data.id);
97
        a.setAttribute('title', data.name);
98
        a.textContent = data.name;
99
        return a.outerHTML;
100
    };
101
102
    // A render function for batch status
103
    var createStatus = function (x, y, data) {
104
        return data.status.name;
105
    };
106
107
    // A render function for our patron link
108
    var createPatronLink = function (data) {
109
        var link = document.createElement('a');
110
        link.setAttribute('title', ill_batch_borrower_details);
111
        link.setAttribute('href', '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + data.borrowernumber);
112
        var displayText = [data.firstname, data.surname].join(' ') + ' ( ' + data.cardnumber + ' )';
113
        link.appendChild(document.createTextNode(displayText));
114
115
        return link.outerHTML;
116
    };
117
118
    // A render function for our row action buttons
119
    var createActions = function (data, type, row) {
120
        var div = document.createElement('div');
121
        div.setAttribute('class', 'action-buttons');
122
123
        var editButton = document.createElement('button');
124
        editButton.setAttribute('type', 'button');
125
        editButton.setAttribute('class', 'editButton btn btn-xs btn-default');
126
        editButton.setAttribute('data-batch-id', row.id);
127
        editButton.appendChild(document.createTextNode(ill_batch_edit));
128
129
        var deleteButton = document.createElement('button');
130
        deleteButton.setAttribute('type', 'button');
131
        deleteButton.setAttribute('class', 'deleteButton btn btn-xs btn-danger');
132
        deleteButton.setAttribute('data-batch-id', row.id);
133
        deleteButton.appendChild(document.createTextNode(ill_batch_delete));
134
135
        div.appendChild(editButton);
136
        div.appendChild(deleteButton);
137
138
        return div.outerHTML;
139
    };
140
141
    // Add event listeners to our row action buttons
142
    var addEventListeners = function () {
143
        var del = document.querySelectorAll('.deleteButton');
144
        del.forEach(function (el) {
145
            el.addEventListener('click', handleDeleteClick);
146
        });
147
148
        var edit = document.querySelectorAll('.editButton');
149
        edit.forEach(function (elEdit) {
150
            elEdit.addEventListener('click', handleEditClick);
151
        });
152
    };
153
154
    // Remove all added event listeners
155
    var removeEventListeners = function () {
156
        var del = document.querySelectorAll('.deleteButton');
157
        del.forEach(function (el) {
158
            el.removeEventListener('click', handleDeleteClick);
159
        });
160
        window.removeEventListener('load', onload);
161
        window.removeEventListener('beforeunload', removeEventListeners);
162
    };
163
164
    // Handle "Delete" clicks
165
    var handleDeleteClick = function(e) {
166
        var el = e.srcElement;
167
        if (confirm(ill_batch_confirm_delete)) {
168
            deleteBatch(el);
169
        }
170
    };
171
172
    // Handle "Edit" clicks
173
    var handleEditClick = function(e) {
174
        var el = e.srcElement;
175
        var id = el.dataset.batchId;
176
        window.openBatchModal(id);
177
    };
178
179
    // Delete a batch
180
    // - Make the API call
181
    // - Handle errors
182
    // - Update our proxy data
183
    var deleteBatch = function (el) {
184
        var id = el.dataset.batchId;
185
        doBatchApiRequest(
186
            '/' + id,
187
            { method: 'DELETE' }
188
        )
189
        .then(function (response) {
190
            if (!response.ok) {
191
                window.handleApiError(ill_batch_delete_fail);
192
            } else {
193
                removeBatch(el.dataset.batchId);
194
            }
195
        })
196
        .catch(function (response) {
197
            window.handleApiError(ill_batch_delete_fail);
198
        })
199
    };
200
201
    // Remove a batch from our proxy data
202
    var removeBatch = function(id) {
203
        batchesProxy.data = batchesProxy.data.filter(function (batch) {
204
            return batch.id != id;
205
        });
206
    };
207
208
    // Redraw the table
209
    var updateTable = function () {
210
        table.api()
211
            .clear()
212
            .rows.add(batchesProxy.data)
213
            .draw();
214
    };
215
216
})();
(-)a/koha-tmpl/intranet-tmpl/prog/js/ill-batch.js (+49 lines)
Line 0 Link Here
1
(function () {
2
    // Enable the modal to be opened from anywhere
3
    // If we're working with an existing batch, set the ID so the
4
    // modal can access it
5
    window.openBatchModal = function (id, backend) {
6
        var idEl = document.getElementById('ill-batch-details');
7
        idEl.dataset.backend = backend;
8
        if (id) {
9
            idEl.dataset.batchId = id;
10
        }
11
        $('#ill-batch-modal').modal({ show: true });
12
    };
13
14
    // Make a batch API call, returning the resulting promise
15
    window.doBatchApiRequest = function (url, options) {
16
        var batchListApi = '/api/v1/illbatches';
17
        var fullUrl = batchListApi + (url ? url : '');
18
        return doApiRequest(fullUrl, options);
19
    };
20
21
    // Make a "create local ILL submission" call
22
    window.doCreateSubmission = function (body, options) {
23
        options = Object.assign(
24
            options || {},
25
            {
26
                headers: {
27
                    'Content-Type': 'application/json'
28
                },
29
                method: 'POST',
30
                body: JSON.stringify(body)
31
            }
32
        );
33
        return doApiRequest(
34
            '/api/v1/ill/requests',
35
            options
36
        )
37
    }
38
39
    // Make an API call, returning the resulting promise
40
    window.doApiRequest = function (url, options) {
41
        return fetch(url, options);
42
    };
43
44
    // Display an API error
45
    window.handleApiError = function (error) {
46
        alert(error);
47
    };
48
49
})();
(-)a/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js (-2 / +16 lines)
Lines 79-87 $(document).ready(function() { Link Here
79
        },
79
        },
80
        "me.borrowernumber": function(){
80
        "me.borrowernumber": function(){
81
            let borrowernumber_pre_filter = additional_prefilters.find(e => e.key === 'borrowernumber');
81
            let borrowernumber_pre_filter = additional_prefilters.find(e => e.key === 'borrowernumber');
82
            if ( additional_prefilters.length == 0 || typeof borrowernumber_pre_filter === undefined) return "";
82
            if ( additional_prefilters.length == 0 || typeof borrowernumber_pre_filter === "undefined") return "";
83
            return { "=": borrowernumber_pre_filter["value"] }
83
            return { "=": borrowernumber_pre_filter["value"] }
84
        },
84
        },
85
        "me.batch_id": function(){
86
            let batch_pre_filter = additional_prefilters.find(e => e.key === 'batch_id');
87
            if ( additional_prefilters.length == 0 || typeof batch_pre_filter === "undefined") return "";
88
            return { "=": batch_pre_filter["value"] }
89
        },
85
        "-or": function(){
90
        "-or": function(){
86
            let patron = $("#illfilter_patron").val();
91
            let patron = $("#illfilter_patron").val();
87
            let status = $("#illfilter_status").val();
92
            let status = $("#illfilter_status").val();
Lines 187-192 $(document).ready(function() { Link Here
187
            'biblio',
192
            'biblio',
188
            'comments+count',
193
            'comments+count',
189
            'extended_attributes',
194
            'extended_attributes',
195
            'batch',
190
            'library',
196
            'library',
191
            'id_prefix',
197
            'id_prefix',
192
            'patron'
198
            'patron'
Lines 204-209 $(document).ready(function() { Link Here
204
                            '">' + escape_str(row.id_prefix) + escape_str(data) + '</a>';
210
                            '">' + escape_str(row.id_prefix) + escape_str(data) + '</a>';
205
                }
211
                }
206
            },
212
            },
213
            {
214
                "data": "", // batch
215
                "orderable": false,
216
                "render": function(data, type, row, meta) {
217
                    return row.batch ?
218
                        '<a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + row.batch_id + '">' + row.batch.name + '</a>'
219
                        : "";
220
                }
221
            },
207
            {
222
            {
208
                "data": "", // author
223
                "data": "", // author
209
                "orderable": false,
224
                "orderable": false,
210
- 

Return to bug 30719