Bugzilla – Attachment 156640 Details for
Bug 30708
Creation of a new 'Preservation' module
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 30708: Vue app
Bug-30708-Vue-app.patch (text/plain), 130.64 KB, created by
Marcel de Rooy
on 2023-10-07 06:40:03 UTC
(
hide
)
Description:
Bug 30708: Vue app
Filename:
MIME Type:
Creator:
Marcel de Rooy
Created:
2023-10-07 06:40:03 UTC
Size:
130.64 KB
patch
obsolete
>From 015c74bd50d6ae5a89f4238c6dc030376ae33b1e Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Date: Fri, 7 Apr 2023 14:50:11 +0200 >Subject: [PATCH] Bug 30708: Vue app >Content-Type: text/plain; charset=utf-8 > >This commit contains the main commit message > >The description of the original need is described in documents attached by the sponsor on the bug report specifically "[En] Preservation module - Main principles". >The idea is to develop a whole new module to track the status of the documents that are sent for processings/treatments in order to preserve them (eg. covering). > >This is a first step, more are certainly coming later. > >The author and sponsors have worked for several months before providing this MVP version. The different discussion and needs can be found at https://tree.taiga.io/project/joubu-koha-preservation-module/kanban >Some ideas of the next steps are also listed. > >The first iterations have been done using the classic .pl/.tt Koha style but we finally switched to a new Vue module, for more fun. > >These patches made the following main changes: >New files >* Koha objects under Koha/Preservation >* REST API controllers under Koha/REST/V1/Preservation >* preservation/home.pl and preservation/home.tt >* Vue components under js/vue/components/Preservation >* tests under t/db_dependent/Koha/Preservation and t/db_dependent/api/v1/preservation_* >* Cypress tests under t/cypress/integration/Preservation >DB: >* 3 new sysprefs PreservationModule, PreservationNotForLoanWaitingListIn, PreservationNotForLoanDefaultTrainIn >* 1 new permission "preservation" (will be split into subpermissions later) >* 5 new tables: >- preservation_processings >- preservation_trains >- preservation_processing_attributes >- preservation_trains_items >- preservation_processing_attributes_items > >Terminology and workflow: >*Processings* are the different treatments an item can receive during its stay in the preservation module >A *processing* is defined by a list of *attributes*. To make the module as easy to use for the librarians in charge of the preservation area a list of processings will be defined when the module will be set up. An *attribute* is a name and a value. That's it. However it also has a type, to define what the value is coming from: *free text*, *authorised value* or *database column*. >For instance if you are defining a processing that will handling the book cover you could have 3 *attributes*: >- first named "Barcode" that will be automatically filled with "items.barcode" (type *database column*" >- second attribute named "color" linked with a new PRES_COLOR authorised value category you would have previously defined with "red", "blue", "green", etc. (type *authorised value*) >- third attribute named "notes", because librarians like notes (type *free text*) >Important: Even if the attribute is linked with a DB column or AV category, the value will be automatically pre filled but will stay editable (could be a config option to restrict the edition, later, if needed). > >The *status* of an item will change during the preservation process. First it will arrive in the preservation area and be on a *waiting list*. It is not processed already but is not available anymore for the patrons of the library. That's why we are going to use the "not for loan" (items.notforloan) value for this. This *waiting list* is a fictional concept, it simply lists all the items in the library with a specific *status*. >A *train* is... how they call that at the BULAC, a train (same in French!). And we quite like the word so we kept it. It is what it is: a list of items/waggons, one after each other. We could have picked "cart", "list", but the concepts were already used in different places. We are not strongly attached to the term and it can be modified (but it's spread all over the code already and will be tedious to modify!) if you have a very good suggestion :) >So, a *train* is where items are going after they have been sent to the waiting list. It's a stack of items that will be sent to a provider. When you create a new train you will be asked for the "Status for item added to this train", that will be the "not for loan" value to set to the items added to this train, and a "Default processing" that will be the processing used. But keep in mind that a train can have items that have different processings (specific case, will see later). >When all items have been added to a *train*, you can *close* it. You cannot add items anymore to it! Then you can *send* it, and finally *receive* it. They are just statuses to keep track of the dates, and filter trains by status. >However when a train is received you can *copy* an item to another (opened) train. It means that you have the item on hand but something went wrong, you are not happy with the work done by the supplier and want to send it back, so you create a new train (that can have different items, and it is the case where you will have items in a train that don't all have the same processing!). > >Test plan: >A. Prerequisites >0. Just `reset_all` and jump to B, or: >1. Apache configuration >You will need to edit /etc/koha/apache-shared-intranet-git.conf and add the following lines after the RewriteRule for erm (line.24?) >RewriteCond %{REQUEST_URI} !^/cgi-bin/koha/preservation/.*.pl$ >RewriteRule ^/cgi-bin/koha/preservation/.*$ /cgi-bin/koha/preservation/home.pl [PT] >The RewriteCond is only useful if you are testing the "print slips" bugs as well, but it cannot hurt to have it! >2. `yarn js:build` to regenerate the Vue app for the preservation module >3. `updatedatabase` >4. `restart_all` >B. Settings >0. >Create 2 different values for NOTLOAN, eg. 'In preservation' and 'In preservation external' >Create different authorised values for a new category, eg. PRES_COLORS: RED, BLUE, GREEN. Feel free to create more categories. >1. You can turn on the "PreservationModule" syspref and go to the Koha homepage to see a new "Preservation" link >2. You landed on the empty home page of the preservation, no worry! We need to fill this page with useful information! (see #2 on the kanban) >3. Go to settings >4. Set "Status for item added to waiting list" to "In preservation" >and "Default status for item added to train": "In preservation external" >Create a new processing and define some attributes. Ideally at least one of each type. >5. Go to "Waiting list" and add some items >6. Go to "Trains" and create several trains (at least 2). Notice that the "Status for item added to this train" value is set to the value defined in the settings, but can be modified. Notice that this status can be set when a train is created but it won't be possible to edit later. >7. Add items to a train. You can only add items that are already in the waiting list. Add values for the attributes. Notice that the attributes linked with a database column are automatically pre filled. Notice that attributes linked with an authorised value are displayed with a dropdown list but that a different value can be set (remember, this is a feature!). Notice that attributes can be multivalued. >8. Add other items to the waiting list, notice the "Add last X items to a train" link at the top of the waiting list table, click it >9. You can now add several items to a train, directly (for instance if you don't really need to pass through the waiting list). Values can be set for the batch, but attributes linked with a database column are not editable (they will be prefilled automatically) >10. Once you have a train with several items, look at the "show train" view and notice the item list. If all of them are using the same processing then a table is displayed, one column per attribute. However if at least one item of the train has a different processing then the items are not listed in a table. >11. Edit items and confirm that the values are correctly saved. >12. Close, send and receive a train >13. Once a train is closed you can no longer add items to it >14. Once a train is received notice that you can "copy" an item to another (opened) train > >QA notes: >The patch is huge! New enhancements and improvements have been moved to separate bug reports but this cannot be split. We need a ground base to build on top. >The size is mainly coming from Vue components, Koha::Objects, REST API controllers and specs, and tests. Nothing hard ;) > >More to come: >- See the kanban! >- Print slips (bug 33547 and bug 34030) >- Put something on the landing page! >- Link with the acquisition module (suppliers, funds, etc.) > >Sponsored-by: BULAC - http://www.bulac.fr/ > >Signed-off-by: BULAC - http://www.bulac.fr/ >Signed-off-by: Heather Hernandez <heather_hernandez@nps.gov> >Signed-off-by: Laurence Rault <laurence.rault@biblibre.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >--- > .../prog/js/vue/components/KohaTable.vue | 6 + > .../js/vue/components/Preservation/Home.vue | 11 + > .../js/vue/components/Preservation/Main.vue | 194 +++++++ > .../vue/components/Preservation/Settings.vue | 130 +++++ > .../Preservation/SettingsProcessings.vue | 110 ++++ > .../SettingsProcessingsFormAdd.vue | 286 +++++++++ > .../Preservation/SettingsProcessingsShow.vue | 151 +++++ > .../components/Preservation/TrainsFormAdd.vue | 196 +++++++ > .../Preservation/TrainsFormAddItem.vue | 327 +++++++++++ > .../components/Preservation/TrainsList.vue | 270 +++++++++ > .../components/Preservation/TrainsShow.vue | 548 ++++++++++++++++++ > .../components/Preservation/TrainsToolbar.vue | 16 + > .../components/Preservation/WaitingList.vue | 344 +++++++++++ > .../prog/js/vue/fetch/api-client.js | 2 + > .../vue/fetch/authorised-values-api-client.js | 4 + > .../js/vue/fetch/preservation-api-client.js | 184 ++++++ > .../prog/js/vue/modules/preservation.ts | 74 +++ > .../prog/js/vue/routes/preservation.js | 220 +++++++ > .../prog/js/vue/stores/authorised-values.js | 1 + > .../prog/js/vue/stores/preservation.js | 10 + > webpack.config.js | 1 + > 21 files changed, 3085 insertions(+) > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Home.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Main.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Settings.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessings.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsFormAdd.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsShow.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAdd.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItem.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsList.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsToolbar.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/stores/preservation.js > >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue >index 0e27318e6a..740953ccb4 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/KohaTable.vue >@@ -105,6 +105,12 @@ export default { > this.$__("Delete") + > "</a>" > ) >+ } else if (action == "remove") { >+ content.push( >+ '<a class="remove btn btn-default btn-xs" role="button"><i class="fa fa-remove"></i> ' + >+ this.$__("Remove") + >+ "</a>" >+ ) > } > }) > return content.join(" ") >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Home.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Home.vue >new file mode 100644 >index 0000000000..e924a04a2b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Home.vue >@@ -0,0 +1,11 @@ >+<template></template> >+ >+<script> >+export default { >+ data() { >+ return {} >+ }, >+ methods: {}, >+ components: {}, >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Main.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Main.vue >new file mode 100644 >index 0000000000..07794196f2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Main.vue >@@ -0,0 +1,194 @@ >+<template> >+ <div v-if="initialized && PreservationModule == 1"> >+ <Breadcrumb /> >+ <div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-sm-10 col-sm-push-2"> >+ <main> >+ <Dialog /> >+ <router-view /> >+ </main> >+ </div> >+ >+ <div class="col-sm-2 col-sm-pull-10"> >+ <aside> >+ <div id="navmenu"> >+ <div id="navmenulist"> >+ <h5>{{ $__("Preservation") }}</h5> >+ <ul> >+ <li> >+ <router-link >+ to="/cgi-bin/koha/preservation/home.pl" >+ > >+ <i class="fa fa-home"></i> >+ {{ $__("Home") }}</router-link >+ > >+ </li> >+ <li> >+ <router-link >+ to="/cgi-bin/koha/preservation/trains" >+ > >+ <i class="fa fa-train"></i> >+ {{ $__("Trains") }}</router-link >+ > >+ </li> >+ <li> >+ <router-link >+ to="/cgi-bin/koha/preservation/waiting-list" >+ > >+ <i class="fa fa-recycle"></i> >+ {{ $__("Waiting list") }} >+ </router-link> >+ </li> >+ <li> >+ <router-link >+ to="/cgi-bin/koha/preservation/settings" >+ > >+ <i class="fa fa-cog"></i> >+ {{ $__("Settings") }} >+ </router-link> >+ </li> >+ </ul> >+ </div> >+ </div> >+ </aside> >+ </div> >+ </div> >+ </div> >+ </div> >+ <div class="main container-fluid" v-else> >+ <Dialog /> >+ </div> >+</template> >+ >+<script> >+import { inject } from "vue" >+import Breadcrumb from "../Breadcrumb.vue" >+import Dialog from "../Dialog.vue" >+import { APIClient } from "../../fetch/api-client.js" >+import "vue-select/dist/vue-select.css" >+ >+export default { >+ setup() { >+ const AVStore = inject("AVStore") >+ >+ const mainStore = inject("mainStore") >+ >+ const { loading, loaded, setError } = mainStore >+ >+ const PreservationStore = inject("PreservationStore") >+ return { >+ AVStore, >+ loading, >+ loaded, >+ setError, >+ PreservationStore, >+ } >+ }, >+ data() { >+ return { >+ initialized: false, >+ PreservationModule: null, >+ } >+ }, >+ beforeCreate() { >+ this.loading() >+ >+ const fetch_config = () => { >+ const sysprefs_client = APIClient.sysprefs >+ const av_client = APIClient.authorised_values >+ let promises = [ >+ sysprefs_client.sysprefs >+ .get("PreservationNotForLoanWaitingListIn") >+ .then( >+ value => { >+ this.PreservationStore.settings.not_for_loan_waiting_list_in = >+ value.value >+ }, >+ error => {} >+ ), >+ sysprefs_client.sysprefs >+ .get("PreservationNotForLoanDefaultTrainIn") >+ .then( >+ value => { >+ this.PreservationStore.settings.not_for_loan_default_train_in = >+ value.value >+ }, >+ error => {} >+ ), >+ av_client.values.get("NOT_LOAN").then( >+ values => { >+ this.AVStore.av_notforloan = values >+ }, >+ error => {} >+ ), >+ ] >+ >+ return Promise.all(promises) >+ } >+ >+ const sysprefs_client = APIClient.sysprefs >+ sysprefs_client.sysprefs >+ .get("PreservationModule") >+ .then(value => { >+ this.PreservationModule = value.value >+ if (this.PreservationModule != 1) { >+ return this.setError( >+ this.$__( >+ 'The preservation module is disabled, turn on <a href="/cgi-bin/koha/admin/preferences.pl?tab=&op=search&searchfield=PreservationModule">PreservationModule</a> to use it' >+ ), >+ false >+ ) >+ } >+ return fetch_config() >+ }) >+ .then(() => { >+ this.loaded() >+ this.initialized = true >+ }) >+ }, >+ >+ components: { >+ Breadcrumb, >+ Dialog, >+ }, >+} >+</script> >+ >+<style> >+#navmenulist a.router-link-active { >+ font-weight: 700; >+} >+#menu ul ul, >+#navmenulist ul ul { >+ 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"]), >+textarea { >+ border-color: rgba(60, 60, 60, 0.26); >+ border-width: 1px; >+ border-radius: 4px; >+ min-width: 30%; >+} >+.flatpickr-input { >+ width: 30%; >+} >+ >+#navmenulist ul li a.disabled { >+ color: #666; >+ pointer-events: none; >+ font-weight: 700; >+} >+#navmenulist ul li a.disabled.router-link-active { >+ color: #000; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Settings.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Settings.vue >new file mode 100644 >index 0000000000..a093bef9fa >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/Settings.vue >@@ -0,0 +1,130 @@ >+<template> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else id="settings"> >+ <h2> >+ {{ $__("Edit preservation settings") }} >+ </h2> >+ <div> >+ <form @submit="onSubmit($event)"> >+ <fieldset class="rows"> >+ <legend>{{ $__("General settings") }}</legend> >+ <ol> >+ <li> >+ <label >+ class="required" >+ for="not_for_loan_waiting_list_in" >+ >{{ >+ $__( >+ "Status for item added to waiting list" >+ ) >+ }}:</label >+ > >+ <v-select >+ id="not_for_loan_waiting_list_in" >+ v-model="settings.not_for_loan_waiting_list_in" >+ label="description" >+ :reduce="av => av.value" >+ :options="av_notforloan" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required=" >+ !settings.not_for_loan_waiting_list_in >+ " >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ </li> >+ <li> >+ <label for="not_for_loan_default_train_in" >+ >{{ >+ $__( >+ "Default status for item added to train" >+ ) >+ }}:</label >+ > >+ <v-select >+ id="not_for_loan_default_train_in" >+ v-model="settings.not_for_loan_default_train_in" >+ label="description" >+ :reduce="av => av.value" >+ :options="av_notforloan" >+ /> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Submit" /> >+ <router-link >+ to="/cgi-bin/koha/preservation/home.pl" >+ role="button" >+ class="cancel" >+ >{{ $__("Cancel") }}</router-link >+ > >+ </fieldset> >+ </form> >+ <SettingsProcessings /> >+ </div> >+ </div> >+</template> >+ >+<script> >+import { inject } from "vue" >+import { APIClient } from "../../fetch/api-client.js" >+import { storeToRefs } from "pinia" >+import SettingsProcessings from "./SettingsProcessings.vue" >+ >+export default { >+ setup() { >+ const AVStore = inject("AVStore") >+ const { av_notforloan } = storeToRefs(AVStore) >+ >+ const { setMessage, setWarning } = inject("mainStore") >+ const PreservationStore = inject("PreservationStore") >+ const { settings } = storeToRefs(PreservationStore) >+ >+ return { av_notforloan, setMessage, setWarning, settings } >+ }, >+ data() { >+ return { >+ initialized: true, >+ } >+ }, >+ methods: { >+ checkForm(train) { >+ let errors = [] >+ >+ errors.forEach(function (e) { >+ setWarning(e) >+ }) >+ return !errors.length >+ }, >+ onSubmit(e) { >+ e.preventDefault() >+ const client = APIClient.sysprefs >+ client.sysprefs >+ .update( >+ "PreservationNotForLoanWaitingListIn", >+ this.settings.not_for_loan_waiting_list_in >+ ) >+ .then( >+ client.sysprefs.update( >+ "PreservationNotForLoanDefaultTrainIn", >+ this.settings.not_for_loan_default_train_in || 0 >+ ) >+ ) >+ .then( >+ success => { >+ this.setMessage(this.$__("Settings updated"), true) >+ }, >+ error => {} >+ ) >+ }, >+ }, >+ components: { SettingsProcessings }, >+ name: "Settings", >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessings.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessings.vue >new file mode 100644 >index 0000000000..ac7e54d7aa >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessings.vue >@@ -0,0 +1,110 @@ >+<template> >+ <fieldset> >+ <legend>{{ $__("Processings") }}</legend> >+ <ol> >+ <li >+ :id="`processing_${counter}`" >+ class="rows" >+ v-for="(processing, counter) in processings" >+ v-bind:key="counter" >+ > >+ <router-link >+ :to="`/cgi-bin/koha/preservation/settings/processings/${processing.processing_id}`" >+ > >+ {{ processing.name }} >+ </router-link> >+ >+ <span class="action_links"> >+ <a @click="deleteProcessing(processing)" >+ ><i class="fa fa-trash"></i> >+ {{ $__("Remove this processing") }}</a >+ > >+ >+ <router-link >+ :to="`/cgi-bin/koha/preservation/settings/processings/edit/${processing.processing_id}`" >+ ><i class="fa fa-pencil"></i> >+ {{ $__("Edit this processing") }}</router-link >+ > >+ </span> >+ </li> >+ </ol> >+ <router-link >+ to="/cgi-bin/koha/preservation/settings/processings/add" >+ role="button" >+ class="btn btn-default" >+ ><font-awesome-icon icon="plus" /> >+ {{ $__("Add new processing") }}</router-link >+ > >+ </fieldset> >+</template> >+ >+<script> >+import { inject } from "vue" >+import { APIClient } from "../../fetch/api-client.js" >+ >+export default { >+ setup() { >+ const { setConfirmationDialog, setMessage } = inject("mainStore") >+ return { setConfirmationDialog, setMessage } >+ }, >+ data() { >+ return { >+ processings: [], >+ } >+ }, >+ beforeCreate() { >+ // FIXME Do we want that or a props passed from parent? >+ const client = APIClient.preservation >+ client.processings.getAll().then( >+ processings => { >+ this.processings = processings >+ }, >+ error => {} >+ ) >+ }, >+ methods: { >+ deleteProcessing(processing) { >+ this.setConfirmationDialog( >+ { >+ title: this.$__( >+ "Are you sure you want to remove this processing?" >+ ), >+ message: processing.name, >+ accept_label: this.$__("Yes, delete"), >+ cancel_label: this.$__("No, do not delete"), >+ }, >+ () => { >+ const client = APIClient.preservation >+ client.processings.delete(processing.processing_id).then( >+ success => { >+ this.setMessage( >+ this.$__("Processing %s deleted").format( >+ processing.name >+ ), >+ true >+ ) >+ client.processings.getAll().then( >+ processings => { >+ this.processings = processings >+ }, >+ error => {} >+ ) >+ }, >+ error => {} >+ ) >+ } >+ ) >+ }, >+ }, >+ props: {}, >+ name: "SettingsProcessings", >+} >+</script> >+ >+<style scoped> >+.action_links a { >+ padding-left: 0.2em; >+ font-size: 11px; >+ cursor: pointer; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsFormAdd.vue >new file mode 100644 >index 0000000000..14b29cd639 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsFormAdd.vue >@@ -0,0 +1,286 @@ >+<template> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else id="processings_add"> >+ <h2 v-if="processing.processing_id"> >+ {{ $__("Edit processing #%s").format(processing.processing_id) }} >+ </h2> >+ <h2 v-else>{{ $__("New processing") }}</h2> >+ <div> >+ <form @submit="onSubmit($event)"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="processing_name" >+ >{{ $__("Processing name") }}:</label >+ > >+ <input >+ id="processing_name" >+ v-model="processing.name" >+ :placeholder="$__('Processing name')" >+ required >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="rows"> >+ <legend>{{ $__("Attributes") }}</legend> >+ <fieldset >+ :id="`attribute_${counter}`" >+ class="rows" >+ v-for="(attribute, counter) in processing.attributes" >+ v-bind:key="counter" >+ > >+ <legend> >+ {{ $__("Attribute %s").format(counter + 1) }} >+ <a >+ href="#" >+ @click.prevent="deleteAttribute(counter)" >+ ><i class="fa fa-trash"></i> >+ {{ $__("Remove this attribute") }}</a >+ > >+ </legend> >+ <ol> >+ <li> >+ <label >+ :for="`attribute_name_${counter}`" >+ class="required" >+ >{{ $__("Name") }}: >+ </label> >+ <input >+ :id="`attribute_name_${counter}`" >+ type="text" >+ :name="`attribute_name_${counter}`" >+ v-model="attribute.name" >+ required >+ /> >+ <span class="required">{{ >+ $__("Required") >+ }}</span> >+ </li> >+ <li> >+ <label >+ :for="`attribute_type_${counter}`" >+ class="required" >+ >{{ $__("Type") }}: >+ </label> >+ <v-select >+ :id="`attribute_type_${counter}`" >+ v-model="attribute.type" >+ :options="attribute_types" >+ :reduce="o => o.code" >+ @option:selected=" >+ attribute.option_source = null >+ " >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required="!attribute.type" >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ <span class="required">{{ >+ $__("Required") >+ }}</span> >+ </li> >+ <li v-if="attribute.type == 'authorised_value'"> >+ <label >+ :for="`attribute_option_${counter}`" >+ class="required" >+ >{{ $__("Options") }}: >+ </label> >+ <v-select >+ :id="`attribute_option_${counter}`" >+ v-model="attribute.option_source" >+ :options="authorised_value_categories" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required="!attribute.option_source" >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ <span class="required">{{ >+ $__("Required") >+ }}</span> >+ </li> >+ <li v-if="attribute.type == 'db_column'"> >+ <label >+ :for="`attribute_option_${counter}`" >+ class="required" >+ >{{ $__("Options") }}: >+ </label> >+ <v-select >+ :id="`attribute_option_${counter}`" >+ v-model="attribute.option_source" >+ :options="db_column_options" >+ :reduce="o => o.code" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required="!attribute.option_source" >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ <span class="required">{{ >+ $__("Required") >+ }}</span> >+ </li> >+ </ol> >+ </fieldset> >+ <a class="btn btn-default" @click="addAttribute" >+ ><font-awesome-icon icon="plus" /> >+ {{ $__("Add new attribute") }}</a >+ > >+ </fieldset> >+ >+ <fieldset class="action"> >+ <input type="submit" value="Submit" /> >+ <router-link >+ to="/cgi-bin/koha/preservation/settings" >+ role="button" >+ class="cancel" >+ >{{ $__("Cancel") }}</router-link >+ > >+ </fieldset> >+ </form> >+ </div> >+ </div> >+</template> >+ >+<script> >+import { inject } from "vue" >+import { APIClient } from "../../fetch/api-client.js" >+import { storeToRefs } from "pinia" >+ >+export default { >+ setup() { >+ const AVStore = inject("AVStore") >+ const {} = storeToRefs(AVStore) >+ >+ const { setMessage, setWarning } = inject("mainStore") >+ >+ const db_column_options = Object.keys(db_columns).map(function (c) { >+ return { label: "%s (%s)".format(db_columns[c], c), code: c } >+ }) >+ return { >+ setMessage, >+ setWarning, >+ authorised_value_categories, >+ db_column_options, >+ } >+ }, >+ data() { >+ return { >+ processing: { >+ processing_id: null, >+ name: "", >+ attributes: [], >+ }, >+ attribute_types: [ >+ { >+ label: this.$__("Authorized value"), >+ code: "authorised_value", >+ }, >+ { >+ label: this.$__("Free text"), >+ code: "free_text", >+ }, >+ { >+ label: this.$__("Database column"), >+ code: "db_column", >+ }, >+ ], >+ initialized: false, >+ } >+ }, >+ beforeRouteEnter(to, from, next) { >+ next(vm => { >+ if (to.params.processing_id) { >+ vm.processing = vm.getProcessing(to.params.processing_id) >+ } else { >+ vm.initialized = true >+ } >+ }) >+ }, >+ methods: { >+ async getProcessing(processing_id) { >+ const client = APIClient.preservation >+ await client.processings.get(processing_id).then( >+ processing => { >+ this.processing = processing >+ this.initialized = true >+ }, >+ error => {} >+ ) >+ }, >+ checkForm(processing) { >+ let errors = [] >+ >+ let attributes = processing.attributes >+ >+ errors.forEach(function (e) { >+ setWarning(e) >+ }) >+ >+ return !errors.length >+ }, >+ onSubmit(e) { >+ e.preventDefault() >+ >+ let processing = JSON.parse(JSON.stringify(this.processing)) // copy >+ let processing_id = processing.processing_id >+ delete processing.processing_id >+ >+ if (!this.checkForm(processing)) { >+ return false >+ } >+ >+ processing.attributes = processing.attributes.map( >+ ({ processing_id, processing_attribute_id, ...keepAttrs }) => >+ keepAttrs >+ ) >+ >+ const client = APIClient.preservation >+ if (processing_id) { >+ client.processings.update(processing, processing_id).then( >+ success => { >+ this.setMessage(this.$__("Processing updated")) >+ this.$router.push("/cgi-bin/koha/preservation/settings") >+ }, >+ error => {} >+ ) >+ } else { >+ client.processings.create(processing).then( >+ success => { >+ this.setMessage(this.$__("Processing created")) >+ this.$router.push("/cgi-bin/koha/preservation/settings") >+ }, >+ error => {} >+ ) >+ } >+ }, >+ addAttribute() { >+ this.processing.attributes.push({ >+ name: "", >+ type: null, >+ option_source: null, >+ }) >+ }, >+ deleteAttribute(counter) { >+ this.processing.attributes.splice(counter, 1) >+ }, >+ }, >+ components: {}, >+ name: "SettingsProcessingsFormAdd", >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsShow.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsShow.vue >new file mode 100644 >index 0000000000..25a34781f3 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/SettingsProcessingsShow.vue >@@ -0,0 +1,151 @@ >+<template> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else id="processing_show"> >+ <h2> >+ {{ $__("Processing #%s").format(processing.processing_id) }} >+ <span class="action_links"> >+ <router-link >+ :to="`/cgi-bin/koha/preservation/settings/processings/edit/${processing.processing_id}`" >+ :title="$__('Edit')" >+ ><i class="fa fa-pencil"></i >+ ></router-link> >+ <a @click="doDelete()"><i class="fa fa-trash"></i></a> >+ </span> >+ </h2> >+ <div> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label>{{ $__("Processing name") }}:</label> >+ <span> >+ {{ processing.name }} >+ </span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="rows"> >+ <legend>{{ $__("Attributes") }}</legend> >+ <ol v-if="processing.attributes.length"> >+ <li >+ v-for="(attribute, counter) in processing.attributes" >+ v-bind:key="counter" >+ > >+ <label>{{ attribute.name }}</label> >+ <span v-if="attribute.type == 'authorised_value'">{{ >+ $__("Authorized value") >+ }}</span> >+ <span v-else-if="attribute.type == 'free_text'">{{ >+ $__("Free text") >+ }}</span> >+ <span v-else-if="attribute.type == 'db_column'">{{ >+ $__("Database column") >+ }}</span> >+ <span v-else >+ >{{ $__("Unknown") }} - {{ attribute.type }}</span >+ > >+ </li> >+ </ol> >+ <span v-else> >+ {{ >+ $__( >+ "There are no attributes defined for this processing." >+ ) >+ }} >+ </span> >+ </fieldset> >+ <fieldset class="action"> >+ <router-link >+ to="/cgi-bin/koha/preservation/settings" >+ role="button" >+ class="cancel" >+ >{{ $__("Close") }}</router-link >+ > >+ </fieldset> >+ </div> >+ </div> >+</template> >+ >+<script> >+import { inject } from "vue" >+import { APIClient } from "../../fetch/api-client.js" >+ >+export default { >+ setup() { >+ const { setConfirmationDialog, setMessage } = inject("mainStore") >+ >+ return { >+ setConfirmationDialog, >+ setMessage, >+ } >+ }, >+ data() { >+ return { >+ processing: { >+ processing_id: null, >+ name: "", >+ attributes: [], >+ }, >+ initialized: false, >+ } >+ }, >+ beforeRouteEnter(to, from, next) { >+ next(vm => { >+ vm.getProcessing(to.params.processing_id) >+ }) >+ }, >+ beforeRouteUpdate(to, from) { >+ this.processing = this.getProcessing(to.params.processing_id) >+ }, >+ methods: { >+ async getProcessing(processing_id) { >+ const client = APIClient.preservation >+ await client.processings.get(processing_id).then( >+ processing => { >+ this.processing = processing >+ this.initialized = true >+ }, >+ error => {} >+ ) >+ }, >+ doDelete: function () { >+ this.setConfirmationDialog( >+ { >+ title: this.$__( >+ "Are you sure you want to remove this processing?" >+ ), >+ message: this.processing.name, >+ accept_label: this.$__("Yes, delete"), >+ cancel_label: this.$__("No, do not delete"), >+ }, >+ () => { >+ const client = APIClient.preservation >+ client.processings >+ .delete(this.processing.processing_id) >+ .then( >+ success => { >+ this.setMessage( >+ this.$__("Processing %s deleted").format( >+ this.processing.name >+ ), >+ true >+ ) >+ this.$router.push( >+ "/cgi-bin/koha/preservation/settings" >+ ) >+ }, >+ error => {} >+ ) >+ } >+ ) >+ }, >+ }, >+ name: "ProcessingsShow", >+} >+</script> >+<style scoped> >+.action_links a { >+ padding-left: 0.2em; >+ font-size: 11px; >+ cursor: pointer; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAdd.vue >new file mode 100644 >index 0000000000..001e13f2c1 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAdd.vue >@@ -0,0 +1,196 @@ >+<template> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else id="trains_add"> >+ <h2 v-if="train.train_id"> >+ {{ $__("Edit train #%s").format(train.train_id) }} >+ </h2> >+ <h2 v-else>{{ $__("New train") }}</h2> >+ <div> >+ <form @submit="onSubmit($event)"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="train_name" >+ >{{ $__("name") }}:</label >+ > >+ <input >+ id="train_name" >+ v-model="train.name" >+ :placeholder="$__('Name')" >+ required >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ </li> >+ <li> >+ <label class="required" for="train_description" >+ >{{ $__("Description") }}: >+ </label> >+ <textarea >+ id="train_description" >+ v-model="train.description" >+ :placeholder="$__('Description')" >+ required >+ rows="10" >+ cols="50" >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ </li> >+ <li> >+ <label for="not_for_loan_waiting_list_in" >+ >{{ >+ $__("Status for item added to this train") >+ }}:</label >+ > >+ <v-select >+ id="not_for_loan" >+ v-model="train.not_for_loan" >+ label="description" >+ :reduce="av => av.value" >+ :options="av_notforloan" >+ /> >+ </li> >+ <li> >+ <label >+ class="required" >+ for="train_default_processing" >+ >{{ $__("Default processing") }}: >+ </label> >+ <v-select >+ id="train_default_processing" >+ label="name" >+ v-model="train.default_processing_id" >+ :reduce="p => p.processing_id" >+ :options="processings" >+ :required="!train.default_processing_id" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required="!train.default_processing_id" >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Submit" /> >+ <router-link >+ to="/cgi-bin/koha/preservation/trains" >+ role="button" >+ class="cancel" >+ >{{ $__("Cancel") }}</router-link >+ > >+ </fieldset> >+ </form> >+ </div> >+ </div> >+</template> >+ >+<script> >+import { inject } from "vue" >+import { storeToRefs } from "pinia" >+import { APIClient } from "../../fetch/api-client.js" >+ >+export default { >+ setup() { >+ const AVStore = inject("AVStore") >+ const { av_notforloan } = storeToRefs(AVStore) >+ >+ const { setMessage, setWarning } = inject("mainStore") >+ >+ const PreservationStore = inject("PreservationStore") >+ const { settings } = storeToRefs(PreservationStore) >+ >+ return { av_notforloan, setMessage, setWarning, settings } >+ }, >+ data() { >+ return { >+ train: { >+ train_id: null, >+ name: "", >+ description: "", >+ not_for_loan: this.settings.not_for_loan_default_train_in, >+ default_processing_id: null, >+ created_on: null, >+ closed_on: null, >+ sent_on: null, >+ received_on: null, >+ }, >+ processings: [], >+ initialized: false, >+ } >+ }, >+ beforeRouteEnter(to, from, next) { >+ next(vm => { >+ if (to.params.train_id) { >+ vm.train = vm.getTrain(to.params.train_id) >+ } else { >+ vm.initialized = true >+ } >+ }) >+ }, >+ beforeCreate() { >+ const client = APIClient.preservation >+ client.processings.getAll().then( >+ processings => { >+ this.processings = processings >+ }, >+ error => {} >+ ) >+ }, >+ methods: { >+ async getTrain(train_id) { >+ const client = APIClient.preservation >+ client.trains.get(train_id).then(train => { >+ this.train = train >+ this.initialized = true >+ }) >+ }, >+ checkForm(train) { >+ let errors = [] >+ >+ errors.forEach(function (e) { >+ setWarning(e) >+ }) >+ return !errors.length >+ }, >+ onSubmit(e) { >+ e.preventDefault() >+ >+ let train = JSON.parse(JSON.stringify(this.train)) // copy >+ let train_id = train.train_id >+ if (!this.checkForm(train)) { >+ return false >+ } >+ >+ delete train.train_id >+ delete train.default_processing >+ delete train.items >+ >+ const client = APIClient.preservation >+ if (train_id) { >+ client.trains.update(train, train_id).then( >+ success => { >+ this.setMessage(this.$__("Train updated")) >+ this.$router.push("/cgi-bin/koha/preservation/trains") >+ }, >+ error => {} >+ ) >+ } else { >+ client.trains.create(train).then( >+ success => { >+ this.setMessage(this.$__("Train created")) >+ this.$router.push("/cgi-bin/koha/preservation/trains") >+ }, >+ error => {} >+ ) >+ } >+ }, >+ }, >+ components: {}, >+ name: "TrainsFormAdd", >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItem.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItem.vue >new file mode 100644 >index 0000000000..7294fee78f >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItem.vue >@@ -0,0 +1,327 @@ >+<template> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else id="trains_add_item"> >+ <h2 v-if="train_item && train_item.train_item_id"> >+ {{ $__("Edit item #%s").format(train_item.item_id) }} >+ </h2> >+ <h2 v-else>{{ $__("Add new item to %s").format(train.name) }}</h2> >+ <div v-if="train_item"> >+ <form @submit="onSubmit($event)"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label for="itemnumber" >+ >{{ $__("Itemnumber") }}:</label >+ > >+ <span>{{ train_item.item_id }}</span> >+ </li> >+ <li> >+ <label for="processing" >+ >{{ $__("Processing") }}: >+ </label> >+ <v-select >+ id="processing" >+ label="name" >+ v-model="train_item.processing_id" >+ @option:selected="refreshAttributes(1)" >+ :reduce="p => p.processing_id" >+ :options="processings" >+ :clearable="false" >+ /> >+ </li> >+ <li >+ class="attribute" >+ v-for="(attribute, counter) in attributes" >+ v-bind:key="counter" >+ > >+ <label :for="`attribute_${counter}`" >+ >{{ attribute.name }}: >+ </label> >+ <span v-if="attribute.type == 'authorised_value'"> >+ <v-select >+ :id="`attribute_${counter}`" >+ v-model="attribute.value" >+ label="description" >+ :reduce="av => av.value" >+ :options=" >+ av_options[attribute.option_source] >+ " >+ /> >+ </span> >+ <span v-else-if="attribute.type == 'free_text'"> >+ <input >+ :id="`attribute_${counter}`" >+ v-model="attribute.value" >+ /> >+ </span> >+ <span v-else-if="attribute.type == 'db_column'"> >+ <input >+ :id="`attribute_${counter}`" >+ v-model="attribute.value" >+ /> >+ </span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Submit" /> >+ <router-link >+ to="/cgi-bin/koha/preservation/trains" >+ role="button" >+ class="cancel" >+ >{{ $__("Cancel") }}</router-link >+ > >+ </fieldset> >+ </form> >+ </div> >+ <div v-else> >+ <form @submit="getItemFromWaitingList($event)"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="barcode" >+ >{{ $__("barcode") }}:</label >+ > >+ <input >+ id="barcode" >+ v-model="barcode" >+ :placeholder="$__('barcode')" >+ required >+ /> >+ <span class="required">{{ $__("Required") }}</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Submit" /> >+ <router-link >+ :to="`/cgi-bin/koha/preservation/trains/${train.train_id}`" >+ role="button" >+ class="cancel" >+ >{{ $__("Cancel") }}</router-link >+ > >+ </fieldset> >+ </form> >+ </div> >+ </div> >+</template> >+ >+<script> >+import { inject } from "vue" >+import { APIClient } from "../../fetch/api-client" >+ >+export default { >+ setup() { >+ const { setMessage, setWarning, loading, loaded } = inject("mainStore") >+ return { >+ setMessage, >+ setWarning, >+ loading, >+ loaded, >+ api_mappings, >+ } >+ }, >+ data() { >+ return { >+ train: { >+ train_id: null, >+ name: "", >+ description: "", >+ }, >+ item: { item_id: null }, >+ train_item: null, >+ barcode: "", >+ processings: [], >+ processing: null, >+ initialized: false, >+ av_options: {}, >+ default_values: {}, >+ attributes: [], >+ } >+ }, >+ beforeCreate() { >+ const client = APIClient.preservation >+ client.processings >+ .getAll() >+ .then(processings => (this.processings = processings)) >+ }, >+ beforeRouteEnter(to, from, next) { >+ next(vm => { >+ if (to.params.train_item_id) { >+ vm.train = vm >+ .getTrain(to.params.train_id) >+ .then(() => >+ vm >+ .getTrainItem( >+ to.params.train_id, >+ to.params.train_item_id >+ ) >+ .then(() => >+ vm >+ .refreshAttributes() >+ .then(() => (vm.initialized = true)) >+ ) >+ ) >+ } else { >+ vm.train = vm >+ .getTrain(to.params.train_id) >+ .then(() => (vm.initialized = true)) >+ } >+ }) >+ }, >+ methods: { >+ async getTrain(train_id) { >+ const client = APIClient.preservation >+ await client.trains.get(train_id).then( >+ train => { >+ this.train = train >+ }, >+ error => {} >+ ) >+ }, >+ async getTrainItem(train_id, train_item_id) { >+ const client = APIClient.preservation >+ await client.train_items.get(train_id, train_item_id).then( >+ train_item => { >+ this.train_item = train_item >+ this.item = train_item.catalogue_item >+ }, >+ error => {} >+ ) >+ }, >+ async getItemFromWaitingList(e) { >+ e.preventDefault() >+ const client = APIClient.preservation >+ client.waiting_list_items.get_from_barcode(this.barcode).then( >+ item => { >+ if (!item) { >+ this.setWarning( >+ this.$__( >+ "Cannot find item with this barcode. It must be in the waiting list." >+ ) >+ ) >+ return >+ } >+ this.item = item >+ this.train_item = { >+ item_id: item.item_id, >+ processing_id: this.train.default_processing_id, >+ } >+ this.refreshAttributes(1) >+ }, >+ error => {} >+ ) >+ }, >+ columnApiMapping(db_column) { >+ let table_col = db_column.split(".") >+ let table = table_col[0] >+ let col = table_col[1] >+ let api_attribute = this.api_mappings[table][col] || col >+ return table == "biblio" || table == "biblioitems" >+ ? this.item.biblio[api_attribute] >+ : this.item[api_attribute] >+ }, >+ updateDefaultValues() { >+ this.processing.attributes >+ .filter(attribute => attribute.type == "db_column") >+ .forEach(attribute => { >+ this.default_values[attribute.processing_attribute_id] = >+ this.columnApiMapping(attribute.option_source) >+ }) >+ }, >+ async refreshAttributes(apply_default_value) { >+ this.loading() >+ >+ const client = APIClient.preservation >+ await client.processings.get(this.train_item.processing_id).then( >+ processing => (this.processing = processing), >+ error => {} >+ ) >+ this.updateDefaultValues() >+ this.attributes = this.processing.attributes.map(attribute => { >+ let value = "" >+ if (!apply_default_value) { >+ let existing_attribute = this.train_item.attributes.find( >+ a => >+ a.processing_attribute_id == >+ attribute.processing_attribute_id >+ ) >+ if (existing_attribute) { >+ value = existing_attribute.value >+ } >+ } else if (attribute.type == "db_column") { >+ value = >+ this.default_values[attribute.processing_attribute_id] >+ } >+ return { >+ processing_attribute_id: attribute.processing_attribute_id, >+ name: attribute.name, >+ type: attribute.type, >+ option_source: attribute.option_source, >+ value, >+ } >+ }) >+ const client_av = APIClient.authorised_values >+ let av_cat_array = this.processing.attributes >+ .filter(attribute => attribute.type == "authorised_value") >+ .map(attribute => attribute.option_source) >+ >+ client_av.values >+ .getCategoriesWithValues([ >+ ...new Set(av_cat_array.map(av_cat => '"' + av_cat + '"')), >+ ]) // unique >+ .then(av_categories => { >+ av_cat_array.forEach(av_cat => { >+ let av_match = av_categories.find( >+ element => element.category_name == av_cat >+ ) >+ this.av_options[av_cat] = av_match.authorised_values >+ }) >+ }) >+ .then(() => this.loaded()) >+ }, >+ onSubmit(e) { >+ e.preventDefault() >+ >+ let train_item_id = this.train_item.train_item_id >+ let train_item = { >+ item_id: this.train_item.item_id, >+ processing_id: this.train_item.processing_id, >+ attributes: this.attributes.map(a => ({ >+ processing_attribute_id: a.processing_attribute_id, >+ value: a.value, >+ })), >+ } >+ >+ const client = APIClient.preservation >+ if (train_item_id) { >+ client.train_items >+ .update(train_item, this.train.train_id, train_item_id) >+ .then( >+ success => { >+ this.setMessage(this.$__("Item updated")) >+ this.$router.push( >+ "/cgi-bin/koha/preservation/trains/" + >+ this.train.train_id >+ ) >+ }, >+ error => {} >+ ) >+ } else { >+ client.train_items.create(train_item, this.train.train_id).then( >+ success => { >+ this.setMessage(this.$__("Item added to train")) >+ this.$router.push( >+ "/cgi-bin/koha/preservation/trains/" + >+ this.train.train_id >+ ) >+ }, >+ error => {} >+ ) >+ } >+ }, >+ }, >+ components: {}, >+ name: "TrainsFormAdd", >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsList.vue >new file mode 100644 >index 0000000000..d35304b410 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsList.vue >@@ -0,0 +1,270 @@ >+<template> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else id="trains_list"> >+ <Toolbar /> >+ <fieldset v-if="count_trains > 0" class="filters"> >+ <label>{{ $__("Filter by") }}:</label> >+ <input >+ type="radio" >+ id="all_status_filter" >+ v-model="filters.status" >+ value="" >+ /><label for="all_status_filter">{{ $__("All") }}</label> >+ <input >+ type="radio" >+ id="closed_status_filter" >+ v-model="filters.status" >+ value="closed" >+ /><label for="closed_status_filter">{{ $__("Closed") }}</label> >+ <input >+ type="radio" >+ id="sent_status_filter" >+ v-model="filters.status" >+ value="sent" >+ /><label for="sent_status_filter">{{ $__("Sent") }}</label> >+ <input >+ type="radio" >+ id="received_status_filter" >+ v-model="filters.status" >+ value="received" >+ /><label for="received_status_filter">{{ $__("Received") }}</label> >+ <input >+ @click="filter_table" >+ id="filter_table" >+ type="button" >+ :value="$__('Filter')" >+ /> >+ </fieldset> >+ <div v-if="count_trains > 0" class="page-section"> >+ <KohaTable >+ ref="table" >+ v-bind="tableOptions" >+ @show="doShow" >+ @edit="doEdit" >+ @delete="doDelete" >+ @addItems="doAddItems" >+ ></KohaTable> >+ </div> >+ >+ <div v-else class="dialog message"> >+ {{ $__("There are no trains defined") }} >+ </div> >+ </div> >+</template> >+ >+<script> >+import flatPickr from "vue-flatpickr-component" >+import Toolbar from "./TrainsToolbar.vue" >+import { inject, ref, reactive } from "vue" >+import { APIClient } from "../../fetch/api-client" >+import { build_url } from "../../composables/datatables" >+import KohaTable from "../KohaTable.vue" >+ >+export default { >+ setup() { >+ const AVStore = inject("AVStore") >+ const { get_lib_from_av, map_av_dt_filter } = AVStore >+ const { setConfirmationDialog, setMessage } = inject("mainStore") >+ const table = ref() >+ const filters = reactive({ status: "" }) >+ return { >+ get_lib_from_av, >+ map_av_dt_filter, >+ setConfirmationDialog, >+ setMessage, >+ table, >+ filters, >+ } >+ }, >+ data: function () { >+ this.filters.status = this.$route.query.status || "" >+ return { >+ fp_config: flatpickr_defaults, >+ count_trains: 0, >+ initialized: false, >+ tableOptions: { >+ columns: this.getTableColumns(), >+ url: this.table_url, >+ add_filters: true, >+ actions: { >+ 0: ["show"], >+ "-1": [ >+ "edit", >+ "delete", >+ { >+ addItems: { >+ text: this.$__("Add items"), >+ icon: "fa fa-plus", >+ }, >+ }, >+ ], >+ }, >+ }, >+ } >+ }, >+ beforeRouteEnter(to, from, next) { >+ next(vm => { >+ vm.getCountTrains() >+ }) >+ }, >+ computed: {}, >+ methods: { >+ async getCountTrains() { >+ const client = APIClient.preservation >+ client.trains.count().then( >+ count => { >+ this.count_trains = count >+ this.initialized = true >+ }, >+ error => {} >+ ) >+ }, >+ doShow: function (train, dt, event) { >+ event.preventDefault() >+ this.$router.push( >+ "/cgi-bin/koha/preservation/trains/" + train.train_id >+ ) >+ }, >+ doEdit: function (train, dt, event) { >+ this.$router.push( >+ "/cgi-bin/koha/preservation/trains/edit/" + train.train_id >+ ) >+ }, >+ doDelete: function (train, dt, event) { >+ this.setConfirmationDialog( >+ { >+ title: this.$__( >+ "Are you sure you want to remove this train?" >+ ), >+ message: train.name, >+ accept_label: this.$__("Yes, delete"), >+ cancel_label: this.$__("No, do not delete"), >+ }, >+ () => { >+ const client = APIClient.preservation >+ client.trains.delete(train.train_id).then( >+ success => { >+ this.setMessage( >+ this.$__("Train %s deleted").format(train.name), >+ true >+ ) >+ dt.draw() >+ }, >+ error => {} >+ ) >+ } >+ ) >+ }, >+ doAddItems: function (train, dt, event) { >+ this.$router.push( >+ "/cgi-bin/koha/preservation/trains/" + >+ train.train_id + >+ "/items/add" >+ ) >+ }, >+ table_url() { >+ let url = "/api/v1/preservation/trains" >+ let q >+ if (this.filters.status == "closed") { >+ q = { >+ "me.closed_on": { "!=": null }, >+ "me.sent_on": null, >+ "me.received_on": null, >+ } >+ } else if (this.filters.status == "sent") { >+ q = { >+ "me.closed_on": { "!=": null }, >+ "me.sent_on": { "!=": null }, >+ "me.received_on": null, >+ } >+ } else if (this.filters.status == "received") { >+ q = { >+ "me.closed_on": { "!=": null }, >+ "me.sent_on": { "!=": null }, >+ "me.received_on": { "!=": null }, >+ } >+ } >+ if (q) { >+ url += "?" + new URLSearchParams({ q: JSON.stringify(q) }) >+ } >+ >+ return url >+ }, >+ filter_table: async function () { >+ let new_route = build_url( >+ "/cgi-bin/koha/preservation/trains", >+ this.filters >+ ) >+ this.$router.push(new_route) >+ if (this.$refs.table) { >+ this.$refs.table.redraw(this.table_url()) >+ } >+ }, >+ getTableColumns: function () { >+ let escape_str = this.escape_str >+ return [ >+ { >+ title: __("Name"), >+ data: "me.train_id:me.name", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return `<a href="/cgi-bin/koha/preservation/trains/${row.train_id}" class="show">${row.name} (#${row.train_id})</a>` >+ }, >+ }, >+ { >+ title: __("Created on"), >+ data: "created_on", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return $date(row.created_on) >+ }, >+ }, >+ { >+ title: __("Closed on"), >+ data: "closed_on", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return $date(row.closed_on) >+ }, >+ }, >+ { >+ title: __("Sent on"), >+ data: "sent_on", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return $date(row.sent_on) >+ }, >+ }, >+ { >+ title: __("Received on"), >+ data: "received_on", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return $date(row.received_on) >+ }, >+ }, >+ ] >+ }, >+ }, >+ components: { flatPickr, Toolbar, KohaTable }, >+ name: "trainsList", >+ emits: ["select-train", "close"], >+} >+</script> >+ >+<style scoped> >+#train_list { >+ display: table; >+} >+.filters > input[type="radio"] { >+ min-width: 0 !important; >+} >+.filters > input[type="button"] { >+ margin-left: 1rem; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue >new file mode 100644 >index 0000000000..4038b43fc6 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue >@@ -0,0 +1,548 @@ >+<template> >+ <transition name="modal"> >+ <div v-if="show_modal" class="modal"> >+ <h2>{{ $__("Copy item to the following train") }}</h2> >+ <form @submit="copyItem($event)"> >+ <div class="page-section"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="train_list" >+ >{{ $__("Select a train") }}:</label >+ > >+ <v-select >+ v-model="train_id_selected_for_copy" >+ label="name" >+ :options="train_list" >+ :reduce="t => t.train_id" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required=" >+ !train_id_selected_for_copy >+ " >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ <span class="required">{{ >+ $__("Required") >+ }}</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Copy" /> >+ <input >+ type="button" >+ @click="show_modal = false" >+ :value="$__('Close')" >+ /> >+ </fieldset> >+ </div> >+ </form> >+ </div> >+ </transition> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else id="trains_show"> >+ <div id="toolbar" class="btn-toolbar"> >+ <router-link >+ :to="`/cgi-bin/koha/preservation/trains/${train.train_id}/items/add`" >+ class="btn btn-default" >+ ><font-awesome-icon icon="plus" /> >+ {{ $__("Add items") }}</router-link >+ > >+ <router-link >+ :to="`/cgi-bin/koha/preservation/trains/edit/${train.train_id}`" >+ class="btn btn-default" >+ ><font-awesome-icon icon="pencil" /> >+ {{ $__("Edit") }}</router-link >+ > >+ <a @click="deleteTrain(train)" class="btn btn-default" >+ ><font-awesome-icon icon="trash" /> {{ $__("Delete") }}</a >+ > >+ <a >+ v-if="!train.closed_on" >+ class="btn btn-default" >+ @click="closeTrain" >+ ><font-awesome-icon icon="remove" /> {{ $__("Close") }}</a >+ > >+ <a >+ v-else-if="!train.sent_on" >+ class="btn btn-default" >+ @click="sendTrain" >+ ><font-awesome-icon icon="paper-plane" /> {{ $__("Send") }}</a >+ > >+ <a >+ v-else-if="!train.received_on" >+ class="btn btn-default" >+ @click="receiveTrain" >+ ><font-awesome-icon icon="inbox" /> {{ $__("Receive") }}</a >+ > >+ </div> >+ <h2> >+ {{ $__("Train #%s").format(train.train_id) }} >+ </h2> >+ <div> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label>{{ $__("Name") }}:</label> >+ <span> >+ {{ train.name }} >+ </span> >+ </li> >+ <li> >+ <label>{{ $__("Description") }}:</label> >+ <span> >+ {{ train.description }} >+ </span> >+ </li> >+ <li v-if="train.closed_on"> >+ <label>{{ $__("Closed on") }}:</label> >+ <span> >+ {{ format_date(train.closed_on) }} >+ </span> >+ </li> >+ <li v-if="train.sent_on"> >+ <label>{{ $__("Sent on") }}:</label> >+ <span> >+ {{ format_date(train.sent_on) }} >+ </span> >+ </li> >+ <li v-if="train.received_on"> >+ <label>{{ $__("Received on") }}:</label> >+ <span> >+ {{ format_date(train.received_on) }} >+ </span> >+ </li> >+ <li> >+ <label >+ >{{ >+ $__("Status for item added to this train") >+ }}:</label >+ > >+ <span>{{ >+ get_lib_from_av("av_notforloan", train.not_for_loan) >+ }}</span> >+ </li> >+ <li> >+ <label>{{ $__("Default processing") }}:</label> >+ <span> >+ {{ train.default_processing.name }} >+ </span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset v-if="train.items.length" class="rows"> >+ <legend>{{ $__("Items") }}</legend> >+ <table v-if="item_table.display" :id="table_id"></table> >+ <ol v-else> >+ <li >+ :id="`item_${counter}`" >+ class="rows" >+ v-for="(item, counter) in train.items" >+ v-bind:key="counter" >+ > >+ <!-- FIXME Counter here may change, we should pass an order by clause when retrieving the items --> >+ <label >+ >{{ counter + 1 }} >+ <span class="action_links"> >+ <router-link >+ :to="`/cgi-bin/koha/preservation/trains/${train.train_id}/items/edit/${item.train_item_id}`" >+ :title="$__('Edit')" >+ ><i class="fa fa-pencil"></i></router-link >+ ></span> >+ </label> >+ <div class="attributes_values"> >+ <span >+ :id="`attribute_${counter_attribute}`" >+ class="attribute_value" >+ v-for="( >+ attribute, counter_attribute >+ ) in item.attributes" >+ v-bind:key="counter_attribute" >+ > >+ <!-- FIXME We need to display the description of the AV here --> >+ {{ attribute.processing_attribute.name }}={{ >+ attribute.value >+ }} >+ </span> >+ </div> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <router-link >+ to="/cgi-bin/koha/preservation/trains" >+ role="button" >+ class="cancel" >+ >{{ $__("Close") }}</router-link >+ > >+ </fieldset> >+ </div> >+ </div> >+</template> >+ >+<script> >+import { inject, createVNode, render } from "vue" >+import { APIClient } from "../../fetch/api-client" >+import { useDataTable } from "../../composables/datatables" >+ >+export default { >+ setup() { >+ const format_date = $date >+ >+ const AVStore = inject("AVStore") >+ const { get_lib_from_av } = AVStore >+ >+ const { setConfirmationDialog, setMessage } = inject("mainStore") >+ >+ const table_id = "item_list" >+ useDataTable(table_id) >+ >+ return { >+ format_date, >+ get_lib_from_av, >+ table_id, >+ setConfirmationDialog, >+ setMessage, >+ } >+ }, >+ data() { >+ return { >+ train: { >+ train_id: null, >+ name: "", >+ description: "", >+ }, >+ initialized: false, >+ show_modal: false, >+ item_table: { >+ display: false, >+ data: [], >+ columns: [], >+ }, >+ train_list: [], >+ train_id_selected_for_copy: null, >+ train_item_id_to_copy: null, >+ } >+ }, >+ beforeRouteEnter(to, from, next) { >+ next(vm => { >+ vm.getTrain(to.params.train_id).then(() => vm.build_datatable()) >+ vm.getTrainList() >+ }) >+ }, >+ methods: { >+ async getTrain(train_id) { >+ const client = APIClient.preservation >+ await client.trains.get(train_id).then( >+ train => { >+ this.train = train >+ let display = this.train.items.every( >+ item => >+ item.processing_id == >+ this.train.default_processing_id >+ ) >+ if (display) { >+ this.item_table.data = [] >+ this.train.items.forEach(item => { >+ let item_row = {} >+ this.train.default_processing.attributes.forEach( >+ attribute => { >+ if (item.attributes.length <= 0) return "" >+ item_row[ >+ attribute.processing_attribute_id >+ ] = item.attributes.find( >+ a => >+ a.processing_attribute_id == >+ attribute.processing_attribute_id >+ ).value >+ } >+ ) >+ item_row.item = item >+ this.item_table.data.push(item_row) >+ }) >+ this.item_table.columns = [] >+ this.item_table.columns.push({ >+ name: "id", >+ title: this.$__("ID"), >+ render: (data, type, row) => { >+ return 42 >+ }, >+ }) >+ train.default_processing.attributes.forEach(a => >+ this.item_table.columns.push({ >+ name: a.name, >+ title: a.name, >+ data: a.processing_attribute_id, >+ }) >+ ) >+ this.item_table.columns.push({ >+ name: "actions", >+ className: "actions noExport", >+ title: this.$__("Actions"), >+ searchable: false, >+ orderable: false, >+ render: (data, type, row) => { >+ return "" >+ }, >+ }) >+ } >+ this.initialized = true >+ this.item_table.display = display >+ }, >+ error => {} >+ ) >+ }, >+ getTrainList: function () { >+ const client = APIClient.preservation >+ let q = { "me.closed_on": null } >+ client.trains.getAll(q).then( >+ trains => (this.train_list = trains), >+ error => {} >+ ) >+ }, >+ deleteTrain: function (train) { >+ this.setConfirmationDialog( >+ { >+ title: this.$__( >+ "Are you sure you want to remove this train?" >+ ), >+ message: train.name, >+ accept_label: this.$__("Yes, delete"), >+ cancel_label: this.$__("No, do not delete"), >+ }, >+ () => { >+ const client = APIClient.preservation >+ client.trains.delete(train.train_id).then( >+ success => { >+ this.setMessage( >+ this.$__("Train %s deleted").format(train.name), >+ true >+ ) >+ }, >+ error => {} >+ ) >+ } >+ ) >+ }, >+ async updateTrainDate(attribute) { >+ let train = JSON.parse(JSON.stringify(this.train)) >+ let train_id = train.train_id >+ delete train.train_id >+ delete train.items >+ delete train.default_processing >+ train[attribute] = new Date() >+ const client = APIClient.preservation >+ if (train_id) { >+ client.trains >+ .update(train, train_id) >+ .then(() => this.getTrain(this.train.train_id)) >+ } else { >+ client.trains >+ .create(train) >+ .then(() => this.getTrain(this.train.train_id)) >+ } >+ }, >+ closeTrain() { >+ this.updateTrainDate("closed_on") >+ }, >+ sendTrain() { >+ this.updateTrainDate("sent_on") >+ }, >+ receiveTrain() { >+ this.updateTrainDate("received_on") >+ }, >+ editItem(train_item_id) { >+ this.$router.push( >+ `/cgi-bin/koha/preservation/trains/${this.train.train_id}/items/edit/${train_item_id}` >+ ) >+ }, >+ removeItem(train_item_id) { >+ this.setConfirmationDialog( >+ { >+ title: this.$__( >+ "Are you sure you want to remove this item?" >+ ), >+ accept_label: this.$__("Yes, remove"), >+ cancel_label: this.$__("No, do not remove"), >+ }, >+ () => { >+ const client = APIClient.preservation >+ client.train_items >+ .delete(this.train.train_id, train_item_id) >+ .then( >+ success => { >+ this.setMessage(this.$__("Item removed"), true) >+ this.getTrain(this.train.train_id).then(() => { >+ $("#" + this.table_id) >+ .DataTable() >+ .destroy() >+ this.build_datatable() >+ }) >+ }, >+ error => {} >+ ) >+ } >+ ) >+ }, >+ selectTrainForCopy(train_item_id) { >+ this.show_modal = true >+ this.train_item_id_to_copy = train_item_id >+ }, >+ copyItem(event) { >+ event.preventDefault() >+ const client = APIClient.preservation >+ let new_train_item = {} >+ client.train_items >+ .get(this.train.train_id, this.train_item_id_to_copy) >+ .then(train_item => { >+ new_train_item = { >+ attributes: train_item.attributes.map(attr => { >+ return { >+ processing_attribute_id: >+ attr.processing_attribute_id, >+ value: attr.value, >+ } >+ }), >+ item_id: train_item.item_id, >+ processing_id: train_item.processing_id, >+ } >+ }) >+ .then(() => >+ client.train_items >+ .create(new_train_item, this.train_id_selected_for_copy) >+ .then( >+ success => { >+ this.setMessage( >+ this.$__("Item copied successfully.") >+ ) >+ this.show_modal = false >+ }, >+ error => {} >+ ) >+ ) >+ }, >+ build_datatable: function () { >+ let table_id = this.table_id >+ let item_table = this.item_table >+ let removeItem = this.removeItem >+ let editItem = this.editItem >+ let selectTrainForCopy = this.selectTrainForCopy >+ let train = this.train >+ >+ let table = KohaTable(table_id, { >+ data: item_table.data, >+ ordering: false, >+ autoWidth: false, >+ columns: item_table.columns, >+ drawCallback: function (settings) { >+ var api = new $.fn.dataTable.Api(settings) >+ $.each($(this).find("td.actions"), function (index, e) { >+ let tr = $(this).parent() >+ let train_item_id = api.row(tr).data() >+ .item.train_item_id >+ >+ let editButton = createVNode( >+ "a", >+ { >+ class: "btn btn-default btn-xs", >+ role: "button", >+ onClick: () => { >+ editItem(train_item_id) >+ }, >+ }, >+ [ >+ createVNode("i", { >+ class: "fa fa-pencil", >+ "aria-hidden": "true", >+ }), >+ __("Edit"), >+ ] >+ ) >+ >+ let removeButton = createVNode( >+ "a", >+ { >+ class: "btn btn-default btn-xs", >+ role: "button", >+ onClick: () => { >+ removeItem(train_item_id) >+ }, >+ }, >+ [ >+ createVNode("i", { >+ class: "fa fa-trash", >+ "aria-hidden": "true", >+ }), >+ __("Remove"), >+ ] >+ ) >+ let buttons = [editButton, "", removeButton] >+ >+ if (train.received_on !== null) { >+ buttons.push("") >+ buttons.push( >+ createVNode( >+ "a", >+ { >+ class: "btn btn-default btn-xs", >+ role: "button", >+ onClick: () => { >+ selectTrainForCopy(train_item_id) >+ }, >+ }, >+ [ >+ createVNode("i", { >+ class: "fa fa-copy", >+ "aria-hidden": "true", >+ }), >+ __("Copy"), >+ ] >+ ) >+ ) >+ } >+ >+ let n = createVNode("span", {}, buttons) >+ render(n, e) >+ }) >+ }, >+ }) >+ }, >+ }, >+ name: "TrainsShow", >+} >+</script> >+<style scoped> >+.action_links a { >+ padding-left: 0.2em; >+ font-size: 11px; >+} >+.attributes_values { >+ float: left; >+} >+.attribute_value { >+ display: block; >+} >+.modal { >+ position: fixed; >+ z-index: 9998; >+ top: 0; >+ left: 0; >+ width: 35%; >+ height: 30%; >+ background-color: rgba(0, 0, 0, 0.5); >+ display: table; >+ transition: opacity 0.3s ease; >+ margin: auto; >+ padding: 20px 30px; >+ background-color: #fff; >+ border-radius: 2px; >+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33); >+ transition: all 0.3s ease; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsToolbar.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsToolbar.vue >new file mode 100644 >index 0000000000..874b829976 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsToolbar.vue >@@ -0,0 +1,16 @@ >+<template> >+ <div id="toolbar" class="btn-toolbar"> >+ <router-link >+ to="/cgi-bin/koha/preservation/trains/add" >+ class="btn btn-default" >+ ><font-awesome-icon icon="plus" /> >+ {{ $__("New train") }}</router-link >+ > >+ </div> >+</template> >+ >+<script> >+export default { >+ name: "TrainsToolbar", >+} >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue >new file mode 100644 >index 0000000000..ca87c139ba >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue >@@ -0,0 +1,344 @@ >+<template> >+ <transition name="modal_add_to_waiting_list"> >+ <div v-if="show_modal_add_to_waiting_list" class="modal"> >+ <h2>{{ $__("Add items to waiting list") }}</h2> >+ <form @submit="addItemsToWaitingList($event)"> >+ <div class="page-section"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="barcode_list" >+ >{{ $__("Barcode list") }}:</label >+ > >+ <textarea >+ id="barcode_list" >+ v-model="barcode_list" >+ :placeholder="$__('Barcodes')" >+ rows="10" >+ cols="50" >+ required >+ /> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Submit" /> >+ <input >+ type="button" >+ @click="show_modal_add_to_waiting_list = false" >+ :value="$__('Close')" >+ /> >+ </fieldset> >+ </div> >+ </form> >+ </div> >+ </transition> >+ <transition name="modal_add_to_train"> >+ <div v-if="show_modal_add_to_train" class="modal"> >+ <h2>{{ $__("Add items to a train") }}</h2> >+ <form @submit="addItemsToTrain($event)"> >+ <div class="page-section"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="train_list" >+ >{{ $__("Select a train") }}:</label >+ > >+ <v-select >+ id="train_id" >+ v-model="train_id_selected_for_add" >+ label="name" >+ :options="train_list" >+ :reduce="t => t.train_id" >+ > >+ <template #search="{ attributes, events }"> >+ <input >+ :required=" >+ !train_id_selected_for_add >+ " >+ class="vs__search" >+ v-bind="attributes" >+ v-on="events" >+ /> >+ </template> >+ </v-select> >+ <span class="required">{{ >+ $__("Required") >+ }}</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Submit" /> >+ <input >+ type="button" >+ @click="show_modal_add_to_train = false" >+ :value="$__('Close')" >+ /> >+ </fieldset> >+ </div> >+ </form> >+ </div> >+ </transition> >+ <div v-if="!initialized">{{ $__("Loading") }}</div> >+ <div v-else-if="!settings.not_for_loan_waiting_list_in" id="waiting-list"> >+ {{ $__("You need to configure this module first.") }} >+ </div> >+ <div v-else id="waiting-list"> >+ <div id="toolbar" class="btn-toolbar"> >+ <a >+ class="btn btn-default" >+ @click="show_modal_add_to_waiting_list = true" >+ ><font-awesome-icon icon="plus" /> >+ {{ $__("Add to waiting list") }}</a >+ > >+ <a >+ v-if="last_items.length > 0" >+ class="btn btn-default" >+ @click="show_modal_add_to_train = true" >+ ><font-awesome-icon icon="plus" /> >+ {{ >+ $__("Add last %s items to a train").format( >+ last_items.length >+ ) >+ }}</a >+ > >+ </div> >+ <div v-if="count_waiting_list_items > 0" class="page-section"> >+ <KohaTable >+ ref="table" >+ v-bind="tableOptions" >+ @remove="doRemoveItem" >+ ></KohaTable> >+ </div> >+ <div v-else class="dialog message"> >+ {{ $__("There are no items in the waiting list") }} >+ </div> >+ </div> >+</template> >+ >+<script> >+import flatPickr from "vue-flatpickr-component" >+import { inject, ref } from "vue" >+import { storeToRefs } from "pinia" >+import { APIClient } from "../../fetch/api-client" >+import KohaTable from "../KohaTable.vue" >+ >+export default { >+ setup() { >+ const table = ref() >+ >+ const PreservationStore = inject("PreservationStore") >+ const { settings } = storeToRefs(PreservationStore) >+ >+ const { setMessage, setConfirmationDialog, loading, loaded } = >+ inject("mainStore") >+ >+ return { >+ table, >+ settings, >+ setMessage, >+ setConfirmationDialog, >+ loading, >+ loaded, >+ } >+ }, >+ data: function () { >+ return { >+ fp_config: flatpickr_defaults, >+ count_waiting_list_items: 0, >+ barcode_list: "", >+ initialized: false, >+ show_modal_add_to_waiting_list: false, >+ show_modal_add_to_train: false, >+ tableOptions: { >+ columns: this.getTableColumns(), >+ url: "/api/v1/preservation/waiting-list/items", >+ options: { embed: "biblio" }, >+ add_filters: true, >+ actions: { >+ 0: ["show"], >+ "-1": ["remove"], >+ }, >+ }, >+ last_items: [], >+ train_list: [], >+ train_id_selected_for_add: null, >+ } >+ }, >+ beforeRouteEnter(to, from, next) { >+ next(vm => { >+ vm.getCountWaitingListItems() >+ vm.getTrainList() >+ }) >+ }, >+ methods: { >+ async getCountWaitingListItems() { >+ const client = APIClient.preservation >+ client.waiting_list_items.count().then(count => { >+ this.count_waiting_list_items = count >+ this.initialized = true >+ }) >+ }, >+ getTrainList: function () { >+ const client = APIClient.preservation >+ client.trains.getAll().then( >+ trains => (this.train_list = trains), >+ error => {} >+ ) >+ }, >+ addItemsToTrain: function (e) { >+ e.preventDefault() >+ this.loading() >+ let item_ids = Object.values(this.last_items) >+ const client = APIClient.preservation >+ let promises = [] >+ this.last_items.forEach(i => >+ promises.push( >+ client.train_items.create( >+ { item_id: i.item_id }, >+ this.train_id_selected_for_add >+ ) >+ ) >+ ) >+ Promise.all(promises) >+ .then(() => { >+ this.setMessage( >+ this.$__( >+ "The items have been added to train %s." >+ ).format(this.train_id_selected_for_add), >+ true >+ ) >+ >+ this.$refs.table.redraw( >+ "/api/v1/preservation/waiting-list/items" >+ ) >+ this.show_modal_add_to_train = false >+ this.last_items = [] >+ }) >+ .then(() => this.loaded()) >+ }, >+ addItemsToWaitingList: function (e) { >+ e.preventDefault() >+ this.show_modal_add_to_waiting_list = false >+ let items = [] >+ this.barcode_list >+ .split("\n") >+ .forEach(barcode => items.push({ barcode })) >+ const client = APIClient.preservation >+ client.waiting_list_items.createAll(items).then( >+ result => { >+ if (result.length) { >+ this.setMessage( >+ this.$__("%s new items added.").format( >+ result.length >+ ), >+ true >+ ) >+ this.last_items = result >+ if (this.$refs.table) { >+ this.$refs.table.redraw( >+ "/api/v1/preservation/waiting-list/items" >+ ) >+ } else { >+ this.getCountWaitingListItems() >+ } >+ } else { >+ this.setMessage(this.$__("No items added")) >+ } >+ }, >+ error => {} >+ ) >+ this.barcode_list = "" >+ }, >+ doShow: function (biblio, dt, event) { >+ event.preventDefault() >+ location.href = >+ "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + >+ biblio.biblio_id >+ }, >+ doRemoveItem: function (item, dt, event) { >+ this.setConfirmationDialog( >+ { >+ title: this.$__( >+ "Are you sure you want to remove this item from the waiting list?" >+ ), >+ message: item.barcode, >+ accept_label: this.$__("Yes, remove"), >+ cancel_label: this.$__("No, do not remove"), >+ }, >+ () => { >+ const client = APIClient.preservation >+ client.waiting_list_items.delete(item.item_id).then( >+ success => { >+ this.setMessage( >+ this.$__("Item removed from the waiting list"), >+ true >+ ) >+ dt.draw() >+ }, >+ error => {} >+ ) >+ } >+ ) >+ }, >+ getTableColumns: function () { >+ let escape_str = this.escape_str >+ return [ >+ { >+ data: "biblio.title", >+ title: __("Title"), >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return `<a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=${row.biblio.biblio_id}">${row.biblio.title}</a>` >+ }, >+ }, >+ { >+ data: "biblio.author", >+ title: __("Author"), >+ searchable: true, >+ orderable: true, >+ }, >+ { >+ data: "callnumber", >+ title: __("Callnumber"), >+ searchable: true, >+ orderable: true, >+ }, >+ { >+ data: "external_id", >+ title: __("Barcode"), >+ searchable: true, >+ orderable: true, >+ }, >+ ] >+ }, >+ }, >+ components: { flatPickr, KohaTable }, >+ name: "WaitingList", >+} >+</script> >+ >+<style scoped> >+#waiting_list { >+ display: table; >+} >+.modal { >+ position: fixed; >+ z-index: 9990; >+ top: 0; >+ left: 0; >+ width: 35%; >+ height: 30%; >+ background-color: rgba(0, 0, 0, 0.5); >+ display: table; >+ transition: opacity 0.3s ease; >+ margin: auto; >+ padding: 20px 30px; >+ background-color: #fff; >+ border-radius: 2px; >+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33); >+ transition: all 0.3s ease; >+} >+</style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js >index e570309fa1..294693c238 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js >@@ -4,6 +4,7 @@ import AcquisitionAPIClient from "./acquisition-api-client"; > import AVAPIClient from "./authorised-values-api-client"; > import ItemAPIClient from "./item-api-client"; > import SysprefAPIClient from "./system-preferences-api-client"; >+import PreservationAPIClient from "./preservation-api-client"; > > export const APIClient = { > erm: new ERMAPIClient(), >@@ -12,4 +13,5 @@ export const APIClient = { > authorised_values: new AVAPIClient(), > item: new ItemAPIClient(), > sysprefs: new SysprefAPIClient(), >+ preservation: new PreservationAPIClient(), > }; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/authorised-values-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/authorised-values-api-client.js >index e502ac7046..1498cd7c98 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/authorised-values-api-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/authorised-values-api-client.js >@@ -9,6 +9,10 @@ export class AVAPIClient extends HttpClient { > > get values() { > return { >+ get: category => >+ this.get({ >+ endpoint: `/${category}/authorised_values`, >+ }), > getCategoriesWithValues: cat_array => > this.get({ > endpoint: >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js >new file mode 100644 >index 0000000000..c9ea4bf9f4 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js >@@ -0,0 +1,184 @@ >+import HttpClient from "./http-client"; >+ >+export class PreservationAPIClient extends HttpClient { >+ constructor() { >+ super({ >+ baseURL: "/api/v1/preservation/", >+ }); >+ } >+ >+ get trains() { >+ return { >+ get: (id) => >+ this.get({ >+ endpoint: "trains/" + id, >+ headers: { >+ "x-koha-embed": >+ "default_processing,default_processing.attributes,items,items.attributes,items.attributes.processing_attribute", >+ }, >+ }), >+ getAll: (query = {}) => >+ this.get({ >+ endpoint: "trains?" + >+ new URLSearchParams({ >+ _per_page: -1, >+ ...(query && { q: JSON.stringify(query) }), >+ }), >+ }), >+ delete: (id) => >+ this.delete({ >+ endpoint: "trains/" + id, >+ }), >+ create: (train) => >+ this.post({ >+ endpoint: "trains", >+ body: train, >+ }), >+ update: (train, id) => >+ this.put({ >+ endpoint: "trains/" + id, >+ body: train, >+ }), >+ count: (query = {}) => >+ this.count({ >+ endpoint: >+ "trains?" + >+ new URLSearchParams({ >+ _page: 1, >+ _per_page: 1, >+ ...(query && { q: JSON.stringify(query) }), >+ }), >+ }), >+ }; >+ } >+ >+ get processings() { >+ return { >+ get: (id) => >+ this.get({ >+ endpoint: "processings/" + id, >+ headers: { >+ "x-koha-embed": "attributes", >+ }, >+ }), >+ getAll: (query) => >+ this.get({ >+ endpoint: "processings?" + >+ new URLSearchParams({ >+ _per_page: -1, >+ ...(query && { q: JSON.stringify(query) }), >+ }), >+ }), >+ >+ delete: (id) => >+ this.delete({ >+ endpoint: "processings/" + id, >+ }), >+ create: (processing) => >+ this.post({ >+ endpoint: "processings", >+ body: processing, >+ }), >+ update: (processing, id) => >+ this.put({ >+ endpoint: "processings/" + id, >+ body: processing, >+ }), >+ count: (query = {}) => >+ this.count({ >+ endpoint: >+ "processings?" + >+ new URLSearchParams({ >+ _page: 1, >+ _per_page: 1, >+ ...(query && { q: JSON.stringify(query) }), >+ }), >+ }), >+ }; >+ } >+ >+ get train_items() { >+ return { >+ get: (train_id, id) => >+ this.get({ >+ endpoint: "trains/" + train_id + "/items/" + id, >+ headers: { >+ "x-koha-embed": >+ "attributes,catalogue_item,catalogue_item.biblio", >+ }, >+ }), >+ delete: (train_id, id) => >+ this.delete({ >+ endpoint: "trains/" + train_id + "/items/" + id, >+ }), >+ create: (train_item, train_id) => >+ this.post({ >+ endpoint: "trains/" + train_id + "/items", >+ body: train_item, >+ }), >+ update: (train_item, train_id, id) => >+ this.put({ >+ endpoint: "trains/" + train_id + "/items/" + id, >+ body: train_item, >+ }), >+ count: (train_id, query = {}) => >+ this.count({ >+ endpoint: >+ "trains/" + >+ train_id + >+ "/items?" + >+ new URLSearchParams({ >+ _page: 1, >+ _per_page: 1, >+ ...(query && { q: JSON.stringify(query) }), >+ }), >+ }), >+ }; >+ } >+ >+ get waiting_list_items() { >+ return { >+ get_from_barcode: (barcode) => { >+ const q = { >+ "me.barcode": barcode, >+ }; >+ >+ const params = { >+ _page: 1, >+ _per_page: 1, >+ q: JSON.stringify(q), >+ }; >+ return this.get({ >+ endpoint: >+ "waiting-list/items?" + new URLSearchParams(params), >+ headers: { >+ "x-koha-embed": "biblio", >+ }, >+ }).then((response) => { >+ return response.length ? response[0] : undefined; >+ }); >+ }, >+ delete: (id) => >+ this.delete({ >+ endpoint: "waiting-list/items/" + id, >+ }), >+ createAll: (items) => >+ this.post({ >+ endpoint: "waiting-list/items", >+ body: items, >+ }), >+ count: (query = {}) => >+ this.count({ >+ endpoint: >+ "waiting-list/items?" + >+ new URLSearchParams({ >+ _page: 1, >+ _per_page: 1, >+ ...(query && { q: JSON.stringify(query) }), >+ }), >+ }), >+ }; >+ } >+} >+ >+export default PreservationAPIClient; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts >new file mode 100644 >index 0000000000..379e7f3d55 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts >@@ -0,0 +1,74 @@ >+import { createApp } from "vue"; >+import { createWebHistory, createRouter } from "vue-router"; >+import { createPinia } from "pinia"; >+ >+import { library } from "@fortawesome/fontawesome-svg-core"; >+import { >+ faPlus, >+ faMinus, >+ faPencil, >+ faTrash, >+ faSpinner, >+ faClose, >+ faPaperPlane, >+ faInbox, >+} from "@fortawesome/free-solid-svg-icons"; >+import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; >+import vSelect from "vue-select"; >+ >+library.add( >+ faPlus, >+ faMinus, >+ faPencil, >+ faTrash, >+ faSpinner, >+ faClose, >+ faPaperPlane, >+ faInbox >+); >+ >+import App from "../components/Preservation/Main.vue"; >+ >+import { routes } from "../routes/preservation"; >+ >+const router = createRouter({ >+ history: createWebHistory(), >+ linkActiveClass: "current", >+ routes, >+}); >+ >+import { useMainStore } from "../stores/main"; >+import { useAVStore } from "../stores/authorised-values"; >+import { usePreservationStore } from "../stores/preservation"; >+ >+const pinia = createPinia(); >+ >+const i18n = { >+ install: (app, options) => { >+ app.config.globalProperties.$__ = key => { >+ return window["__"](key); >+ }; >+ }, >+}; >+ >+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; >+const mainStore = useMainStore(pinia); >+app.provide("mainStore", mainStore); >+app.provide("AVStore", useAVStore(pinia)); >+app.provide("PreservationStore", usePreservationStore(pinia)); >+ >+app.mount("#preservation"); >+ >+const { removeMessages } = mainStore; >+router.beforeEach((to, from) => { >+ removeMessages(); // This will actually flag the messages as displayed already >+}); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js >new file mode 100644 >index 0000000000..55325bdb57 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js >@@ -0,0 +1,220 @@ >+import Home from "../components/Preservation/Home.vue"; >+import TrainsList from "../components/Preservation/TrainsList.vue"; >+import TrainsShow from "../components/Preservation/TrainsShow.vue"; >+import TrainsFormAdd from "../components/Preservation/TrainsFormAdd.vue"; >+import TrainsFormAddItem from "../components/Preservation/TrainsFormAddItem.vue"; >+import WaitingList from "../components/Preservation/WaitingList.vue"; >+import Settings from "../components/Preservation/Settings.vue"; >+import SettingsProcessingsShow from "../components/Preservation/SettingsProcessingsShow.vue"; >+import SettingsProcessingsFormAdd from "../components/Preservation/SettingsProcessingsFormAdd.vue"; >+ >+const breadcrumbs = { >+ home: { >+ text: "Home", // $t("Home") >+ path: "/cgi-bin/koha/mainpage.pl", >+ }, >+ preservation_home: { >+ text: "Preservation", //$t("Preservation") >+ path: "/cgi-bin/koha/preservation/home.pl", >+ }, >+ trains: { >+ text: "Trains", // $t("Trains") >+ path: "/cgi-bin/koha/preservation/trains", >+ }, >+ waiting_list: { >+ text: "Waiting list", // $t("Waiting list") >+ path: "/cgi-bin/koha/preservation/waiting-list", >+ }, >+ settings: { >+ home: { >+ text: "Settings", // $t("Settings") >+ path: "/cgi-bin/koha/preservation/settings", >+ }, >+ processings: { >+ home: { >+ text: "Processings", //$t("Processings") >+ }, >+ }, >+ }, >+}; >+const breadcrumb_paths = { >+ trains: [ >+ breadcrumbs.home, >+ breadcrumbs.preservation_home, >+ breadcrumbs.trains, >+ ], >+ settings: [ >+ breadcrumbs.home, >+ breadcrumbs.preservation_home, >+ breadcrumbs.settings.home, >+ ], >+ settings_processings: [ >+ breadcrumbs.home, >+ breadcrumbs.preservation_home, >+ breadcrumbs.settings.home, >+ ], >+}; >+ >+function build_breadcrumb(parent_breadcrumb, current) { >+ let breadcrumb = parent_breadcrumb.flat(Infinity); >+ if (current) { >+ breadcrumb.push({ >+ text: current, >+ }); >+ } >+ return breadcrumb; >+} >+ >+export const routes = [ >+ { >+ path: "/cgi-bin/koha/mainpage.pl", >+ beforeEnter(to, from, next) { >+ window.location.href = "/cgi-bin/koha/mainpage.pl"; >+ }, >+ }, >+ { >+ path: "/cgi-bin/koha/preservation/home.pl", >+ component: Home, >+ meta: { >+ breadcrumb: () => [breadcrumbs.home, breadcrumbs.preservation_home], >+ }, >+ }, >+ { >+ path: "/cgi-bin/koha/preservation/trains", >+ children: [ >+ { >+ path: "", >+ component: TrainsList, >+ meta: { >+ breadcrumb: () => breadcrumb_paths.trains, >+ }, >+ }, >+ { >+ path: ":train_id", >+ children: [ >+ { >+ path: "", >+ component: TrainsShow, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.trains, >+ "Show train" // $t("Show train") >+ ), >+ }, >+ }, >+ { >+ path: "items", >+ children: [ >+ { >+ path: "add", >+ component: TrainsFormAddItem, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.trains, >+ "Add item to train" // $t("Add item to train") >+ ), >+ }, >+ }, >+ { >+ path: "edit/:train_item_id", >+ component: TrainsFormAddItem, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.trains, >+ "Edit item in train" // $t("Edit item in train") >+ ), >+ }, >+ }, >+ ], >+ }, >+ ], >+ }, >+ { >+ path: "add", >+ component: TrainsFormAdd, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.trains, >+ "Add train" // $t("Add train") >+ ), >+ }, >+ }, >+ { >+ path: "edit/:train_id", >+ component: TrainsFormAdd, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.trains, >+ "Edit train" // $t("Edit train") >+ ), >+ }, >+ }, >+ ], >+ }, >+ { >+ path: "/cgi-bin/koha/preservation/waiting-list", >+ component: WaitingList, >+ meta: { >+ breadcrumb: () => [ >+ breadcrumbs.home, >+ breadcrumbs.preservation_home, >+ breadcrumbs.waiting_list, >+ ], >+ }, >+ }, >+ { >+ path: "/cgi-bin/koha/preservation/settings", >+ children: [ >+ { >+ path: "", >+ component: Settings, >+ meta: { >+ breadcrumb: () => breadcrumb_paths.settings, >+ }, >+ }, >+ { >+ path: "processings", >+ children: [ >+ { >+ path: ":processing_id", >+ component: SettingsProcessingsShow, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.settings_processings, >+ "Show processing" // $t("Show processing") >+ ), >+ }, >+ }, >+ { >+ path: "add", >+ component: SettingsProcessingsFormAdd, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.settings_processings, >+ "Add processing" // $t("Add processing") >+ ), >+ }, >+ }, >+ { >+ path: "edit/:processing_id", >+ component: SettingsProcessingsFormAdd, >+ meta: { >+ breadcrumb: () => >+ build_breadcrumb( >+ breadcrumb_paths.settings_processings, >+ "Edit processing" // $t("Edit processing") >+ ), >+ }, >+ }, >+ ], >+ }, >+ ], >+ }, >+]; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/authorised-values.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/authorised-values.js >index 1792958f42..e9e606fc83 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/authorised-values.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/authorised-values.js >@@ -36,6 +36,7 @@ export const useAVStore = defineStore("authorised_values", { > av_package_types: [], > av_package_content_types: [], > av_title_publication_types: [], >+ av_notforloan: [], > }), > actions: { > get_lib_from_av(arr_name, av) { >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/preservation.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/preservation.js >new file mode 100644 >index 0000000000..164dde6a68 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/preservation.js >@@ -0,0 +1,10 @@ >+import { defineStore } from "pinia"; >+ >+export const usePreservationStore = defineStore("preservation", { >+ state: () => ({ >+ settings: { >+ not_for_loan_waiting_list_in: null, >+ not_for_loan_default_train_in: 0, >+ }, >+ }), >+}); >diff --git a/webpack.config.js b/webpack.config.js >index bbdcced844..c22256785a 100644 >--- a/webpack.config.js >+++ b/webpack.config.js >@@ -6,6 +6,7 @@ const webpack = require('webpack'); > module.exports = { > entry: { > erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts", >+ preservation: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/preservation.ts", > }, > output: { > filename: "[name].js", >-- >2.30.2
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 30708
:
134715
|
134716
|
134732
|
134733
|
134734
|
134736
|
134737
|
134738
|
134782
|
134783
|
134784
|
134785
|
152939
|
152940
|
152941
|
152942
|
152943
|
152944
|
152945
|
152946
|
152947
|
152948
|
152949
|
152950
|
152951
|
152952
|
152953
|
152954
|
152955
|
152956
|
152957
|
152958
|
152959
|
152960
|
152961
|
152962
|
152963
|
152964
|
152965
|
152966
|
152967
|
152968
|
152969
|
152970
|
153102
|
153576
|
154167
|
154168
|
154169
|
154170
|
154171
|
154172
|
154173
|
154174
|
154175
|
154176
|
154177
|
154178
|
154179
|
154180
|
154181
|
154182
|
154183
|
154184
|
154185
|
154186
|
154187
|
154188
|
154189
|
154190
|
154191
|
154192
|
154193
|
154194
|
154195
|
154196
|
154197
|
154198
|
154199
|
154200
|
154201
|
154202
|
154203
|
156592
|
156596
|
156597
|
156636
|
156637
|
156638
|
156639
| 156640 |
156641
|
156642
|
156643
|
156644
|
156645
|
156646
|
156647
|
156648
|
156649
|
156650
|
156651
|
156652
|
156653
|
156654
|
156655
|
156656
|
156657
|
156658
|
156659
|
156660
|
156661
|
156662
|
156663
|
156664
|
156665
|
156666
|
156667
|
156668
|
156669
|
156670
|
156671
|
156672
|
156673
|
156674
|
156675
|
157502
|
157503
|
157506
|
157508
|
157512
|
157514
|
157557