Bug 37762: VueJS ILL Module

Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Pedro Amorim 2025-08-06 10:13:47 +00:00 committed by Lucas Gass
parent b307692fb1
commit 6dab32ee02
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
22 changed files with 2699 additions and 3 deletions

View File

@ -24,6 +24,8 @@ RewriteRule ^(.*)_[0-9]{2}\.[0-9]{7}\.(js|css)$ $1.$2 [L]
RewriteRule ^/cgi-bin/koha/erm/.*$ /cgi-bin/koha/erm/erm.pl [PT]
RewriteRule ^/cgi-bin/koha/sip2/.*$ /cgi-bin/koha/sip2/sip2.pl [PT]
RewriteCond %{REQUEST_URI} !ill-requests\.pl$
RewriteRule ^/cgi-bin/koha/ill/.*$ /cgi-bin/koha/ill/ill.pl [PT]
RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$
RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT]
RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT]

View File

@ -236,6 +236,8 @@
RewriteRule ^/cgi-bin/koha/erm/.*$ /cgi-bin/koha/erm/erm.pl [PT]
RewriteRule ^/cgi-bin/koha/sip2/.*$ /cgi-bin/koha/sip2/sip2.pl [PT]
RewriteCond %{REQUEST_URI} !ill-requests\.pl$
RewriteRule ^/cgi-bin/koha/ill/.*$ /cgi-bin/koha/ill/ill.pl [PT]
RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$
RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT]
RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record_sources.pl$1 [PT]

36
ill/ill.pl Executable file
View File

@ -0,0 +1,36 @@
#!/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 <https://www.gnu.org/licenses>.
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 $input = CGI->new;
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => "ill/ill.tt",
query => $input,
type => "intranet",
flagsrequired => { ill => '*' },
}
);
output_html_with_http_headers $input, $cookie, $template->output;

View File

@ -56,7 +56,7 @@
[% END %]
<li><a class="dropdown-item" href="/cgi-bin/koha/authorities/authorities-home.pl">Authorities</a></li>
[% IF Koha.Preference('ILLModule') && CAN_user_ill %]
<li><a class="dropdown-item" href="/cgi-bin/koha/ill/ill-requests.pl">ILL requests</a></li>
<li><a class="dropdown-item" href="/cgi-bin/koha/ill/ill.pl">Interlibrary loans</a></li>
[% END %]
[% IF ( CAN_user_serials ) %]
<li><a class="dropdown-item" href="/cgi-bin/koha/serials/serials-home.pl">Serials</a></li>

View File

@ -32,6 +32,9 @@
[% WRAPPER 'sub-header.inc' %]
[% WRAPPER breadcrumbs %]
[% WRAPPER breadcrumb_item %]
<a href="/cgi-bin/koha/ill/ill.pl">Interlibrary loans</a>
[% END %]
[% WRAPPER breadcrumb_item bc_active = (op == 'illlist' || op == 'batch_list' ? 1 : 0) %]
[% IF op == 'batch_list' %]
<a href="/cgi-bin/koha/ill/ill-requests.pl">ILL batch requests</a>

View File

@ -0,0 +1,35 @@
[% USE raw %]
[% USE To %]
[% USE Asset %]
[% USE KohaDates %]
[% USE TablesSettings %]
[% PROCESS 'i18n.inc' %]
[% SET footerjs = 1 %]
[% PROCESS 'patron-search.inc' %]
[% INCLUDE 'doc-head-open.inc' %]
<title
>[% FILTER collapse %]
[% t("Interlibrary loans") | html %]
&rsaquo; [% t("Koha") | html %]
[% END %]</title
>
[% INCLUDE 'doc-head-close.inc' %]
</head>
<body id="ill_module" class="ill">
[% WRAPPER 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
[% END %]
<div id="ill"></div>
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'calendar.inc' %]
[% INCLUDE 'datatables.inc' %]
[% INCLUDE 'js-date-format.inc' %]
<script nonce="[% Koha.CSPNonce | $raw %]">
const supplying_ill_table_settings = [% TablesSettings.GetTableSettings( 'illrequests', 'ill-requests', 'ill-requests', 'json' ) | $raw %];
</script>
[% Asset.js("js/vue/dist/ill.js") | $raw %]
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -115,7 +115,7 @@
</li>
[% IF Koha.Preference('ILLModule') && CAN_user_ill %]
<li>
<a class="icon_general icon_ill" href="/cgi-bin/koha/ill/ill-requests.pl"><i class="fa fa-fw fa-retweet"></i>ILL requests</a>
<a class="icon_general icon_ill" href="/cgi-bin/koha/ill/ill.pl"><i class="fa fa-fw fa-retweet"></i>Interlibrary loans</a>
</li>
[% END %]

View File

@ -0,0 +1,92 @@
export class ILLAPIClient {
constructor(HttpClient) {
this.httpClient = new HttpClient({
baseURL: "/api/v1/ill/",
});
}
get config() {
return {
get: () => {
ILLModule: 1;
}, // TODO: Fetch from API
};
}
get supplying() {
return {
get: id =>
this.httpClient.get({
endpoint: "iso18626_requests/" + id,
headers: {
"x-koha-embed": "requesting_agency,messages,hold",
},
}),
getAll: (query, params) =>
this.httpClient.getAll({
endpoint: "iso18626_requests",
query,
params,
}),
patch: (iso18626_request, id) =>
this.httpClient.patch({
endpoint: "iso18626_requests/" + id,
body: iso18626_request,
}),
count: (query = {}) =>
this.httpClient.count({
endpoint:
"iso18626_requests?" +
new URLSearchParams({
_page: 1,
_per_page: 20,
...(query && { q: JSON.stringify(query) }),
}),
}),
};
}
get requesting_agencies() {
return {
get: id =>
this.httpClient.get({
endpoint: "iso18626_requesting_agencies/" + id,
headers: {
"x-koha-embed": "ill_partner",
},
}),
getAll: (query, params) =>
this.httpClient.getAll({
endpoint: "iso18626_requesting_agencies",
query,
params,
}),
delete: id =>
this.httpClient.delete({
endpoint: "iso18626_requesting_agencies/" + id,
}),
create: iso18626_requesting_agency =>
this.httpClient.post({
endpoint: "iso18626_requesting_agencies",
body: iso18626_requesting_agency,
}),
update: (iso18626_requesting_agency, id) =>
this.httpClient.put({
endpoint: "iso18626_requesting_agencies/" + id,
body: iso18626_requesting_agency,
}),
count: (query = {}) =>
this.httpClient.count({
endpoint:
"iso18626_requesting_agencies?" +
new URLSearchParams({
_page: 1,
_per_page: 1,
...(query && { q: JSON.stringify(query) }),
}),
}),
};
}
}
export default ILLAPIClient;

View File

@ -0,0 +1,19 @@
<template>
<ModuleDashboard name="ILL" :availableWidgets="availableWidgets" />
</template>
<script>
import ModuleDashboard from "../ModuleDashboard/ModuleDashboard.vue";
import ILLRequests from "../ModuleDashboard/Widgets/ILLRequests.vue";
import { markRaw } from "vue";
export default {
components: { ModuleDashboard },
setup() {
const availableWidgets = [markRaw(ILLRequests)];
return {
availableWidgets,
};
},
};
</script>

View File

@ -0,0 +1,376 @@
import { $__ } from "@koha-vue/i18n";
export function ISO18626() {
const getCodesForElement = element => {
const codes = [
{
element: "courierName",
values: [
{
value: "DB Schenker",
description: $__("DB Schenker"),
},
{
value: "DHL",
description: $__("DHL"),
},
{
value: "Fedex",
description: $__("Fedex"),
},
{
value: "PostNord AB",
description: $__("PostNord AB"),
},
{
value: "UPS",
description: $__("UPS"),
},
],
},
{
element: "deliveryMethod",
values: [
{
value: "ArticleExchange",
description: $__("Article Exchange"),
},
{
value: "Courier",
description: $__("Courier"),
},
{
value: "Email",
description: $__("Email"),
},
{
value: "FTP",
description: $__("FTP – File Transfer Protocol"),
},
{
value: "Mail",
description: $__("Mail"),
},
{
value: "Odyssey",
description: $__("Odyssey"),
},
{
value: "URL",
description: $__("Website to download from"),
},
],
},
{
element: "itemFormat",
values: [
{
value: "CassetteTape",
description: $__("Cassette tape"),
},
{
value: "CD",
description: $__("CD"),
},
{
value: "CD-ROM",
description: $__("CD-ROM"),
},
{
value: "Daisy-ROM",
description: $__("Daisy-ROM"),
},
{
value: "DVD",
description: $__("DVD"),
},
{
value: "EPUB",
description: $__("EPUB"),
},
{
value: "EPUB2",
description: $__("EPUB2"),
},
{
value: "EPUB3",
description: $__("EPUB3"),
},
{
value: "JPEG",
description: $__("JPEG"),
},
{
value: "LargePrint",
description: $__("Large print"),
},
{
value: "LP",
description: $__("LP"),
},
{
value: "Microform",
description: $__("Microform"),
},
{
value: "MP3",
description: $__("MP3"),
},
{
value: "Multimedia",
description: $__("Multimedia"),
},
{
value: "PaperCopy",
description: $__("Paper copy"),
},
{
value: "PDF",
description: $__("PDF"),
},
{
value: "Printed",
description: $__("Printed"),
},
{
value: "Tape",
description: $__("Tape"),
},
{
value: "TIFF",
description: $__("TIFF"),
},
{
value: "UltraHD",
description: $__("Ultra HD"),
},
{
value: "VHS",
description: $__("VHS"),
},
],
},
{
element: "loanCondition",
values: [
{
value: "LibraryUseOnly",
description: $__("Use in library only"),
},
{
value: "WatchLibraryUseOnly",
description: $__("Supervised use in library only"),
},
{
value: "NoReproduction",
description: $__("No reproduction"),
},
{
value: "SignatureRequired",
description: $__("Signature required"),
},
{
value: "SpecCollSupervReq",
description: $__(
"Special collections supervision required"
),
},
],
},
{
element: "paymentMethod",
values: [
{
value: "BankTransfer",
description: $__("Bank transfer"),
},
{
value: "CreditCard",
description: $__("Credit card"),
},
{
value: "DebitCard",
description: $__("Debit card"),
},
{
value: "EFTS",
description: $__("Electronic Fund Transfer System"),
},
{
value: "IBS",
description: $__(
"Interloan Billing System (New Zealand)"
),
},
{
value: "IIBS",
description: $__(
"International Interloan Billing System (New Zealand)"
),
},
{
value: "IFLAVoucher",
description: $__("IFLA Voucher"),
},
{
value: "IFM",
description: $__("OCLC fee management system"),
},
{
value: "LAPS",
description: $__("Libraries Australia Payment Service"),
},
{
value: "Paypal",
description: $__("Paypal"),
},
],
},
{
element: "reasonRetry",
values: [
{
value: "AtBindery",
description: $__("At bindery"),
},
{
value: "CostExceedsMaxCost",
description: $__("Cost exceeds max cost"),
},
{
value: "CourierNotSupp",
description: $__("Courier not supported"),
},
{
value: "MultiVolAvail",
description: $__(
"More than one volume can fulfil the request"
),
},
{
value: "MustMeetLoanCondition",
description: $__("Loan condition shall be met"),
},
{
value: "NotCurrentAvailableForILL",
description: $__("Not currently available for ILL"),
},
{
value: "NotFoundAsCited",
description: $__("Not found as cited"),
},
{
value: "OnLoan",
description: $__("On loan"),
},
{
value: "OnOrder",
description: $__("On order"),
},
{
value: "ReqDelDateNotPossible",
description: $__(
"Requested delivery date not possible"
),
},
{
value: "ReqDelMethodNotSupp",
description: $__(
"Requested delivery method not supported"
),
},
{
value: "ReqEditionNotPossible",
description: $__(
"Requested edition cannot be provided"
),
},
{
value: "ReqFormatNotPossible",
description: $__("Requested format not possible"),
},
{
value: "ReqPayMethodNotSupported",
description: $__(
"Requested payment method not supported"
),
},
{
value: "ReqServLevelNotSupp",
description: $__(
"Requested service level not supported"
),
},
{
value: "ReqServTypeNotPossible",
description: $__("Requested service type not possible"),
},
],
},
{
element: "reasonUnfilled",
values: [
{
value: "NonCirculating",
description: $__("Non-circulating (e.g. handbook)"),
},
{
value: "NotAvailableForILL",
description: $__("Not available for ILL"),
},
{
value: "NotHeld",
description: $__("Not held"),
},
{
value: "NotOnShelf",
description: $__("Not on shelf"),
},
{
value: "PolicyProblem",
description: $__("Policy problem"),
},
{
value: "PoorCondition",
description: $__("Poor condition"),
},
],
},
{
element: "serviceLevel",
values: [
{
value: "Express",
description: $__("Express (Australia)"),
},
{
value: "Normal",
description: $__("Normal (Prioritaire A)"),
},
{
value: "Rush",
description: $__("Rush (Australia)"),
},
{
value: "SecondaryMail",
description: $__("Secondary mail (Prioritaire B)"),
},
{
value: "Standard",
description: $__("Standard (Australia)"),
},
{
value: "Urgent",
description: $__("Urgent"),
},
],
},
];
return codes.find(code => code.element === element).values;
};
return {
getCodesForElement,
};
}

View File

@ -0,0 +1,238 @@
<template>
<FormElement
:resource="messageFilterOptions"
:attr="{
label: 'Show confirmations',
name: 'showConfirmationRows',
type: 'checkbox',
}"
/>
<table id="iso18626_messages">
<thead>
<tr>
<th>Type</th>
<th>Information</th>
<th>Timestamp</th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr
v-for="message in filteredMessages"
v-bind:key="message.iso18626_message_id"
>
<td>
<i
v-if="message.type.includes('request')"
class="fas fa-download"
aria-hidden="true"
></i>
<i v-else class="fas fa-upload" aria-hidden="true"></i>
&nbsp;
{{ message.type }}
</td>
<td>
{{ getMessageData(message.content) }}
</td>
<td>
{{ new Date(message.timestamp).toLocaleString() }}
</td>
<td>
<button
class="btn btn-default"
@click="openModal(message.content)"
>
<i class="fas fa-envelope" aria-hidden="true"></i>
&nbsp; View message
</button>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import FormElement from "../FormElement.vue";
import { inject, ref, computed } from "vue";
export default {
props: {
iso18626_request: Object,
},
setup(props) {
const modalVisible = ref(false);
const messageFilterOptions = ref({
showConfirmationRows: false,
});
const prettifiedMessage = ref(null);
const { setConfirmationDialog, setMessage, setError } =
inject("mainStore");
const filteredMessages = computed(() => {
return props.iso18626_request.messages.filter(message => {
if (messageFilterOptions.value.showConfirmationRows) {
return true;
}
return !message.type.toLowerCase().includes("confirmation");
});
});
const getMessageData = (message, type) => {
const parsedMessage = Object.values(JSON.parse(message))[0];
let message_data;
message_data =
parsedMessage?.action !== undefined &&
parsedMessage?.action !== null
? parsedMessage?.action
: null;
if (message_data === null) {
message_data =
parsedMessage?.messageInfo?.reasonForMessage !==
undefined &&
parsedMessage?.messageInfo?.reasonForMessage !== null
? parsedMessage?.messageInfo?.reasonForMessage
: null;
if (message_data !== null) {
message_data = "Reason: " + message_data;
}
} else {
message_data = "Action: " + message_data;
}
return message_data;
};
function openModal(message) {
const messageObj = Object.values(JSON.parse(message))[0];
const container = document.createElement("div");
container.classList.add("accordion");
generateCollapsibleTabs(messageObj, container);
const message_type = Object.keys(JSON.parse(message))[0];
setConfirmationDialog({
title: `ISO18626 message: ${message_type}`,
message: container.outerHTML,
size: "modal-lg",
});
}
function generateCollapsibleTabs(obj, parent) {
const sortedKeys = Object.keys(obj).sort((a, b) => {
const order = [
"action",
"header",
"confirmationHeader",
"bibliographicInfo",
"messageInfo",
"statusInfo",
];
const indexA =
order.indexOf(a) !== -1 ? order.indexOf(a) : Infinity;
const indexB =
order.indexOf(b) !== -1 ? order.indexOf(b) : Infinity;
return indexA - indexB;
});
const top_level_key_number = Object.keys(obj).length;
for (const key of sortedKeys) {
if (typeof obj[key] === "object") {
const details = document.createElement("fieldset");
details.classList.add("rows");
const summary = document.createElement("legend");
const keySpan = document.createElement("span");
keySpan.textContent =
key.charAt(0).toUpperCase() + key.slice(1);
summary.appendChild(keySpan);
summary.setAttribute("data-bs-toggle", "collapse");
summary.setAttribute("data-bs-target", "#collapse_" + key);
if (key === "header") {
summary.setAttribute("aria-expanded", "false");
summary.classList.add("collapsed");
}
summary.classList.add("expanded");
summary.setAttribute("aria-controls", "collapseExample");
summary.innerHTML =
'<i class="fa fa-caret-down"></i> ' + summary.innerHTML;
const nestedContainer = document.createElement("ol");
nestedContainer.setAttribute("id", "collapse_" + key);
nestedContainer.classList.add("collapse");
if (key !== "header") {
nestedContainer.classList.add("show");
}
const nestedTable = document.createElement("table");
// Call generateTableRows for the nested object
generateTableRows(obj[key], nestedTable);
nestedContainer.appendChild(nestedTable);
details.appendChild(summary);
details.appendChild(nestedContainer);
parent.appendChild(details);
} else {
// Handle regular top-level items
const div = document.createElement("div");
div.textContent =
key.charAt(0).toUpperCase() +
key.slice(1) +
": " +
obj[key];
div.classList.add("fs-2", "fw-bold", "p-2");
parent.appendChild(div);
}
}
}
function generateTableRows(obj, parent) {
for (const key in obj) {
if (typeof obj[key] === "object") {
const tr = document.createElement("tr");
const th = document.createElement("th");
th.textContent = key;
tr.appendChild(th);
const td = document.createElement("td");
tr.appendChild(td);
const nestedTable = document.createElement("table");
nestedTable.classList.add("nested-table");
nestedTable.style.paddingLeft = "20px";
nestedTable.style.borderLeft = "1px solid #ccc";
generateTableRows(obj[key], nestedTable);
td.appendChild(nestedTable);
parent.appendChild(tr);
} else {
const tr = document.createElement("tr");
const th = document.createElement("th");
th.textContent = key;
tr.appendChild(th);
const td = document.createElement("td");
td.textContent = obj[key];
tr.appendChild(td);
parent.appendChild(tr);
}
}
}
return {
modalVisible,
prettifiedMessage,
openModal,
filteredMessages,
messageFilterOptions,
getMessageData,
};
},
components: {
FormElement,
},
name: "ResourceFormSave",
};
</script>

View File

@ -0,0 +1,129 @@
<template>
<div v-if="initialized && config.settings.ILLModule == 1">
<div id="sub-header">
<Breadcrumbs />
<Help />
</div>
<div class="main container-fluid">
<div class="row">
<div class="col-md-10 order-md-2 order-sm-1">
<main>
<Dialog />
<router-view />
</main>
</div>
<div class="col-md-2 order-sm-2 order-md-1">
<LeftMenu :title="$__('Interlibrary loans')"></LeftMenu>
</div>
</div>
</div>
</div>
<div class="main container-fluid" v-else>
<Dialog />
</div>
</template>
<script>
import { inject, onBeforeMount, ref } from "vue";
import { APIClient } from "../../fetch/api-client.js";
import Breadcrumbs from "../Breadcrumbs.vue";
import Help from "../Help.vue";
import LeftMenu from "../LeftMenu.vue";
import Dialog from "../Dialog.vue";
import "vue-select/dist/vue-select.css";
import { storeToRefs } from "pinia";
import { $__ } from "@koha-vue/i18n";
export default {
setup() {
const mainStore = inject("mainStore");
const { loading, loaded, setError } = mainStore;
const ILLStore = inject("ILLStore");
const { config } = storeToRefs(ILLStore);
const initialized = ref(false);
onBeforeMount(() => {
loading();
const client = APIClient.sysprefs;
client.sysprefs
.getAll(["ILLPartnerCode", "ILLModule"])
.then(pref_values => {
config.value.settings.ILLPartnerCode =
pref_values.value.ILLPartnerCode;
config.value.settings.ILLModule =
pref_values.value.ILLModule;
if (config.value.settings.ILLModule != 1) {
loaded();
return setError(
$__(
"The ILL module is disabled, turn on <a href='/cgi-bin/koha/admin/preferences.pl?tab=&op=search&searchfield=ILLModule'>ILLModule</a> to use it"
),
false
);
}
loaded();
});
initialized.value = true;
});
return {
ILLStore,
config,
setError,
loading,
loaded,
initialized,
};
},
components: {
Breadcrumbs,
Dialog,
Help,
LeftMenu,
},
};
</script>
<style>
#menu ul ul,
.sidebar_menu ul ul {
background-color: transparent;
padding-left: 2em;
font-size: 100%;
}
form .v-select {
display: inline-block;
background-color: white;
width: 30%;
}
.v-select,
input:not([type="submit"]):not([type="search"]):not([type="button"]):not(
[type="checkbox"]
):not([type="radio"]),
textarea {
border-color: rgba(60, 60, 60, 0.26);
border-width: 1px;
border-radius: 4px;
min-width: 30%;
}
.flatpickr-input {
width: 30%;
}
.sidebar_menu ul li a.current.disabled {
background-color: inherit;
border-left: 5px solid transparent;
color: #000;
}
.sidebar_menu ul li a.disabled {
color: #666;
pointer-events: none;
font-weight: 700;
}
</style>

View File

@ -0,0 +1,221 @@
<template>
<h2>{{ $__("Progress request") }}</h2>
<div v-if="!initialized">{{ $__("Loading") }}</div>
<template v-if="initialized">
<div class="page-section">
<p>
This request's status is
<strong>{{ iso18626_request.status }}</strong
>.
</p>
<p>Below are the possible actions in order to progress it.</p>
</div>
<fieldset class="rows">
<table id="request-status" class="table table-bordered">
<thead>
<tr>
<th>{{ $__("Action") }}</th>
<th>{{ $__("Description") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(status, key) in statuses" :key="key">
<template
v-if="
status.show_if_status_in.includes(
iso18626_request.status
)
"
>
<td>
<button
class="btn"
:class="`${status.btn_class ? status.btn_class : 'btn-default'}`"
@click="progressRequest(key)"
>
<span
:class="`fa-solid ${status.icon}`"
></span>
{{ status.button_label }}
</button>
</td>
<td>
{{ status.description }}
</td>
</template>
</tr>
</tbody>
</table>
</fieldset>
<div
v-if="iso18626_request.status == 'CopyCompleted'"
class="page-section"
>
<p>This request has been completed. Nothing to do here.</p>
</div>
</template>
</template>
<script>
import { useRoute, useRouter } from "vue-router";
import { createVNode, onBeforeMount, onMounted, ref, render } from "vue";
import { APIClient } from "../../fetch/api-client.js";
import { useDataTable } from "../../composables/datatables";
import { inject } from "vue";
import { $__ } from "@koha-vue/i18n";
export default {
setup() {
const initialized = ref(false);
const status = ref("RequestReceived"); // <--- define it here
const iso18626_request = ref(null);
const router = useRouter();
const { setConfirmationDialog, setMessage, setError } =
inject("mainStore");
const statuses = ref({
ExpectToSupply: {
confirm_message: $__("TODO: Write this"),
description: $__(
"Supplying library expects to fill the request, based on e.g. information in the local OPAC"
),
button_label: $__("Expect to supply"),
icon: "fa-calendar-days",
show_if_status_in: ["RequestReceived"],
},
CopyCompleted: {
confirm_message: $__(
"By changing this request's status to CopyCompleted, a message will be sent to the requesting library notifying them that the transaction has concluded."
),
description: $__(
"The supplying library has sent the requested item (this status is used when there is no need to return the item supplied)"
),
button_label: $__("Copy completed"),
icon: "fa-check",
btn_class: "btn-primary",
show_if_status_in: ["RequestReceived"],
},
Loaned: {
confirm_message: $__(
"By changing this request's status to Loaned, a message will be sent to the requesting library notifying them that the item has been supplied."
),
description: $__(
"The item is currently on loan to the requesting library for this request"
),
button_label: $__("Mark as loaned"),
icon: "fa-box",
show_if_status_in: [
"RequestReceived",
"ExpectToSupply",
"WillSupply",
],
},
RetryPossible: {
confirm_message: $__(
"By changing this request's status to RetryPossible, a message will be sent to the requesting library and they can submit a Retry request."
),
description: $__(
"The supplying library cannot fill the request based on information provided or may be able to supply at a later date. The requesting library may submit a Retry request which may include updated information"
),
button_label: $__("Ask for retry"),
icon: "fa-repeat",
show_if_status_in: [
"RequestReceived",
"WillSupply",
"ExpectToSupply",
],
},
WillSupply: {
confirm_message: $__(
"By changing this request's status to WillSupply, a message will be sent to the requesting library notifying them that the item will be supplied."
),
description: $__(
"Supplying library has located the item but has not sent it yet"
),
button_label: $__("Will supply"),
icon: "fa-calendar-days",
show_if_status_in: ["RequestReceived", "ExpectToSupply"],
},
Unfilled: {
confirm_message: $__(
"By changing this request's status to Unfilled, a message will be sent to the requesting library notifying them that the item will not be supplied."
),
description: $__(
"The supplying library cannot fill the request. The explanation may be provided in the ReasonUnfilled data element"
),
button_label: $__("Unfilled"),
icon: "fa-calendar-days",
btn_class: "btn-danger",
show_if_status_in: ["RequestReceived"],
},
});
const progressRequest = action => {
setConfirmationDialog(
{
title: $__(
"Update this request's status to <strong>%s</strong>?"
).format(action),
message: statuses.value[action].confirm_message,
accept_label: $__("Yes, update"),
cancel_label: $__("No, do not update"),
inputs: [
{
name: "status",
type: "hidden",
value: action,
label: $__("Status"),
required: false,
},
],
},
(callback_result, inputFields) => {
const client = APIClient.ill.supplying;
client
.patch(
{ status: inputFields.status },
iso18626_request.value.iso18626_request_id
)
.then(
success => {
router.push({
name: "SupplyingShow",
params: {
iso18626_request_id:
iso18626_request.value
.iso18626_request_id,
},
});
},
error => {}
);
}
);
};
onBeforeMount(() => {
const route = useRoute();
const client = APIClient.ill.supplying;
client.get(route.params.iso18626_request_id).then(
result => {
iso18626_request.value = result;
initialized.value = true;
},
error => {}
);
});
onMounted(() => {
// buildDatatable();
});
return {
status,
initialized,
progressRequest,
statuses,
iso18626_request,
};
},
name: "ProgressRequest",
};
</script>

View File

@ -0,0 +1,201 @@
<template>
<BaseResource
:routeAction="routeAction"
:instancedResource="this"
></BaseResource>
</template>
<script>
import { inject } from "vue";
import BaseResource from "../BaseResource.vue";
import { useBaseResource } from "../../composables/base-resource.js";
import { storeToRefs } from "pinia";
import { APIClient } from "../../fetch/api-client.js";
import { $__ } from "@koha-vue/i18n";
export default {
props: {
routeAction: String,
},
setup(props) {
const ILLStore = inject("ILLStore");
const { config } = ILLStore;
const baseResource = useBaseResource({
resourceName: "iso18626_requesting_agency",
nameAttr: "name",
idAttr: "iso18626_requesting_agency_id",
components: {
show: "RequestingAgenciesShow",
list: "RequestingAgenciesList",
add: "RequestingAgenciesFormAdd",
edit: "RequestingAgenciesFormAddEdit",
},
apiClient: APIClient.ill.requesting_agencies,
i18n: {
deleteConfirmationMessage: $__(
"Are you sure you want to remove this requesting agency?"
),
deleteSuccessMessage: $__("Requesting agency %s deleted"),
displayName: $__("Requesting agency"),
editLabel: $__("Edit requesting agency #%s"),
emptyListMessage: $__(
"There are no requesting agencies defined"
),
newLabel: $__("New requesting agency"),
},
table: {
resourceTableUrl:
APIClient.ill.httpClient._baseURL +
"iso18626_requesting_agencies",
},
resourceAttrs: [
{
name: "iso18626_requesting_agency_id",
label: $__("ID"),
type: "text",
hideIn: ["Form"],
group: $__("Details"),
},
{
name: "patron_id",
type: "patronAutoComplete",
label: __("ILL partner"),
placeholder: __("Search for an ILL partner"),
patronAutoCompleteOptions: {
"additional-filters": {
"me.category_id": config.settings.ILLPartnerCode,
},
},
patronEmbedName: "ill_partner",
required: true,
group: $__("Details"),
},
{
name: "name",
label: $__("Name"),
required: true,
type: "text",
group: $__("Details"),
},
{
name: "type",
label: $__("Type"),
required: true,
type: "select",
options: [
{
value: "DNUCNI",
description: __("DNUCNI"),
},
{
value: "ICOLC",
description: __("ICOLC"),
},
{
value: "ISIL",
description: __("ISIL"),
},
],
requiredKey: "value",
selectLabel: "description",
group: $__("Details"),
},
{
name: "account_id",
label: $__("Account ID"),
required: true,
type: "text",
group: $__("ISO18626 authentication"),
},
{
name: "securityCode",
label: $__("Security Code"),
required: true,
type: "text",
hideIn: ["List"],
group: $__("ISO18626 authentication"),
},
{
name: "callback_endpoint",
label: $__("Callback endpoint"),
type: "text",
hideIn: ["List"],
group: $__("Details"),
},
],
moduleStore: "ILLStore",
props: props,
});
const tableOptions = {
url: () => tableUrl(),
options: {
embed: "ill_partner",
},
actions: {
0: ["show"],
1: ["show"],
"-1": ["edit", "delete"],
},
};
const onFormSave = (e, requestingAgencyToSave) => {
e.preventDefault();
let iso18626_requesting_agency = JSON.parse(
JSON.stringify(requestingAgencyToSave)
); // copy
let iso18626_requesting_agency_id =
iso18626_requesting_agency.iso18626_requesting_agency_id;
delete iso18626_requesting_agency.iso18626_requesting_agency_id;
delete iso18626_requesting_agency.ill_partner;
if (iso18626_requesting_agency_id) {
return baseResource.apiClient
.update(
iso18626_requesting_agency,
iso18626_requesting_agency_id
)
.then(
requesting_agency => {
baseResource.setMessage(
$__("Requesting agency updated")
);
return requesting_agency;
},
error => {}
);
} else {
return baseResource.apiClient
.create(iso18626_requesting_agency)
.then(
requesting_agency => {
baseResource.setMessage(
$__("Requesting agency created")
);
return requesting_agency;
},
error => {}
);
}
};
const tableUrl = filters => {
return baseResource.getResourceTableUrl();
};
return {
...baseResource,
tableOptions,
onFormSave,
tableUrl,
};
},
emits: ["select-resource"],
name: "RequestingAgencyResource",
components: {
BaseResource,
},
};
</script>

View File

@ -0,0 +1,877 @@
<template>
<BaseResource
:routeAction="routeAction"
:instancedResource="this"
></BaseResource>
</template>
<script>
import { useRouter } from "vue-router";
import { ISO18626 } from "./ISO18626.js";
import BaseResource from "../BaseResource.vue";
import { useBaseResource } from "../../composables/base-resource.js";
import { inject, ref } from "vue";
import { APIClient } from "../../fetch/api-client.js";
import { $__ } from "@koha-vue/i18n";
export default {
props: {
routeAction: String,
},
setup(props) {
const router = useRouter();
const { getCodesForElement } = ISO18626();
const {
setConfirmationDialog,
setMessage,
updateConfirmationDialogInputs,
} = inject("mainStore");
const conditionalInputs = ref([]);
const statuses = ref([
{
id: "RequestReceived",
next_actions: [
"ExpectToSupply",
"CopyCompleted",
"Loaned",
"RetryPossible",
"WillSupply",
"Unfilled",
"Cancelled",
],
},
{
id: "ExpectToSupply",
confirm_message: $__(
"Supplying library expects to fill the request, based on e.g. information in the local OPAC. The message may include the ExpectedDeliveryDate"
),
button_label: $__("Expect to supply"),
icon: "fa-calendar-days",
next_actions: [
"Loaned",
"WillSupply",
"RetryPossible",
"Unfilled",
],
action_inputs: [
{
name: "expectedDeliveryDate",
type: "date",
label: $__("Expected delivery date"),
toolTip: $__(
"Date and time the supplying library expects to deliver the item."
),
},
],
index: 0,
},
{
id: "WillSupply",
confirm_message: $__(
"Supplying library has located the item but has not sent it yet."
),
button_label: $__("Will supply"),
icon: "fa-calendar-days",
next_actions: [
"Loaned",
"RetryPossible",
"CopyCompleted",
"Unfilled",
],
action_inputs: [],
},
{
id: "Loaned",
confirm_message: $__(
"The item is currently on loan to the requesting library for this request"
),
button_label: $__("Mark as loaned"),
icon: "fa-box",
next_actions: [
"Overdue",
"LoanCompleted",
"CompletedWithoutReturn",
], //[ 'Recalled', 'HoldReturn' ]
action_inputs: [],
},
{
id: "Overdue",
confirm_message: $__(
"The item currently on loan to the requesting library for this request is now overdue"
),
button_label: $__("Mark as ovedue"),
icon: "fa-box",
next_actions: ["LoanCompleted", "CompletedWithoutReturn"], //[ 'Recalled', 'HoldReturn', 'LoanCompleted' ]
action_inputs: [],
},
{
id: "Recalled",
confirm_message: $__(
"The item currently on loan to the requesting library for this request has been recalled"
),
button_label: $__("Ask for recall"),
icon: "fa-box",
next_actions: ["LoanCompleted", "CompletedWithoutReturn"],
action_inputs: [],
},
{
id: "RetryPossible",
confirm_message: $__(
"The supplying library cannot fill the request based on information provided or may be able to supply at a later date. Additional information is provided in the RetryInfo section. The requesting library may submit a Retry request which may include updated information"
),
button_label: $__("Ask for retry"),
icon: "fa-repeat",
next_actions: ["Cancelled", "Unfilled"],
action_inputs: [
{
name: "retryBefore",
label: $__("Retry before"),
type: "date",
toolTip: $__(
"Specify that a retry should be attempted before the specified date."
),
},
{
name: "retryAfter",
label: $__("Retry after"),
type: "date",
toolTip: $__(
"Specify that a retry should be attempted only after the specified date."
),
},
{
name: "reasonRetry",
label: $__("Reason for retry"),
required: true,
toolTip: $__(
"Specify the reason why a retry from the requesting agency is necessary"
),
type: "select",
onSelected: resource => {
conditionalInputs.value = [];
if (resource.reasonRetry == "CostExceedsMaxCost") {
conditionalInputs.value.push({
name: "offeredCostsCurrencyCode",
type: "select",
label: $__("Currency code"),
required: true,
toolTip: $__(
"Specify the currency code for the offered costs"
),
options: [
{
value: "USD",
description: __("USD"),
},
{
value: "EUR",
description: __("EUR"),
},
{
value: "GBP",
description: __("GBP"),
},
{
value: "AUD",
description: __("AUD"),
},
{
value: "SEK",
description: __("SEK"),
},
],
requiredKey: "value",
selectLabel: "description",
});
conditionalInputs.value.push({
name: "offeredCostsMonetaryValue",
type: "number",
label: $__("Monetary value"),
required: true,
toolTip: $__(
"Specify the monetary value for the offered costs"
),
});
} else if (
resource.reasonRetry == "MultiVolAvail"
) {
conditionalInputs.value.push({
name: "volume",
type: "text",
label: $__("Volume(s)"),
required: true,
placeholder: "14,15",
toolTip: $__(
"Specify which volume(s) are available, separated by comma ','"
),
});
} else if (
resource.reasonRetry == "MustMeetLoanCondition"
) {
conditionalInputs.value.push({
name: "loanCondition",
label: $__("Loan condition(s)"),
required: true,
type: "select",
toolTip: $__(
"Specify the condition(s) of use that need to be met once the requested item is delivered"
),
allowMultipleChoices: true,
options:
getCodesForElement("loanCondition"),
requiredKey: "value",
selectLabel: "description",
});
} else if (
resource.reasonRetry == "ReqDelMethodNotSupp"
) {
conditionalInputs.value.push({
name: "deliveryMethod",
type: "select",
vselectStyle: {
dropdownMaxHeight: "150px",
},
toolTip: $__(
"Specify which delivery method(s) can be supplied"
),
allowMultipleChoices: true,
label: $__("Delivery method(s)"),
required: true,
options:
getCodesForElement("deliveryMethod"),
requiredKey: "value",
selectLabel: "description",
onUpdated: inputValues => {
const deliveryMethods =
inputValues.deliveryMethod;
if (
deliveryMethods &&
deliveryMethods.includes("Courier")
) {
const existingInput =
conditionalInputs.value.find(
input =>
input.name ===
"courierName"
);
if (!existingInput) {
conditionalInputs.value.push({
name: "courierName",
type: "select",
vselectStyle: {
dropdownMaxHeight:
"150px",
},
allowMultipleChoices: true,
toolTip: $__(
"Specify which courier(s) can be used"
),
label: $__(
"Courier name(s)"
),
required: true,
options:
getCodesForElement(
"courierName"
),
requiredKey: "value",
selectLabel: "description",
});
updateConfirmationDialogInputs(
getDialogInputs(inputValues)
);
}
} else {
const existingInput =
conditionalInputs.value.find(
input =>
input.name ===
"courierName"
);
if (existingInput) {
const index =
conditionalInputs.value.indexOf(
existingInput
);
if (index > -1) {
conditionalInputs.value.splice(
index,
1
);
updateConfirmationDialogInputs(
getDialogInputs(
inputValues
)
);
}
}
}
},
});
} else if (
resource.reasonRetry == "ReqEditionNotPossible"
) {
conditionalInputs.value.push({
name: "edition",
type: "text",
toolTip: $__(
"Specify which edition(s) are available, separated by comma ','"
),
label: $__("Edition(s)"),
required: true,
placeholder: "14,15",
});
} else if (
resource.reasonRetry == "ReqFormatNotPossible"
) {
conditionalInputs.value.push({
name: "itemFormat",
type: "select",
vselectStyle: {
dropdownMaxHeight: "150px",
},
toolTip: $__(
"Specify which format(s) can be supplied"
),
allowMultipleChoices: true,
label: $__("Item format(s)"),
required: true,
options: getCodesForElement("itemFormat"),
requiredKey: "value",
selectLabel: "description",
});
} else if (
resource.reasonRetry ==
"ReqPayMethodNotSupported"
) {
conditionalInputs.value.push({
name: "paymentMethod",
type: "select",
toolTip: $__(
"Specify which payment method(s) can be used"
),
allowMultipleChoices: true,
label: $__("Payment method(s)"),
required: true,
options:
getCodesForElement("paymentMethod"),
requiredKey: "value",
selectLabel: "description",
});
} else if (
resource.reasonRetry == "ReqServLevelNotSupp"
) {
conditionalInputs.value.push({
name: "serviceLevel",
type: "select",
toolTip: $__(
"Select which service level(s) are supported"
),
allowMultipleChoices: true,
label: $__("Service level(s)"),
required: true,
options: getCodesForElement("serviceLevel"),
requiredKey: "value",
selectLabel: "description",
});
} else if (
resource.reasonRetry == "ReqServTypeNotPossible"
) {
conditionalInputs.value.push({
name: "serviceType",
type: "select",
toolTip: $__(
"Select the service type which can be supplied"
),
label: $__("Service type"),
required: true,
options: [
//TODO: Only show the 2 options that dont match current resource's service_type
{
value: "Copy",
description: __("Copy"),
},
{
value: "CopyOrLoan",
description: __("CopyOrLoan"),
},
{
value: "Loan",
description: __("Loan"),
},
],
requiredKey: "value",
selectLabel: "description",
});
}
updateConfirmationDialogInputs(
getDialogInputs(resource)
);
},
vselectStyle: {
dropdownMaxHeight: "150px",
},
options: getCodesForElement("reasonRetry"),
requiredKey: "value",
selectLabel: "description",
},
],
},
{
id: "Unfilled",
confirm_message: $__(
"The supplying library cannot fill the request. The explanation may be provided in the ReasonUnfilled data element"
),
button_label: $__("Unfilled"),
icon: "fa-ban",
btn_class: "btn btn-danger",
next_actions: [],
action_inputs: [
{
name: "reasonUnfilled",
label: $__("Reason unfilled"),
required: true,
type: "select",
options: getCodesForElement("reasonUnfilled"),
requiredKey: "value",
selectLabel: "description",
},
],
},
//HoldReturn
//ReleaseHoldReturn
{
id: "CopyCompleted",
confirm_message: $__(
"The supplying library has sent the requested item (this status is used when there is no need to return the item supplied)"
),
button_label: $__("Copy completed"),
icon: "fa-check",
btn_class: "btn btn-primary",
next_actions: [],
dont_show: iso18626_request =>
iso18626_request.service_type === "Loan",
index: -20,
action_inputs: [],
},
{
id: "LoanCompleted",
confirm_message: $__(
"The supplying library has received the borrowed item from the requesting agency (this status is used for requests when the item supplied shall be returned by the requesting library, i.e. a loan)"
),
button_label: $__("Loan completed"),
icon: "fa-check",
btn_class: "btn btn-primary",
next_actions: [],
action_inputs: [],
},
{
id: "CompletedWithoutReturn",
confirm_message: $__(
"The supplying library has closed the request without the return of supplied item, e.g. because of loss or damage"
),
button_label: $__("Complete without return"),
icon: "fa-check",
btn_class: "btn btn-primary",
next_actions: [],
action_inputs: [],
},
{
id: "Cancelled",
confirm_message: $__(
"You are responding to this request's cancellation action (as indicated by the requesting library)"
),
button_label: $__("Cancel"),
icon: "fa-xmark",
btn_class: "btn btn-danger",
next_actions: [],
action_inputs: [
{
name: "answerYesNo",
type: "boolean",
label: $__("Can cancel?"),
value: true,
},
],
dont_show: iso18626_request =>
iso18626_request.pending_requesting_agency_action !==
"Cancel",
},
]);
const statusToUpdate = ref({});
const action = ref();
const progressRequest = (actionClicked, iso18626_request) => {
conditionalInputs.value = [];
statusToUpdate.value = statuses.value.find(
status => status.id === actionClicked
);
action.value = actionClicked;
setConfirmationDialog(
{
size: "modal-lg",
title: $__(
"Update this request's status to <strong>%s</strong>?"
).format(actionClicked),
message: statusToUpdate.value.confirm_message,
accept_label: $__("Confirm"),
cancel_label: $__("Cancel"),
inputs: getDialogInputs(),
},
(callback_result, inputFields) => {
const client = APIClient.ill.supplying;
inputFields.answerYesNo =
inputFields.answerYesNo === true
? "Y"
: inputFields.answerYesNo === false
? "N"
: undefined;
client
.patch(
inputFields,
iso18626_request.iso18626_request_id
)
.then(
success => {
for (const key in success) {
if (iso18626_request.hasOwnProperty(key)) {
iso18626_request[key] = success[key];
}
}
setMessage(
$__("ISO18626 request #%s updated").format(
iso18626_request.iso18626_request_id
),
true
);
baseResource.refreshTemplateState();
},
error => {}
);
}
);
};
const getDialogInputs = inputValues => {
if (inputValues && statusToUpdate.value.action_inputs) {
for (const actionInput of statusToUpdate.value.action_inputs) {
if (
inputValues &&
inputValues.hasOwnProperty(actionInput.name)
) {
actionInput.value = inputValues[actionInput.name];
}
}
}
if (inputValues && conditionalInputs.value) {
for (const conditionalInput of conditionalInputs.value) {
if (
inputValues &&
inputValues.hasOwnProperty(conditionalInput.name)
) {
conditionalInput.value =
inputValues[conditionalInput.name];
}
}
}
return [
...(statusToUpdate.value.action_inputs
? statusToUpdate.value.action_inputs
: []),
...(conditionalInputs.value ? conditionalInputs.value : []),
{
name: "messageInfoNote",
type: "textarea",
textAreaRows: 7,
label: $__("Message note"),
placeholder: $__(
"Note to be sent to the requesting agency"
),
value:
inputValues &&
inputValues.hasOwnProperty("messageInfoNote")
? inputValues.messageInfoNote
: "",
required: false,
},
{
name: "status",
type: "hidden",
value: action.value,
},
];
};
const additionalToolbarButtons = resource => {
const show_buttons = [];
const currentStatus = statuses.value.find(
status => status.id === resource.status
);
if (currentStatus) {
currentStatus.next_actions.forEach(nextStatus => {
const nextStatusDef = statuses.value.find(
status => status.id === nextStatus
);
if (
nextStatusDef.dont_show &&
nextStatusDef.dont_show(resource)
) {
return;
}
show_buttons.push({
cssClass: nextStatusDef.btn_class,
title: nextStatusDef.button_label,
icon: nextStatusDef.icon,
index: nextStatusDef.index,
onClick: () =>
progressRequest(nextStatusDef.id, resource),
});
});
}
return {
show: show_buttons,
};
};
const defaultToolbarButtons = () => {
return {
list: [],
show: [],
};
};
const baseResource = useBaseResource({
resourceName: "iso18626_request",
nameAttr: "iso18626_request_id",
idAttr: "iso18626_request_id",
components: {
show: "SupplyingShow",
list: "SupplyingList",
},
apiClient: APIClient.ill.supplying,
additionalToolbarButtons,
defaultToolbarButtons,
i18n: {
deleteConfirmationMessage: $__(
"Are you sure you want to remove this supplying ILL?"
),
deleteSuccessMessage: $__("Supplying ILL %s deleted"),
displayName: $__("Supplying ILL"),
editLabel: $__("Edit supplying ILL #%s"),
emptyListMessage: $__("There are no supplying ILLs defined"),
newLabel: $__("New supplying ILL"),
},
table: {
resourceTableUrl:
APIClient.ill.httpClient._baseURL + "iso18626_requests",
},
resourceAttrs: [
{
name: "iso18626_request_id",
label: $__("ID"),
type: "text",
hideIn: ["Form"],
group: $__("Request details"),
},
{
name: "supplyingAgencyId",
label: $__("Supplying Agency ID"),
type: "text",
hideIn: ["Form"],
group: $__("Request details"),
},
{
name: "iso18626_requesting_agency_id",
label: $__("Requesting Agency"),
type: "relationshipSelect",
showElement: {
type: "text",
value: "requesting_agency.name",
link: {
href: "/cgi-bin/koha/ill/iso18626_requesting_agencies",
slug: "iso18626_requesting_agency_id",
},
},
relationshipAPIClient: APIClient.ill.requesting_agencies,
relationshipOptionLabelAttr: "name",
relationshipRequiredKey: "iso18626_requesting_agency_id",
group: $__("Request details"),
},
{
name: "status",
label: $__("Status"),
type: "text",
hideIn: ["Form"],
group: $__("Request details"),
},
{
name: "service_type",
label: $__("Service type"),
type: "text",
hideIn: ["Form"],
group: $__("Request details"),
},
{
name: "pending_requesting_agency_action",
label: $__("Pending RA action"),
type: "text",
hideIn: ["List", "Show", "Form"],
group: $__("Request details"),
},
{
name: "timestamp",
label: $__("Last modified"),
type: "date",
hideIn: ["Form"],
group: $__("Request details"),
},
{
name: "requestingAgencyRequestId",
label: $__("Requesting Agency Request ID"),
type: "text",
hideIn: ["List", "Form"],
group: $__("Request details"),
},
{
group: $__("ISO18626 Messages"),
name: "messages",
label: "",
type: "relationshipWidget",
hideIn: ["List"],
type: "component",
columnData: "messages",
hidden: iso18626_request => 1,
showElement: {
componentPath:
"@koha-vue/components/ILL/ISO18626MessageDisplay.vue",
componentProps: {
iso18626_request: {
type: "resource",
value: null,
},
},
},
},
],
moduleStore: "ILLStore",
props: props,
});
const tableOptions = {
url: () => tableUrl(),
options: { embed: "requesting_agency,messages" },
//table_settings: supplying_ill_table_settings, #FIXME: This causes error from datatables.js -> out of this scope
table_settings: {
columns: [
{
columnname: "iso18626_request_id",
cannot_be_modified: 0,
is_hidden: 0,
cannot_be_toggled: 0,
},
{
columnname: "supplyingAgencyId",
is_hidden: 0,
cannot_be_modified: 0,
cannot_be_toggled: 0,
},
{
is_hidden: 0,
cannot_be_toggled: 0,
cannot_be_modified: 0,
columnname: "requestingAgencyId",
},
{
is_hidden: 0,
cannot_be_toggled: 0,
cannot_be_modified: 0,
columnname: "status",
},
{
is_hidden: 0,
cannot_be_modified: 0,
cannot_be_toggled: 0,
columnname: "timestamp",
},
{
is_hidden: 0,
cannot_be_toggled: 0,
cannot_be_modified: 0,
columnname: "requestingAgencyRequestId",
},
],
default_display_length: null,
table: "iso18626_requests",
module: "ill",
default_save_state: 1,
page: "ill",
default_sort_order: null,
default_save_state_search: 0,
},
actions: {
0: ["show"],
1: [],
"-1": [
{
receive: {
text: $__("Manage request"),
icon: "fa fa-pencil",
should_display: row => 1,
callback: ({ iso18626_request_id }, dt, event) => {
event.preventDefault();
router.push({
name: "SupplyingShow",
params: {
iso18626_request_id:
iso18626_request_id,
},
});
},
},
},
],
},
};
const afterResourceFetch = (componentData, resource, caller) => {
if (caller === "show") {
//TODO: Use dateformat sys pref?
resource.timestamp = new Date(
resource.timestamp
).toLocaleString();
}
};
const onFormSave = (e, supplyingILLToSave) => {
e.preventDefault();
// Nothing to do here
};
const tableUrl = filters => {
return baseResource.getResourceTableUrl();
};
return {
...baseResource,
tableOptions,
onFormSave,
tableUrl,
afterResourceFetch,
};
},
emits: ["select-resource"],
name: "SupplyingResource",
components: {
BaseResource,
},
};
</script>

View File

@ -15,7 +15,9 @@
>
<slot />
</a>
<slot v-else />
<template v-else v-html>
<slot />
</template>
</template>
<script>

View File

@ -0,0 +1,200 @@
<template>
<WidgetWrapper v-bind="widgetWrapperProps">
<template #default>
<KohaTable
ref="table"
v-bind="tableOptions"
:key="JSON.stringify(tableOptions)"
@view="viewILLRequest"
/>
<div>
<button
class="btn btn-primary"
type="button"
@click="visitOldILLModule"
>
{{ visitOldILLModuleText }}
</button>
</div>
</template>
</WidgetWrapper>
</template>
<script>
import { ref, computed } from "vue";
import WidgetWrapper from "../WidgetWrapper.vue";
import KohaTable from "../../KohaTable.vue";
import { useBaseWidget } from "../../../composables/base-widget.js";
import { $__ } from "@koha-vue/i18n";
export default {
name: "ILLRequests",
components: { WidgetWrapper, KohaTable },
props: {
display: String,
dashboardColumn: String,
},
emits: ["removed", "added", "moveWidget"],
setup(props, { emit }) {
const table = ref();
const default_settings = {
//TODO: Grab logged in user, filter by user branch by default
status: [], //["NEW", "UNAUTH"],
per_page: 5,
};
const visitOldILLModuleText = $__(
"Visit ILL requests (previously ILL Module)"
);
const settings = ref(default_settings);
const settings_definitions = ref([
{
name: "status",
type: "select",
label: __("Status"),
showInTable: true,
options: settings.value.status.map(status => ({
value: status.value,
description: "my ets",
})),
allowMultipleChoices: true,
requiredKey: "value",
selectLabel: "description",
},
{
name: "per_page",
type: "select",
label: __("Show"),
showInTable: true,
options: [
{ value: 5, description: "5" },
{ value: 10, description: "10" },
{ value: 20, description: "20" },
],
requiredKey: "value",
selectLabel: "description",
},
]);
function settingsToQueryParams(settings) {
const params = {};
if (settings.status && settings.status.length > 0) {
params["me.status"] = {
"-in": settings.status,
};
}
return params;
}
const tableOptions = computed(() => ({
columns: [
{
title: __("ID"),
data: "ill_request_id",
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return (
'<a target="_blank" href="/cgi-bin/koha/ill/ill-requests.pl?' +
"op=illview&amp;illrequest_id=" +
encodeURIComponent(data) +
'">' +
escape_str(row.id_prefix) +
escape_str(data) +
"</a>"
);
},
},
{
title: __("Status"),
data: "_strings.status.str",
searchable: true,
orderable: true,
},
{
title: __("Library"),
data: "library.name",
searchable: true,
orderable: true,
},
{
title: __("Patron"),
data: "patron.firstname:patron.surname:patron.cardnumber",
render: function (data, type, row, meta) {
return row.patron
? $patron_to_html(row.patron, {
display_cardnumber: true,
url: true,
})
: "";
},
},
{
title: __("Backend"),
data: "ill_backend_id",
searchable: true,
orderable: true,
},
],
options: {
dom: "t",
embed: "patron,library,extended_attributes,+strings",
pageLength: settings.value.per_page || 5,
processing: false,
},
url: "/api/v1/ill/requests",
default_filters: settingsToQueryParams(settings.value),
actions: {
"-1": [
{
view: {
text: $__("View"),
icon: "fa fa-eye",
},
},
],
},
}));
const baseWidget = useBaseWidget(
{
id: "ILLRequests",
name: $__("ILL Requests"),
icon: "fa fa-download",
description: $__(
"Show ILL requests that need action. It filters requests by status and library. Provides a button to visit the ILL requests (previously ILL Module). This widget is configurable."
),
loading: false,
settings: settings,
settings_definitions: settings_definitions,
...props,
},
emit
);
function viewILLRequest(req, dt, event) {
event.preventDefault();
window.open(
"/cgi-bin/koha/ill/ill-requests.pl?op=illview&illrequest_id=" +
encodeURIComponent(req.ill_request_id),
"_blank"
);
}
function visitOldILLModule() {
const url = "/cgi-bin/koha/ill/ill-requests.pl";
window.location.href = url;
}
return {
...baseWidget,
table,
tableOptions,
viewILLRequest,
visitOldILLModuleText,
visitOldILLModule,
};
},
};
</script>

View File

@ -1,6 +1,7 @@
import HttpClient from "./http-client";
import ERMAPIClient from "@fetch/erm-api-client";
import ILLAPIClient from "@fetch/ill-api-client";
import PatronAPIClient from "@fetch/patron-api-client";
import AcquisitionAPIClient from "@fetch/acquisition-api-client";
import AdditionalFieldsAPIClient from "@fetch/additional-fields-api-client";
@ -14,6 +15,7 @@ import PreservationAPIClient from "@fetch/preservation-api-client";
export const APIClient = {
erm: new ERMAPIClient(HttpClient),
ill: new ILLAPIClient(HttpClient),
patron: new PatronAPIClient(HttpClient),
acquisition: new AcquisitionAPIClient(HttpClient),
additional_fields: new AdditionalFieldsAPIClient(HttpClient),

View File

@ -0,0 +1,110 @@
import { createApp } from "vue";
import { createWebHistory, createRouter } from "vue-router";
import { createPinia } from "pinia";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faList,
faPlus,
faMinus,
faPencil,
faTrash,
faSpinner,
faGlobe,
faUpload,
faDownload,
faBuildingColumns,
faCheck,
faXmark,
faRepeat,
faBox,
faCalendarDays,
faBan,
faCog,
faEllipsisVertical,
faArrowRight,
faArrowLeft,
faArrowUp,
faArrowDown,
faSearch,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import vSelect from "vue-select";
library.add(
faList,
faPlus,
faMinus,
faPencil,
faTrash,
faSpinner,
faGlobe,
faUpload,
faDownload,
faBuildingColumns,
faCheck,
faXmark,
faRepeat,
faBox,
faCalendarDays,
faBan,
faCog,
faEllipsisVertical,
faArrowRight,
faArrowLeft,
faArrowUp,
faArrowDown,
faSearch
);
import App from "../components/ILL/Main.vue";
import { routes as routesDef } from "../routes/ill";
import { useMainStore } from "../stores/main";
import { useVendorStore } from "../stores/vendors";
import { useILLStore } from "../stores/ill";
import { useNavigationStore } from "../stores/navigation";
import i18n from "@koha-vue/i18n";
const pinia = createPinia();
const mainStore = useMainStore(pinia);
const navigationStore = useNavigationStore(pinia);
const routes = navigationStore.setRoutes(routesDef);
const router = createRouter({
history: createWebHistory(),
linkActiveClass: "current",
routes,
});
const app = createApp(App);
const rootComponent = app
.use(i18n)
.use(pinia)
.use(router)
.component("font-awesome-icon", FontAwesomeIcon)
.component("v-select", vSelect);
app.config.unwrapInjectedRef = true;
app.provide("vendorStore", useVendorStore(pinia));
app.provide("mainStore", mainStore);
app.provide("navigationStore", navigationStore);
const ILLStore = useILLStore(pinia);
app.provide("ILLStore", ILLStore);
const { removeMessages } = mainStore;
router.beforeEach((to, from) => {
navigationStore.$patch({
current: to.matched,
params: to.params || {},
from,
});
removeMessages();
});
router.isReady().then(() => {
app.mount("#ill");
});

View File

@ -0,0 +1,130 @@
import { markRaw } from "vue";
import Home from "../components/ILL/Home.vue";
import ProgressRequest from "../components/ILL/ProgressRequest.vue";
import ResourceWrapper from "../components/ResourceWrapper.vue";
import { $__ } from "@koha-vue/i18n";
export const routes = [
{
path: "/cgi-bin/koha/ill/ill.pl",
redirect: "/cgi-bin/koha/ill/home",
is_default: true,
is_base: true,
title: $__("Interlibrary loans"),
children: [
{
path: "/cgi-bin/koha/ill/home",
name: "Home",
component: markRaw(Home),
title: $__("Home"),
icon: "fa fa-home",
},
{
path: "/cgi-bin/koha/ill",
title: $__("Borrowing"),
icon: "fa fa-arrow-circle-down",
disabled: true,
children: [
{
path: "/cgi-bin/koha/ill/ill-requests.pl",
title: $__("Requests"),
icon: "fa fa-download",
is_external: true,
is_navigation_item: true,
},
{
path: "/cgi-bin/koha/ill/ill-requests.pl?method=batch_list",
title: $__("Batches"),
icon: "fa fa-clone",
is_external: true,
is_navigation_item: true,
},
],
},
{
path: "/cgi-bin/koha/ill",
title: $__("Lending"),
icon: "fa fa-arrow-circle-up",
disabled: true,
children: [
{
path: "/cgi-bin/koha/ill/iso18626_requests",
title: $__("Supplying ILLs"),
icon: "fa fa-upload",
is_end_node: true,
resource: "ILL/SupplyingResource.vue",
children: [
{
path: "",
name: "SupplyingList",
component: markRaw(ResourceWrapper),
},
{
path: ":iso18626_request_id",
name: "SupplyingShow",
component: markRaw(ResourceWrapper),
title: "{iso18626_request_id}",
},
{
path: "progress-request/:iso18626_request_id",
name: "ProgressRequest",
component: markRaw(ProgressRequest),
title: $__(
"Progress request {iso18626_request_id}"
),
},
],
},
{
path: "/cgi-bin/koha/ill/iso18626_requesting_agencies",
title: $__("Requesting Agencies"),
icon: "fa fa-building-columns",
is_end_node: true,
resource: "ILL/RequestingAgencyResource.vue",
children: [
{
path: "",
name: "RequestingAgenciesList",
component: markRaw(ResourceWrapper),
},
{
path: ":iso18626_requesting_agency_id",
name: "RequestingAgenciesShow",
component: markRaw(ResourceWrapper),
title: "{name}",
},
{
path: "add",
name: "RequestingAgenciesFormAdd",
component: markRaw(ResourceWrapper),
title: $__("Add requesting agency"),
},
{
path: "edit/:iso18626_requesting_agency_id",
name: "RequestingAgenciesFormAddEdit",
component: markRaw(ResourceWrapper),
title: "{name}",
breadcrumbFormat: ({
match,
params,
query,
}) => {
match.name = "RequestingAgenciesShow";
return match;
},
additionalBreadcrumbs: [
{
title: $__("Modify requesting agency"),
disabled: true,
},
],
},
],
},
],
},
],
},
];

View File

@ -0,0 +1,17 @@
import { defineStore } from "pinia";
import { reactive, toRefs } from "vue";
export const useILLStore = defineStore("ill", () => {
const store = reactive({
config: {
settings: {
ILLModule: 1,
ILLPartnerCode: "IL",
},
},
});
return {
...toRefs(store),
};
});

View File

@ -133,6 +133,10 @@ module.exports = [
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
"./koha-tmpl/intranet-tmpl/prog/js/vue/modules/sip2.ts",
],
ill: [
"./koha-tmpl/intranet-tmpl/prog/js/vue/csp-nonce.js",
"./koha-tmpl/intranet-tmpl/prog/js/vue/modules/ill.ts",
],
},
output: {
filename: "[name].js",