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 3753-3758 input.renew { Link Here
3753
}
3753
}
3754
3754
3755
#interlibraryloans {
3755
#interlibraryloans {
3756
3757
    .ill-toolbar {
3758
        display: flex;
3759
    }
3760
3761
    #ill-batch {
3762
        flex-grow: 1;
3763
        display: flex;
3764
        justify-content: flex-end;
3765
        gap: 5px;
3766
    }
3767
3768
    #ill-batch-requests {
3769
        .action-buttons {
3770
            display: flex;
3771
            gap: 5px;
3772
            justify-content: center;
3773
        }
3774
    }
3775
3776
    #ill-batch-modal {
3777
        .modal-footer {
3778
            display: flex;
3779
            & > * {
3780
                flex: 1;
3781
            }
3782
            #lhs {
3783
                text-align: left;
3784
            }
3785
        }
3786
        #create-progress {
3787
            margin-top: 17px;
3788
        }
3789
        .fetch-failed {
3790
            background-color: rgba(255,0,0,0.1);
3791
            & > * {
3792
                background-color: inherit;
3793
            }
3794
        }
3795
        .progress {
3796
            margin-bottom: 0;
3797
            margin-top: 17px;
3798
        }
3799
        #create-requests {
3800
            display: flex;
3801
            justify-content: flex-end;
3802
        }
3803
        .action-column {
3804
            text-align: center;
3805
            & > * {
3806
                margin-left: 5px;
3807
            }
3808
            & > *:first-child {
3809
                margin-left: 0;
3810
            }
3811
        }
3812
        .metadata-row:not(:first-child) {
3813
            margin-top: 0.5em;
3814
        }
3815
        .metadata-label {
3816
            font-weight: 600;
3817
        }
3818
        .more-less {
3819
            text-align: right;
3820
            margin: 2px 0;
3821
        }
3822
3823
    }
3824
3825
    #batch-form {
3826
        legend {
3827
            margin-bottom: 2em;
3828
        }
3829
        textarea {
3830
            width: 100%;
3831
            min-height: 100px;
3832
            padding: 5px;
3833
            resize: vertical;
3834
        }
3835
        #new-batch-form {
3836
            display: flex;
3837
            gap: 20px;
3838
        }
3839
        li#process-button {
3840
            display: flex;
3841
            justify-content: flex-end;
3842
        }
3843
        #textarea-metadata {
3844
            padding: 0 15px;
3845
            display: flex;
3846
            justify-content: space-between;
3847
        }
3848
        #textarea-errors {
3849
            display: flex;
3850
            flex-direction: column;
3851
            gap: 10px;
3852
            padding: 20px 15px 10px
3853
        }
3854
        .batch-modal-actions {
3855
            text-align: center;
3856
        }
3857
        fieldset {
3858
            border: 2px solid #b9d8d9;
3859
        }
3860
    }
3861
3756
    #dataPreviewLabel {
3862
    #dataPreviewLabel {
3757
        margin: .3em 0;
3863
        margin: .3em 0;
3758
    }
3864
    }
(-)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 (+3 lines)
Lines 1-11 Link Here
1
[% IF patron.borrowernumber %]
1
[% IF patron.borrowernumber %]
2
<table id="ill-requests-patron-[% patron.borrowernumber | html %]">
2
<table id="ill-requests-patron-[% patron.borrowernumber | html %]">
3
[% ELSIF batch.id %]
4
<table id="ill-requests-batch-[% batch.id | html %]">
3
[% ELSE %]
5
[% ELSE %]
4
<table id="ill-requests">
6
<table id="ill-requests">
5
[% END %]
7
[% END %]
6
    <thead>
8
    <thead>
7
        <tr id="ill_requests_header">
9
        <tr id="ill_requests_header">
8
            <th scope="col">Request ID</th>
10
            <th scope="col">Request ID</th>
11
            <th scope="col">Batch</th>
9
            <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>
12
            <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>
10
            <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>
13
            <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>
11
            <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>
14
            <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 114-119 Link Here
114
            <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>
114
            <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>
115
        [% ELSE %]
115
        [% ELSE %]
116
                [% INCLUDE 'ill-toolbar.inc' %]
116
                [% INCLUDE 'ill-toolbar.inc' %]
117
                [% INCLUDE 'ill-batch-modal.inc' %]
117
118
118
                [% IF whole.error %]
119
                [% IF whole.error %]
119
                    <h1>Error performing operation</h1>
120
                    <h1>Error performing operation</h1>
Lines 427-432 Link Here
427
                                        [% END %]
428
                                        [% END %]
428
                                    </select>
429
                                    </select>
429
                                </li>
430
                                </li>
431
                                [% IF batches.count > 0 %]
432
                                <li class="batch">
433
                                    <label class="batch_label">Batch:</label>
434
                                    <select id="batch_id" name="batch_id">
435
                                        <option value="">
436
                                        [% FOREACH batch IN batches %]
437
                                            [% IF batch.id == request.batch_id %]
438
                                            <option value="[% batch.id | html %]" selected>
439
                                            [% ELSE %]
440
                                            <option value="[% batch.id | html %]">
441
                                            [% END %]
442
                                                [% batch.name | html %]
443
                                            </option>
444
                                        [% END %]
445
                                    </select>
446
                                </li>
447
                                [% END %]
430
                                <li class="updated">
448
                                <li class="updated">
431
                                    <label class="updated">Last updated:</label>
449
                                    <label class="updated">Last updated:</label>
432
                                    [% request.updated | $KohaDates  with_hours => 1 %]
450
                                    [% request.updated | $KohaDates  with_hours => 1 %]
Lines 641-646 Link Here
641
                                            [% END %]
659
                                            [% END %]
642
                                        [% END %]
660
                                        [% END %]
643
                                    </li>
661
                                    </li>
662
                                    [% IF request.batch > 0 %]
663
                                    <li class="batch">
664
                                        <span class="label batch">Batch:</span>
665
                                        <a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=[% request.batch.id | html %]">
666
                                        [% request.batch.name | html %]
667
                                        </a>
668
                                    </li>
669
                                    [% END %]
644
                                    <li class="updated">
670
                                    <li class="updated">
645
                                        <span class="label updated">Last updated:</span>
671
                                        <span class="label updated">Last updated:</span>
646
                                        [% request.updated | $KohaDates  with_hours => 1 %]
672
                                        [% request.updated | $KohaDates  with_hours => 1 %]
Lines 777-783 Link Here
777
803
778
                [% ELSIF query_type == 'illlist' %]
804
                [% ELSIF query_type == 'illlist' %]
779
                    <!-- illlist -->
805
                    <!-- illlist -->
780
                    <h1>View ILL requests</h1>
806
                    <h1>
807
                        View ILL requests
808
                        [% IF batch %]
809
                        for batch "[% batch.name | html %]"
810
                        [% END %]
811
                    </h1>
781
                    <div id="results" class="page-section">
812
                    <div id="results" class="page-section">
782
                        <h2>Details for all requests</h2>
813
                        <h2>Details for all requests</h2>
783
                         [% INCLUDE 'ill-list-table.inc' %]
814
                         [% INCLUDE 'ill-list-table.inc' %]
Lines 855-860 Link Here
855
                            </fieldset>
886
                            </fieldset>
856
                        </form>
887
                        </form>
857
                    </div>
888
                    </div>
889
                [% ELSIF query_type == 'batch_list' || query_type == 'batch_create' %]
890
                    [% INCLUDE 'ill-batch.inc' %]
858
                [% ELSE %]
891
                [% ELSE %]
859
                <!-- Custom Backend Action -->
892
                <!-- Custom Backend Action -->
860
                [% PROCESS $whole.template %]
893
                [% PROCESS $whole.template %]
Lines 874-879 Link Here
874
    [% INCLUDE 'columns_settings.inc' %]
907
    [% INCLUDE 'columns_settings.inc' %]
875
    [% INCLUDE 'calendar.inc' %]
908
    [% INCLUDE 'calendar.inc' %]
876
    [% INCLUDE 'select2.inc' %]
909
    [% INCLUDE 'select2.inc' %]
910
    [% IF metadata_enrichment_services %]
911
    <script>
912
        var metadata_enrichment_services = [% metadata_enrichment_services | $raw %];
913
    </script>
914
    <script>
915
        [% IF batch_availability_services %]
916
        var batch_availability_services = [% batch_availability_services | $raw %];
917
        [% ELSE %]
918
        var batch_availability_services = [];
919
        [% END %]
920
    </script>
921
    [% END %]
877
    <script>
922
    <script>
878
        var prefilters = '[% prefilters | $raw %]';
923
        var prefilters = '[% prefilters | $raw %]';
879
        // Set column settings
924
        // Set column settings
Lines 898-904 Link Here
898
        });
943
        });
899
    </script>
944
    </script>
900
    [% INCLUDE 'ill-list-table-strings.inc' %]
945
    [% INCLUDE 'ill-list-table-strings.inc' %]
946
    [% INCLUDE 'ill-batch-table-strings.inc' %]
947
    [% INCLUDE 'ill-batch-modal-strings.inc' %]
901
    [% Asset.js("js/ill-list-table.js") | $raw %]
948
    [% Asset.js("js/ill-list-table.js") | $raw %]
949
    [% Asset.js("js/ill-batch.js") | $raw %]
950
    [% Asset.js("js/ill-batch-table.js") | $raw %]
951
    [% Asset.js("js/ill-batch-modal.js") | $raw %]
902
    [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %]
952
    [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %]
903
        [% Asset.js("js/ill-availability.js") | $raw %]
953
        [% Asset.js("js/ill-availability.js") | $raw %]
904
    [% END %]
954
    [% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js (+1089 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
        var payload = {
310
            batch_id: batchId,
311
            ill_backend_id: batch.data.backend,
312
            patron_id: batch.data.patron.borrowernumber,
313
            library_id: batch.data.branchcode,
314
            extended_attributes: extended_attributes
315
        };
316
        window.doCreateSubmission(payload)
317
            .then(function (response) {
318
                return response.json();
319
            })
320
            .then(function (data) {
321
                tableContent.data = tableContent.data.map(function (row) {
322
                    if (row.value === identifier) {
323
                        row.requestId = data.ill_request_id;
324
                        row.requestStatus = data.status;
325
                    }
326
                    return row;
327
                });
328
            })
329
            .catch(function () {
330
                window.handleApiError(ill_batch_api_request_fail);
331
            });
332
    };
333
334
    function updateProcessTotals() {
335
        var init = {
336
            total: 0,
337
            count: 0,
338
            failed: 0
339
        };
340
        progressTotals.data = init;
341
        var toUpdate = progressTotals.data;
342
        tableContent.data.forEach(function (row) {
343
            toUpdate.total++;
344
            if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) {
345
                toUpdate.count++;
346
            }
347
            if (Object.keys(row.failed).length > 0) {
348
                toUpdate.failed++;
349
            }
350
        });
351
        createProgressTotal.innerHTML = toUpdate.total;
352
        createProgressCount.innerHTML = toUpdate.count;
353
        createProgressFailed.innerHTML = toUpdate.failed;
354
        var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100);
355
        createProgressBar.setAttribute('aria-valuenow', percentDone);
356
        createProgressBar.innerHTML = percentDone + '%';
357
        createProgressBar.style.width = percentDone + '%';
358
        progressTotals.data = toUpdate;
359
    };
360
361
    function displayPatronName() {
362
        var span = document.getElementById('patron_link');
363
        if (batch.data.patron) {
364
            var link = createPatronLink();
365
            span.appendChild(link);
366
        } else {
367
            if (span.children.length > 0) {
368
                span.removeChild(span.firstChild);
369
            }
370
        }
371
    };
372
373
    function updateStatusesSelect() {
374
        while (statusesSelect.options.length > 0) {
375
            statusesSelect.remove(0);
376
        }
377
        statuses.data.forEach(function (status) {
378
            var option = document.createElement('option')
379
            option.value = status.code;
380
            option.text = status.name;
381
            if (batch.data.id && batch.data.statuscode === status.code) {
382
                option.selected = true;
383
            }
384
            statusesSelect.add(option);
385
        });
386
        if (isUpdate) {
387
            statusesSelect.parentElement.style.display = 'block';
388
        }
389
    };
390
391
    function removeEventListeners() {
392
        textarea.removeEventListener('paste', processButtonState);
393
        textarea.removeEventListener('keyup', processButtonState);
394
        processButton.removeEventListener('click', processIdentifiers);
395
        nameInput.removeEventListener('keyup', createButtonState);
396
        cardnumberInput.removeEventListener('keyup', createButtonState);
397
        branchcodeSelect.removeEventListener('change', createButtonState);
398
        createButton.removeEventListener('click', createBatch);
399
        identifierTable.removeEventListener('click', toggleMetadata);
400
        identifierTable.removeEventListener('click', removeRow);
401
        createRequestsButton.remove('click', requestRequestable);
402
    };
403
404
    function finishButtonEventListener() {
405
        finishButton.addEventListener('click', doFinish);
406
    };
407
408
    function identifierTextareaEventListener() {
409
        textarea.addEventListener('paste', textareaUpdate);
410
        textarea.addEventListener('keyup', textareaUpdate);
411
    };
412
413
    function processButtonEventListener() {
414
        processButton.addEventListener('click', processIdentifiers);
415
    };
416
417
    function createRequestsButtonEventListener() {
418
        createRequestsButton.addEventListener('click', requestRequestable);
419
    };
420
421
    function createButtonEventListener() {
422
        createButton.addEventListener('click', createBatch);
423
    };
424
425
    function batchInputsEventListeners() {
426
        nameInput.addEventListener('keyup', createButtonState);
427
        cardnumberInput.addEventListener('keyup', createButtonState);
428
        branchcodeSelect.addEventListener('change', createButtonState);
429
    };
430
431
    function moreLessEventListener() {
432
        identifierTable.addEventListener('click', toggleMetadata);
433
    };
434
435
    function removeRowEventListener() {
436
        identifierTable.addEventListener('click', removeRow);
437
    };
438
439
    function textareaUpdate() {
440
        processButtonState();
441
        updateRowCount();
442
    };
443
444
    function processButtonState() {
445
        if (textarea.value.length > 0) {
446
            processButton.removeAttribute('disabled');
447
            processButton.removeAttribute('aria-disabled');
448
        } else {
449
            processButton.setAttribute('disabled', true);
450
            processButton.setAttribute('aria-disabled', true);
451
        }
452
    };
453
454
    function disableCardnumberInput() {
455
        if (batch.data.patron) {
456
            cardnumberInput.setAttribute('disabled', true);
457
            cardnumberInput.setAttribute('aria-disabled', true);
458
        } else {
459
            cardnumberInput.removeAttribute('disabled');
460
            cardnumberInput.removeAttribute('aria-disabled');
461
        }
462
    };
463
464
    function createButtonState() {
465
        if (
466
            nameInput.value.length > 0 &&
467
            cardnumberInput.value.length > 0 &&
468
            branchcodeSelect.selectedOptions.length === 1
469
        ) {
470
            createButton.removeAttribute('disabled');
471
            createButton.setAttribute('display', 'inline-block');
472
        } else {
473
            createButton.setAttribute('disabled', 1);
474
            createButton.setAttribute('display', 'none');
475
        }
476
    };
477
478
    function doFinish() {
479
        updateBatch()
480
            .then(function () {
481
                $('#ill-batch-modal').modal({ show: false });
482
                location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.id;
483
            });
484
    };
485
486
    // Get all batch statuses
487
    function fetchStatuses() {
488
        window.doApiRequest('/api/v1/illbatchstatuses')
489
            .then(function (response) {
490
                return response.json();
491
            })
492
            .then(function (jsoned) {
493
                statuses.data = jsoned;
494
            })
495
            .catch(function (e) {
496
                window.handleApiError(ill_batch_statuses_api_fail);
497
            });
498
    };
499
500
    // Get the batch
501
    function fetchBatch() {
502
        window.doBatchApiRequest("/" + batchId)
503
            .then(function (response) {
504
                return response.json();
505
            })
506
            .then(function (jsoned) {
507
                batch.data = {
508
                    id: jsoned.id,
509
                    name: jsoned.name,
510
                    backend: jsoned.backend,
511
                    cardnumber: jsoned.cardnumber,
512
                    branchcode: jsoned.branchcode,
513
                    statuscode: jsoned.statuscode
514
                }
515
                return jsoned;
516
            })
517
            .then(function (data) {
518
                batch.data = data;
519
            })
520
            .catch(function () {
521
                window.handleApiError(ill_batch_api_fail);
522
            });
523
    };
524
525
    function createBatch() {
526
        var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
527
        var selectedStatuscode = statusesSelect.selectedOptions[0].value;
528
        return doBatchApiRequest('', {
529
            method: 'POST',
530
            headers: {
531
                'Content-type': 'application/json'
532
            },
533
            body: JSON.stringify({
534
                name: nameInput.value,
535
                backend: backend,
536
                cardnumber: cardnumberInput.value,
537
                branchcode: selectedBranchcode,
538
                statuscode: selectedStatuscode
539
            })
540
        })
541
            .then(function (response) {
542
                if ( response.ok ) {
543
                    return response.json();
544
                }
545
                return Promise.reject(response);
546
            })
547
            .then(function (body) {
548
                batchId = body.id;
549
                batch.data = {
550
                    id: body.id,
551
                    name: body.name,
552
                    backend: body.backend,
553
                    cardnumber: body.patron.cardnumber,
554
                    branchcode: body.branchcode,
555
                    statuscode: body.statuscode,
556
                    patron: body.patron,
557
                    status: body.status
558
                };
559
                initPostCreate();
560
            })
561
            .catch(function (response) {
562
                response.json().then((json) => {
563
                    if( json.error ) {
564
                        handleApiError(json.error);
565
                    } else {
566
                        handleApiError(ill_batch_create_api_fail);
567
                    }
568
                })
569
            });
570
    };
571
572
    function updateBatch() {
573
        var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
574
        var selectedStatuscode = statusesSelect.selectedOptions[0].value;
575
        return doBatchApiRequest('/' + batch.data.id, {
576
            method: 'PUT',
577
            headers: {
578
                'Content-type': 'application/json'
579
            },
580
            body: JSON.stringify({
581
                name: nameInput.value,
582
                backend: batch.data.backend,
583
                cardnumber: batch.data.patron.cardnumber,
584
                branchcode: selectedBranchcode,
585
                statuscode: selectedStatuscode
586
            })
587
        })
588
            .catch(function () {
589
                handleApiError(ill_batch_update_api_fail);
590
            });
591
    };
592
593
    function displaySupportedIdentifiers() {
594
        var names = Object.keys(supportedIdentifiers).map(function (identifier) {
595
            return window['ill_batch_' + identifier];
596
        });
597
        var displayEl = document.getElementById('supported_identifiers');
598
        displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none;
599
    }
600
601
    function updateRowCount() {
602
        var textEl = document.getElementById('row_count_value');
603
        var val = textarea.value.trim();
604
        var cnt = 0;
605
        if (val.length > 0) {
606
            cnt = val.split(/\n/).length;
607
        }
608
        textEl.textContent = cnt;
609
    }
610
611
    function showProgress() {
612
        var el = document.getElementById('create-progress');
613
        el.style.display = 'block';
614
    }
615
616
    function showCreateRequestsButton() {
617
        var data = progressTotals.data;
618
        var el = document.getElementById('create-requests');
619
        el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none';
620
    }
621
622
    async function processIdentifiers() {
623
        var content = textarea.value;
624
        hideErrors();
625
        if (content.length === 0) return;
626
627
        disableProcessButton();
628
        var label = document.getElementById('progress-label').firstChild;
629
        label.innerHTML = ill_batch_retrieving_metadata;
630
        showProgress();
631
632
        // Errors encountered when processing
633
        var processErrors = {};
634
635
        // Prepare the content, including trimming each row
636
        var contentArr = content.split(/\n/);
637
        var trimmed = contentArr.map(function (row) {
638
            return row.trim();
639
        });
640
641
        var parsed = [];
642
643
        trimmed.forEach(function (identifier) {
644
            var match = identifyIdentifier(identifier);
645
            // If this identifier is not identifiable or
646
            // looks like more than one type, we can't be sure
647
            // what it is
648
            if (match.length != 1) {
649
                parsed.push({
650
                    type: 'unknown',
651
                    value: identifier
652
                });
653
            } else {
654
                parsed.push(match[0]);
655
            }
656
        });
657
658
        var unknownIdentifiers = parsed
659
            .filter(function (parse) {
660
                if (parse.type == 'unknown') {
661
                    return parse;
662
                }
663
            })
664
            .map(function (filtered) {
665
                return filtered.value;
666
            });
667
668
        if (unknownIdentifiers.length > 0) {
669
            processErrors.badidentifiers = {
670
                element: 'badids',
671
                values: unknownIdentifiers.join(', ')
672
            };
673
        };
674
675
        // Deduping
676
        var deduped = [];
677
        var dupes = {};
678
        parsed.forEach(function (row) {
679
            var value = row.value;
680
            var alreadyInDeduped = deduped.filter(function (d) {
681
                return d.value === value;
682
            });
683
            if (alreadyInDeduped.length > 0 && !dupes[value]) {
684
                dupes[value] = 1;
685
            } else if (alreadyInDeduped.length === 0) {
686
                row.metadata = {};
687
                row.failed = {};
688
                row.requestId = null;
689
                deduped.push(row);
690
            }
691
        });
692
        // Update duplicate error if dupes were found
693
        if (Object.keys(dupes).length > 0) {
694
            processErrors.duplicates = {
695
                element: 'dupelist',
696
                values: Object.keys(dupes).join(', ')
697
            };
698
        }
699
700
        // Display any errors
701
        displayErrors(processErrors);
702
703
        // Now build and display the table
704
        if (!table) {
705
            buildTable();
706
        }
707
708
        // We may be appending new values to an existing table,
709
        // in which case, ensure we don't create duplicates
710
        var tabIdentifiers = tableContent.data.map(function (tabId) {
711
            return tabId.value;
712
        });
713
        var notInTable = deduped.filter(function (ded) {
714
            if (!tabIdentifiers.includes(ded.value)) {
715
                return ded;
716
           }
717
        });
718
        if (notInTable.length > 0) {
719
            tableContent.data = tableContent.data.concat(notInTable);
720
        }
721
722
        // Populate metadata for those records that need it
723
        var newData = tableContent.data;
724
        for (var i = 0; i < tableContent.data.length; i++) {
725
            var row = tableContent.data[i];
726
            // Skip rows that don't need populating
727
            if (
728
                Object.keys(tableContent.data[i].metadata).length > 0 ||
729
                Object.keys(tableContent.data[i].failed).length > 0
730
            ) continue;
731
            var identifier = { type: row.type, value: row.value };
732
            try {
733
                var populated = await populateMetadata(identifier);
734
                row.metadata = populated.results.result || {};
735
            } catch (e) {
736
                row.failed = ill_populate_failed;
737
            }
738
            newData[i] = row;
739
            tableContent.data = newData;
740
        }
741
    }
742
743
    function disableProcessButton() {
744
        processButton.setAttribute('disabled', true);
745
        processButton.setAttribute('aria-disabled', true);
746
    }
747
748
    function disableCreateButton() {
749
        createButton.setAttribute('disabled', true);
750
        createButton.setAttribute('aria-disabled', true);
751
    }
752
753
    async function populateMetadata(identifier) {
754
        // All services that support this identifier type
755
        var services = supportedIdentifiers[identifier.type];
756
        // Check each service and use the first results we get, if any
757
        for (var i = 0; i < services.length; i++) {
758
            var service = services[i];
759
            var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value;
760
            var metadata = await getMetadata(endpoint);
761
            if (metadata.errors.length === 0) {
762
                var parsed = await parseMetadata(metadata, service);
763
                if (parsed.errors.length > 0) {
764
                    throw Error(metadata.errors.map(function (error) {
765
                        return error.message;
766
                    }).join(', '));
767
                }
768
                return parsed;
769
            }
770
        }
771
    };
772
773
    async function getMetadata(endpoint) {
774
        var response = await debounce(doApiRequest)(endpoint);
775
        return response.json();
776
    };
777
778
    async function parseMetadata(metadata, service) {
779
        var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint;
780
        var response = await doApiRequest(endpoint, {
781
            method: 'POST',
782
            headers: {
783
                'Content-type': 'application/json'
784
            },
785
            body: JSON.stringify(metadata)
786
        });
787
        return response.json();
788
    }
789
790
    // A render function for identifier type
791
    function createIdentifierType(data) {
792
        return window['ill_batch_' + data];
793
    };
794
795
    // Get an item's title
796
    function getTitle(meta) {
797
        if (meta.article_title && meta.article_title.length > 0) {
798
            return 'article_title';
799
            return {
800
                prop: 'article_title',
801
                value: meta.article_title
802
            };
803
        } else if (meta.title && meta.title.length > 0) {
804
            return 'title';
805
            return {
806
                prop: 'title',
807
                value: meta.title
808
            };
809
        }
810
    };
811
812
    // Create a metadata row
813
    function createMetadataRow(data, meta, prop) {
814
        if (!meta[prop]) return;
815
816
        var div = document.createElement('div');
817
        div.classList.add('metadata-row');
818
        var label = document.createElement('span');
819
        label.classList.add('metadata-label');
820
        label.innerText = ill_batch_metadata[prop] + ': ';
821
822
        // Add a link to the availability URL if appropriate
823
        var value;
824
        if (!data.url) {
825
            value = document.createElement('span');
826
        } else {
827
            value = document.createElement('a');
828
            value.setAttribute('href', data.url);
829
            value.setAttribute('target', '_blank');
830
            value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier);
831
        }
832
        value.classList.add('metadata-value');
833
        value.innerText = meta[prop];
834
        div.appendChild(label);
835
        div.appendChild(value);
836
837
        return div;
838
    }
839
840
    // A render function for displaying metadata
841
    function createMetadata(x, y, data) {
842
        // If the fetch failed
843
        if (data.failed.length > 0) {
844
            return data.failed;
845
        }
846
847
        // If we've not yet got any metadata back
848
        if (Object.keys(data.metadata).length === 0) {
849
            return ill_populate_waiting;
850
        }
851
852
        var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume'];
853
        var meta = data.metadata;
854
855
        var container = document.createElement('div');
856
        container.classList.add('metadata-container');
857
858
        // Create the title row
859
        var title = getTitle(meta);
860
        if (title) {
861
            // Remove the title element from the props
862
            // we're about to iterate
863
            core = core.filter(function (i) {
864
                return i !== title;
865
            });
866
            var titleRow = createMetadataRow(data, meta, title);
867
            container.appendChild(titleRow);
868
        }
869
870
        var remainder = document.createElement('div');
871
        remainder.classList.add('metadata-remainder');
872
        remainder.style.display = 'none';
873
        // Create the remaining rows
874
        core.sort().forEach(function (prop) {
875
            var div = createMetadataRow(data, meta, prop);
876
            if (div) {
877
                remainder.appendChild(div);
878
            }
879
        });
880
        container.appendChild(remainder);
881
882
        // Add a more/less toggle
883
        var firstField = container.firstChild;
884
        var moreLess = document.createElement('div');
885
        moreLess.classList.add('more-less');
886
        var moreLessLink = document.createElement('a');
887
        moreLessLink.setAttribute('href', '#');
888
        moreLessLink.classList.add('more-less-link');
889
        moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']';
890
        moreLess.appendChild(moreLessLink);
891
        firstField.appendChild(moreLess);
892
893
        return container.outerHTML;
894
    };
895
896
    function removeRow(ev) {
897
        if (ev.target.className.includes('remove-row')) {
898
            if (!confirm(ill_batch_item_remove)) return;
899
            // Find the parent row
900
            var ancestor = ev.target.closest('tr');
901
            var identifier = ancestor.querySelector('.identifier').innerText;
902
            tableContent.data = tableContent.data.filter(function (row) {
903
                return row.value !== identifier;
904
            });
905
        }
906
    }
907
908
    function toggleMetadata(ev) {
909
        if (ev.target.className === 'more-less-link') {
910
            // Find the element we need to show
911
            var ancestor = ev.target.closest('.metadata-container');
912
            var meta = ancestor.querySelector('.metadata-remainder');
913
914
            // Display or hide based on its current state
915
            var display = window.getComputedStyle(meta).display;
916
917
            meta.style.display = display === 'block' ? 'none' : 'block';
918
919
            // Update the More / Less text
920
            ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]';
921
        }
922
    }
923
924
    // A render function for the link to a request ID
925
    function createRequestId(x, y, data) {
926
        return data.requestId || '-';
927
    }
928
929
    function createRequestStatus(x, y, data) {
930
        return data.requestStatus || '-';
931
    }
932
933
    function buildTable(identifiers) {
934
        table = KohaTable('identifier-table', {
935
            processing: true,
936
            deferRender: true,
937
            ordering: false,
938
            paging: false,
939
            searching: false,
940
            autoWidth: false,
941
            columns: [
942
                {
943
                    data: 'type',
944
                    width: '13%',
945
                    render: createIdentifierType
946
                },
947
                {
948
                    data: 'value',
949
                    width: '25%',
950
                    className: 'identifier'
951
                },
952
                {
953
                    data: 'metadata',
954
                    render: createMetadata
955
                },
956
                {
957
                    data: 'requestId',
958
                    width: '6.5%',
959
                    render: createRequestId
960
                },
961
                {
962
                    data: 'requestStatus',
963
                    width: '6.5%',
964
                    render: createRequestStatus
965
                },
966
                {
967
                    width: '18%',
968
                    render: createActions,
969
                    className: 'action-column'
970
                }
971
            ],
972
            createdRow: function (row, data) {
973
                if (data.failed.length > 0) {
974
                    row.classList.add('fetch-failed');
975
                }
976
            }
977
        });
978
    }
979
980
    function createActions(x, y, data) {
981
        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>';
982
    }
983
984
    // Redraw the table
985
    function updateTable() {
986
        if (!table) return;
987
        tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none';
988
        tableEl.style.width = '100%';
989
        table.api()
990
            .clear()
991
            .rows.add(tableContent.data)
992
            .draw();
993
    };
994
995
    function identifyIdentifier(identifier) {
996
        var matches = [];
997
998
        // Iterate our available services to see if any can identify this identifier
999
        Object.keys(supportedIdentifiers).forEach(function (identifierType) {
1000
            // Since all the services supporting this identifier type should use the same
1001
            // regex to identify it, we can just use the first
1002
            var service = supportedIdentifiers[identifierType][0];
1003
            var regex = new RegExp(service.identifiers_supported[identifierType].regex);
1004
            var match = identifier.match(regex);
1005
            if (match && match.groups && match.groups.identifier) {
1006
                matches.push({
1007
                    type: identifierType,
1008
                    value: match.groups.identifier
1009
                });
1010
            }
1011
        });
1012
        return matches;
1013
    }
1014
1015
    function displayErrors(errors) {
1016
        var keys = Object.keys(errors);
1017
        if (keys.length > 0) {
1018
            keys.forEach(function (key) {
1019
                var el = document.getElementById(errors[key].element);
1020
                el.textContent = errors[key].values;
1021
                el.style.display = 'inline';
1022
                var container = document.getElementById(key);
1023
                container.style.display = 'block';
1024
            });
1025
            var el = document.getElementById('textarea-errors');
1026
            el.style.display = 'flex';
1027
        }
1028
    }
1029
1030
    function hideErrors() {
1031
        var dupelist = document.getElementById('dupelist');
1032
        var badids = document.getElementById('badids');
1033
        dupelist.textContent = '';
1034
        dupelist.parentElement.style.display = 'none';
1035
        badids.textContent = '';
1036
        badids.parentElement.style.display = 'none';
1037
        var tae = document.getElementById('textarea-errors');
1038
        tae.style.display = 'none';
1039
    }
1040
1041
    function manageBatchItemsDisplay() {
1042
        batchItemsDisplay.style.display = batch.data.id ? 'block' : 'none'
1043
    };
1044
1045
    function updateBatchInputs() {
1046
        nameInput.value = batch.data.name || '';
1047
        cardnumberInput.value = batch.data.cardnumber || '';
1048
        branchcodeSelect.value = batch.data.branchcode || '';
1049
    }
1050
1051
    function debounce(func) {
1052
        var timeout;
1053
        return function (...args) {
1054
            return new Promise(function (resolve) {
1055
                if (timeout) {
1056
                    clearTimeout(timeout);
1057
                }
1058
                timeout = setTimeout(function () {
1059
                    return resolve(func(...args));
1060
                }, debounceDelay);
1061
            });
1062
        }
1063
    }
1064
1065
    function patronAutocomplete() {
1066
        patron_autocomplete(
1067
            $('#batch-form #batchcardnumber'),
1068
            {
1069
              'on-select-callback': function( event, ui ) {
1070
                $("#batch-form #batchcardnumber").val( ui.item.cardnumber );
1071
                return false;
1072
              }
1073
            }
1074
          );
1075
    };
1076
1077
    function createPatronLink() {
1078
        if (!batch.data.patron) return;
1079
        var patron = batch.data.patron;
1080
        var a = document.createElement('a');
1081
        var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber;
1082
        var text = patron.surname + ' (' + patron.cardnumber + ')';
1083
        a.setAttribute('title', ill_borrower_details);
1084
        a.setAttribute('href', href);
1085
        a.textContent = text;
1086
        return a;
1087
    };
1088
1089
})();
(-)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 / +20 lines)
Lines 51-57 $(document).ready(function() { Link Here
51
        return '';
51
        return '';
52
    }
52
    }
53
53
54
    // At the moment, the only prefilter possible is borrowernumber
54
    // Possible prefilters: borrowernumber, batch_id
55
    // see ill/ill-requests.pl and members/ill-requests.pl
55
    // see ill/ill-requests.pl and members/ill-requests.pl
56
    let additional_prefilters = [];
56
    let additional_prefilters = [];
57
    if(prefilters){
57
    if(prefilters){
Lines 80-85 $(document).ready(function() { Link Here
80
        "me.borrowernumber": function(){
80
        "me.borrowernumber": function(){
81
            return (borrower_prefilter_value = get_prefilter_value('borrowernumber')) ? { "=": borrower_prefilter_value } : "";
81
            return (borrower_prefilter_value = get_prefilter_value('borrowernumber')) ? { "=": borrower_prefilter_value } : "";
82
        },
82
        },
83
        "me.batch_id": function(){
84
            return (batch_prefilter_value = get_prefilter_value('batch_id')) ? { "=": batch_prefilter_value } : "";
85
        },
83
        "-or": function(){
86
        "-or": function(){
84
            let patron = $("#illfilter_patron").val();
87
            let patron = $("#illfilter_patron").val();
85
            let status = $("#illfilter_status").val();
88
            let status = $("#illfilter_status").val();
Lines 179-184 $(document).ready(function() { Link Here
179
    let table_id = "#ill-requests";
182
    let table_id = "#ill-requests";
180
    if (borrower_prefilter_value = get_prefilter_value('borrowernumber')) {
183
    if (borrower_prefilter_value = get_prefilter_value('borrowernumber')) {
181
        table_id += "-patron-" + borrower_prefilter_value;
184
        table_id += "-patron-" + borrower_prefilter_value;
185
    } else if (batch_id_prefilter_value = get_prefilter_value('batch_id')) {
186
        table_id += "-batch-" + batch_id_prefilter_value;
182
    }
187
    }
183
188
184
    var ill_requests_table = $(table_id).kohaTable({
189
    var ill_requests_table = $(table_id).kohaTable({
Lines 190-195 $(document).ready(function() { Link Here
190
            'biblio',
195
            'biblio',
191
            'comments+count',
196
            'comments+count',
192
            'extended_attributes',
197
            'extended_attributes',
198
            'batch',
193
            'library',
199
            'library',
194
            'id_prefix',
200
            'id_prefix',
195
            'patron'
201
            'patron'
Lines 207-212 $(document).ready(function() { Link Here
207
                            '">' + escape_str(row.id_prefix) + escape_str(data) + '</a>';
213
                            '">' + escape_str(row.id_prefix) + escape_str(data) + '</a>';
208
                }
214
                }
209
            },
215
            },
216
            {
217
                "data": "batch.name", // batch
218
                "orderable": false,
219
                "render": function(data, type, row, meta) {
220
                    return row.batch ?
221
                        '<a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=' +
222
                        row.batch_id +
223
                        '">' +
224
                        row.batch.name +
225
                        '</a>'
226
                        : "";
227
                }
228
            },
210
            {
229
            {
211
                "data": "", // author
230
                "data": "", // author
212
                "orderable": false,
231
                "orderable": false,
213
- 

Return to bug 30719