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

(-)a/Koha/Booking.pm (-18 / +69 lines)
Lines 253-282 sub to_api_mapping { Link Here
253
sub delete {
253
sub delete {
254
    my ($self) = @_;
254
    my ($self) = @_;
255
255
256
    my $deleted = $self->SUPER::delete($self);
257
    return $deleted;
258
}
259
260
=head3 edit
261
262
This method adds possibility to edit a booking
263
264
=cut
265
266
sub edit {
267
    my ( $self, $params ) = @_;
268
269
    if ( $params->{status} ) {
270
        if ( $params->{status} eq 'cancelled' ) {
271
            $self->cancel( { send_letter => 1 } );
272
        }
273
        $self->_set_status( $params->{status} );
274
    }
275
    $self->store();
276
277
    return $self;
278
}
279
280
=head3 cancel
281
282
This method adds possibility of cancelling a booking (kept in table but flagged with 'cancelled' status)
283
Also adds param to send a letter to the borrower affected by the cancellation
284
285
=cut
286
287
sub cancel {
288
    my ( $self, $params ) = @_;
289
256
    my $patron         = $self->patron;
290
    my $patron         = $self->patron;
257
    my $pickup_library = $self->pickup_library;
291
    my $pickup_library = $self->pickup_library;
258
292
259
    my $letter = C4::Letters::GetPreparedLetter(
293
    if ( $params->{send_letter} ) {
260
        module                 => 'bookings',
294
        my $letter = C4::Letters::GetPreparedLetter(
261
        letter_code            => 'BOOKING_CANCELLATION',
295
            module                 => 'bookings',
262
        message_transport_type => 'email',
296
            letter_code            => 'BOOKING_CANCELLATION',
263
        branchcode             => $pickup_library->branchcode,
297
            message_transport_type => 'email',
264
        lang                   => $patron->lang,
298
            branchcode             => $pickup_library->branchcode,
265
        objects                => { booking => $self }
299
            lang                   => $patron->lang,
266
    );
300
            objects                => { booking => $self }
267
268
    if ($letter) {
269
        C4::Letters::EnqueueLetter(
270
            {
271
                letter                 => $letter,
272
                borrowernumber         => $patron->borrowernumber,
273
                message_transport_type => 'email',
274
            }
275
        );
301
        );
302
303
        if ($letter) {
304
            C4::Letters::EnqueueLetter(
305
                {
306
                    letter                 => $letter,
307
                    borrowernumber         => $patron->borrowernumber,
308
                    message_transport_type => 'email',
309
                }
310
            );
311
        }
276
    }
312
    }
313
}
277
314
278
    my $deleted = $self->SUPER::delete($self);
315
279
    return $deleted;
316
=head3 set_status
317
318
This method changes the status of a booking
319
320
=cut
321
322
sub _set_status {
323
    my ( $self, $new_status ) = @_;
324
325
    my @valid_statuses = qw(pending completed cancelled);
326
    unless ( grep { $_ eq $new_status } @valid_statuses ) {
327
        die "Invalid status: $new_status";
328
    }
329
330
    $self->status($new_status);
280
}
331
}
281
332
282
=head2 Internal methods
333
=head2 Internal methods
(-)a/Koha/Bookings.pm (-2 / +7 lines)
Lines 36-48 Koha::Bookings - Koha Booking object set class Link Here
36
36
37
    $bookings->filter_by_active;
37
    $bookings->filter_by_active;
38
38
39
Will return the bookings that have not ended.
39
Will return the bookings that have not ended and without "CANCELLED" status.
40
40
41
=cut
41
=cut
42
42
43
sub filter_by_active {
43
sub filter_by_active {
44
    my ($self) = @_;
44
    my ($self) = @_;
45
    return $self->search( { end_date => { '>=' => \'NOW()' } } );
45
    return $self->search(
46
        {
47
            end_date => { '>=' => \'NOW()' },
48
            status   => { '!=' => 'cancelled' }
49
        }
50
    );
46
}
51
}
47
52
48
=head2 Internal Methods
53
=head2 Internal Methods
(-)a/Koha/REST/V1/Bookings.pm (-1 / +26 lines)
Lines 37-43 sub list { Link Here
37
    my $c = shift->openapi->valid_input or return;
37
    my $c = shift->openapi->valid_input or return;
38
38
39
    return try {
39
    return try {
40
        my $bookings = $c->objects->search( Koha::Bookings->new );
40
        my $bookings = $c->objects->search( Koha::Bookings->filter_by_active );
41
        return $c->render( status => 200, openapi => $bookings );
41
        return $c->render( status => 200, openapi => $bookings );
42
    } catch {
42
    } catch {
43
        $c->unhandled_exception($_);
43
        $c->unhandled_exception($_);
Lines 148-151 sub delete { Link Here
148
    };
148
    };
149
}
149
}
150
150
151
=head3 edit
152
153
Controller function that editing an existing booking
154
155
=cut
156
157
sub edit {
158
    my $c    = shift->openapi->valid_input or return;
159
    my $body = $c->req->json;
160
161
    my $booking = Koha::Bookings->find( $c->param('booking_id') );
162
    return $c->render_resource_not_found("Booking")
163
        unless $booking;
164
165
    return try {
166
        $booking->edit($body);
167
        return $c->render(
168
            status  => 200,
169
            openapi => $c->objects->to_api($booking),
170
        );
171
    } catch {
172
        $c->unhandled_exception($_);
173
    };
174
}
175
151
1;
176
1;
(-)a/api/v1/swagger/paths/bookings.yaml (+55 lines)
Lines 252-254 Link Here
252
      permissions:
252
      permissions:
253
        circulate: manage_bookings
253
        circulate: manage_bookings
254
    x-mojo-to: Bookings#update
254
    x-mojo-to: Bookings#update
255
  patch:
256
    x-mojo-to: Bookings#edit
257
    operationId: editBooking
258
    tags:
259
      - bookings
260
    summary: Edit booking
261
    parameters:
262
      - $ref: "../swagger.yaml#/parameters/booking_id_pp"
263
      - name: body
264
        in: body
265
        description: A JSON object containing fields to modify
266
        required: true
267
        schema:
268
          type: object
269
          properties:
270
            status:
271
              description: Set booking status
272
              type: string
273
          additionalProperties: false
274
    consumes:
275
      - application/json
276
    produces:
277
      - application/json
278
    responses:
279
      200:
280
        description: Updated booking
281
        schema:
282
          $ref: ../swagger.yaml#/definitions/booking
283
      400:
284
        description: Bad request
285
        schema:
286
          $ref: ../swagger.yaml#/definitions/error
287
      401:
288
        description: Authentication required
289
        schema:
290
          $ref: ../swagger.yaml#/definitions/error
291
      403:
292
        description: Access forbidden
293
        schema:
294
          $ref: ../swagger.yaml#/definitions/error
295
      404:
296
        description: Booking not found
297
        schema:
298
          $ref: ../swagger.yaml#/definitions/error
299
      500:
300
        description: Internal error
301
        schema:
302
          $ref: ../swagger.yaml#/definitions/error
303
      503:
304
        description: Under maintenance
305
        schema:
306
          $ref: ../swagger.yaml#/definitions/error
307
    x-koha-authorization:
308
      permissions:
309
        circulate: manage_bookings
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt (-23 / +57 lines)
Lines 11-16 Link Here
11
    [% t("Koha") | html %]
11
    [% t("Koha") | html %]
12
[% END %]</title>
12
[% END %]</title>
13
[% INCLUDE 'doc-head-close.inc' %]
13
[% INCLUDE 'doc-head-close.inc' %]
14
    <style>
15
        #bookings-timeline .vis-item.vis-range {
16
            &.cancelled {
17
               background: rgba(128, 128, 128, 0.3);
18
            }
19
        }
20
    </style>
14
</head>
21
</head>
15
22
16
<body id="circ_request" class="catalog">
23
<body id="circ_request" class="catalog">
Lines 43-50 Link Here
43
                    <h1>Bookings for [% INCLUDE 'biblio-title-head.inc' %]</h1>
50
                    <h1>Bookings for [% INCLUDE 'biblio-title-head.inc' %]</h1>
44
                    <div class="page-section" id="bookings-timeline"></div>
51
                    <div class="page-section" id="bookings-timeline"></div>
45
                    <div class="page-section">
52
                    <div class="page-section">
46
                        <fieldset class="action filters" style="cursor:pointer;">
53
                        <fieldset class="action filters" style="cursor:pointer; display: flex; flex-direction: column;">
47
                            <a id="expired_filter" class="filtered"><i class="fa fa-bars"></i> Show expired</a>
54
                            <a id="expired_filter" class="filtered"><i class="fa fa-bars"></i> Show expired</a>
55
                            <a id="cancelled_filter" class="filtered"><i class="fa fa-bars"></i> Show cancelled</a>
48
                        </fieldset>
56
                        </fieldset>
49
57
50
                        <table id="bookings_table"></table>
58
                        <table id="bookings_table"></table>
Lines 122-136 Link Here
122
                            url: false
130
                            url: false
123
                        }),
131
                        }),
124
                        [% IF CAN_user_circulate_manage_bookings %]
132
                        [% IF CAN_user_circulate_manage_bookings %]
125
                        editable: { remove: true, updateTime: true },
133
                        editable: booking.status != 'cancelled' ? { remove: true, updateTime: true } : false,
126
                        [% ELSE %]
134
                        [% ELSE %]
127
                        editable: false,
135
                        editable: false,
128
                        [% END %]
136
                        [% END %]
129
                        type: 'range',
137
                        type: 'range',
130
                        group: booking.item_id ? booking.item_id : 0
138
                        group: booking.item_id ? booking.item_id : 0,
139
                        className: booking.status,
131
                    });
140
                    });
132
                }
141
                }
133
142
143
                const cancelledItems = document.querySelectorAll('#bookings-timeline .vis-item.vis-range.cancelled');
144
                cancelledItems.forEach(item => {
145
                    item.style.background = 'repeating-linear-gradient(' +
146
                        '135deg,' +
147
                        'rgba(211, 211, 211, 0.5) 0,' +
148
                        'rgba(211, 211, 211, 0.5) 10px,' +
149
                        'transparent 10px,' +
150
                        'transparent 20px' +
151
                    ');';
152
                });
153
134
                var container = document.getElementById('bookings-timeline');
154
                var container = document.getElementById('bookings-timeline');
135
                var options = {
155
                var options = {
136
                    stack: true,
156
                    stack: true,
Lines 206-211 Link Here
206
        );
226
        );
207
227
208
        let filter_expired = true;
228
        let filter_expired = true;
229
        let filter_cancelled = false;
209
        let additional_filters = {
230
        let additional_filters = {
210
            end_date: function(){
231
            end_date: function(){
211
                if ( filter_expired ) {
232
                if ( filter_expired ) {
Lines 214-219 Link Here
214
                } else {
235
                } else {
215
                    return;
236
                    return;
216
                }
237
                }
238
            },
239
            status: function(){
240
                if ( filter_cancelled ) {
241
                    return { "=": 'cancelled' }
242
                } else {
243
                    return;
244
                }
217
            }
245
            }
218
        };
246
        };
219
247
Lines 241-250 Link Here
241
                visible: false,
269
                visible: false,
242
                render: function (data, type, row, meta) {
270
                render: function (data, type, row, meta) {
243
                    let is_expired = dayjs(row.end_date).isBefore(new Date());
271
                    let is_expired = dayjs(row.end_date).isBefore(new Date());
272
                    let is_cancelled = row.status == 'cancelled' ? 1 : 0;
244
                    if (is_expired) {
273
                    if (is_expired) {
245
                        return '<span class="badge rounded-pill bg-secondary">' + _("Expired") + '</span>';
274
                        return '<span class="badge rounded-pill bg-secondary">' + _("Expired") + '</span>';
246
                    }
275
                    }
247
276
                    if (is_cancelled) {
277
                        return '<span class="badge rounded-pill bg-secondary">' + _("Cancelled") + '</span>';
278
                    }
248
                    return '<span class="badge rounded-pill bg-success">' + _("Active") + '</span>';
279
                    return '<span class="badge rounded-pill bg-success">' + _("Active") + '</span>';
249
                }
280
                }
250
            },
281
            },
Lines 309-342 Link Here
309
                "orderable": false,
340
                "orderable": false,
310
                "render": function(data, type, row, meta) {
341
                "render": function(data, type, row, meta) {
311
                    let result = "";
342
                    let result = "";
343
                    let is_cancelled = row.status == 'cancelled' ? 1 : 0;
312
                    [% IF CAN_user_circulate_manage_bookings %]
344
                    [% IF CAN_user_circulate_manage_bookings %]
313
                    result += '<button type="button" class="btn btn-default btn-xs edit-action" data-bs-toggle="modal" data-bs-target="#placeBookingModal" data-booking="'+row.booking_id+'" data-biblionumber="[% biblionumber | uri %]" data-itemnumber="'+row.item_id+'" data-patron="'+row.patron_id+'" data-pickup_library="'+row.pickup_library_id+'" data-start_date="'+row.start_date+'" data-end_date="'+row.end_date+'"><i class="fa fa-pencil" aria-hidden="true"></i> '+_("Edit")+'</button>';
345
                    if( !is_cancelled ){
314
                    result += '<button type="button" class="btn btn-default btn-xs cancel-action" data-bs-toggle="modal" data-bs-target="#cancelBookingModal" data-booking="'+row.booking_id+'"><i class="fa fa-trash" aria-hidden="true"></i> '+_("Cancel")+'</button>';
346
                        result += '<button type="button" class="btn btn-default btn-xs edit-action" data-bs-toggle="modal" data-bs-target="#placeBookingModal" data-booking="'+row.booking_id+'" data-biblionumber="[% biblionumber | uri %]" data-itemnumber="'+row.item_id+'" data-patron="'+row.patron_id+'" data-pickup_library="'+row.pickup_library_id+'" data-start_date="'+row.start_date+'" data-end_date="'+row.end_date+'"><i class="fa fa-pencil" aria-hidden="true"></i> '+_("Edit")+'</button>';
347
                        result += '<button type="button" class="btn btn-default btn-xs cancel-action" data-bs-toggle="modal" data-bs-target="#cancelBookingModal" data-booking="'+row.booking_id+'"><i class="fa fa-trash" aria-hidden="true"></i> '+_("Cancel")+'</button>';
348
                    }
315
                    [% END %]
349
                    [% END %]
316
                    return result;
350
                    return result;
317
                }
351
                }
318
            }]
352
            }]
319
        }, [], 0, additional_filters);
353
        }, [], 0, additional_filters);
320
354
321
        var txtActivefilter = _("Show expired");
355
        function setupFilter(filterId, activeText, inactiveText, filterVariable) {
322
        var txtInactivefilter = _("Hide expired");
356
            $(filterId).on("click", function() {
323
        $("#expired_filter").on("click", function() {
357
                if ($(this).hasClass('filtered')) {
324
            if ($(this).hasClass('filtered')){
358
                    filterVariable = false;
325
                filter_expired = false;
359
                    $(this).html('<i class="fa fa-filter"></i> ' + inactiveText);
326
                $(this).html('<i class="fa fa-filter"></i> '+txtInactivefilter);
360
                } else {
327
            } else {
361
                    filterVariable = true;
328
                filter_expired = true;
362
                    $(this).html('<i class="fa fa-bars"></i> ' + activeText);
329
                $(this).html('<i class="fa fa-bars"></i> '+txtActivefilter);
363
                }
330
            }
331
364
332
            bookings_table.DataTable().ajax.reload(() => {
365
                bookings_table.DataTable().ajax.reload(() => {
333
                bookings_table
366
                    bookings_table.DataTable().column("status:name").visible(!filterVariable, false);
334
                    .DataTable()
367
                });
335
                    .column("status:name")
368
                $(this).toggleClass('filtered');
336
                    .visible(!filter_expired, false);
337
            });
369
            });
338
            $(this).toggleClass('filtered');
370
        }
339
        });
371
372
        setupFilter("#expired_filter", _("Show expired"), _("Hide expired"), filter_expired);
373
        setupFilter("#cancelled_filter", _("Show cancelled"), _("Hide cancelled"), filter_cancelled);
340
374
341
    });
375
    });
342
</script>
376
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/cancel_booking_modal.js (-6 / +8 lines)
Lines 10-21 $("#cancelBookingForm").on('submit', function(e) { Link Here
10
    var booking_id = $('#cancel_booking_id').val();
10
    var booking_id = $('#cancel_booking_id').val();
11
    var url = '/api/v1/bookings/'+booking_id;
11
    var url = '/api/v1/bookings/'+booking_id;
12
12
13
    var deleting = $.ajax({
13
    var cancelling = $.ajax({
14
        'method': "DELETE",
14
        'method': "PATCH",
15
        'url': url
15
        'url': url,
16
        'data': JSON.stringify({"status": "cancelled"}),
17
        'contentType': "application/json"
16
    });
18
    });
17
19
18
    deleting.done(function(data) {
20
21
    cancelling.done(function(data) {
19
        cancel_success = 1;
22
        cancel_success = 1;
20
        if (bookings_table) {
23
        if (bookings_table) {
21
            bookings_table.api().ajax.reload();
24
            bookings_table.api().ajax.reload();
Lines 27-33 $("#cancelBookingForm").on('submit', function(e) { Link Here
27
        $('#cancelBookingModal').modal('hide');
30
        $('#cancelBookingModal').modal('hide');
28
    });
31
    });
29
32
30
    deleting.fail(function(data) {
33
    cancelling.fail(function(data) {
31
        $('#cancel_booking_result').replaceWith('<div id="booking_result" class="alert alert-danger">'+__("Failure")+'</div>');
34
        $('#cancel_booking_result').replaceWith('<div id="booking_result" class="alert alert-danger">'+__("Failure")+'</div>');
32
    });
35
    });
33
});
36
});
34
- 

Return to bug 38175