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

(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt (-203 / +201 lines)
Lines 77-102 Link Here
77
        });
77
        });
78
    </script>
78
    </script>
79
    <script>
79
    <script>
80
        $(document).ready(function(){
80
        $(document).ready(function () {
81
82
            var items = $.ajax({
81
            var items = $.ajax({
83
                url: '/api/v1/biblios/%s/items?bookable=1'.format(biblionumber),
82
                url: "/api/v1/biblios/%s/items?bookable=1".format(biblionumber),
84
                dataType: 'json',
83
                dataType: "json",
85
                type: 'GET'
84
                type: "GET",
86
            });
85
            });
87
            var bookings = $.ajax({
86
            var bookings = $.ajax({
88
                url: '/api/v1/biblios/%s/bookings?_per_page=-1'.format(biblionumber),
87
                url: "/api/v1/biblios/%s/bookings?_per_page=-1".format(biblionumber),
89
                headers: { 'x-koha-embed': 'patron' },
88
                headers: { "x-koha-embed": "patron" },
90
                dataType: 'json',
89
                dataType: "json",
91
                type: 'GET',
90
                type: "GET",
92
            });
91
            });
93
92
94
            $.when(items, bookings).then(
93
            $.when(items, bookings).then(
95
                function(items, bookings){
94
                function (items, bookings) {
96
                    var itemsSet = new vis.DataSet([{
95
                    var itemsSet = new vis.DataSet([
97
                        id: 0,
96
                        {
98
                        content: _("Record level")
97
                            id: 0,
99
                    }]);
98
                            content: _("Record level"),
99
                        },
100
                    ]);
100
                    for (item of items[0]) {
101
                    for (item of items[0]) {
101
                        itemsSet.add({
102
                        itemsSet.add({
102
                            id: item.item_id,
103
                            id: item.item_id,
Lines 105-115 Link Here
105
                    }
106
                    }
106
107
107
                    var bookingsSet = new vis.DataSet();
108
                    var bookingsSet = new vis.DataSet();
108
                    for (booking of bookings[0]){
109
                    for (booking of bookings[0]) {
109
                        const isActive = ["new", "pending", "active"].includes(booking.status);
110
                        const isActive = ["new", "pending", "active"].includes(booking.status);
110
                        const patronContent = $patron_to_html(booking.patron, {
111
                        const patronContent = $patron_to_html(booking.patron, {
111
                            display_cardnumber: true,
112
                            display_cardnumber: true,
112
                            url: false
113
                            url: false,
113
                        });
114
                        });
114
                        bookingsSet.add({
115
                        bookingsSet.add({
115
                            id: booking.booking_id,
116
                            id: booking.booking_id,
Lines 120-144 Link Here
120
                            end: dayjs(booking.end_date).toDate(),
121
                            end: dayjs(booking.end_date).toDate(),
121
                            content: !isActive ? `<s>${patronContent}</s>` : patronContent,
122
                            content: !isActive ? `<s>${patronContent}</s>` : patronContent,
122
                            className: booking.status === "cancelled" ? "cancelled" : "",
123
                            className: booking.status === "cancelled" ? "cancelled" : "",
123
                            ...(permissions.CAN_user_circulate_manage_bookings
124
                            ...(permissions.CAN_user_circulate_manage_bookings ? { editable: !["cancelled", "completed"].includes(booking.status) ? { remove: true, updateTime: true } : false } : { editable: false }),
124
                                ? {editable: !["cancelled", "completed"].includes(booking.status) ? { remove: true, updateTime: true } : false}
125
                            type: "range",
125
                                : {editable: false}
126
                            ),
127
                            type: 'range',
128
                            group: booking.item_id ?? 0,
126
                            group: booking.item_id ?? 0,
129
                        });
127
                        });
130
                    }
128
                    }
131
129
132
                    var container = document.getElementById('bookings-timeline');
130
                    var container = document.getElementById("bookings-timeline");
133
                    var options = {
131
                    var options = {
134
                        stack: true,
132
                        stack: true,
135
                        verticalScroll: true,
133
                        verticalScroll: true,
136
                        orientation: {
134
                        orientation: {
137
                            axis: "both",
135
                            axis: "both",
138
                            item: "top"
136
                            item: "top",
139
                        },
137
                        },
140
                        timeAxis: {scale: 'day', step: 1},
138
                        timeAxis: { scale: "day", step: 1 },
141
                        dataAttributes: [ 'booking' ],
139
                        dataAttributes: ["booking"],
142
140
143
                        // always snap to full days, independent of the scale
141
                        // always snap to full days, independent of the scale
144
                        snap: function (date, scale, step) {
142
                        snap: function (date, scale, step) {
Lines 151-165 Link Here
151
                        // prevent overlapping bookings
149
                        // prevent overlapping bookings
152
                        onMoving: function (item, callback) {
150
                        onMoving: function (item, callback) {
153
                            var overlapping = bookingsSet.get({
151
                            var overlapping = bookingsSet.get({
154
                                filter: function(testItem) {
152
                                filter: function (testItem) {
155
                                    if (testItem.id == item.id) {
153
                                    if (testItem.id == item.id) {
156
                                        return false;
154
                                        return false;
157
                                    }
155
                                    }
158
                                    if (testItem.group != item.group) {
156
                                    if (testItem.group != item.group) {
159
                                        return false;
157
                                        return false;
160
                                    }
158
                                    }
161
                                    return ((item.start < testItem.end) && (item.end > testItem.start));
159
                                    return item.start < testItem.end && item.end > testItem.start;
162
                                }
160
                                },
163
                            });
161
                            });
164
162
165
                            if (overlapping.length == 0) {
163
                            if (overlapping.length == 0) {
Lines 172-181 Link Here
172
                            let startDate = dayjs(data.start);
170
                            let startDate = dayjs(data.start);
173
171
174
                            // set end datetime hours and minutes to the end of the day
172
                            // set end datetime hours and minutes to the end of the day
175
                            let endDate = dayjs(data.end).endOf('day');
173
                            let endDate = dayjs(data.end).endOf("day");
176
174
177
                            $('#placeBookingModal').modal('show', $('<button data-booking="'+data.id+'"  data-biblionumber="' + biblionumber + '"  data-itemnumber="'+data.group+'" data-patron="'+data.patron+'" data-pickup_library="'+data.pickup_library+'" data-start_date="'+startDate.toISOString()+'" data-end_date="'+endDate.toISOString()+'">'));
175
                            $("#placeBookingModal").modal(
178
                            $('#placeBookingModal').on('hide.bs.modal', function(e) {
176
                                "show",
177
                                $(
178
                                    '<button data-booking="' +
179
                                        data.id +
180
                                        '"  data-biblionumber="' +
181
                                        biblionumber +
182
                                        '"  data-itemnumber="' +
183
                                        data.group +
184
                                        '" data-patron="' +
185
                                        data.patron +
186
                                        '" data-pickup_library="' +
187
                                        data.pickup_library +
188
                                        '" data-start_date="' +
189
                                        startDate.toISOString() +
190
                                        '" data-end_date="' +
191
                                        endDate.toISOString() +
192
                                        '">'
193
                                )
194
                            );
195
                            $("#placeBookingModal").on("hide.bs.modal", function (e) {
179
                                if (update_success) {
196
                                if (update_success) {
180
                                    update_success = 0;
197
                                    update_success = 0;
181
                                    callback(data);
198
                                    callback(data);
Lines 185-194 Link Here
185
                            });
202
                            });
186
                        },
203
                        },
187
                        onRemove: function (item, callback) {
204
                        onRemove: function (item, callback) {
188
                            const button = document.createElement('button');
205
                            const button = document.createElement("button");
189
                            button.dataset.booking = item.booking || item.id;
206
                            button.dataset.booking = item.booking || item.id;
190
                            $('#cancelBookingModal').modal('show', button);
207
                            $("#cancelBookingModal").modal("show", button);
191
                            $('#cancelBookingModal').on('hide.bs.modal', function(e) {
208
                            $("#cancelBookingModal").on("hide.bs.modal", function (e) {
192
                                if (cancel_success) {
209
                                if (cancel_success) {
193
                                    cancel_success = 0;
210
                                    cancel_success = 0;
194
                                    callback(item);
211
                                    callback(item);
Lines 196-206 Link Here
196
                                    callback(null);
213
                                    callback(null);
197
                                }
214
                                }
198
                            });
215
                            });
199
                        }
216
                        },
200
                    };
217
                    };
201
                    timeline = new vis.Timeline(container, bookingsSet, itemsSet, options);
218
                    timeline = new vis.Timeline(container, bookingsSet, itemsSet, options);
202
                },
219
                },
203
                function(jqXHR, textStatus, errorThrown){
220
                function (jqXHR, textStatus, errorThrown) {
204
                    console.log("Fetch failed");
221
                    console.log("Fetch failed");
205
                }
222
                }
206
            );
223
            );
Lines 211-217 Link Here
211
                })
228
                })
212
                .build({
229
                .build({
213
                    end_date: ({ filters, isNotApplied }) => {
230
                    end_date: ({ filters, isNotApplied }) => {
214
                        if (isNotApplied(filters['filter-expired'])) {
231
                        if (isNotApplied(filters["filter-expired"])) {
215
                            let today = new Date();
232
                            let today = new Date();
216
                            return { ">=": today.toISOString() };
233
                            return { ">=": today.toISOString() };
217
                        }
234
                        }
Lines 219-374 Link Here
219
                    status: ({ filters, isNotApplied }) => {
236
                    status: ({ filters, isNotApplied }) => {
220
                        const defaults = ["new", "pending", "active"];
237
                        const defaults = ["new", "pending", "active"];
221
                        const filtered = [...defaults];
238
                        const filtered = [...defaults];
222
                        if (isNotApplied(filters['filter-cancelled'])) {
239
                        if (isNotApplied(filters["filter-cancelled"])) {
223
                            filtered.push("cancelled");
240
                            filtered.push("cancelled");
224
                        }
241
                        }
225
                        if (isNotApplied(filters['filter-completed'])) {
242
                        if (isNotApplied(filters["filter-completed"])) {
226
                            filtered.push("completed");
243
                            filtered.push("completed");
227
                        }
244
                        }
228
                        return { "-in": filtered };
245
                        return { "-in": filtered };
229
                    }
246
                    },
230
                });
247
                });
231
248
232
233
            var bookings_table_url = "/api/v1/biblios/%s/bookings".format(biblionumber);
249
            var bookings_table_url = "/api/v1/biblios/%s/bookings".format(biblionumber);
234
            bookings_table = $('#bookings_table').kohaTable({
250
            bookings_table = $("#bookings_table").kohaTable(
235
                ajax: {
236
                    "url": bookings_table_url
237
                },
238
                embed: [
239
                    "item",
240
                    "patron",
241
                    "pickup_library"
242
                ],
243
                columns: [{
244
                    data: "booking_id",
245
                    title: _("Booking ID"),
246
                    visible: false
247
                },
248
                {
251
                {
249
                    data: "",
252
                    ajax: {
250
                    title: _("Status"),
253
                        url: bookings_table_url,
251
                    name: "status",
254
                    },
252
                    searchable: false,
255
                    embed: ["item", "patron", "pickup_library"],
253
                    orderable: false,
256
                    columns: [
254
                    visible: true,
257
                        {
255
                    render: function (data, type, row, meta) {
258
                            data: "booking_id",
256
                        const isExpired = date => dayjs(date).isBefore(new Date());
259
                            title: _("Booking ID"),
257
                        const isActive = (startDate, endDate) => {
260
                            visible: false,
258
                            const now = dayjs();
261
                        },
259
                            return (
262
                        {
260
                                now.isAfter(dayjs(startDate)) &&
263
                            data: "",
261
                                now.isBefore(dayjs(endDate).add(1, "day"))
264
                            title: _("Status"),
262
                            );
265
                            name: "status",
263
                        };
266
                            searchable: false,
267
                            orderable: false,
268
                            visible: true,
269
                            render: function (data, type, row, meta) {
270
                                const isExpired = date => dayjs(date).isBefore(new Date());
271
                                const isActive = (startDate, endDate) => {
272
                                    const now = dayjs();
273
                                    return now.isAfter(dayjs(startDate)) && now.isBefore(dayjs(endDate).add(1, "day"));
274
                                };
264
275
265
                        const statusMap = {
276
                                const statusMap = {
266
                            new: () => {
277
                                    new: () => {
267
                                if (isExpired(row.end_date)) {
278
                                        if (isExpired(row.end_date)) {
268
                                    return _("Expired");
279
                                            return _("Expired");
269
                                }
280
                                        }
270
281
271
                                if (isActive(row.start_date, row.end_date)) {
282
                                        if (isActive(row.start_date, row.end_date)) {
272
                                    return _("Active");
283
                                            return _("Active");
273
                                }
284
                                        }
274
285
275
                                if (dayjs(row.start_date).isAfter(new Date())) {
286
                                        if (dayjs(row.start_date).isAfter(new Date())) {
276
                                    return _("Pending");
287
                                            return _("Pending");
277
                                }
288
                                        }
278
289
279
                                return _("New");
290
                                        return _("New");
280
                            },
291
                                    },
281
                            cancelled: () =>
292
                                    cancelled: () => [_("Cancelled"), row.cancellation_reason].filter(Boolean).join(": "),
282
                                [_("Cancelled"), row.cancellation_reason]
293
                                    completed: () => _("Completed"),
283
                                    .filter(Boolean)
294
                                };
284
                                    .join(": "),
285
                            completed: () => _("Completed"),
286
                        };
287
295
288
                        const statusText = statusMap[row.status]
296
                                const statusText = statusMap[row.status] ? statusMap[row.status]() : _("Unknown");
289
                            ? statusMap[row.status]()
290
                            : _("Unknown");
291
297
292
                        const classMap = [
298
                                const classMap = [
293
                            { status: _("Expired"), class: "bg-secondary" },
299
                                    { status: _("Expired"), class: "bg-secondary" },
294
                            { status: _("Cancelled"), class: "bg-secondary" },
300
                                    { status: _("Cancelled"), class: "bg-secondary" },
295
                            { status: _("Pending"), class: "bg-warning" },
301
                                    { status: _("Pending"), class: "bg-warning" },
296
                            { status: _("Active"), class: "bg-primary" },
302
                                    { status: _("Active"), class: "bg-primary" },
297
                            { status: _("Completed"), class: "bg-info" },
303
                                    { status: _("Completed"), class: "bg-info" },
298
                            { status: _("New"), class: "bg-success" },
304
                                    { status: _("New"), class: "bg-success" },
299
                        ];
305
                                ];
300
306
301
                        const badgeClass =
307
                                const badgeClass = classMap.find(mapping => statusText.startsWith(mapping.status))?.class || "bg-secondary";
302
                            classMap.find(mapping => statusText.startsWith(mapping.status))
303
                                ?.class || "bg-secondary";
304
308
305
                        return `<span class="badge rounded-pill ${badgeClass}">${statusText}</span>`;
309
                                return `<span class="badge rounded-pill ${badgeClass}">${statusText}</span>`;
306
                    }
310
                            },
307
                },
311
                        },
308
                {
312
                        {
309
                    data: "item.external_id",
313
                            data: "item.external_id",
310
                    title: _("Item"),
314
                            title: _("Item"),
311
                    searchable: true,
315
                            searchable: true,
312
                    orderable: true,
316
                            orderable: true,
313
                    defaultContent: _("Any item"),
317
                            defaultContent: _("Any item"),
314
                    render: function(data,type,row,meta) {
318
                            render: function (data, type, row, meta) {
315
                        if ( row.item ) {
319
                                if (row.item) {
316
                            return row.item.external_id + " (" + row.booking_id + ")";
320
                                    return row.item.external_id + " (" + row.booking_id + ")";
317
                        } else {
321
                                } else {
318
                            return null;
322
                                    return null;
319
                        }
323
                                }
320
                    }
324
                            },
321
                },
325
                        },
322
                {
326
                        {
323
                    data: "patron.firstname:patron.surname",
327
                            data: "patron.firstname:patron.surname",
324
                    title: _("Patron"),
328
                            title: _("Patron"),
325
                    searchable: true,
329
                            searchable: true,
326
                    orderable: true,
330
                            orderable: true,
327
                    render: function(data, type, row, meta) {
331
                            render: function (data, type, row, meta) {
328
                        return $patron_to_html(row.patron, {
332
                                return $patron_to_html(row.patron, {
329
                            display_cardnumber: true,
333
                                    display_cardnumber: true,
330
                            url: true
334
                                    url: true,
331
                        });
335
                                });
332
                    }
336
                            },
333
                },
337
                        },
334
                {
338
                        {
335
                    data: "pickup_library.name",
339
                            data: "pickup_library.name",
336
                    title: _("Pickup library"),
340
                            title: _("Pickup library"),
337
                    searchable: true,
341
                            searchable: true,
338
                    orderable: true,
342
                            orderable: true,
339
                    render: function(data, type, row, meta) {
343
                            render: function (data, type, row, meta) {
340
                        return row.pickup_library.name;
344
                                return row.pickup_library.name;
341
                    }
345
                            },
342
                },
346
                        },
343
                {
347
                        {
344
                    data: "start_date",
348
                            data: "start_date",
345
                    title: _("Start date"),
349
                            title: _("Start date"),
346
                    searchable: true,
350
                            searchable: true,
347
                    orderable: true,
351
                            orderable: true,
348
                    render: function(data, type, row, meta) {
352
                            render: function (data, type, row, meta) {
349
                        return $date(row.start_date);
353
                                return $date(row.start_date);
350
                    }
354
                            },
351
                },
355
                        },
352
                {
356
                        {
353
                    data: "end_date",
357
                            data: "end_date",
354
                    title: _("End date"),
358
                            title: _("End date"),
355
                    searchable: true,
359
                            searchable: true,
356
                    orderable: true,
360
                            orderable: true,
357
                    render: function(data, type, row, meta) {
361
                            render: function (data, type, row, meta) {
358
                        return $date(row.end_date);
362
                                return $date(row.end_date);
359
                    }
363
                            },
360
                },
364
                        },
361
                {
365
                        {
362
                    data: "",
366
                            data: "",
363
                    title: _("Actions"),
367
                            title: _("Actions"),
364
                    class:"actions",
368
                            class: "actions",
365
                    searchable: false,
369
                            searchable: false,
366
                    orderable: false,
370
                            orderable: false,
367
                    render: function(data, type, row, meta) {
371
                            render: function (data, type, row, meta) {
368
                        let result = "";
372
                                let result = "";
369
                        const is_readonly = ["cancelled", "completed"].includes(row.status);
373
                                const is_readonly = ["cancelled", "completed"].includes(row.status);
370
                        if (permissions.CAN_user_circulate_manage_bookings && !is_readonly) {
374
                                if (permissions.CAN_user_circulate_manage_bookings && !is_readonly) {
371
                            result += `
375
                                    result += `
372
                                <button type="button" class="btn btn-default btn-xs edit-action"
376
                                <button type="button" class="btn btn-default btn-xs edit-action"
373
                                    data-bs-toggle="modal"
377
                                    data-bs-toggle="modal"
374
                                    data-bs-target="#placeBookingModal"
378
                                    data-bs-target="#placeBookingModal"
Lines 384-400 Link Here
384
                                    <i class="fa fa-pencil" aria-hidden="true"></i> %s
388
                                    <i class="fa fa-pencil" aria-hidden="true"></i> %s
385
                                </button>
389
                                </button>
386
                            `.format(
390
                            `.format(
387
                                escape_str(row.booking_id),
391
                                        escape_str(row.booking_id),
388
                                escape_str(biblionumber),
392
                                        escape_str(biblionumber),
389
                                escape_str(row.item_id),
393
                                        escape_str(row.item_id),
390
                                escape_str(row.patron_id),
394
                                        escape_str(row.patron_id),
391
                                escape_str(row.pickup_library_id),
395
                                        escape_str(row.pickup_library_id),
392
                                escape_str(row.start_date),
396
                                        escape_str(row.start_date),
393
                                escape_str(row.end_date),
397
                                        escape_str(row.end_date),
394
                                escape_str(row.item.item_type_id),
398
                                        escape_str(row.item.item_type_id),
395
                                _("Edit")
399
                                        _("Edit")
396
                            );
400
                                    );
397
                            result += `
401
                                    result += `
398
                                <button type="button" class="btn btn-default btn-xs cancel-action"
402
                                <button type="button" class="btn btn-default btn-xs cancel-action"
399
                                    data-bs-toggle="modal"
403
                                    data-bs-toggle="modal"
400
                                    data-bs-target="#cancelBookingModal"
404
                                    data-bs-target="#cancelBookingModal"
Lines 402-413 Link Here
402
                                >
406
                                >
403
                                    <i class="fa fa-trash" aria-hidden="true"></i> %s
407
                                    <i class="fa fa-trash" aria-hidden="true"></i> %s
404
                                </button>
408
                                </button>
405
                            `.format(
409
                            `.format(escape_str(row.booking_id), _("Cancel"));
406
                                escape_str(row.booking_id),
410
                                    const csrf_token = $('meta[name="csrf-token"]').attr("content");
407
                                _("Cancel")
411
                                    result += `
408
                            );
409
                            const csrf_token = $('meta[name="csrf-token"]').attr("content");
410
                            result += `
411
                                <form name="checkout-transform" method="post" action="/cgi-bin/koha/circ/circulation.pl?borrowernumber=%s">
412
                                <form name="checkout-transform" method="post" action="/cgi-bin/koha/circ/circulation.pl?borrowernumber=%s">
412
                                    <input type="hidden" name="csrf_token" value="${csrf_token}" />
413
                                    <input type="hidden" name="csrf_token" value="${csrf_token}" />
413
                                    <input type="hidden" name="op" value="cud-checkout"/>
414
                                    <input type="hidden" name="op" value="cud-checkout"/>
Lines 418-442 Link Here
418
                                       <i class="fa fa-check-circle" aria-hidden="true"></i> %s
419
                                       <i class="fa fa-check-circle" aria-hidden="true"></i> %s
419
                                    </button>
420
                                    </button>
420
                                </form>
421
                                </form>
421
                            `.format(
422
                            `.format(escape_str(row.patron_id), escape_str(row.patron_id), escape_str(row.item.external_id), escape_str(row.end_date), _("Transform to checkout"));
422
                                escape_str(row.patron_id),
423
                                }
423
                                escape_str(row.patron_id),
424
                                return result;
424
                                escape_str(row.item.external_id),
425
                            },
425
                                escape_str(row.end_date),
426
                        },
426
                                _("Transform to checkout")
427
                    ],
427
                            );
428
                },
428
                        }
429
                undefined,
429
                        return result;
430
                0,
430
                    }
431
                additional_filters
431
                }]
432
            );
432
            }, undefined, 0, additional_filters);
433
433
434
            $("#cancellation-reason").comboBox({
434
            $("#cancellation-reason").comboBox({
435
                displayProperty: 'name',
435
                displayProperty: "name",
436
                placeholder: _("Select or type a reason"),
436
                placeholder: _("Select or type a reason"),
437
                context: 'modal',
437
                context: "modal",
438
            });
438
            });
439
440
        });
439
        });
441
    </script>
440
    </script>
442
[% END %]
441
[% END %]
443
- 

Return to bug 41565