From 461bd5f689f2991611f48d6945e1ce7fe2ed6518 Mon Sep 17 00:00:00 2001
From: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Date: Tue, 25 Apr 2023 12:12:01 +0200
Subject: [PATCH] Bug 30708: Set default values for items added in batch to a
 train
Content-Type: text/plain; charset=utf-8

Technical notes: Ideally we would have split TrainsFormAddItem to make some part
reusable, but it turned out into a complicated component that would have
been hard to maintain. It seems easier to have two different components.
Ideas to improve this area are welcome!

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>
---
 Koha/Preservation/Train.pm                    |   4 +-
 .../Preservation/TrainsFormAddItems.vue       | 285 ++++++++++++++++++
 .../components/Preservation/WaitingList.vue   |  41 +--
 .../prog/js/vue/routes/preservation.js        |  12 +
 t/cypress/integration/Preservation/Trains.ts  |  60 ++++
 .../integration/Preservation/WaitingList.ts   |  46 ---
 6 files changed, 367 insertions(+), 81 deletions(-)
 create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue

diff --git a/Koha/Preservation/Train.pm b/Koha/Preservation/Train.pm
index 7ecd7908b4..46abf54682 100644
--- a/Koha/Preservation/Train.pm
+++ b/Koha/Preservation/Train.pm
@@ -120,7 +120,9 @@ sub add_items {
     my @added_items;
     for my $train_item (@$train_items) {
         try {
-            push @added_items, $self->add_item($train_item);
+            my $added_item = $self->add_item($train_item);
+            $added_item->attributes($train_item->{attributes});
+            push @added_items, $added_item;
         } catch {
 
             # FIXME Do we rollback and raise an error or just skip it?
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue
new file mode 100644
index 0000000000..814ab7eb9b
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsFormAddItems.vue
@@ -0,0 +1,285 @@
+<template>
+    <div v-if="!initialized">{{ $__("Loading") }}</div>
+    <div v-else id="trains_add_items">
+        <h2>{{ $__("Add new items to %s").format(train.name) }}</h2>
+        <form @submit="onSubmit($event)">
+            <fieldset class="rows">
+                <ol>
+                    <li>
+                        <label for="itemnumbers"
+                            >{{ $__("Itemnumbers") }}:</label
+                        >
+                        <span>{{ items.map(i => i.item_id).join(", ") }}</span>
+                    </li>
+                    <li>
+                        <label for="processing"
+                            >{{ $__("Processing") }}:
+                        </label>
+                        <v-select
+                            id="processing"
+                            label="name"
+                            v-model="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]"
+                                taggable
+                                :create-option="
+                                    attribute => ({
+                                        value: attribute,
+                                        description: attribute,
+                                    })
+                                "
+                            />
+                        </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'">
+                            {{
+                                $__(
+                                    "Cannot be edited now, the value will be retrieved from %s"
+                                ).format(attribute.option_source)
+                            }}
+                        </span>
+                        <a
+                            v-if="
+                                attribute.type != 'db_column' &&
+                                (attributes.length == counter + 1 ||
+                                    attributes[counter + 1]
+                                        .processing_attribute_id !=
+                                        attribute.processing_attribute_id)
+                            "
+                            class="btn btn-link"
+                            @click="
+                                addAttribute(attribute.processing_attribute_id)
+                            "
+                            ><font-awesome-icon icon="plus" />
+                            {{ $__("Add") }}</a
+                        >
+                        <a
+                            v-else-if="attribute.type != 'db_column'"
+                            class="btn btn-link"
+                            @click="removeAttribute(counter)"
+                            ><font-awesome-icon icon="minus" />
+                            {{ $__("Remove") }}</a
+                        >
+                    </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>
+</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: "",
+            },
+            items: [],
+            train_items: [],
+            processings: [],
+            processing: null,
+            processing_id: null,
+            initialized: false,
+            av_options: {},
+            attributes: [],
+        }
+    },
+    beforeCreate() {
+        const client = APIClient.preservation
+        client.processings
+            .getAll()
+            .then(processings => (this.processings = processings))
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.train = vm
+                .getTrain(to.params.train_id)
+                .then(() =>
+                    vm
+                        .getItems(to.params.item_ids.split(","))
+                        .then(() =>
+                            vm
+                                .refreshAttributes()
+                                .then(() => (vm.initialized = true))
+                        )
+                )
+        })
+    },
+    methods: {
+        async getTrain(train_id) {
+            const client = APIClient.preservation
+            await client.trains.get(train_id).then(
+                train => {
+                    this.train = train
+                    this.processing_id = train.default_processing_id
+                },
+                error => {}
+            )
+        },
+        async getItems(item_ids) {
+            const client = APIClient.item
+            let q = { "me.item_id": item_ids }
+            await client.items
+                .getAll(q, { headers: { "x-koha-embed": "biblio" } })
+                .then(
+                    items => {
+                        this.items = items
+                    },
+                    error => {}
+                )
+        },
+        columnApiMapping(item, 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"
+                ? item.biblio[api_attribute]
+                : item[api_attribute]
+        },
+        async refreshAttributes() {
+            this.loading()
+
+            const client = APIClient.preservation
+            await client.processings.get(this.processing_id).then(
+                processing => (this.processing = processing),
+                error => {}
+            )
+            this.attributes = []
+            this.processing.attributes.forEach(attribute => {
+                this.attributes.push({
+                    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())
+        },
+        addAttribute(processing_attribute_id) {
+            let last_index = this.attributes.findLastIndex(
+                attribute =>
+                    attribute.processing_attribute_id == processing_attribute_id
+            )
+            let new_attribute = (({ value, ...keepAttrs }) => keepAttrs)(
+                this.attributes[last_index]
+            )
+            this.attributes.splice(last_index + 1, 0, new_attribute)
+        },
+        removeAttribute(counter) {
+            this.attributes.splice(counter, 1)
+        },
+        onSubmit(e) {
+            e.preventDefault()
+
+            let train_items = this.items.map(item => {
+                return {
+                    item_id: item.item_id,
+                    processing_id: this.processing_id,
+                    attributes: this.attributes.map(a => {
+                        let value =
+                            a.type == "db_column"
+                                ? this.columnApiMapping(item, a.option_source)
+                                : a.value
+                        return {
+                            processing_attribute_id: a.processing_attribute_id,
+                            value,
+                        }
+                    }),
+                }
+            })
+
+            const client = APIClient.preservation
+            client.train_items.createAll(train_items, this.train.train_id).then(
+                result => {
+                    if (result.length) {
+                        this.setMessage(
+                            this.$__(
+                                "%s items have been added to train %s."
+                            ).format(result.length, this.train.train_id)
+                        )
+
+                        this.$router.push(
+                            "/cgi-bin/koha/preservation/trains/" +
+                                this.train.train_id
+                        )
+                    } else {
+                        this.setMessage(
+                            this.$__("No items have been added to the train.")
+                        )
+                    }
+                },
+                error => {}
+            )
+        },
+    },
+    components: {},
+    name: "TrainsFormAddItems",
+}
+</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
index a1d627ecd5..a290ca5615 100644
--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/WaitingList.vue
@@ -189,40 +189,13 @@ export default {
         },
         addItemsToTrain: function (e) {
             e.preventDefault()
-            this.loading()
-            let item_ids = Object.values(this.last_items)
-            const client = APIClient.preservation
-            client.train_items
-                .createAll(item_ids, this.train_id_selected_for_add)
-                .then(
-                    result => {
-                        if (result.length) {
-                            this.setMessage(
-                                this.$__(
-                                    "%s items have been added to train %s."
-                                ).format(
-                                    result.length,
-                                    this.train_id_selected_for_add
-                                ),
-                                true
-                            )
-                        } else {
-                            this.setMessage(
-                                this.$__(
-                                    "No items have been added to the train."
-                                )
-                            )
-                        }
-
-                        this.$refs.table.redraw(
-                            "/api/v1/preservation/waiting-list/items"
-                        )
-                        this.show_modal_add_to_train = false
-                        this.last_items = []
-                    },
-                    error => {}
-                )
-                .then(() => this.loaded())
+            let item_ids = this.last_items.map(i => i.item_id)
+            this.$router.push(
+                "/cgi-bin/koha/preservation/trains/" +
+                    this.train_id_selected_for_add +
+                    "/items/add/" +
+                    item_ids.join(",")
+            )
         },
         addItemsToWaitingList: function (e) {
             e.preventDefault()
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js
index 55325bdb57..f95d3705e1 100644
--- a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/preservation.js
@@ -3,6 +3,7 @@ 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 TrainsFormAddItems from "../components/Preservation/TrainsFormAddItems.vue";
 import WaitingList from "../components/Preservation/WaitingList.vue";
 import Settings from "../components/Preservation/Settings.vue";
 import SettingsProcessingsShow from "../components/Preservation/SettingsProcessingsShow.vue";
@@ -117,6 +118,17 @@ export const routes = [
                                         ),
                                 },
                             },
+                            {
+                                path: "add/:item_ids",
+                                component: TrainsFormAddItems,
+                                meta: {
+                                    breadcrumb: () =>
+                                        build_breadcrumb(
+                                            breadcrumb_paths.trains,
+                                            "Add items to train" // $t("Add items to train")
+                                        ),
+                                },
+                            },
                             {
                                 path: "edit/:train_item_id",
                                 component: TrainsFormAddItem,
diff --git a/t/cypress/integration/Preservation/Trains.ts b/t/cypress/integration/Preservation/Trains.ts
index b7a515c76b..858d523ebd 100644
--- a/t/cypress/integration/Preservation/Trains.ts
+++ b/t/cypress/integration/Preservation/Trains.ts
@@ -596,4 +596,64 @@ describe("Trains", () => {
             });
         });
     });
+
+    it("Add to waiting list then add to a train", () => {
+        let train = get_train();
+        let processing = get_processings()[0];
+        cy.intercept("GET", "/api/v1/preservation/trains*", [train]);
+        cy.intercept("GET", "/api/v1/preservation/trains/1", train);
+        cy.intercept("GET", "/api/v1/preservation/processings/1", processing);
+        cy.visit("/cgi-bin/koha/preservation/waiting-list");
+
+        cy.intercept("GET", "/api/v1/preservation/waiting-list/items*", {
+            statusCode: 200,
+            body: get_items(),
+            headers: {
+                "X-Base-Total-Count": "2",
+                "X-Total-Count": "2",
+            },
+        }).as("get-items");
+        cy.intercept("POST", "/api/v1/preservation/waiting-list/items", [
+            { item_id: 1 },
+            { item_id: 2 },
+        ]);
+        cy.get("#waiting-list").contains("Add to waiting list").click();
+        cy.get("#barcode_list").type("bc_1\nbc_2\nbc_3");
+        cy.contains("Submit").click();
+        cy.wait("@get-items");
+        cy.get("main div[class='dialog message']").contains(
+            "2 new items added."
+        );
+        cy.contains("Add last 2 items to a train").click();
+        cy.get("#train_id .vs__search").type(train.name + "{enter}");
+        cy.intercept("GET", "/api/v1/items*", {
+            statusCode: 200,
+            body: get_items().filter(
+                item => item.item_id == 1 || item.item_id == 2
+            ),
+            headers: {
+                "X-Base-Total-Count": "2",
+                "X-Total-Count": "2",
+            },
+        });
+        cy.intercept(
+            "POST",
+            "/api/v1/preservation/trains/" + train.train_id + "/items/batch",
+            req => {
+                req.reply({
+                    statusCode: 201,
+                    body: req.body,
+                });
+            }
+        );
+        cy.contains("Submit").click(); // Select train
+        train.items = get_train_items().filter(
+            train_item => train_item.item_id == 1 || train_item.item_id == 2
+        );
+        cy.intercept("GET", "/api/v1/preservation/trains/1", train);
+        cy.contains("Submit").click(); // Submit add items form
+        cy.get("main div[class='dialog message']").contains(
+            `2 items have been added to train ${train.train_id}.`
+        );
+    });
 });
diff --git a/t/cypress/integration/Preservation/WaitingList.ts b/t/cypress/integration/Preservation/WaitingList.ts
index 4e4e1c2b5d..e5c956198f 100644
--- a/t/cypress/integration/Preservation/WaitingList.ts
+++ b/t/cypress/integration/Preservation/WaitingList.ts
@@ -103,52 +103,6 @@ describe("WaitingList", () => {
         );
     });
 
-    it("Add to waiting list then add to a train", () => {
-        let train = {
-            description: "yet another train",
-            name: "a train",
-            train_id: 1,
-        };
-        cy.intercept("GET", "/api/v1/preservation/trains*", [train]);
-        cy.visit("/cgi-bin/koha/preservation/waiting-list");
-
-        cy.intercept("GET", "/api/v1/preservation/waiting-list/items*", {
-            statusCode: 200,
-            body: get_items(),
-            headers: {
-                "X-Base-Total-Count": "2",
-                "X-Total-Count": "2",
-            },
-        }).as("get-items");
-        cy.intercept("POST", "/api/v1/preservation/waiting-list/items", [
-            { item_id: 1 },
-            { item_id: 3 },
-        ]);
-        cy.get("#waiting-list").contains("Add to waiting list").click();
-        cy.get("#barcode_list").type("bc_1\nbc_2\nbc_3");
-        cy.contains("Submit").click();
-        cy.wait("@get-items");
-        cy.get("main div[class='dialog message']").contains(
-            "2 new items added."
-        );
-        cy.contains("Add last 2 items to a train").click();
-        cy.get("#train_id .vs__search").type(train.name + "{enter}");
-        cy.intercept(
-            "POST",
-            "/api/v1/preservation/trains/" + train.train_id + "/items/batch",
-            req => {
-                req.reply({
-                    statusCode: 201,
-                    body: req.body,
-                });
-            }
-        );
-        cy.contains("Submit").click();
-        cy.get("main div[class='dialog message']").contains(
-            `2 items have been added to train ${train.train_id}.`
-        );
-    });
-
     it("Remove item from waiting list", () => {
         cy.intercept("GET", "/api/v1/preservation/waiting-list/items*", {
             statusCode: 200,
-- 
2.30.2