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

(-)a/Koha/Illrequest.pm (-3 lines)
Lines 156-164 sub batch { Link Here
156
    my ( $self ) = @_;
156
    my ( $self ) = @_;
157
157
158
    return Koha::Illbatches->find($self->_result->batch_id);
158
    return Koha::Illbatches->find($self->_result->batch_id);
159
#    return Koha::Illbatch->_new_from_dbic(
160
#        scalar $self->_result->batch_id
161
#    );
162
}
159
}
163
160
164
=head3 statusalias
161
=head3 statusalias
(-)a/Koha/REST/V1/Illrequests.pm (-12 / +95 lines)
Lines 22-27 use Mojo::Base 'Mojolicious::Controller'; Link Here
22
use C4::Context;
22
use C4::Context;
23
use Koha::Illrequests;
23
use Koha::Illrequests;
24
use Koha::Illrequestattributes;
24
use Koha::Illrequestattributes;
25
use Koha::Illbatches;
25
use Koha::Libraries;
26
use Koha::Libraries;
26
use Koha::Patrons;
27
use Koha::Patrons;
27
use Koha::Libraries;
28
use Koha::Libraries;
Lines 43-50 sub list { Link Here
43
    my $c = shift->openapi->valid_input or return;
44
    my $c = shift->openapi->valid_input or return;
44
45
45
    my $args = $c->req->params->to_hash // {};
46
    my $args = $c->req->params->to_hash // {};
46
    my $output = [];
47
47
    my @format_dates = ( 'placed', 'updated', 'completed' );
48
    # Get the pipe-separated string of hidden ILL statuses
49
    my $hidden_statuses_string = C4::Context->preference('ILLHiddenRequestStatuses') // q{};
50
    # Turn into arrayref
51
    my $hidden_statuses = [ split /\|/, $hidden_statuses_string ];
48
52
49
    # Create a hash where all keys are embedded values
53
    # Create a hash where all keys are embedded values
50
    # Enables easy checking
54
    # Enables easy checking
Lines 55-65 sub list { Link Here
55
        delete $args->{embed};
59
        delete $args->{embed};
56
    }
60
    }
57
61
58
    # Get the pipe-separated string of hidden ILL statuses
59
    my $hidden_statuses_string = C4::Context->preference('ILLHiddenRequestStatuses') // q{};
60
    # Turn into arrayref
61
    my $hidden_statuses = [ split /\|/, $hidden_statuses_string ];
62
63
    # Get all requests
62
    # Get all requests
64
    # If necessary, restrict the resultset
63
    # If necessary, restrict the resultset
65
    my @requests = Koha::Illrequests->search({
64
    my @requests = Koha::Illrequests->search({
Lines 74-79 sub list { Link Here
74
        : ()
73
        : ()
75
    })->as_list;
74
    })->as_list;
76
75
76
    my $output = _form_request(\@requests, \%embed);
77
78
    return $c->render( status => 200, openapi => $output );
79
}
80
81
=head3 add
82
83
Adds a new ILL request
84
85
=cut
86
87
sub add {
88
    my $c = shift->openapi->valid_input or return;
89
90
    my $body = $c->validation->param('body');
91
92
    return try {
93
        my $request = Koha::Illrequest->new->load_backend( $body->{backend} );
94
95
        my $create_api = $request->_backend->capabilities('create_api');
96
97
        if (!$create_api) {
98
            return $c->render(
99
                status  => 405,
100
                openapi => {
101
                    errors => [ 'This backend does not allow request creation via API' ]
102
                }
103
            );
104
        }
105
106
        my $create_result = &{$create_api}($body, $request);
107
        my $new_id = $create_result->illrequest_id;
108
109
        my @new_req = Koha::Illrequests->search({
110
            illrequest_id => $new_id
111
        })->as_list;
112
113
        my $output = _form_request(\@new_req, {
114
            metadata           => 1,
115
            patron             => 1,
116
            library            => 1,
117
            status_alias       => 1,
118
            comments           => 1,
119
            requested_partners => 1
120
        });
121
122
        return $c->render(
123
            status  => 201,
124
            openapi => $output->[0]
125
        );
126
    } catch {
127
        return $c->render(
128
            status => 500,
129
            openapi => { error => 'Unable to create request' }
130
        )
131
    };
132
}
133
134
sub _form_request {
135
    my ($requests_hash, $embed_hash) = @_;
136
137
    my @requests = @{$requests_hash};
138
    my %embed = %{$embed_hash};
139
140
    my $output = [];
141
    my @format_dates = ( 'placed', 'updated', 'completed' );
142
77
    my $fetch_backends = {};
143
    my $fetch_backends = {};
78
    foreach my $request (@requests) {
144
    foreach my $request (@requests) {
79
        $fetch_backends->{ $request->backend } ||=
145
        $fetch_backends->{ $request->backend } ||=
Lines 83-99 sub list { Link Here
83
    # Pre-load the backend object to avoid useless backend lookup/loads
149
    # Pre-load the backend object to avoid useless backend lookup/loads
84
    @requests = map { $_->_backend( $fetch_backends->{ $_->backend } ); $_ } @requests;
150
    @requests = map { $_->_backend( $fetch_backends->{ $_->backend } ); $_ } @requests;
85
151
86
    # Identify patrons & branches that
152
    # Identify additional stuff that
87
    # we're going to need and get them
153
    # we're going to need and get them
88
    my $to_fetch = {
154
    my $to_fetch = {
89
        patrons      => {},
155
        patrons      => {},
90
        branches     => {},
156
        branches     => {},
91
        capabilities => {}
157
        capabilities => {},
158
        batches      => {}
92
    };
159
    };
93
    foreach my $req (@requests) {
160
    foreach my $req (@requests) {
94
        $to_fetch->{patrons}->{$req->borrowernumber} = 1 if $embed{patron};
161
        $to_fetch->{patrons}->{$req->borrowernumber} = 1 if $embed{patron};
95
        $to_fetch->{branches}->{$req->branchcode} = 1 if $embed{library};
162
        $to_fetch->{branches}->{$req->branchcode} = 1 if $embed{library};
96
        $to_fetch->{capabilities}->{$req->backend} = 1 if $embed{capabilities};
163
        $to_fetch->{capabilities}->{$req->backend} = 1 if $embed{capabilities};
164
        $to_fetch->{batches}->{$req->batch_id} = 1 if $req->batch_id;
97
    }
165
    }
98
166
99
    # Fetch the patrons we need
167
    # Fetch the patrons we need
Lines 130-136 sub list { Link Here
130
        }
198
        }
131
    }
199
    }
132
200
133
    # Now we've got all associated users and branches,
201
    # Fetch the batches we need
202
    my $batch_arr = [];
203
    my @batch_ids = keys %{$to_fetch->{batches}};
204
    if (scalar @batch_ids > 0) {
205
        my $where = {
206
            id => { -in => \@batch_ids }
207
        };
208
        $batch_arr = Koha::Illbatches->search($where)->unblessed;
209
    }
210
211
    # Now we've got all associated stuff
134
    # we can augment the request objects
212
    # we can augment the request objects
135
    my @output = ();
213
    my @output = ();
136
    foreach my $req(@requests) {
214
    foreach my $req(@requests) {
Lines 166-171 sub list { Link Here
166
                last;
244
                last;
167
            }
245
            }
168
        }
246
        }
247
        foreach my $b(@{$batch_arr}) {
248
            if ($b->{id} eq $req->batch_id) {
249
                $to_push->{batch} = $b;
250
                last;
251
            }
252
        }
169
        if ($embed{metadata}) {
253
        if ($embed{metadata}) {
170
            my $metadata = Koha::Illrequestattributes->search(
254
            my $metadata = Koha::Illrequestattributes->search(
171
                { illrequest_id => $req->illrequest_id },
255
                { illrequest_id => $req->illrequest_id },
Lines 191-198 sub list { Link Here
191
        }
275
        }
192
        push @output, $to_push;
276
        push @output, $to_push;
193
    }
277
    }
194
278
    return \@output;
195
    return $c->render( status => 200, openapi => \@output );
196
}
279
}
197
280
198
1;
281
1;
(-)a/admin/columns_settings.yml (+2 lines)
Lines 802-807 modules: Link Here
802
        columns:
802
        columns:
803
            -
803
            -
804
              columnname: illrequest_id
804
              columnname: illrequest_id
805
            -
806
              columnname: batch
805
            -
807
            -
806
              columnname: metadata_author
808
              columnname: metadata_author
807
            -
809
            -
(-)a/api/v1/swagger/definitions/illrequest.yaml (+144 lines)
Line 0 Link Here
1
---
2
type: object
3
properties:
4
  accessurl:
5
    type:
6
      - string
7
      - "null"
8
    description: A URL for accessing the resource
9
  backend:
10
    type: string
11
    description: Name of the request's ILL supplier
12
  batch_id:
13
    type:
14
      - string
15
      - "null"
16
    description: The ID of the batch the request belongs to
17
  batch:
18
    type:
19
      - object
20
      - "null"
21
    description: The batch the request belongs to
22
  biblio_id:
23
    type:
24
      - string
25
      - "null"
26
    description: The ID of the biblio created from the requested item
27
  borrowernumber:
28
    type: string
29
    description: Borrower number of the patron of the ILL request
30
  branchcode:
31
    type: string
32
    description: The branch code associated with the request
33
  capabilities:
34
    type:
35
      - object
36
      - "null"
37
    description: A list of the valid actions that can be carried out on this request
38
  comments:
39
    type:
40
      - string
41
      - "null"
42
    description: The number of comments this request has associated with it
43
  completed:
44
    type:
45
      - string
46
      - "null"
47
    description: Is this request completed
48
  cost:
49
    type:
50
      - string
51
      - "null"
52
    description: The recorded cost for this request
53
  id_prefix:
54
    type:
55
      - string
56
      - "null"
57
    description: A prefix that should be prepended to the ID of this request during display
58
  illrequest_id:
59
    type:
60
      - string
61
      - "null"
62
    description: The ID of this request
63
  library:
64
    type:
65
      - object
66
      - "null"
67
    description: The library object associated with this request
68
  medium:
69
    type:
70
      - string
71
      - "null"
72
    description: The material type associated with this request
73
  metadata:
74
    type: object
75
    description: Metadata that formed this request
76
  notesopac:
77
    type:
78
      - string
79
      - "null"
80
    description: Notes that have been entered for display in the OPAC
81
  notesstaff:
82
    type:
83
      - string
84
      - "null"
85
    description: Notes that have been entered for display in the staff interface
86
  orderid:
87
    type:
88
      - string
89
      - "null"
90
    description: The supplier's ID for the request
91
  patron:
92
    type:
93
      - object
94
      - "null"
95
    description: The patron associated with this request
96
  placed:
97
    type:
98
      - string
99
      - "null"
100
    description: The timestamp the request was placed
101
  placed_formatted:
102
    type:
103
      - string
104
      - "null"
105
    description: The timestamp the request was placed (formatted for display)
106
  completed_formatted:
107
    type:
108
      - string
109
      - "null"
110
    description: The timestamp the request was completed (formatted for display)
111
  price_paid:
112
    type:
113
      - string
114
      - "null"
115
    description: The amount ultimately paid for the request
116
  replied:
117
    type:
118
      - string
119
      - "null"
120
    description: N/A (Not in use)
121
  requested_partners:
122
    type:
123
      - string
124
      - "null"
125
    description: The email addresses of partners this request been requested from
126
  status:
127
    type: string
128
    description: The request's current status
129
  status_alias:
130
    type:
131
      - string
132
      - "null"
133
    description: The ID of a user defined status for this request
134
  updated:
135
    type:
136
      - string
137
      - "null"
138
    description: The timestamp the request was last updated
139
  updated_formatted:
140
    type:
141
      - string
142
      - "null"
143
    description: The timestamp the request was last updated (formatted for display)
144
additionalProperties: false
(-)a/api/v1/swagger/definitions/illrequests.yaml (+5 lines)
Line 0 Link Here
1
---
2
type: array
3
items:
4
  $ref: "illrequest.yaml"
5
additionalProperties: false
(-)a/api/v1/swagger/paths/illrequests.yaml (+52 lines)
Lines 108-113 Link Here
108
    responses:
108
    responses:
109
      "200":
109
      "200":
110
        description: A list of ILL requests
110
        description: A list of ILL requests
111
        schema:
112
          $ref: "../swagger.yaml#/definitions/illrequests"
111
      "401":
113
      "401":
112
        description: Authentication required
114
        description: Authentication required
113
        schema:
115
        schema:
Lines 134-136 Link Here
134
    x-koha-authorization:
136
    x-koha-authorization:
135
      permissions:
137
      permissions:
136
        ill: "1"
138
        ill: "1"
139
  post:
140
    x-mojo-to: Illrequests#add
141
    operationId: addIllrequest
142
    tags:
143
      - illrequests
144
    summary: Add ILL request
145
    parameters:
146
      - name: body
147
        in: body
148
        description: A JSON object containing informations about the new request
149
        required: true
150
        schema:
151
          $ref: "../swagger.yaml#/definitions/illrequest"
152
    produces:
153
      - application/json
154
    responses:
155
      "201":
156
        description: Request added
157
        schema:
158
          $ref: "../swagger.yaml#/definitions/illrequest"
159
      "400":
160
        description: Bad request
161
        schema:
162
          $ref: "../swagger.yaml#/definitions/error"
163
      "401":
164
        description: Authentication required
165
        schema:
166
          $ref: "../swagger.yaml#/definitions/error"
167
      "403":
168
        description: Access forbidden
169
        schema:
170
          $ref: "../swagger.yaml#/definitions/error"
171
      "409":
172
        description: Conflict in creating resource
173
        schema:
174
          $ref: "../swagger.yaml#/definitions/error"
175
      "500":
176
        description: |
177
          Internal server error. Possible `error_code` attribute values:
178
179
          * `internal_server_error`
180
        schema:
181
          $ref: "../swagger.yaml#/definitions/error"
182
      "503":
183
        description: Under maintenance
184
        schema:
185
          $ref: "../swagger.yaml#/definitions/error"
186
    x-koha-authorization:
187
      permissions:
188
        ill: "1"
(-)a/api/v1/swagger/swagger.yaml (+4 lines)
Lines 52-57 definitions: Link Here
52
    $ref: ./definitions/illbatch.yaml
52
    $ref: ./definitions/illbatch.yaml
53
  illbatches:
53
  illbatches:
54
    $ref: ./definitions/illbatches.yaml
54
    $ref: ./definitions/illbatches.yaml
55
  illrequest:
56
    $ref: ./definitions/illrequest.yaml
57
  illrequests:
58
    $ref: ./definitions/illrequests.yaml
55
  import_batch_profile:
59
  import_batch_profile:
56
    $ref: ./definitions/import_batch_profile.yaml
60
    $ref: ./definitions/import_batch_profile.yaml
57
  import_batch_profiles:
61
  import_batch_profiles:
(-)a/ill/ill-requests.pl (-5 / +98 lines)
Lines 27-35 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::Availability;
31
use Koha::Illrequest::Availability;
31
use Koha::Libraries;
32
use Koha::Libraries;
32
use Koha::Token;
33
use Koha::Token;
34
use Koha::Plugins;
33
35
34
use Try::Tiny qw( catch try );
36
use Try::Tiny qw( catch try );
35
use URI::Escape qw( uri_escape_utf8 );
37
use URI::Escape qw( uri_escape_utf8 );
Lines 65-74 my $has_branch = $cfg->has_branch; Link Here
65
my $backends_available = ( scalar @{$backends} > 0 );
67
my $backends_available = ( scalar @{$backends} > 0 );
66
$template->param(
68
$template->param(
67
    backends_available => $backends_available,
69
    backends_available => $backends_available,
68
    has_branch         => $has_branch
70
    has_branch         => $has_branch,
71
    have_batch         => have_batch_backends($backends)
69
);
72
);
70
73
71
if ( $backends_available ) {
74
if ( $backends_available ) {
75
    # Establish what metadata enrichment plugins we have available
76
    my $enrichment_services = get_metadata_enrichment();
77
    if (scalar @{$enrichment_services} > 0) {
78
        $template->param(
79
            metadata_enrichment_services => encode_json($enrichment_services)
80
        );
81
    }
82
    # Establish whether we have any availability services that can provide availability
83
    # for the batch identifier types we support
84
    my $batch_availability_services = get_ill_availability($enrichment_services);
85
    if (scalar @{$batch_availability_services} > 0) {
86
        $template->param(
87
            batch_availability_services => encode_json($batch_availability_services)
88
        );
89
    }
90
72
    if ( $op eq 'illview' ) {
91
    if ( $op eq 'illview' ) {
73
        # View the details of an ILL
92
        # View the details of an ILL
74
        my $request = Koha::Illrequests->find($params->{illrequest_id});
93
        my $request = Koha::Illrequests->find($params->{illrequest_id});
Lines 147-154 if ( $backends_available ) { Link Here
147
        } else {
166
        } else {
148
            my $backend_result = $request->backend_create($params);
167
            my $backend_result = $request->backend_create($params);
149
            $template->param(
168
            $template->param(
150
                whole   => $backend_result,
169
                whole     => $backend_result,
151
                request => $request
170
                request   => $request
152
            );
171
            );
153
            handle_commit_maybe($backend_result, $request);
172
            handle_commit_maybe($backend_result, $request);
154
        }
173
        }
Lines 215-220 if ( $backends_available ) { Link Here
215
        # We simulate the API for backend requests for uniformity.
234
        # We simulate the API for backend requests for uniformity.
216
        # So, init:
235
        # So, init:
217
        my $request = Koha::Illrequests->find($params->{illrequest_id});
236
        my $request = Koha::Illrequests->find($params->{illrequest_id});
237
        my $batches = Koha::Illbatches->search(undef, {
238
            order_by => { -asc => 'name' }
239
        });
218
        if ( !$params->{stage} ) {
240
        if ( !$params->{stage} ) {
219
            my $backend_result = {
241
            my $backend_result = {
220
                error   => 0,
242
                error   => 0,
Lines 227-239 if ( $backends_available ) { Link Here
227
            };
249
            };
228
            $template->param(
250
            $template->param(
229
                whole          => $backend_result,
251
                whole          => $backend_result,
230
                request        => $request
252
                request        => $request,
253
                batches        => $batches
231
            );
254
            );
232
        } else {
255
        } else {
233
            # Commit:
256
            # Commit:
234
            # Save the changes
257
            # Save the changes
235
            $request->borrowernumber($params->{borrowernumber});
258
            $request->borrowernumber($params->{borrowernumber});
236
            $request->biblio_id($params->{biblio_id});
259
            $request->biblio_id($params->{biblio_id});
260
            $request->batch_id($params->{batch_id});
237
            $request->branchcode($params->{branchcode});
261
            $request->branchcode($params->{branchcode});
238
            $request->price_paid($params->{price_paid});
262
            $request->price_paid($params->{price_paid});
239
            $request->notesopac($params->{notesopac});
263
            $request->notesopac($params->{notesopac});
Lines 365-371 if ( $backends_available ) { Link Here
365
    } elsif ( $op eq 'illlist') {
389
    } elsif ( $op eq 'illlist') {
366
390
367
        # If we receive a pre-filter, make it available to the template
391
        # If we receive a pre-filter, make it available to the template
368
        my $possible_filters = ['borrowernumber'];
392
        my $possible_filters = ['borrowernumber', 'batch_id'];
369
        my $active_filters = {};
393
        my $active_filters = {};
370
        foreach my $filter(@{$possible_filters}) {
394
        foreach my $filter(@{$possible_filters}) {
371
            if ($params->{$filter}) {
395
            if ($params->{$filter}) {
Lines 384-389 if ( $backends_available ) { Link Here
384
        $template->param(
408
        $template->param(
385
            prefilters => join("&", @tpl_arr)
409
            prefilters => join("&", @tpl_arr)
386
        );
410
        );
411
412
        if ($active_filters->{batch_id}) {
413
            my $batch_id = $active_filters->{batch_id};
414
            if ($batch_id) {
415
                my $batch = Koha::Illbatches->find($batch_id);
416
                $template->param(
417
                    batch => $batch
418
                );
419
            }
420
        }
421
387
    } elsif ( $op eq "save_comment" ) {
422
    } elsif ( $op eq "save_comment" ) {
388
        die "Wrong CSRF token" unless Koha::Token->new->check_csrf({
423
        die "Wrong CSRF token" unless Koha::Token->new->check_csrf({
389
           session_id => scalar $cgi->cookie('CGISESSID'),
424
           session_id => scalar $cgi->cookie('CGISESSID'),
Lines 418-423 if ( $backends_available ) { Link Here
418
            scalar $params->{illrequest_id} . $append
453
            scalar $params->{illrequest_id} . $append
419
        );
454
        );
420
        exit;
455
        exit;
456
    } elsif ( $op eq "batch_list" ) {
457
    } elsif ( $op eq "batch_create" ) {
458
        # Batch create
421
    } else {
459
    } else {
422
        my $request = Koha::Illrequests->find($params->{illrequest_id});
460
        my $request = Koha::Illrequests->find($params->{illrequest_id});
423
        my $backend_result = $request->custom_capability($op, $params);
461
        my $backend_result = $request->custom_capability($op, $params);
Lines 475-477 sub redirect_to_list { Link Here
475
    print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl');
513
    print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl');
476
    exit;
514
    exit;
477
}
515
}
516
517
# Do any of the available backends provide batch requesting
518
sub have_batch_backends {
519
    my ( $backends ) = @_;
520
521
    my @have_batch = ();
522
523
    foreach my $backend(@{$backends}) {
524
        my $can_batch = can_batch($backend);
525
        if ($can_batch) {
526
            push @have_batch, $backend;
527
        }
528
    }
529
    return \@have_batch;
530
}
531
532
# Does a given backend provide batch requests
533
sub can_batch {
534
    my ( $backend ) = @_;
535
    my $request = Koha::Illrequest->new->load_backend( $backend );
536
    return $request->_backend_capability( 'provides_batch_requests' );
537
}
538
539
# Get available metadata enrichment plugins
540
sub get_metadata_enrichment {
541
    my @candidates = Koha::Plugins->new()->GetPlugins({
542
        method => 'provides_api'
543
    });
544
    my @services = ();
545
    foreach my $plugin(@candidates) {
546
        my $supported = $plugin->provides_api();
547
        if ($supported->{type} eq 'search') {
548
            push @services, $supported;
549
        }
550
    }
551
    return \@services;
552
}
553
554
# Get ILL availability plugins that can help us with the batch identifier types
555
# we support
556
sub get_ill_availability {
557
    my ( $services ) = @_;
558
559
    my $id_types = {};
560
    foreach my $service(@{$services}) {
561
        foreach my $id_supported(keys %{$service->{identifiers_supported}}) {
562
            $id_types->{$id_supported} = 1;
563
        }
564
    }
565
566
    my $availability = Koha::Illrequest::Availability->new($id_types);
567
    return $availability->get_services({
568
        ui_context => 'staff'
569
    });
570
}
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+106 lines)
Lines 3769-3774 input.renew { Link Here
3769
}
3769
}
3770
3770
3771
#interlibraryloans {
3771
#interlibraryloans {
3772
3773
    .ill-toolbar {
3774
        display: flex;
3775
    }
3776
3777
    #ill-batch {
3778
        flex-grow: 1;
3779
        display: flex;
3780
        justify-content: flex-end;
3781
        gap: 5px;
3782
    }
3783
3784
    #ill-batch-requests {
3785
        .action-buttons {
3786
            display: flex;
3787
            gap: 5px;
3788
            justify-content: center;
3789
        }
3790
    }
3791
3792
    #ill-batch-modal {
3793
        .modal-footer {
3794
            display: flex;
3795
            & > * {
3796
                flex: 1;
3797
            }
3798
            #lhs {
3799
                text-align: left;
3800
            }
3801
        }
3802
        #create-progress {
3803
            margin-top: 17px;
3804
        }
3805
        .fetch-failed {
3806
            background-color: rgba(255,0,0,0.1);
3807
            & > * {
3808
                background-color: inherit;
3809
            }
3810
        }
3811
        .progress {
3812
            margin-bottom: 0;
3813
            margin-top: 17px;
3814
        }
3815
        #create-requests {
3816
            display: flex;
3817
            justify-content: flex-end;
3818
        }
3819
        .action-column {
3820
            text-align: center;
3821
            & > * {
3822
                margin-left: 5px;
3823
            }
3824
            & > *:first-child {
3825
                margin-left: 0;
3826
            }
3827
        }
3828
        .metadata-row:not(:first-child) {
3829
            margin-top: 0.5em;
3830
        }
3831
        .metadata-label {
3832
            font-weight: 600;
3833
        }
3834
        .more-less {
3835
            text-align: right;
3836
            margin: 2px 0;
3837
        }
3838
3839
    }
3840
3841
    #batch-form {
3842
        legend {
3843
            margin-bottom: 2em;
3844
        }
3845
        textarea {
3846
            width: 100%;
3847
            min-height: 100px;
3848
            padding: 5px;
3849
            resize: vertical;
3850
        }
3851
        #new-batch-form {
3852
            display: flex;
3853
            gap: 20px;
3854
        }
3855
        li#process-button {
3856
            display: flex;
3857
            justify-content: flex-end;
3858
        }
3859
        #textarea-metadata {
3860
            padding: 0 15px;
3861
            display: flex;
3862
            justify-content: space-between;
3863
        }
3864
        #textarea-errors {
3865
            display: flex;
3866
            flex-direction: column;
3867
            gap: 10px;
3868
            padding: 20px 15px 10px
3869
        }
3870
        .batch-modal-actions {
3871
            text-align: center;
3872
        }
3873
        fieldset {
3874
            border: 2px solid #b9d8d9;
3875
        }
3876
    }
3877
3772
    #dataPreviewLabel {
3878
    #dataPreviewLabel {
3773
        margin: .3em 0;
3879
        margin: .3em 0;
3774
    }
3880
    }
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc (+36 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_api_request_fail = _("Unable to create local request");
9
    var ill_batch_requests_api_fail = _("Unable to retrieve batch requests");
10
    var ill_batch_unknown = _("Unknown");
11
    var ill_batch_doi = _("DOI");
12
    var ill_batch_pmid = _("PubMed ID");
13
    var ill_populate_waiting = _("Retrieving...");
14
    var ill_populate_failed = _("Failed to retrieve");
15
    var ill_button_remove = _("Remove");
16
    var ill_batch_create_api_fail = _("Unable to create batch request");
17
    var ill_batch_update_api_fail = _("Unable to updatecreate batch request");
18
    var ill_batch_item_remove = _("Are you sure you want to remove this item from the batch");
19
    var ill_batch_metadata_more = _("More");
20
    var ill_batch_metadata_less = _("Less");
21
    var ill_batch_available_via = _("Available via");
22
    var ill_batch_metadata = {
23
        'doi': _("DOI"),
24
        'pmid': _("PubMed ID"),
25
        'issn': _("ISSN"),
26
        'title': _("Title"),
27
        'year': _("Year"),
28
        'issue': _("Issue"),
29
        'pages': _("Pages"),
30
        'publisher': _("Publisher"),
31
        'article_title': _("Article title"),
32
        'article_author': _("Article author"),
33
        'volume': _("Volume")
34
    };
35
</script>
36
<!-- / ill-batch-table-strings.inc -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc (+92 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="cardnumber">Card number, username or surname:</label>
22
                                    <input type="text" autocomplete="off" name="cardnumber" id="cardnumber" 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
                            </ol>
32
                        </fieldset>
33
                        <fieldset id="add_batch_items" class="rows" style="display:none">
34
                            <legend id="legend">Add batch items</legend>
35
                            <div id="textarea-metadata">
36
                                <div id="supported">Supported identifiers: <span id="supported_identifiers"></span></div>
37
                                <div id="row_count">Row count: <span id="row_count_value"></span></div>
38
                            </div>
39
                            <div id="textarea-errors" style="display:none" class="error">
40
                                <div id="duplicates" style="display:none">The following duplicates were found, these have been de-duplicated: <span id="dupelist"></span></div>
41
                                <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>
42
                            </div>
43
                            <ol>
44
                                <li>
45
                                    <textarea id="identifiers_input" placeholder="Enter identifiers, one per line"></textarea>
46
                                </li>
47
                                <li id="process-button">
48
                                    <button id="process_button" disabled type="button">Process identifiers</button>
49
                                </li>
50
                            </ol>
51
                        </fieldset>
52
                    </form>
53
                    <div id="create-progress" class="alert alert-info" role="alert" style="display:none">
54
                        <span id="progress-label"><strong></strong></span> -
55
                        Items processed: <span id="processed_count">0</span> out of <span id="processed_total">0</span>.
56
                        Items failed: <span id="processed_failed">0</span>.
57
                        <div class="progress">
58
                            <div id="processed_progress_bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 2em;">
59
                                0%
60
                            </div>
61
                        </div>
62
                    </div>
63
                    <div id="create-requests" style="display:none">
64
                        <button id="create-requests-button" type="button" class="btn btn-xs btn-success">Add items to batch</button>
65
                    </div>
66
                    <table id="identifier-table" style="display:none">
67
                        <thead>
68
                            <tr id="identifier-table-header">
69
                                <th scope="col">Identifier type</th>
70
                                <th scope="col">Identifier value</th>
71
                                <th scope="col">Metadata</th>
72
                                <th scope="col">Request ID</th>
73
                                <th scope="col"></th>
74
                            </tr>
75
                        </thead>
76
                        <tbody id="identifier-table-body">
77
                        </tbody>
78
                    </table>
79
                </div>
80
            </div>
81
            <div class="modal-footer">
82
                <div id="lhs">
83
                    <button class="btn btn-default" data-dismiss="modal" aria-hidden="false">Close</button>
84
                </div>
85
                <div id="rhs">
86
                    <button id="button_create_batch" class="btn btn-default" aria-hidden="true" disabled>Continue</button>
87
                    <a id="button_finish" disabled type="button" class="btn btn-default" aria-hidden="true">Finish and view batch</a>
88
                </div>
89
            </div>
90
        </div>
91
    </div>
92
</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 (+20 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">Patron</th>
10
                <th scope="col">Branch</th>
11
                <th scope="col"></th>
12
            </tr>
13
        </thead>
14
        <tbody id="ill-batch-body">
15
        </tbody>
16
    </table>
17
</div>
18
[% ELSIF query_type == "batch_create" %]
19
20
[% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc (+1 lines)
Lines 7-12 Link Here
7
        <thead>
7
        <thead>
8
            <tr id="illview-header">
8
            <tr id="illview-header">
9
                <th scope="col">Request ID</th>
9
                <th scope="col">Request ID</th>
10
                <th scope="col">Batch</th>
10
                <th scope="col">Author</th>
11
                <th scope="col">Author</th>
11
                <th scope="col">Title</th>
12
                <th scope="col">Title</th>
12
                <th scope="col">Article title</th>
13
                <th scope="col">Article title</th>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc (-1 / +18 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 %]
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 / +49 lines)
Lines 120-125 Link Here
120
            <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>
120
            <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>
121
        [% ELSE %]
121
        [% ELSE %]
122
                [% INCLUDE 'ill-toolbar.inc' %]
122
                [% INCLUDE 'ill-toolbar.inc' %]
123
                [% INCLUDE 'ill-batch-modal.inc' %]
123
124
124
                [% IF whole.error %]
125
                [% IF whole.error %]
125
                    <h1>Error performing operation</h1>
126
                    <h1>Error performing operation</h1>
Lines 433-438 Link Here
433
                                        [% END %]
434
                                        [% END %]
434
                                    </select>
435
                                    </select>
435
                                </li>
436
                                </li>
437
                                [% IF batches.count > 0 %]
438
                                <li class="batch">
439
                                    <label class="batch_label">Batch:</label>
440
                                    <select id="batch_id" name="batch_id">
441
                                        <option value="">
442
                                        [% FOREACH batch IN batches %]
443
                                            [% IF batch.id == request.batch_id %]
444
                                            <option value="[% batch.id | html %]" selected>
445
                                            [% ELSE %]
446
                                            <option value="[% batch.id | html %]">
447
                                            [% END %]
448
                                                [% batch.name | html %]
449
                                            </option>
450
                                        [% END %]
451
                                    </select>
452
                                </li>
453
                                [% END %]
436
                                <li class="updated">
454
                                <li class="updated">
437
                                    <label class="updated">Last updated:</label>
455
                                    <label class="updated">Last updated:</label>
438
                                    [% request.updated | $KohaDates  with_hours => 1 %]
456
                                    [% request.updated | $KohaDates  with_hours => 1 %]
Lines 647-652 Link Here
647
                                            [% END %]
665
                                            [% END %]
648
                                        [% END %]
666
                                        [% END %]
649
                                    </li>
667
                                    </li>
668
                                    [% IF request.batch > 0 %]
669
                                    <li class="batch">
670
                                        <span class="label batch">Batch:</span>
671
                                        <a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=[% request.batch.id | html %]">
672
                                        [% request.batch.name | html %]
673
                                        </a>
674
                                    </li>
675
                                    [% END %]
650
                                    <li class="updated">
676
                                    <li class="updated">
651
                                        <span class="label updated">Last updated:</span>
677
                                        <span class="label updated">Last updated:</span>
652
                                        [% request.updated | $KohaDates  with_hours => 1 %]
678
                                        [% request.updated | $KohaDates  with_hours => 1 %]
Lines 783-789 Link Here
783
809
784
                [% ELSIF query_type == 'illlist' %]
810
                [% ELSIF query_type == 'illlist' %]
785
                    <!-- illlist -->
811
                    <!-- illlist -->
786
                    <h1>View ILL requests</h1>
812
                    <h1>
813
                        View ILL requests
814
                        [% IF batch %]
815
                        for batch "[% batch.name | html %]"
816
                        [% END %]
817
                    </h1>
787
                    <div id="results">
818
                    <div id="results">
788
                        <h3>Details for all requests</h3>
819
                        <h3>Details for all requests</h3>
789
                        [% INCLUDE 'ill-list-table.inc' %]
820
                        [% INCLUDE 'ill-list-table.inc' %]
Lines 823-828 Link Here
823
                            [% INCLUDE 'ill-availability-table.inc' service=service %]
854
                            [% INCLUDE 'ill-availability-table.inc' service=service %]
824
                        [% END %]
855
                        [% END %]
825
                    </div>
856
                    </div>
857
                [% ELSIF query_type == 'batch_list' || query_type == 'batch_create' %]
858
                    [% INCLUDE 'ill-batch.inc' %]
826
                [% ELSE %]
859
                [% ELSE %]
827
                <!-- Custom Backend Action -->
860
                <!-- Custom Backend Action -->
828
                [% PROCESS $whole.template %]
861
                [% PROCESS $whole.template %]
Lines 840-845 Link Here
840
    [% INCLUDE 'columns_settings.inc' %]
873
    [% INCLUDE 'columns_settings.inc' %]
841
    [% INCLUDE 'calendar.inc' %]
874
    [% INCLUDE 'calendar.inc' %]
842
    [% INCLUDE 'select2.inc' %]
875
    [% INCLUDE 'select2.inc' %]
876
    [% IF metadata_enrichment_services %]
877
    <script>
878
        var metadata_enrichment_services = [% metadata_enrichment_services | $raw %];
879
    </script>
880
    [% END %]
881
    [% IF batch_availability_services %]
882
    <script>
883
        var batch_availability_services = [% batch_availability_services | $raw %];
884
    </script>
885
    [% END %]
843
    <script>
886
    <script>
844
        var prefilters = '[% prefilters | $raw %]';
887
        var prefilters = '[% prefilters | $raw %]';
845
        // Set column settings
888
        // Set column settings
Lines 864-870 Link Here
864
        });
907
        });
865
    </script>
908
    </script>
866
    [% INCLUDE 'ill-list-table-strings.inc' %]
909
    [% INCLUDE 'ill-list-table-strings.inc' %]
910
    [% INCLUDE 'ill-batch-table-strings.inc' %]
911
    [% INCLUDE 'ill-batch-modal-strings.inc' %]
867
    [% Asset.js("js/ill-list-table.js") | $raw %]
912
    [% Asset.js("js/ill-list-table.js") | $raw %]
913
    [% Asset.js("js/ill-batch.js") | $raw %]
914
    [% Asset.js("js/ill-batch-table.js") | $raw %]
915
    [% Asset.js("js/ill-batch-modal.js") | $raw %]
868
    [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %]
916
    [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %]
869
        [% Asset.js("js/ill-availability.js") | $raw %]
917
        [% Asset.js("js/ill-availability.js") | $raw %]
870
    [% END %]
918
    [% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js (+993 lines)
Line 0 Link Here
1
(function () {
2
    window.addEventListener('load', onload);
3
4
    // Delay between API requests
5
    var debounceDelay = 1000;
6
7
    // Elements we work frequently with
8
    var textarea = document.getElementById("identifiers_input");
9
    var nameInput = document.getElementById("name");
10
    var cardnumberInput = document.getElementById("cardnumber");
11
    var branchcodeSelect = document.getElementById("branchcode");
12
    var processButton = document.getElementById("process_button");
13
    var createButton = document.getElementById("button_create_batch");
14
    var finishButton = document.getElementById("button_finish");
15
    var batchItemsDisplay = document.getElementById("add_batch_items");
16
    var createProgressTotal = document.getElementById("processed_total");
17
    var createProgressCount = document.getElementById("processed_count");
18
    var createProgressFailed = document.getElementById("processed_failed");
19
    var createProgressBar = document.getElementById("processed_progress_bar");
20
    var identifierTable = document.getElementById('identifier-table');
21
    var createRequestsButton = document.getElementById('create-requests-button');
22
23
24
    // We need a data structure keyed on identifier type, which tells us how to parse that
25
    // identifier type and what services can get its metadata. We receive an array of
26
    // available services
27
    var supportedIdentifiers = {};
28
    metadata_enrichment_services.forEach(function (service) {
29
        // Iterate the identifiers that this service supports
30
        Object.keys(service.identifiers_supported).forEach(function (idType) {
31
            if (!supportedIdentifiers[idType]) {
32
                supportedIdentifiers[idType] = [];
33
            }
34
            supportedIdentifiers[idType].push(service);
35
        });
36
    });
37
38
    // An object for when we're creating a new batch
39
    var emptyBatch = {
40
        name: '',
41
        backend: null,
42
        cardnumber: '',
43
        branchcode: ''
44
    };
45
46
    // The object that holds the batch we're working with
47
    // It's a proxy so we can update portions of the UI
48
    // upon changes
49
    var batch = new Proxy(
50
        { data: {} },
51
        {
52
            get: function (obj, prop) {
53
                return obj[prop];
54
            },
55
            set: function (obj, prop, value) {
56
                obj[prop] = value;
57
                manageBatchItemsDisplay();
58
                updateBatchInputs();
59
                setFinishButton();
60
                disableCardnumberInput();
61
                displayPatronName();
62
            }
63
        }
64
    );
65
66
    // The object that holds the contents of the table
67
    // It's a proxy so we can make it automatically redraw the
68
    // table upon changes
69
    var tableContent = new Proxy(
70
        { data: [] },
71
        {
72
            get: function (obj, prop) {
73
                return obj[prop];
74
            },
75
            set: function (obj, prop, value) {
76
                obj[prop] = value;
77
                updateTable();
78
                updateRowCount();
79
                updateProcessTotals();
80
                checkAvailability();
81
            }
82
        }
83
    );
84
85
    var progressTotals = new Proxy(
86
        {
87
            data: {}
88
        },
89
        {
90
            get: function (obj, prop) {
91
                return obj[prop];
92
            },
93
            set: function (obj, prop, value) {
94
                obj[prop] = value;
95
                showCreateRequestsButton();
96
            }
97
        }
98
    );
99
100
    // Keep track of submission API calls that are in progress
101
    // so we don't duplicate them
102
    var submissionSent = {};
103
104
    // Keep track of availability API calls that are in progress
105
    // so we don't duplicate them
106
    var availabilitySent = {};
107
108
    // The datatable
109
    var table;
110
    var tableEl = document.getElementById('identifier-table');
111
112
    // The element that potentially holds the ID of the batch
113
    // we're working with
114
    var idEl = document.getElementById('ill-batch-details');
115
    var batchId = null;
116
    var backend = null;
117
118
    function onload() {
119
        $('#ill-batch-modal').on('shown.bs.modal', function () {
120
            init();
121
            patronAutocomplete();
122
            batchInputsEventListeners();
123
            createButtonEventListener();
124
            createRequestsButtonEventListener();
125
            moreLessEventListener();
126
            removeRowEventListener();
127
        });
128
        $('#ill-batch-modal').on('hidden.bs.modal', function () {
129
            // Reset our state when we close the modal
130
            delete idEl.dataset.batchId;
131
            delete idEl.dataset.backend;
132
            batchId = null;
133
            tableEl.style.display = 'none';
134
            tableContent.data = [];
135
            progressTotals.data = {
136
                total: 0,
137
                count: 0,
138
                failed: 0
139
            };
140
            textarea.value = '';
141
            batch.data = {};
142
            // Remove event listeners we created
143
            removeEventListeners();
144
        });
145
    };
146
147
    function init() {
148
        batchId = idEl.dataset.batchId;
149
        backend = idEl.dataset.backend;
150
        emptyBatch.backend = backend;
151
        progressTotals.data = {
152
            total: 0,
153
            count: 0,
154
            failed: 0
155
        };
156
        if (batchId) {
157
            fetchBatch();
158
            setModalHeading(true);
159
        } else {
160
            batch.data = emptyBatch;
161
            setModalHeading();
162
        }
163
        finishButtonEventListener();
164
        processButtonEventListener();
165
        identifierTextareaEventListener();
166
        displaySupportedIdentifiers();
167
        createButtonEventListener();
168
        updateRowCount();
169
    };
170
171
    function initPostCreate() {
172
        disableCreateButton();
173
    };
174
175
    function setFinishButton() {
176
        if (batch.data.patron) {
177
            finishButton.removeAttribute('disabled');
178
        }
179
    };
180
181
    function setModalHeading(isUpdate) {
182
        var heading = document.getElementById('ill-batch-modal-label');
183
        heading.textContent = isUpdate ? ill_batch_update : ill_batch_add;
184
    }
185
186
    // Identify items that have metadata and therefore can have a local request
187
    // created, and do so
188
    function requestRequestable() {
189
        createRequestsButton.setAttribute('disabled', true);
190
        var toCheck = tableContent.data;
191
        toCheck.forEach(function (row) {
192
            if (
193
                !row.requestId &&
194
                Object.keys(row.metadata).length > 0 &&
195
                !submissionSent[row.value]
196
            ) {
197
                submissionSent[row.value] = 1;
198
                makeLocalSubmission(row.value, row.metadata);
199
            }
200
        });
201
    };
202
203
    // Identify items that can have their availability checked, and do it
204
    function checkAvailability() {
205
        // Only proceed if we've got services that can check availability
206
        if (!batch_availability_services || batch_availability_services.length === 0) return;
207
        var toCheck = tableContent.data;
208
        toCheck.forEach(function (row) {
209
            if (
210
                !row.url &&
211
                Object.keys(row.metadata).length > 0 &&
212
                !availabilitySent[row.value]
213
            ) {
214
                availabilitySent[row.value] = 1;
215
                getAvailability(row.value, row.metadata);
216
            }
217
        });
218
    };
219
220
    // Check availability services for immediate availability, if found,
221
    // create a link in the table linking to the item
222
    function getAvailability(identifier, metadata) {
223
        // Prep the metadata for passing to the availability plugins
224
        var prepped = encodeURIComponent(base64EncodeUnicode(JSON.stringify(metadata)));
225
        for (i = 0; i < batch_availability_services.length; i++) {
226
            var service = batch_availability_services[i];
227
            window.doApiRequest(
228
                service.endpoint + prepped
229
            )
230
                .then(function (response) {
231
                    return response.json();
232
                })
233
                .then(function (data) {
234
                    if (data.results.search_results && data.results.search_results.length > 0) {
235
                        var result = data.results.search_results[0];
236
                        tableContent.data = tableContent.data.map(function (row) {
237
                            if (row.value === identifier) {
238
                                row.url = result.url;
239
                                row.availabilitySupplier = service.name;
240
                            }
241
                            return row;
242
                        });
243
                    }
244
                });
245
        }
246
    };
247
248
    // Help btoa with > 8 bit strings
249
    // Shamelessly grabbed from: https://www.base64encoder.io/javascript/
250
    function base64EncodeUnicode(str) {
251
        // First we escape the string using encodeURIComponent to get the UTF-8 encoding of the characters,
252
        // then we convert the percent encodings into raw bytes, and finally feed it to btoa() function.
253
        utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
254
                return String.fromCharCode('0x' + p1);
255
        });
256
257
        return btoa(utf8Bytes);
258
    };
259
260
    // Create a local submission and update our local state
261
    // upon success
262
    function makeLocalSubmission(identifier, metadata) {
263
        var payload = {
264
            batch_id: batchId,
265
            backend: batch.data.backend,
266
            borrowernumber: batch.data.patron.borrowernumber,
267
            branchcode: batch.data.branchcode,
268
            metadata: metadata
269
        };
270
        window.doCreateSubmission(payload)
271
            .then(function (response) {
272
                return response.json();
273
            })
274
            .then(function (data) {
275
                tableContent.data = tableContent.data.map(function (row) {
276
                    if (row.value === identifier) {
277
                        row.requestId = data.illrequest_id;
278
                    }
279
                    return row;
280
                });
281
            })
282
            .catch(function () {
283
                window.handleApiError(ill_batch_api_request_fail);
284
            });
285
    };
286
287
    function updateProcessTotals() {
288
        var init = {
289
            total: 0,
290
            count: 0,
291
            failed: 0
292
        };
293
        progressTotals.data = init;
294
        var toUpdate = progressTotals.data;
295
        tableContent.data.forEach(function (row) {
296
            toUpdate.total++;
297
            if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) {
298
                toUpdate.count++;
299
            }
300
            if (Object.keys(row.failed).length > 0) {
301
                toUpdate.failed++;
302
            }
303
        });
304
        createProgressTotal.innerHTML = toUpdate.total;
305
        createProgressCount.innerHTML = toUpdate.count;
306
        createProgressFailed.innerHTML = toUpdate.failed;
307
        var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100);
308
        createProgressBar.setAttribute('aria-valuenow', percentDone);
309
        createProgressBar.innerHTML = percentDone + '%';
310
        createProgressBar.style.width = percentDone + '%';
311
        progressTotals.data = toUpdate;
312
    };
313
314
    function displayPatronName() {
315
        var span = document.getElementById('patron_link');
316
        if (batch.data.patron) {
317
            var link = createPatronLink();
318
            span.appendChild(link);
319
        } else {
320
            if (span.children.length > 0) {
321
                span.removeChild(span.firstChild);
322
            }
323
        }
324
    };
325
326
    function removeEventListeners() {
327
        textarea.removeEventListener('paste', processButtonState);
328
        textarea.removeEventListener('keyup', processButtonState);
329
        processButton.removeEventListener('click', processIdentifiers);
330
        nameInput.removeEventListener('keyup', createButtonState);
331
        cardnumberInput.removeEventListener('keyup', createButtonState);
332
        branchcodeSelect.removeEventListener('change', createButtonState);
333
        createButton.removeEventListener('click', createBatch);
334
        identifierTable.removeEventListener('click', toggleMetadata);
335
        identifierTable.removeEventListener('click', removeRow);
336
        createRequestsButton.remove('click', requestRequestable);
337
    };
338
339
    function finishButtonEventListener() {
340
        finishButton.addEventListener('click', doFinish);
341
    };
342
343
    function identifierTextareaEventListener() {
344
        textarea.addEventListener('paste', textareaUpdate);
345
        textarea.addEventListener('keyup', textareaUpdate);
346
    };
347
348
    function processButtonEventListener() {
349
        processButton.addEventListener('click', processIdentifiers);
350
    };
351
352
    function createRequestsButtonEventListener() {
353
        createRequestsButton.addEventListener('click', requestRequestable);
354
    };
355
356
    function createButtonEventListener() {
357
        createButton.addEventListener('click', createBatch);
358
    };
359
360
    function batchInputsEventListeners() {
361
        nameInput.addEventListener('keyup', createButtonState);
362
        cardnumberInput.addEventListener('keyup', createButtonState);
363
        branchcodeSelect.addEventListener('change', createButtonState);
364
    };
365
366
    function moreLessEventListener() {
367
        identifierTable.addEventListener('click', toggleMetadata);
368
    };
369
370
    function removeRowEventListener() {
371
        identifierTable.addEventListener('click', removeRow);
372
    };
373
374
    function textareaUpdate() {
375
        processButtonState();
376
        updateRowCount();
377
    };
378
379
    function processButtonState() {
380
        if (textarea.value.length > 0) {
381
            processButton.removeAttribute('disabled');
382
        } else {
383
            processButton.setAttribute('disabled', 1);
384
        }
385
    };
386
387
    function disableCardnumberInput() {
388
        if (batch.data.patron) {
389
            cardnumberInput.setAttribute('disabled', true);
390
        } else {
391
            cardnumberInput.removeAttribute('disabled');
392
        }
393
    };
394
395
    function createButtonState() {
396
        if (
397
            nameInput.value.length > 0 &&
398
            cardnumberInput.value.length > 0 &&
399
            branchcodeSelect.selectedOptions.length === 1
400
        ) {
401
            createButton.removeAttribute('disabled');
402
            createButton.setAttribute('display', 'inline-block');
403
        } else {
404
            createButton.setAttribute('disabled', 1);
405
            createButton.setAttribute('display', 'none');
406
        }
407
    };
408
409
    function doFinish() {
410
        updateBatch()
411
            .then(function () {
412
                $('#ill-batch-modal').modal({ show: false });
413
                location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.id;
414
            });
415
    };
416
417
    // Get the batch
418
    function fetchBatch() {
419
        window.doBatchApiRequest("/" + batchId)
420
            .then(function (response) {
421
                return response.json();
422
            })
423
            .then(function (jsoned) {
424
                batch.data = {
425
                    id: jsoned.id,
426
                    name: jsoned.name,
427
                    backend: jsoned.backend,
428
                    cardnumber: jsoned.cardnumber,
429
                    branchcode: jsoned.branchcode
430
                }
431
                return jsoned;
432
            })
433
            .then(function (data) {
434
                batch.data = data;
435
            })
436
            .catch(function () {
437
                window.handleApiError(ill_batch_api_fail);
438
            });
439
440
    };
441
442
    function createBatch() {
443
        var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
444
        return doBatchApiRequest('', {
445
            method: 'POST',
446
            headers: {
447
                'Content-type': 'application/json'
448
            },
449
            body: JSON.stringify({
450
                name: nameInput.value,
451
                backend: backend,
452
                cardnumber: cardnumberInput.value,
453
                branchcode: selectedBranchcode
454
            })
455
        })
456
            .then(function (response) {
457
                return response.json();
458
            })
459
            .then(function (body) {
460
                batchId = body.id;
461
                batch.data = {
462
                    id: body.id,
463
                    name: body.name,
464
                    backend: body.backend,
465
                    cardnumber: body.patron.cardnumber,
466
                    branchcode: body.branchcode,
467
                    patron: body.patron
468
                };
469
                initPostCreate();
470
            })
471
            .catch(function () {
472
                handleApiError(ill_batch_create_api_fail);
473
            });
474
    };
475
476
    function updateBatch() {
477
        var selectedBranchcode = branchcodeSelect.selectedOptions[0].value;
478
        return doBatchApiRequest('/' + batch.data.id, {
479
            method: 'PUT',
480
            headers: {
481
                'Content-type': 'application/json'
482
            },
483
            body: JSON.stringify({
484
                name: nameInput.value,
485
                backend: batch.data.backend,
486
                cardnumber: batch.data.patron.cardnumber,
487
                branchcode: selectedBranchcode
488
            })
489
        })
490
            .catch(function () {
491
                handleApiError(ill_batch_update_api_fail);
492
            });
493
    };
494
495
    function displaySupportedIdentifiers() {
496
        var names = Object.keys(supportedIdentifiers).map(function (identifier) {
497
            return window['ill_batch_' + identifier];
498
        });
499
        var displayEl = document.getElementById('supported_identifiers');
500
        displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none;
501
    }
502
503
    function updateRowCount() {
504
        var textEl = document.getElementById('row_count_value');
505
        var val = textarea.value.trim();
506
        var cnt = 0;
507
        if (val.length > 0) {
508
            cnt = val.split(/\n/).length;
509
        }
510
        textEl.textContent = cnt;
511
    }
512
513
    function showProgress() {
514
        var el = document.getElementById('create-progress');
515
        el.style.display = 'block';
516
    }
517
518
    function showCreateRequestsButton() {
519
        var data = progressTotals.data;
520
        var el = document.getElementById('create-requests');
521
        el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none';
522
    }
523
524
    async function processIdentifiers() {
525
        var content = textarea.value;
526
        hideErrors();
527
        if (content.length === 0) return;
528
529
        disableProcessButton();
530
        var label = document.getElementById('progress-label').firstChild;
531
        label.innerHTML = ill_batch_retrieving_metadata;
532
        showProgress();
533
534
        // Errors encountered when processing
535
        var processErrors = {};
536
537
        // Prepare the content, including trimming each row
538
        var contentArr = content.split(/\n/);
539
        var trimmed = contentArr.map(function (row) {
540
            return row.trim();
541
        });
542
543
        var parsed = [];
544
545
        trimmed.forEach(function (identifier) {
546
            var match = identifyIdentifier(identifier);
547
            // If this identifier is not identifiable or
548
            // looks like more than one type, we can't be sure
549
            // what it is
550
            if (match.length != 1) {
551
                parsed.push({
552
                    type: 'unknown',
553
                    value: identifier
554
                });
555
            } else {
556
                parsed.push(match[0]);
557
            }
558
        });
559
560
        var unknownIdentifiers = parsed
561
            .filter(function (parse) {
562
                if (parse.type == 'unknown') {
563
                    return parse;
564
                }
565
            })
566
            .map(function (filtered) {
567
                return filtered.value;
568
            });
569
570
        if (unknownIdentifiers.length > 0) {
571
            processErrors.badidentifiers = {
572
                element: 'badids',
573
                values: unknownIdentifiers.join(', ')
574
            };
575
        };
576
577
        // Deduping
578
        var deduped = [];
579
        var dupes = {};
580
        parsed.forEach(function (row) {
581
            var value = row.value;
582
            var alreadyInDeduped = deduped.filter(function (d) {
583
                return d.value === value;
584
            });
585
            if (alreadyInDeduped.length > 0 && !dupes[value]) {
586
                dupes[value] = 1;
587
            } else if (alreadyInDeduped.length === 0) {
588
                row.metadata = {};
589
                row.failed = {};
590
                row.requestId = null;
591
                deduped.push(row);
592
            }
593
        });
594
        // Update duplicate error if dupes were found
595
        if (Object.keys(dupes).length > 0) {
596
            processErrors.duplicates = {
597
                element: 'dupelist',
598
                values: Object.keys(dupes).join(', ')
599
            };
600
        }
601
602
        // Display any errors
603
        displayErrors(processErrors);
604
605
        // Now build and display the table
606
        if (!table) {
607
            buildTable();
608
        }
609
610
        // We may be appending new values to an existing table,
611
        // in which case, ensure we don't create duplicates
612
        var tabIdentifiers = tableContent.data.map(function (tabId) {
613
            return tabId.value;
614
        });
615
        var notInTable = deduped.filter(function (ded) {
616
            if (!tabIdentifiers.includes(ded.value)) {
617
                return ded;
618
           }
619
        });
620
        if (notInTable.length > 0) {
621
            tableContent.data = tableContent.data.concat(notInTable);
622
        }
623
624
        // Populate metadata for those records that need it
625
        var newData = tableContent.data;
626
        for (var i = 0; i < tableContent.data.length; i++) {
627
            var row = tableContent.data[i];
628
            // Skip rows that don't need populating
629
            if (
630
                Object.keys(tableContent.data[i].metadata).length > 0 ||
631
                Object.keys(tableContent.data[i].failed).length > 0
632
            ) continue;
633
            var identifier = { type: row.type, value: row.value };
634
            try {
635
                var populated = await populateMetadata(identifier);
636
                row.metadata = populated.results.result || {};
637
            } catch (e) {
638
                row.failed = ill_populate_failed;
639
            }
640
            newData[i] = row;
641
            tableContent.data = newData;
642
        }
643
    }
644
645
    function disableProcessButton() {
646
        processButton.setAttribute('disabled', true);
647
    }
648
649
    function disableCreateButton() {
650
        createButton.setAttribute('disabled', true);
651
    }
652
653
    function disableRemoveRowButtons() {
654
        var buttons = document.getElementsByClassName('remove-row');
655
        for (var button of buttons) {
656
            button.setAttribute('disabled', true);
657
        }
658
    }
659
660
    async function populateMetadata(identifier) {
661
        // All services that support this identifier type
662
        var services = supportedIdentifiers[identifier.type];
663
        // Check each service and use the first results we get, if any
664
        for (var i = 0; i < services.length; i++) {
665
            var service = services[i];
666
            var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value;
667
            var metadata = await getMetadata(endpoint);
668
            if (metadata.errors.length === 0) {
669
                var parsed = await parseMetadata(metadata, service);
670
                if (parsed.errors.length > 0) {
671
                    throw Error(metadata.errors.map(function (error) {
672
                        return error.message;
673
                    }).join(', '));
674
                }
675
                return parsed;
676
            }
677
        }
678
    };
679
680
    async function getMetadata(endpoint) {
681
        var response = await debounce(doApiRequest)(endpoint);
682
        return response.json();
683
    };
684
685
    async function parseMetadata(metadata, service) {
686
        var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint;
687
        var response = await doApiRequest(endpoint, {
688
            method: 'POST',
689
            headers: {
690
                'Content-type': 'application/json'
691
            },
692
            body: JSON.stringify(metadata)
693
        });
694
        return response.json();
695
    }
696
697
    // A render function for identifier type
698
    function createIdentifierType(data) {
699
        return window['ill_batch_' + data];
700
    };
701
702
    // Get an item's title
703
    function getTitle(meta) {
704
        if (meta.article_title && meta.article_title.length > 0) {
705
            return {
706
                prop: 'article_title',
707
                value: meta.article_title
708
            };
709
        } else if (meta.title && meta.title.length > 0) {
710
            return {
711
                prop: 'title',
712
                value: meta.title
713
            };
714
        }
715
    };
716
717
    // Create a metadata row
718
    function createMetadataRow(data, meta, prop) {
719
        if (!meta[prop]) return;
720
721
        var div = document.createElement('div');
722
        div.classList.add('metadata-row');
723
        var label = document.createElement('span');
724
        label.classList.add('metadata-label');
725
        label.innerText = ill_batch_metadata[prop] + ': ';
726
727
        // Add a link to the availability URL if appropriate
728
        var value;
729
        if (!data.url) {
730
            value = document.createElement('span');
731
        } else {
732
            value = document.createElement('a');
733
            value.setAttribute('href', data.url);
734
            value.setAttribute('target', '_blank');
735
            value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier);
736
        }
737
        value.classList.add('metadata-value');
738
        value.innerText = meta[prop];
739
        div.appendChild(label);
740
        div.appendChild(value);
741
742
        return div;
743
    }
744
745
    // A render function for displaying metadata
746
    function createMetadata(x, y, data) {
747
        // If the fetch failed
748
        if (data.failed.length > 0) {
749
            return data.failed;
750
        }
751
752
        // If we've not yet got any metadata back
753
        if (Object.keys(data.metadata).length === 0) {
754
            return ill_populate_waiting;
755
        }
756
757
        var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume'];
758
        var meta = data.metadata;
759
760
        var container = document.createElement('div');
761
        container.classList.add('metadata-container');
762
763
        // Create the title row
764
        var title = getTitle(meta);
765
        if (title) {
766
            // Remove the title element from the props
767
            // we're about to iterate
768
            core = core.filter(function (i) {
769
                return i !== title.prop;
770
            });
771
            var titleRow = createMetadataRow(data, meta, title.prop);
772
            container.appendChild(titleRow);
773
        }
774
775
        var remainder = document.createElement('div');
776
        remainder.classList.add('metadata-remainder');
777
        remainder.style.display = 'none';
778
        // Create the remaining rows
779
        core.sort().forEach(function (prop) {
780
            var div = createMetadataRow(data, meta, prop);
781
            if (div) {
782
                remainder.appendChild(div);
783
            }
784
        });
785
        container.appendChild(remainder);
786
787
        // Add a more/less toggle
788
        var firstField = container.firstChild;
789
        var moreLess = document.createElement('div');
790
        moreLess.classList.add('more-less');
791
        var moreLessLink = document.createElement('a');
792
        moreLessLink.setAttribute('href', '#');
793
        moreLessLink.classList.add('more-less-link');
794
        moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']';
795
        moreLess.appendChild(moreLessLink);
796
        firstField.appendChild(moreLess);
797
798
        return container.outerHTML;
799
    };
800
801
    function removeRow(ev) {
802
        if (ev.target.className.includes('remove-row')) {
803
            if (!confirm(ill_batch_item_remove)) return;
804
            // Find the parent row
805
            var ancestor = ev.target.closest('tr');
806
            var identifier = ancestor.querySelector('.identifier').innerText;
807
            tableContent.data = tableContent.data.filter(function (row) {
808
                return row.value !== identifier;
809
            });
810
        }
811
    }
812
813
    function toggleMetadata(ev) {
814
        if (ev.target.className === 'more-less-link') {
815
            // Find the element we need to show
816
            var ancestor = ev.target.closest('.metadata-container');
817
            var meta = ancestor.querySelector('.metadata-remainder');
818
819
            // Display or hide based on its current state
820
            var display = window.getComputedStyle(meta).display;
821
822
            meta.style.display = display === 'block' ? 'none' : 'block';
823
824
            // Update the More / Less text
825
            ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]';
826
        }
827
    }
828
829
    // A render function for the link to a request ID
830
    function createRequestId(x, y, data) {
831
        return data.requestId || '-';
832
    }
833
834
    function buildTable(identifiers) {
835
        table = KohaTable('identifier-table', {
836
            processing: true,
837
            deferRender: true,
838
            ordering: false,
839
            paging: false,
840
            searching: false,
841
            autoWidth: false,
842
            columns: [
843
                {
844
                    data: 'type',
845
                    width: '13%',
846
                    render: createIdentifierType
847
                },
848
                {
849
                    data: 'value',
850
                    width: '25%',
851
                    className: 'identifier'
852
                },
853
                {
854
                    data: 'metadata',
855
                    render: createMetadata
856
                },
857
                {
858
                    data: 'requestId',
859
                    width: '13%',
860
                    render: createRequestId
861
                },
862
                {
863
                    width: '18%',
864
                    render: createActions,
865
                    className: 'action-column'
866
                }
867
            ],
868
            createdRow: function (row, data) {
869
                if (data.failed.length > 0) {
870
                    row.classList.add('fetch-failed');
871
                }
872
            }
873
        });
874
    }
875
876
    function createActions(x, y, data) {
877
        return '<button type="button"' + (data.requestId ? ' disabled' : '') + ' class="btn btn-xs btn-danger remove-row">' + ill_button_remove + '</button>';
878
    }
879
880
    // Redraw the table
881
    function updateTable() {
882
        if (!table) return;
883
        tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none';
884
        tableEl.style.width = '100%';
885
        table.api()
886
            .clear()
887
            .rows.add(tableContent.data)
888
            .draw();
889
    };
890
891
    function identifyIdentifier(identifier) {
892
        var matches = [];
893
894
        // Iterate our available services to see if any can identify this identifier
895
        Object.keys(supportedIdentifiers).forEach(function (identifierType) {
896
            // Since all the services supporting this identifier type should use the same
897
            // regex to identify it, we can just use the first
898
            var service = supportedIdentifiers[identifierType][0];
899
            var regex = new RegExp(service.identifiers_supported[identifierType].regex);
900
            var match = identifier.match(regex);
901
            if (match && match.groups && match.groups.identifier) {
902
                matches.push({
903
                    type: identifierType,
904
                    value: match.groups.identifier
905
                });
906
            }
907
        });
908
        return matches;
909
    }
910
911
    function displayErrors(errors) {
912
        var keys = Object.keys(errors);
913
        if (keys.length > 0) {
914
            keys.forEach(function (key) {
915
                var el = document.getElementById(errors[key].element);
916
                el.textContent = errors[key].values;
917
                el.style.display = 'inline';
918
                var container = document.getElementById(key);
919
                container.style.display = 'block';
920
            });
921
            var el = document.getElementById('textarea-errors');
922
            el.style.display = 'flex';
923
        }
924
    }
925
926
    function hideErrors() {
927
        var dupelist = document.getElementById('dupelist');
928
        var badids = document.getElementById('badids');
929
        dupelist.textContent = '';
930
        dupelist.parentElement.style.display = 'none';
931
        badids.textContent = '';
932
        badids.parentElement.style.display = 'none';
933
        var tae = document.getElementById('textarea-errors');
934
        tae.style.display = 'none';
935
    }
936
937
    function manageBatchItemsDisplay() {
938
        batchItemsDisplay.style.display = batch.data.id ? 'block' : 'none'
939
    };
940
941
    function updateBatchInputs() {
942
        nameInput.value = batch.data.name || '';
943
        cardnumberInput.value = batch.data.cardnumber || '';
944
        branchcodeSelect.value = batch.data.branchcode || '';
945
    }
946
947
    function debounce(func) {
948
        var timeout;
949
        return function (...args) {
950
            return new Promise(function (resolve) {
951
                if (timeout) {
952
                    clearTimeout(timeout);
953
                }
954
                timeout = setTimeout(function () {
955
                    return resolve(func(...args));
956
                }, debounceDelay);
957
            });
958
        }
959
    }
960
961
    function patronAutocomplete() {
962
        // Add autocomplete for patron selection
963
        $('#batch-form #cardnumber').autocomplete({
964
            appendTo: '#batch-form',
965
            source: "/cgi-bin/koha/circ/ysearch.pl",
966
            minLength: 3,
967
            select: function (event, ui) {
968
                var field = ui.item.cardnumber;
969
                $('#batch-form #cardnumber').val(field)
970
                return false;
971
            }
972
        })
973
            .data("ui-autocomplete")._renderItem = function (ul, item) {
974
                return $("<li></li>")
975
                    .data("ui-autocomplete-item", item)
976
                    .append("<a>" + item.surname + ", " + item.firstname + " (" + item.cardnumber + ") <small>" + item.address + " " + item.city + " " + item.zipcode + " " + item.country + "</small></a>")
977
                    .appendTo(ul);
978
            };
979
    };
980
981
    function createPatronLink() {
982
        if (!batch.data.patron) return;
983
        var patron = batch.data.patron;
984
        var a = document.createElement('a');
985
        var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber;
986
        var text = patron.surname + ' (' + patron.cardnumber + ')';
987
        a.setAttribute('title', ill_borrower_details);
988
        a.setAttribute('href', href);
989
        a.textContent = text;
990
        return a;
991
    };
992
993
})();
(-)a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js (+205 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
    var initTable = function () {
44
        return KohaTable("ill-batch-requests", {
45
            data: batchesProxy.data,
46
            columns: [
47
                {
48
                    data: 'id',
49
                    width: '10%'
50
                },
51
                {
52
                    data: 'name',
53
                    render: createName,
54
                    width: '30%'
55
                },
56
                {
57
                    data: 'requests_count',
58
                    width: '10%'
59
                },
60
                {
61
                    data: 'patron',
62
                    render: createPatronLink,
63
                    width: '20%'
64
                },
65
                {
66
                    data: 'branch',
67
                    render: createBranch,
68
                    width: '20%'
69
                },
70
                {
71
                    render: createActions,
72
                    width: '10%',
73
                    orderable: false
74
                }
75
            ],
76
            processing: true,
77
            deferRender: true,
78
            drawCallback: addEventListeners
79
        });
80
    }
81
82
    // A render function for branch name
83
    var createBranch = function (data) {
84
        return data.branchname;
85
    }
86
87
    // A render function for batch name
88
    var createName = function (x, y, data) {
89
        var a = document.createElement('a');
90
        a.setAttribute('href', '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + data.id);
91
        a.setAttribute('title', data.name);
92
        a.textContent = data.name;
93
        return a.outerHTML;
94
    };
95
96
    // A render function for our patron link
97
    var createPatronLink = function (data) {
98
        var link = document.createElement('a');
99
        link.setAttribute('title', ill_batch_borrower_details);
100
        link.setAttribute('href', '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + data.borrowernumber);
101
        var displayText = [data.firstname, data.surname].join(' ') + ' ( ' + data.cardnumber + ' )';
102
        link.appendChild(document.createTextNode(displayText));
103
104
        return link.outerHTML;
105
    };
106
107
    // A render function for our row action buttons
108
    var createActions = function (data, type, row) {
109
        var div = document.createElement('div');
110
        div.setAttribute('class', 'action-buttons');
111
112
        var editButton = document.createElement('button');
113
        editButton.setAttribute('type', 'button');
114
        editButton.setAttribute('class', 'editButton btn btn-xs btn-default');
115
        editButton.setAttribute('data-batch-id', row.id);
116
        editButton.appendChild(document.createTextNode(ill_batch_edit));
117
118
        var deleteButton = document.createElement('button');
119
        deleteButton.setAttribute('type', 'button');
120
        deleteButton.setAttribute('class', 'deleteButton btn btn-xs btn-danger');
121
        deleteButton.setAttribute('data-batch-id', row.id);
122
        deleteButton.appendChild(document.createTextNode(ill_batch_delete));
123
124
        div.appendChild(editButton);
125
        div.appendChild(deleteButton);
126
127
        return div.outerHTML;
128
    };
129
130
    // Add event listeners to our row action buttons
131
    var addEventListeners = function () {
132
        var del = document.querySelectorAll('.deleteButton');
133
        del.forEach(function (el) {
134
            el.addEventListener('click', handleDeleteClick);
135
        });
136
137
        var edit = document.querySelectorAll('.editButton');
138
        edit.forEach(function (elEdit) {
139
            elEdit.addEventListener('click', handleEditClick);
140
        });
141
    };
142
143
    // Remove all added event listeners
144
    var removeEventListeners = function () {
145
        var del = document.querySelectorAll('.deleteButton');
146
        del.forEach(function (el) {
147
            el.removeEventListener('click', handleDeleteClick);
148
        });
149
        window.removeEventListener('load', onload);
150
        window.removeEventListener('beforeunload', removeEventListeners);
151
    };
152
153
    // Handle "Delete" clicks
154
    var handleDeleteClick = function(e) {
155
        var el = e.srcElement;
156
        if (confirm(ill_batch_confirm_delete)) {
157
            deleteBatch(el);
158
        }
159
    };
160
161
    // Handle "Edit" clicks
162
    var handleEditClick = function(e) {
163
        var el = e.srcElement;
164
        var id = el.dataset.batchId;
165
        window.openBatchModal(id);
166
    };
167
168
    // Delete a batch
169
    // - Make the API call
170
    // - Handle errors
171
    // - Update our proxy data
172
    var deleteBatch = function (el) {
173
        var id = el.dataset.batchId;
174
        doBatchApiRequest(
175
            '/' + id,
176
            { method: 'DELETE' }
177
        )
178
        .then(function (response) {
179
            if (!response.ok) {
180
                window.handleApiError(ill_batch_delete_fail);
181
            } else {
182
                removeBatch(el.dataset.batchId);
183
            }
184
        })
185
        .catch(function (response) {
186
            window.handleApiError(ill_batch_delete_fail);
187
        })
188
    };
189
190
    // Remove a batch from our proxy data
191
    var removeBatch = function(id) {
192
        batchesProxy.data = batchesProxy.data.filter(function (batch) {
193
            return batch.id != id;
194
        });
195
    };
196
197
    // Redraw the table
198
    var updateTable = function () {
199
        table.api()
200
            .clear()
201
            .rows.add(batchesProxy.data)
202
            .draw();
203
    };
204
205
})();
(-)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/illrequests',
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 / +15 lines)
Lines 221-226 $(document).ready(function() { Link Here
221
        return row.id_prefix + row.illrequest_id;
221
        return row.id_prefix + row.illrequest_id;
222
    };
222
    };
223
223
224
    // Render function for batch
225
    var createBatch = function (data, type, row) {
226
        if (!row.batch) return;
227
        return '<a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + row.batch_id + '">' + row.batch.name + '</a>';
228
    };
229
224
    // Render function for type
230
    // Render function for type
225
    var createType = function(data, type, row) {
231
    var createType = function(data, type, row) {
226
        if (!row.hasOwnProperty('metadata_Type') || !row.metadata_Type) {
232
        if (!row.hasOwnProperty('metadata_Type') || !row.metadata_Type) {
Lines 291-296 $(document).ready(function() { Link Here
291
            func: createActionLink,
297
            func: createActionLink,
292
            skipSanitize: true
298
            skipSanitize: true
293
        },
299
        },
300
        batch: {
301
            func: createBatch,
302
            skipSanitize: true
303
        },
294
        illrequest_id: {
304
        illrequest_id: {
295
            func: createRequestId
305
            func: createRequestId
296
        },
306
        },
Lines 383-389 $(document).ready(function() { Link Here
383
        (
393
        (
384
            // ILL list requests page
394
            // ILL list requests page
385
            window.location.href.match(/ill\/ill-requests\.pl/) &&
395
            window.location.href.match(/ill\/ill-requests\.pl/) &&
386
            window.location.search.length == 0
396
            (
397
                window.location.search.length == 0 ||
398
                /borrowernumber|batch_id/.test(window.location.search)
399
            )
387
        ) ||
400
        ) ||
388
        // Patron profile page
401
        // Patron profile page
389
        window.location.href.match(/members\/ill-requests\.pl/)
402
        window.location.href.match(/members\/ill-requests\.pl/)
Lines 391-397 $(document).ready(function() { Link Here
391
        var ajax = $.ajax(
404
        var ajax = $.ajax(
392
            '/api/v1/illrequests?embed=metadata,patron,capabilities,library,status_alias,comments,requested_partners'
405
            '/api/v1/illrequests?embed=metadata,patron,capabilities,library,status_alias,comments,requested_partners'
393
            + filterParam
406
            + filterParam
394
        ).done(function() {
407
        ).done(function () {
395
            var data = JSON.parse(ajax.responseText);
408
            var data = JSON.parse(ajax.responseText);
396
            // Make a copy, we'll be removing columns next and need
409
            // Make a copy, we'll be removing columns next and need
397
            // to be able to refer to data that has been removed
410
            // to be able to refer to data that has been removed
(-)a/t/db_dependent/api/v1/illrequests.t (-2 / +85 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 1;
20
use Test::More tests => 2;
21
21
22
use Test::MockModule;
22
use Test::MockModule;
23
use Test::MockObject;
23
use Test::MockObject;
Lines 173-178 subtest 'list() tests' => sub { Link Here
173
    $schema->storage->txn_rollback;
173
    $schema->storage->txn_rollback;
174
};
174
};
175
175
176
subtest 'add() tests' => sub {
177
178
    plan tests => 2;
179
180
    $schema->storage->txn_begin;
181
182
    # create an authorized user
183
    my $patron = $builder->build_object({
184
        class => 'Koha::Patrons',
185
        value => { flags => 2 ** 22 } # 22 => ill
186
    });
187
    my $password = 'thePassword123';
188
    $patron->set_password({ password => $password, skip_validation => 1 });
189
    my $userid = $patron->userid;
190
191
    my $library  = $builder->build_object( { class => 'Koha::Libraries' } );
192
193
    # Create an ILL request
194
    my $illrequest = $builder->build_object(
195
        {
196
            class => 'Koha::Illrequests',
197
            value => {
198
                backend        => 'Mock',
199
                branchcode     => $library->branchcode,
200
                borrowernumber => $patron->borrowernumber,
201
                status         => 'STATUS1',
202
            }
203
        }
204
    );
205
206
    # Mock ILLBackend (as object)
207
    my $backend = Test::MockObject->new;
208
    $backend->set_isa('Koha::Illbackends::Mock');
209
    $backend->set_always('name', 'Mock');
210
    $backend->set_always('capabilities', sub {
211
        return $illrequest;
212
    } );
213
    $backend->mock(
214
        'metadata',
215
        sub {
216
            my ( $self, $rq ) = @_;
217
            return {
218
                ID => $rq->illrequest_id,
219
                Title => $rq->patron->borrowernumber
220
            }
221
        }
222
    );
223
    $backend->mock(
224
        'status_graph', sub {},
225
    );
226
227
    # Mock Koha::Illrequest::load_backend (to load Mocked Backend)
228
    my $illreqmodule = Test::MockModule->new('Koha::Illrequest');
229
    $illreqmodule->mock( 'load_backend',
230
        sub { my $self = shift; $self->{_my_backend} = $backend; return $self }
231
    );
232
233
    $schema->storage->txn_begin;
234
235
    Koha::Illrequests->search->delete;
236
237
    my $body = {
238
        backend => 'Mock',
239
        borrowernumber => $patron->borrowernumber,
240
        branchcode => $library->branchcode,
241
        metadata => {
242
            article_author => "Jessop, E. G.",
243
            article_title => "Sleep",
244
            issn => "0957-4832",
245
            issue => "2",
246
            pages => "89-90",
247
            publisher => "OXFORD UNIVERSITY PRESS",
248
            title => "Journal of public health medicine.",
249
            year => "2001"
250
        }
251
    };
252
253
    ## Authorized user test
254
    $t->post_ok( "//$userid:$password@/api/v1/illrequests" => json => $body)
255
      ->status_is(201);
256
257
    $schema->storage->txn_rollback;
258
};
259
176
sub add_formatted {
260
sub add_formatted {
177
    my $req = shift;
261
    my $req = shift;
178
    my @format_dates = ( 'placed', 'updated', 'completed' );
262
    my @format_dates = ( 'placed', 'updated', 'completed' );
179
- 

Return to bug 30719