@@ -, +, @@ catalog, `editcatalogue`. home page if you have the `editcatalogue` permission and not if you do not. of 'catalog concerns pending' on the intranet main page if you have the `editcatalogue` permission. link on the main page to view the new concerns management page. modal with the option to add an update or resolve the concern. concern triggers a notice to be sent to the opac user who first reported the issue. --- C4/Letters.pm | 7 + Koha/REST/V1/Tickets.pm | 98 ++++++ Koha/Ticket/Update.pm | 11 + admin/columns_settings.yml | 17 + api/v1/swagger/definitions/ticket.yaml | 5 + api/v1/swagger/definitions/ticket_update.yaml | 30 ++ api/v1/swagger/paths/tickets.yaml | 103 ++++++ api/v1/swagger/swagger.yaml | 4 + cataloguing/concerns.pl | 37 +++ .../data/mysql/atomicupdate/bug_31028.pl | 16 + .../mysql/en/mandatory/sample_notices.yml | 36 +++ .../prog/css/src/staff-global.scss | 4 + .../prog/en/includes/cat-menu.inc | 7 +- .../en/modules/cataloguing/cataloging-home.tt | 8 +- .../prog/en/modules/cataloguing/concerns.tt | 306 ++++++++++++++++++ .../prog/en/modules/intranet-main.tt | 9 +- koha-tmpl/intranet-tmpl/prog/js/datatables.js | 16 +- mainpage.pl | 12 + 18 files changed, 718 insertions(+), 8 deletions(-) create mode 100644 api/v1/swagger/definitions/ticket_update.yaml create mode 100755 cataloguing/concerns.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt --- a/C4/Letters.pm +++ a/C4/Letters.pm @@ -739,6 +739,7 @@ sub _parseletter_sth { ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" : ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" : ($table eq 'tickets' ) ? "SELECT * FROM $table WHERE id = ?" : + ($table eq 'ticket_updates' ) ? "SELECT * FROM $table WHERE id = ?" : ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" : ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" : ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" : @@ -1736,6 +1737,12 @@ sub _get_tt_params { plural => 'tickets', pk => 'id', }, + ticket_updates => { + module => 'Koha::Ticket::Updates', + singular => 'ticket_update', + plural => 'ticket_updates', + pk => 'id', + }, issues => { module => 'Koha::Checkouts', singular => 'checkout', --- a/Koha/REST/V1/Tickets.pm +++ a/Koha/REST/V1/Tickets.pm @@ -147,4 +147,102 @@ sub delete { }; } +=head3 list_updates + +=cut + +sub list_updates { + my $c = shift->openapi->valid_input or return; + + return try { + my $ticket = Koha::Tickets->find( $c->validation->param('ticket_id') ); + unless ($ticket) { + return $c->render( + status => 404, + openapi => { error => "Ticket not found" } + ); + } + + my $updates_set = $ticket->updates; + my $updates = $c->objects->search($updates_set); + return $c->render( status => 200, openapi => $updates ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 add_update + +=cut + +sub add_update { + my $c = shift->openapi->valid_input or return; + + my $ticket_id_param = $c->validation->param('ticket_id'); + my $ticket_update = $c->validation->param('body'); + $ticket_update->{ticket_id} //= $ticket_id_param; + + if ( $ticket_update->{ticket_id} != $ticket_id_param ) { + return $c->render( + status => 400, + openapi => { error => "Ticket Mismatch" } + ); + } + + return try { + my $state = delete $ticket_update->{state}; + + # Store update + my $update = Koha::Ticket::Update->new_from_api($ticket_update)->store; + $update->discard_changes; + + # Update ticket state if needed + if ( defined($state) && $state eq 'resolved' ) { + my $ticket = $update->ticket; + $ticket->set( + { + resolver_id => $update->user_id, + resolved_date => $update->date + } + )->store; + } + + # Optionally add to message_queue here to notify reporter + if ( $update->public ) { + my $notice = + ( defined($state) && $state eq 'resolved' ) + ? 'TICKET_RESOLVE' + : 'TICKET_UPDATE'; + my $letter = C4::Letters::GetPreparedLetter( + module => 'catalog', + letter_code => $notice, + branchcode => $update->user->branchcode, + tables => { ticket_updates => $update->id } + ); + + if ($letter) { + my $message_id = C4::Letters::EnqueueLetter( + { + letter => $letter, + borrowernumber => $update->user_id, + message_transport_type => 'email', + } + ); + } + } + + # Return + $c->res->headers->location( + $c->req->url->to_string . '/' . $update->id ); + return $c->render( + status => 201, + openapi => $update->to_api + ); + } + catch { + $c->unhandled_exception($_); + }; +} + 1; --- a/Koha/Ticket/Update.pm +++ a/Koha/Ticket/Update.pm @@ -57,6 +57,17 @@ sub user { =head2 Internal methods +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::Ticket::Update object +on the API. + +=cut + +sub to_api_mapping { + return { id => 'update_id', }; +} + =head3 _type =cut --- a/admin/columns_settings.yml +++ a/admin/columns_settings.yml @@ -633,6 +633,23 @@ modules: - columnname: stocknumber + concerns: + table_concerns: + default_sort_order: 0 + columns: + - + columnname: reported + - + columnname: details + - + columnname: title + - + columnname: status + - + columnname: actions + cannot_be_toggled: 1 + cannot_be_modified: 1 + z3950_search: resultst: default_sort_order: 1 --- a/api/v1/swagger/definitions/ticket.yaml +++ a/api/v1/swagger/definitions/ticket.yaml @@ -48,4 +48,9 @@ properties: - "null" format: date-time description: Date the ticket was resolved_date + updates_count: + type: + - integer + - "null" + description: Number of updates additionalProperties: false --- a/api/v1/swagger/definitions/ticket_update.yaml +++ a/api/v1/swagger/definitions/ticket_update.yaml @@ -0,0 +1,30 @@ +--- +type: object +properties: + update_id: + type: integer + description: Internal ticket update identifier + ticket_id: + type: integer + description: Internal ticket identifier + user: + type: + - object + - "null" + description: The object representing the patron who added the update + user_id: + type: integer + description: Internal identifier for the patron who added the update + date: + type: + - string + - "null" + format: date-time + description: Date the ticket update was reported + message: + type: string + description: Ticket update details + public: + type: boolean + description: Is this update intended to be sent to the patron +additionalProperties: true --- a/api/v1/swagger/paths/tickets.yaml +++ a/api/v1/swagger/paths/tickets.yaml @@ -28,6 +28,7 @@ - reporter - resolver - biblio + - updates+count collectionFormat: csv responses: "200": @@ -210,6 +211,108 @@ x-koha-authorization: permissions: editcatalogue: edit_catalogue +"/tickets/{ticket_id}/updates": + get: + x-mojo-to: Tickets#list_updates + operationId: listTicketUpdates + tags: + - tickets + summary: List ticket updates + produces: + - application/json + parameters: + - $ref: "../swagger.yaml#/parameters/ticket_id_pp" + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/q_header" + - $ref: "../swagger.yaml#/parameters/request_id_header" + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - user + collectionFormat: csv + responses: + "200": + description: A list of ticket updates + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/ticket_update" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Ticket not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + catalogue: "1" + post: + x-mojo-to: Tickets#add_update + operationId: addTicketUpdate + tags: + - tickets + summary: Add an update to the ticket + parameters: + - $ref: "../swagger.yaml#/parameters/ticket_id_pp" + - name: body + in: body + description: A ticket update object + required: true + schema: + $ref: "../swagger.yaml#/definitions/ticket_update" + produces: + - application/json + responses: + "201": + description: Ticket added + schema: + $ref: "../swagger.yaml#/definitions/ticket_update" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Ticket not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: Internal error + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + editcatalogue: edit_catalogue /public/tickets: post: x-mojo-to: Tickets#add --- a/api/v1/swagger/swagger.yaml +++ a/api/v1/swagger/swagger.yaml @@ -76,6 +76,8 @@ definitions: $ref: ./definitions/suggestion.yaml ticket: $ref: ./definitions/ticket.yaml + ticket_update: + $ref: ./definitions/ticket_update.yaml transfer_limit: $ref: ./definitions/transfer_limit.yaml vendor: @@ -265,6 +267,8 @@ paths: $ref: "./paths/tickets.yaml#/~1tickets" "/tickets/{ticket_id}": $ref: "./paths/tickets.yaml#/~1tickets~1{ticket_id}" + "/tickets/{ticket_id}/updates": + $ref: "./paths/tickets.yaml#/~1tickets~1{ticket_id}~1updates" /transfer_limits: $ref: ./paths/transfer_limits.yaml#/~1transfer_limits /transfer_limits/batch: --- a/cataloguing/concerns.pl +++ a/cataloguing/concerns.pl @@ -0,0 +1,37 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use CGI qw ( -utf8 ); +use C4::Context; +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); + +my $query = CGI->new; +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "cataloguing/concerns.tt", + query => $query, + type => "intranet", + flagsrequired => { cataloguing => '*' }, + } +); + +output_html_with_http_headers $query, $cookie, $template->output; + +1; --- a/installer/data/mysql/atomicupdate/bug_31028.pl +++ a/installer/data/mysql/atomicupdate/bug_31028.pl @@ -93,5 +93,21 @@ return { } ); say $out "Added new notice 'TICKET_ACKNOWLEDGEMENT'"; + + $dbh->do( + q{ + INSERT IGNORE INTO letter(module,code,branchcode,name,is_html,title,content,message_transport_type) + VALUES ( 'catalog', 'TICKET_UPDATE', '', 'Concern updated', '1', 'Catalog concern updated', "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %],\r\n\r\nThe library has added an update to the concern you reported against [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %].\r\n\r\nThe following comment was left: \r\n[% ticket_update.message %]\r\n\r\nThankyou", 'email' ); + } + ); + say $out "Added new notice 'TICKET_UPDATE'"; + + $dbh->do( + q{ + INSERT IGNORE INTO letter(module,code,branchcode,name,is_html,title,content,message_transport_type) + VALUES ( 'catalog', 'TICKET_RESOLVE', '', 'Concern resolved', '1', 'Catalog concern resolved', "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %],\r\n\r\nThe library has now marked your concern with [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %] as resolved.\r\n\r\nThe following comment was left: \r\n[% ticket_update.message %]\r\n\r\nThankyou", 'email' ); + } + ); + say $out "Added new notice 'TICKET_RESOLVE'"; } } --- a/installer/data/mysql/en/mandatory/sample_notices.yml +++ a/installer/data/mysql/en/mandatory/sample_notices.yml @@ -61,6 +61,42 @@ tables: - "" - "Thankyou" + - module: catalog + code: TICKET_RESOLVE + branchcode: "" + name: "Concern resolved" + is_html: 1 + title: "Catalog concern resolved" + message_transport_type: email + lang: default + content: + - "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %]," + - "" + - "The library has now marked your concern with [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %] as resolved." + - "" + - "The following comment was left: " + - "[% ticket_update.message %]" + - "" + - "Thankyou" + + - module: catalog + code: TICKET_UPDATE + branchcode: "" + name: "Concern updated" + is_html: 1 + title: "Catalog concern updated" + message_transport_type: email + lang: default + content: + - "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %]," + - "" + - "The library has added an update to the concern you reported against [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %]." + - "" + - "The following comment was left: " + - "[% ticket_update.message %]" + - "" + - "Thankyou" + - module: circulation code: ACCOUNT_CREDIT branchcode: "" --- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss +++ a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss @@ -3371,6 +3371,10 @@ label { white-space: pre-wrap; } +.wrapfix { + white-space: pre-wrap; +} + pre { background-color: transparent; border: 0; --- a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc @@ -29,7 +29,7 @@ [% END %] - [% IF ( CAN_user_tools_inventory ) %] + [% IF ( CAN_user_tools_inventory || ( Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue ) ) %]
Reports
[% END %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt @@ -91,7 +91,7 @@ [% END %] - [% IF ( CAN_user_tools_inventory ) %] + [% IF ( CAN_user_tools_inventory || ( Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue ) ) %]

Reports

[% END %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt @@ -0,0 +1,306 @@ +[% USE raw %] +[% USE Asset %] +[% SET footerjs = 1 %] +[% USE TablesSettings %] +[% INCLUDE 'doc-head-open.inc' %] + + Catalog concerns › Tools › Koha + +[% INCLUDE 'doc-head-close.inc' %] + + + + [% INCLUDE 'header.inc' %] + [% INCLUDE 'cataloging-search.inc' %] + + + +
+
+
+
+

Concerns

+ +
+
+ Hide resolved + | Show all +
+ + + + + + + + + + + +
ReportedDetailsTitleStatusActions
+
+
+
+ +
+ +
+
+ + + + +[% MACRO jsinclude BLOCK %] + [% INCLUDE 'datatables.inc' %] + [% INCLUDE 'columns_settings.inc' %] + [% INCLUDE 'js-date-format.inc' %] + [% INCLUDE 'js-patron-format.inc' %] + [% INCLUDE 'js-biblio-format.inc' %] + +[% END %] +[% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt @@ -170,7 +170,7 @@
[%# Following statement must be in one line for translatability %] - [% IF ( CAN_user_tools_moderate_comments && pendingcomments ) || ( CAN_user_tools_moderate_tags && pendingtags ) || ( CAN_user_borrowers_edit_borrowers && pending_borrower_modifications ) || ( CAN_user_suggestions_suggestions_manage && ( pendingsuggestions || all_pendingsuggestions )) || ( CAN_user_borrowers_edit_borrowers && pending_discharge_requests ) || pending_article_requests || ( Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count ) || ( Koha.Preference('OPACReportProblem') && CAN_user_problem_reports && pending_problem_reports.count ) || already_ran_jobs || new_curbside_pickups.count %] + [% IF ( CAN_user_tools_moderate_comments && pendingcomments ) || ( CAN_user_tools_moderate_tags && pendingtags ) || ( CAN_user_borrowers_edit_borrowers && pending_borrower_modifications ) || ( CAN_user_suggestions_suggestions_manage && ( pendingsuggestions || all_pendingsuggestions )) || ( CAN_user_borrowers_edit_borrowers && pending_discharge_requests ) || pending_article_requests || ( Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count ) || ( Koha.preference('OpacCatalogConcerns') && pending_biblio_tickets && CAN_user_editcatalogue_edit_catalogue ) || ( Koha.Preference('OPACReportProblem') && CAN_user_problem_reports && pending_problem_reports.count ) || already_ran_jobs || new_curbside_pickups.count %]
[% IF pending_article_requests %]
@@ -225,6 +225,13 @@
[% END %] + [% IF ( Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue ) %] +
+ Catalog concerns pending: + [% pending_biblio_tickets | html %] +
+ [% END %] + [% IF Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count %]
Checkout notes pending: --- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js +++ a/koha-tmpl/intranet-tmpl/prog/js/datatables.js @@ -584,12 +584,18 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) { var part = {}; var attr = attributes[i]; let criteria = options.criteria; - if ( value.match(/^\^(.*)\$$/) ) { - value = value.replace(/^\^/, '').replace(/\$$/, ''); + if ( value === 'special:undefined' ) { + value = null; criteria = "exact"; - } else { - // escape SQL LIKE special characters % and _ - value = value.replace(/(\%|\\)/g, "\\$1"); + } + if ( value !== null ) { + if ( value.match(/^\^(.*)\$$/) ) { + value = value.replace(/^\^/, '').replace(/\$$/, ''); + criteria = "exact"; + } else { + // escape SQL LIKE special characters % + value = value.replace(/(\%|\\)/g, "\\$1"); + } } part[!attr.includes('.')?'me.'+attr:attr] = criteria === 'exact' ? value --- a/mainpage.pl +++ a/mainpage.pl @@ -35,6 +35,7 @@ use Koha::Quotes; use Koha::Suggestions; use Koha::BackgroundJobs; use Koha::CurbsidePickups; +use Koha::Tickets; my $query = CGI->new; @@ -103,6 +104,17 @@ my $pending_article_requests = Koha::ArticleRequests->search_limited( )->count; my $pending_problem_reports = Koha::ProblemReports->search({ status => 'New' }); +if ( C4::Context->preference('OpacCatalogConcerns') ) { + my $pending_biblio_tickets = Koha::Tickets->search( + { + resolved_date => undef, + biblio_id => { '!=' => undef } + } + ); + $template->param( + pending_biblio_tickets => $pending_biblio_tickets->count ); +} + unless ( $logged_in_user->has_permission( { parameters => 'manage_background_jobs' } ) ) { my $already_ran_jobs = Koha::BackgroundJobs->search( { borrowernumber => $logged_in_user->borrowernumber } )->count ? 1 : 0; --