Bug 32030: ERM - Packages - Vue
authorJonathan Druart <jonathan.druart@bugs.koha-community.org>
Fri, 20 May 2022 10:51:37 +0000 (12:51 +0200)
committerTomas Cohen Arazi <tomascohen@theke.io>
Tue, 8 Nov 2022 12:43:57 +0000 (09:43 -0300)
Signed-off-by: Jonathan Field <jonathan.field@ptfs-europe.com>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
20 files changed:
api/v1/swagger/definitions/vendor.yaml
api/v1/swagger/paths/erm_packages.yaml
cypress/integration/Packages_spec.ts [new file with mode: 0644]
installer/data/mysql/atomicupdate/erm.pl
installer/data/mysql/mandatory/auth_val_cat.sql
koha-tmpl/intranet-tmpl/prog/en/modules/erm/erm.tt
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementRelationships.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsFormAdd.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ERMMain.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesFormAdd.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesFormAdd.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesFormConfirmDelete.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesList.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesShow.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesToolbar.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/fetch.js
koha-tmpl/intranet-tmpl/prog/js/vue/routes.js
koha-tmpl/intranet-tmpl/prog/js/vue/stores/authorised_values.js

index f50dd71..90feafb 100644 (file)
@@ -111,7 +111,7 @@ properties:
     description: Expected delivery time (in days)
   external_id:
     type:
-      - integer
+      - string
       - "null"
     description: External id
 additionalProperties: false
index 61e0afe..8ec0ef0 100644 (file)
@@ -1,8 +1,8 @@
 ---
 /erm/packages:
   get:
-    x-mojo-to: ERM::packages#list
-    operationId: listErmpackages
+    x-mojo-to: ERM::Packages#list
+    operationId: listErmPackages
     tags:
       - package
     summary: List packages
@@ -76,8 +76,8 @@
       permissions:
         erm: 1
   post:
-    x-mojo-to: ERM::packages#add
-    operationId: addErmpackages
+    x-mojo-to: ERM::Packages#add
+    operationId: addErmPackages
     tags:
       - package
     summary: Add package
         erm: 1
 "/erm/packages/{package_id}":
   get:
-    x-mojo-to: ERM::packages#get
-    operationId: getErmpackages
+    x-mojo-to: ERM::Packages#get
+    operationId: getErmPackages
     tags:
       - package
     summary: Get package
       permissions:
         erm: 1
   put:
-    x-mojo-to: ERM::packages#update
-    operationId: updateErmpackages
+    x-mojo-to: ERM::Packages#update
+    operationId: updateErmPackages
     tags:
       - package
     summary: Update package
       permissions:
         erm: 1
   delete:
-    x-mojo-to: ERM::packages#delete
-    operationId: deleteErmpackages
+    x-mojo-to: ERM::Packages#delete
+    operationId: deleteErmPackages
     tags:
       - package
     summary: Delete package
diff --git a/cypress/integration/Packages_spec.ts b/cypress/integration/Packages_spec.ts
new file mode 100644 (file)
index 0000000..5a21a50
--- /dev/null
@@ -0,0 +1,216 @@
+import { mount } from "@cypress/vue";
+const dayjs = require("dayjs"); /* Cannot use our calendar JS code, it's in an include file (!)
+                                   Also note that moment.js is deprecated */
+
+function get_package() {
+    return {
+        package_id: 1,
+        name: "package 1",
+        package_type: "complete",
+        content_type: "print",
+        package_agreements: [],
+    };
+}
+
+describe("Package CRUD operations", () => {
+    beforeEach(() => {
+        cy.login("koha", "koha");
+        cy.title().should("eq", "Koha staff interface");
+    });
+
+    it("List package", () => {
+        // GET package returns 500
+        cy.intercept("GET", "/api/v1/erm/packages", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.visit("/cgi-bin/koha/erm/erm.pl");
+        cy.get("#navmenulist").contains("Packages").click();
+        cy.get("main div[class='dialog alert']").contains(
+            /Something went wrong/
+        );
+
+        // GET packages returns empty list
+        cy.intercept("GET", "/api/v1/erm/packages*", []);
+        cy.visit("/cgi-bin/koha/erm/packages");
+        cy.get("#packages_list").contains("There are no packages defined.");
+
+        // GET packages returns something
+        let erm_package = get_package();
+        let packages = [erm_package];
+
+        cy.intercept("GET", "/api/v1/erm/packages*", {
+            statusCode: 200,
+            body: packages,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/packages/*", erm_package);
+        cy.visit("/cgi-bin/koha/erm/packages");
+        cy.get("#packages_list").contains("Showing 1 to 1 of 1 entries");
+    });
+
+    it("Add package", () => {
+        // Click the button in the toolbar
+        cy.visit("/cgi-bin/koha/erm/packages");
+        cy.contains("New package").click();
+        cy.get("#packages_add h2").contains("New package");
+
+        // Fill in the form for normal attributes
+        let erm_package = get_package();
+
+        cy.get("#packages_add").contains("Submit").click();
+        cy.get("input:invalid,textarea:invalid,select:invalid").should(
+            "have.length",
+            1
+        );
+        cy.get("#package_name").type(erm_package.name);
+        cy.get("#package_type").select(erm_package.package_type);
+        cy.get("#package_content_type").select(erm_package.content_type);
+
+        // Submit the form, get 500
+        cy.intercept("POST", "/api/v1/erm/packages", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.get("#packages_add").contains("Submit").click();
+        cy.get("main div[class='dialog alert']").contains(
+            "Something went wrong: Internal Server Error"
+        );
+
+        // Submit the form, success!
+        cy.intercept("POST", "/api/v1/erm/packages", {
+            statusCode: 201,
+            body: erm_package,
+        });
+        cy.get("#packages_add").contains("Submit").click();
+        cy.get("main div[class='dialog message']").contains(
+            "Package created"
+        );
+    });
+
+    it("Edit package", () => {
+        let erm_package = get_package();
+        let packages = [erm_package];
+        // Click the 'Edit' button from the list
+        cy.intercept("GET", "/api/v1/erm/packages*", {
+            statusCode: 200,
+            body: packages,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/packages/*", erm_package).as(
+            "get-package"
+        );
+        cy.visit("/cgi-bin/koha/erm/packages");
+        cy.get("#packages_list table tbody tr:first")
+            .contains("Edit")
+            .click();
+        cy.wait("@get-package");
+        cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet!
+        cy.get("#packages_add h2").contains("Edit package");
+
+        // Form has been correctly filled in
+        cy.get("#package_name").should("have.value", erm_package.name);
+        cy.get("#package_type").should("have.value", erm_package.package_type);
+        cy.get("#package_content_type").should("have.value", erm_package.content_type);
+
+        // Submit the form, get 500
+        cy.intercept("PUT", "/api/v1/erm/packages/*", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.get("#packages_add").contains("Submit").click();
+        cy.get("main div[class='dialog alert']").contains(
+            "Something went wrong: Internal Server Error"
+        );
+
+        // Submit the form, success!
+        cy.intercept("PUT", "/api/v1/erm/packages/*", {
+            statusCode: 200,
+            body: erm_package,
+        });
+        cy.get("#packages_add").contains("Submit").click();
+        cy.get("main div[class='dialog message']").contains(
+            "Package updated"
+        );
+    });
+
+    it("Show package", () => {
+        let erm_package = get_package();
+        let packages = [erm_package];
+        // Click the "name" link from the list
+        cy.intercept("GET", "/api/v1/erm/packages*", {
+            statusCode: 200,
+            body: packages,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/packages/*", erm_package).as(
+            "get-package"
+        );
+        cy.visit("/cgi-bin/koha/erm/packages");
+        let name_link = cy.get(
+            "#packages_list table tbody tr:first td:first a"
+        );
+        name_link.should(
+            "have.text",
+            erm_package.name + " (#" + erm_package.package_id + ")"
+        );
+        name_link.click();
+        cy.wait("@get-package");
+        cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet!
+        cy.get("#packages_show h2").contains(
+            "Package #" + erm_package.package_id
+        );
+    });
+
+    it("Delete package", () => {
+        let erm_package = get_package();
+        let packages = [erm_package];
+
+        // Click the 'Delete' button from the list
+        cy.intercept("GET", "/api/v1/erm/packages*", {
+            statusCode: 200,
+            body: packages,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/packages/*", erm_package);
+        cy.visit("/cgi-bin/koha/erm/packages");
+
+        cy.get("#packages_list table tbody tr:first")
+            .contains("Delete")
+            .click();
+        cy.get("#packages_confirm_delete h2").contains("Delete package");
+        cy.contains("Package name: " + erm_package.name);
+
+        // Submit the form, get 500
+        cy.intercept("DELETE", "/api/v1/erm/packages/*", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.contains("Yes, delete").click();
+        cy.get("main div[class='dialog alert']").contains(
+            "Something went wrong: Internal Server Error"
+        );
+
+        // Submit the form, success!
+        cy.intercept("DELETE", "/api/v1/erm/packages/*", {
+            statusCode: 204,
+            body: null,
+        });
+        cy.contains("Yes, delete").click();
+        cy.get("main div[class='dialog message']").contains(
+            "Package deleted"
+        );
+    });
+});
index 0682cf8..859b4ca 100755 (executable)
@@ -265,5 +265,28 @@ return {
                 AFTER `deliverytime`
             });
         }
+
+        $dbh->do(q{
+            INSERT IGNORE INTO authorised_value_categories (category_name, is_system)
+            VALUES
+                ('ERM_PACKAGE_TYPE', 1),
+                ('ERM_PACKAGE_CONTENT_TYPE', 1)
+        });
+
+        $dbh->do(q{
+            INSERT IGNORE INTO authorised_values (category, authorised_value, lib)
+            VALUES
+                ('ERM_PACKAGE_TYPE', 'local', 'Local'),
+                ('ERM_PACKAGE_TYPE', 'complete', 'Complete'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'mixed_content', 'Aggregated full'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'mixed_content', 'Abstract and index'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'e_book', 'E-book'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'mixed_content', 'Mixed content'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'e_journal', 'E-journal'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'online_reference', 'Online reference'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'print', 'Print'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'streaming_media', 'Streaming media'),
+                ('ERM_PACKAGE_CONTENT_TYPE', 'unknown', 'Unknown')
+        });
     }
 };
index 8b93670..ddf21f9 100644 (file)
@@ -83,7 +83,9 @@ VALUES
     ('ERM_LICENSE_TYPE', 1),
     ('ERM_LICENSE_STATUS', 1),
     ('ERM_AGREEMENT_LICENSE_STATUS', 1),
-    ('ERM_AGREEMENT_LICENSE_LOCATION', 1);
+    ('ERM_AGREEMENT_LICENSE_LOCATION', 1),
+    ('ERM_PACKAGE_TYPE', 1),
+    ('ERM_PACKAGE_CONTENT_TYPE', 1);
 
 INSERT IGNORE INTO authorised_values (category, authorised_value, lib)
 VALUES
@@ -110,4 +112,16 @@ VALUES
     ('ERM_AGREEMENT_LICENSE_STATUS', 'future', 'Future'),
     ('ERM_AGREEMENT_LICENSE_STATUS', 'history', 'Historic'),
     ('ERM_AGREEMENT_LICENSE_LOCATION', 'filing_cabinet', 'Filing cabinet'),
-    ('ERM_AGREEMENT_LICENSE_LOCATION', 'cupboard', 'Cupboard');
+    ('ERM_AGREEMENT_LICENSE_LOCATION', 'cupboard', 'Cupboard'),
+    ('ERM_PACKAGE_TYPE', 'local', 'Local'),
+    ('ERM_PACKAGE_TYPE', 'complete', 'Complete'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'mixed_content', 'Aggregated full'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'mixed_content', 'Abstract and index'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'e_book', 'E-book'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'mixed_content', 'Mixed content'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'e_journal', 'E-journal'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'online_reference', 'Online reference'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'print', 'Print'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'streaming_media', 'Streaming media'),
+    ('ERM_PACKAGE_CONTENT_TYPE', 'unknown', 'Unknown');
+
index 0ef7d28..79ba61a 100644 (file)
@@ -33,7 +33,7 @@
         const agreement_renewal_priorities = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_RENEWAL_PRIORITY')) | $raw %];
         const agreement_user_roles = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_USER_ROLES')) | $raw %];
 
-        var table_settings = [% TablesSettings.GetTableSettings( 'erm', 'agreements', 'agreements', 'json' ) | $raw %];
+        var agreement_table_settings = [% TablesSettings.GetTableSettings( 'erm', 'agreements', 'agreements', 'json' ) | $raw %];
 
         var agreements_table_url = '/api/v1/erm/agreements?';
         [% IF agreement_name_filter %]
@@ -48,7 +48,7 @@
         const license_types = [% To.json(AuthorisedValues.Get('ERM_LICENSE_TYPE')) | $raw %];
         const license_statuses = [% To.json(AuthorisedValues.Get('ERM_LICENSE_STATUS')) | $raw %];
 
-        var table_settings = [% TablesSettings.GetTableSettings( 'erm', 'agreements', 'agreements', 'json' ) | $raw %];
+        var license_table_settings = [% TablesSettings.GetTableSettings( 'erm', 'licenses', 'licenses', 'json' ) | $raw %];
 
         var licenses_table_url = '/api/v1/erm/licenses?';
         [% IF license_name_filter %]
         const agreement_license_statuses = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_LICENSE_STATUS')) | $raw %];
         const agreement_license_location = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_LICENSE_LOCATION')) | $raw %];
 
+        const package_types = [% To.json(AuthorisedValues.Get('ERM_PACKAGE_TYPE')) | $raw %];
+        const package_content_types = [% To.json(AuthorisedValues.Get('ERM_PACKAGE_CONTENT_TYPE')) | $raw %];
+
+        var package_table_settings = [% TablesSettings.GetTableSettings( 'erm', 'packages', 'packages', 'json' ) | $raw %];
+
+        var packages_table_url = '/api/v1/erm/packages?';
+        [% IF package_name_filter %]
+            var package_name_filter = {
+                'name': {
+                    "like": '%[%- package_name_filter | html -%]%'
+                }
+            };
+            packages_table_url += 'q='+ encodeURIComponent(JSON.stringify(package_name_filter));
+        [% END %]
+
     </script>
 
     [% Asset.js("js/vue/dist/main.js") | $raw %]
index 3de4440..cfad178 100644 (file)
@@ -343,9 +343,6 @@ export default {
             }
         }
     },
-    props: {
-        agreement_id: Number,
-    },
     components: {
         AgreementPeriods,
         AgreementUserRoles,
index 12765f8..a46a529 100644 (file)
@@ -231,7 +231,7 @@ export default {
                 $("#" + table_id).find("thead th").eq(6).attr('data-filter', 'av_agreement_renewal_priorities')
             }
 
-        }, table_settings, 1)
+        }, agreement_table_settings, 1)
     },
     beforeUnmount() {
         $('#agreement_list')
index baa441b..4001bd9 100644 (file)
                                 </li>
                                 <li>
                                     <router-link
+                                        to="/cgi-bin/koha/erm/packages"
+                                    >
+                                        <i class="fa fa-file-text-o"></i>
+                                        {{ $t("Packages") }}</router-link
+                                    >
+                                </li>
+                                <li>
+                                    <router-link
                                         to="/cgi-bin/koha/erm/licenses"
                                     >
                                         <i class="fa fa-file-text-o"></i>
@@ -59,6 +67,8 @@ export default {
         AVStore.av_license_statuses = license_statuses
         AVStore.av_agreement_license_statuses = agreement_license_statuses
         AVStore.av_agreement_license_location = agreement_license_location
+        AVStore.av_package_types = package_types
+        AVStore.av_package_content_types = package_content_types
 
         return {
             vendorStore,
index 9219bf7..a3bab1f 100644 (file)
@@ -218,9 +218,6 @@ export default {
                 }).catch(e => { console.log(e) })
         },
     },
-    props: {
-        license_id: Number,
-    },
     components: {
         flatPickr
     },
index d392520..edea248 100644 (file)
@@ -104,7 +104,7 @@ export default {
                     "searchable": true,
                     "orderable": true,
                     "render": function (data, type, row, meta) {
-                        return escape_str(row.started_on)
+                        return $date(row.started_on)
                     }
                 },
                 {
@@ -113,7 +113,7 @@ export default {
                     "searchable": true,
                     "orderable": true,
                     "render": function (data, type, row, meta) {
-                        return escape_str(row.ended_on)
+                        return $date(row.ended_on)
                     }
                 },
                 {
@@ -170,7 +170,7 @@ export default {
                 $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'av_license_statuses')
             }
 
-        }, table_settings, 1)
+        }, license_table_settings, 1)
     },
     beforeUnmount() {
         $('#license_list')
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesFormAdd.vue
new file mode 100644 (file)
index 0000000..09a52c3
--- /dev/null
@@ -0,0 +1,206 @@
+<template>
+    <div v-if="!this.initialized">{{ $t("Loading") }}</div>
+    <div v-else id="packages_add">
+        <h2 v-if="erm_package.package_id">
+            {{ $t("Edit package.id", { id: erm_package.package_id }) }}
+        </h2>
+        <h2 v-else>{{ $t("New package") }}</h2>
+        <div>
+            <form @submit="onSubmit($event)">
+                <fieldset class="rows">
+                    <ol>
+                        <li>
+                            <label class="required" for="package_name">{{
+                                $t("Package name:")
+                            }}</label>
+                            <input
+                                id="package_name"
+                                v-model="erm_package.name"
+                                :placeholder="$t('Package name')"
+                                required
+                            />
+                            <span class="required">{{ $t("Required") }}</span>
+                        </li>
+                        <li>
+                            <label for="package_vendor_id">{{
+                                $t("Vendor:")
+                            }}</label>
+                            <select
+                                id="package_vendor_id"
+                                v-model="erm_package.vendor_id"
+                            >
+                                <option value=""></option>
+                                <option
+                                    v-for="vendor in vendors"
+                                    :key="vendor.vendor_id"
+                                    :value="vendor.id"
+                                    :selected="
+                                        vendor.id == erm_package.vendor_id
+                                            ? true
+                                            : false
+                                    "
+                                >
+                                    {{ vendor.name }}
+                                </option>
+                            </select>
+                        </li>
+                        <li>
+                            <label for="package_type">{{ $t("Type: ") }}</label>
+                            <select
+                                id="package_type"
+                                v-model="erm_package.package_type"
+                            >
+                                <option value=""></option>
+                                <option
+                                    v-for="type in av_package_types"
+                                    :key="type.authorised_values"
+                                    :value="type.authorised_value"
+                                    :selected="
+                                        type.authorised_value ==
+                                        erm_package.package_type
+                                            ? true
+                                            : false
+                                    "
+                                >
+                                    {{ type.lib }}
+                                </option>
+                            </select>
+                        </li>
+                        <li>
+                            <label for="package_content_type">{{
+                                $t("Content type: ")
+                            }}</label>
+                            <select
+                                id="package_content_type"
+                                v-model="erm_package.content_type"
+                            >
+                                <option value=""></option>
+                                <option
+                                    v-for="type in av_package_content_types"
+                                    :key="type.authorised_values"
+                                    :value="type.authorised_value"
+                                    :selected="
+                                        type.authorised_value ==
+                                        erm_package.content_type
+                                            ? true
+                                            : false
+                                    "
+                                >
+                                    {{ type.lib }}
+                                </option>
+                            </select>
+                        </li>
+
+                        <PackageAgreements
+                            :package_agreements="erm_package.agreements"
+                        />
+                    </ol>
+                </fieldset>
+                <fieldset class="action">
+                    <input type="submit" value="Submit" />
+                    <router-link
+                        to="/cgi-bin/koha/erm/packages"
+                        role="button"
+                        class="cancel"
+                        >{{ $t("Cancel") }}</router-link
+                    >
+                </fieldset>
+            </form>
+        </div>
+    </div>
+</template>
+
+<script>
+import PackageAgreements from "./PackageAgreements.vue"
+import { useVendorStore } from "../../stores/vendors"
+import { useAVStore } from "../../stores/authorised_values"
+import { setMessage, setError } from "../../messages"
+import { fetchPackage } from '../../fetch'
+import { storeToRefs } from "pinia"
+
+export default {
+    setup() {
+        const vendorStore = useVendorStore()
+        const { vendors } = storeToRefs(vendorStore)
+        const AVStore = useAVStore()
+        const {
+            av_package_types,
+            av_package_content_types,
+        } = storeToRefs(AVStore)
+
+        return {
+            vendors,
+            av_package_types,
+            av_package_content_types,
+        }
+    },
+    data() {
+        return {
+            erm_package: {
+                package_id: null,
+                name: '',
+                external_package_id: null,
+                package_type: '',
+                content_type: '',
+            },
+            initialized: false,
+        }
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            if (to.params.package_id) {
+                vm.erm_package = vm.getPackage(to.params.package_id)
+            } else {
+                vm.initialized = true
+            }
+        })
+    },
+    methods: {
+        async getPackage(package_id) {
+            const erm_package = await fetchPackage(package_id)
+            this.erm_package = erm_package
+            this.initialized = true
+        },
+        onSubmit(e) {
+            e.preventDefault()
+
+            let erm_package = JSON.parse(JSON.stringify(this.erm_package)) // copy
+            let apiUrl = '/api/v1/erm/packages'
+
+            let method = 'POST'
+            if (erm_package.package_id) {
+                method = 'PUT'
+                apiUrl += '/' + erm_package.package_id
+            }
+            delete erm_package.package_id
+
+            const options = {
+                method: method,
+                body: JSON.stringify(erm_package),
+                headers: {
+                    'Content-Type': 'application/json;charset=utf-8'
+                },
+            }
+
+            fetch(apiUrl, options)
+                .then(response => {
+                    if (response.status == 200) {
+                        this.$router.push("/cgi-bin/koha/erm/packages")
+                        setMessage('Package updated')
+                    } else if (response.status == 201) {
+                        this.$router.push("/cgi-bin/koha/erm/packages")
+                        setMessage('Package created')
+                    } else {
+                        setError(response.message || response.statusText)
+                    }
+                }, (error) => {
+                    setError(error)
+                }).catch(e => { console.log(e) })
+        },
+    },
+    components: [
+        PackageAgreements,
+    ],
+    name: "PackagesFormAdd",
+}
+</script>
\ No newline at end of file
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesFormConfirmDelete.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesFormConfirmDelete.vue
new file mode 100644 (file)
index 0000000..3232997
--- /dev/null
@@ -0,0 +1,86 @@
+<template>
+    <div v-if="!this.initialized">{{ $t("Loading") }}</div>
+    <div v-else id="packages_confirm_delete">
+        <h2>{{ $t("Delete package") }}</h2>
+        <div>
+            <form @submit="onSubmit($event)">
+                <fieldset class="rows">
+                    <ol>
+                        <li>
+                            {{ $t("Package name:") }}
+                            {{ erm_package.name }}
+                        </li>
+                    </ol>
+                </fieldset>
+                <fieldset class="action">
+                    <input
+                        type="submit"
+                        variant="primary"
+                        :value="$t('Yes, delete')"
+                    />
+                    <router-link
+                        to="/cgi-bin/koha/erm/packages"
+                        role="button"
+                        class="cancel"
+                        >{{ $t("No, do not delete") }}</router-link
+                    >
+                </fieldset>
+            </form>
+        </div>
+    </div>
+</template>
+
+<script>
+import { fetchPackage } from "../../fetch"
+import { setMessage, setError } from "../../messages"
+
+export default {
+    data() {
+        return {
+            erm_package: {},
+            initialized: false,
+        }
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.getPackage(to.params.package_id)
+        })
+    },
+    methods: {
+        async getPackage(package_id) {
+            const erm_package = await fetchPackage(package_id)
+            this.erm_package = erm_package
+            this.initialized = true
+        },
+        onSubmit(e) {
+            e.preventDefault()
+
+            let apiUrl = '/api/v1/erm/packages/' + this.erm_package.package_id
+
+            const options = {
+                method: 'DELETE',
+                headers: {
+                    'Content-Type': 'application/json;charset=utf-8'
+                },
+            }
+
+            fetch(apiUrl, options)
+                .then(
+                    (response) => {
+                        if (response.status == 204) {
+                            setMessage("Package deleted")
+                            this.$router.push("/cgi-bin/koha/erm/packages")
+                        } else {
+                            setError(response.message || response.statusText)
+                        }
+                    }
+                ).catch(
+                    (error) => {
+                        setError(error)
+                    }
+                )
+        }
+    },
+    name: "PackagesFormConfirmDelete",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesList.vue
new file mode 100644 (file)
index 0000000..a1c596e
--- /dev/null
@@ -0,0 +1,216 @@
+<template>
+    <div v-if="!this.initialized">{{ $t("Loading") }}</div>
+    <div v-else-if="this.packages" id="packages_list">
+        <Toolbar />
+        <table v-if="this.packages.length" id="package_list"></table>
+        <div v-else-if="this.initialized" class="dialog message">
+            {{ $t("There are no packages defined") }}
+        </div>
+    </div>
+</template>
+
+<script>
+import Toolbar from "./PackagesToolbar.vue"
+import { createVNode, render } from 'vue'
+import { useVendorStore } from "../../stores/vendors"
+import { useAVStore } from "../../stores/authorised_values"
+import { storeToRefs } from "pinia"
+import { fetchPackages } from "../../fetch"
+
+export default {
+    setup() {
+        const vendorStore = useVendorStore()
+        const { vendors } = storeToRefs(vendorStore)
+
+        const AVStore = useAVStore()
+        const { av_package_types, av_package_content_types } = storeToRefs(AVStore)
+
+        return {
+            vendors,
+            av_package_types,
+            av_package_content_types,
+        }
+    },
+    data: function () {
+        return {
+            packages: [],
+            initialized: false,
+        }
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.getPackages()
+        })
+    },
+    methods: {
+        async getPackages() {
+            const packages = await fetchPackages()
+            this.packages = packages
+            this.initialized = true
+        },
+        show_package: function (package_id) {
+            this.$router.push("/cgi-bin/koha/erm/packages/" + package_id)
+        },
+        edit_package: function (package_id) {
+            this.$router.push("/cgi-bin/koha/erm/packages/edit/" + package_id)
+        },
+        delete_package: function (package_id) {
+            this.$router.push("/cgi-bin/koha/erm/packages/delete/" + package_id)
+        },
+    },
+    updated() {
+        let show_package = this.show_package
+        let edit_package = this.edit_package
+        let delete_package = this.delete_package
+        window['vendors'] = this.vendors.map(e => {
+            e['_id'] = e['id']
+            e['_str'] = e['name']
+            return e
+        })
+        let vendors_map = this.vendors.reduce((map, e) => {
+            map[e.id] = e
+            return map
+        }, {})
+        window['av_package_types'] = this.av_package_types.map(e => {
+            e['_id'] = e['authorised_value']
+            e['_str'] = e['lib']
+            return e
+        })
+        let av_package_types_map = this.av_package_types.reduce((map, e) => {
+            map[e.authorised_value] = e
+            return map
+        }, {})
+        window['av_package_content_types'] = this.av_package_content_types.map(e => {
+            e['_id'] = e['authorised_value']
+            e['_str'] = e['lib']
+            return e
+        })
+        let av_package_content_types_map = this.av_package_content_types.reduce((map, e) => {
+            map[e.authorised_value] = e
+            return map
+        }, {})
+
+        $('#package_list').kohaTable({
+            "ajax": {
+                "url": packages_table_url,
+            },
+            "order": [[1, "asc"]],
+            "columnDefs": [{
+                "targets": [0, 2],
+                "render": function (data, type, row, meta) {
+                    if (type == 'display') {
+                        return escape_str(data)
+                    }
+                    return data
+                }
+            }],
+            "columns": [
+                {
+                    "title": __("Name"),
+                    "data": ["me.package_id", "me.name"],
+                    "searchable": true,
+                    "orderable": true,
+                    // Rendering done in drawCallback
+                },
+                {
+                    "title": __("Vendor"),
+                    "data": "vendor_id",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return row.vendor_id != undefined ? escape_str(vendors_map[row.vendor_id].name) : ""
+                    }
+                },
+                {
+                    "title": __("Type"),
+                    "data": "package_type",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return row.package_type != undefined && row.package_type != "" ? escape_str(av_package_types_map[row.package_type].lib) : ""
+                    }
+                },
+                {
+                    "title": __("Content type"),
+                    "data": "package_type",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return row.content_type != undefined && row.content_type != "" ? escape_str(av_package_content_types_map[row.content_type].lib) : ""
+                    }
+                },
+                {
+                    "title": __("Created on"),
+                    "data": "created_on",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return $date(row.created_on)
+                    }
+                },
+                {
+                    "title": __("Actions"),
+                    "data": function (row, type, val, meta) {
+                        return '<div class="actions"></div>'
+                    },
+                    "className": "actions noExport",
+                    "searchable": false,
+                    "orderable": false
+                }
+            ],
+            drawCallback: function (settings) {
+
+                var api = new $.fn.dataTable.Api(settings)
+
+                $.each($(this).find("td .actions"), function (index, e) {
+                    let package_id = api.row(index).data().package_id
+                    let editButton = createVNode("a", {
+                        class: "btn btn-default btn-xs", role: "button", onClick: () => {
+                            edit_package(package_id)
+                        }
+                    },
+                        [createVNode("i", { class: "fa fa-pencil", 'aria-hidden': "true" }), __("Edit")])
+
+                    let deleteButton = createVNode("a", {
+                        class: "btn btn-default btn-xs", role: "button", onClick: () => {
+                            delete_package(package_id)
+                        }
+                    },
+                        [createVNode("i", { class: "fa fa-trash", 'aria-hidden': "true" }), __("Delete")])
+
+                    let n = createVNode('span', {}, [editButton, " ", deleteButton])
+                    render(n, e)
+                })
+
+                $.each($(this).find("tbody tr td:first-child"), function (index, e) {
+                    let row = api.row(index).data()
+                    if (!row) return // Happen if the table is empty
+                    let n = createVNode("a", {
+                        role: "button",
+                        onClick: () => {
+                            show_package(row.package_id)
+                        }
+                    },
+                        escape_str(`${row.name} (#${row.package_id})`)
+                    )
+                    render(n, e)
+                })
+            },
+            preDrawCallback: function (settings) {
+                var table_id = settings.nTable.id
+                $("#" + table_id).find("thead th").eq(1).attr('data-filter', 'vendors')
+                $("#" + table_id).find("thead th").eq(2).attr('data-filter', 'av_package_types')
+                $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'av_package_content_types')
+            }
+
+        }, package_table_settings, 1)
+    },
+    beforeUnmount() {
+        $('#package_list')
+            .DataTable()
+            .destroy(true)
+    },
+    components: { Toolbar },
+    name: "packagesList",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesShow.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesShow.vue
new file mode 100644 (file)
index 0000000..6fd84c4
--- /dev/null
@@ -0,0 +1,140 @@
+<template>
+    <div v-if="!this.initialized">{{ $t("Loading") }}</div>
+    <div v-else id="packages_show">
+        <h2>
+            {{ $t("Package.id", { id: erm_package.package_id }) }}
+            <span class="action_links">
+                <router-link
+                    :to="`/cgi-bin/koha/erm/packages/edit/${erm_package.package_id}`"
+                    :title="$t('Edit')"
+                    ><i class="fa fa-pencil"></i
+                ></router-link>
+
+                <router-link
+                    :to="`/cgi-bin/koha/erm/packages/delete/${erm_package.package_id}`"
+                    :title="$t('Delete')"
+                    ><i class="fa fa-trash"></i
+                ></router-link>
+            </span>
+        </h2>
+        <div>
+            <fieldset class="rows">
+                <ol>
+                    <li>
+                        <label>{{ $t("Package name:") }}</label>
+                        <span>
+                            {{ erm_package.name }}
+                        </span>
+                    </li>
+                    <li>
+                        <label>{{ $t("Vendor:") }}</label>
+                        <span v-if="erm_package.vendor_id">
+                            {{
+                                vendors.find(
+                                    (e) => e.id == erm_package.vendor_id
+                                ).name
+                            }}
+                        </span>
+                    </li>
+                    <li v-if="erm_package.external_package_id">
+                        <label>{{ $t("External ID:") }}</label>
+                        <span>
+                            {{ erm_package.external_package_id }}
+                        </span>
+                    </li>
+                    <li>
+                        <label>{{ $t("Package type:") }}</label>
+                        <span>{{
+                            get_lib_from_av(
+                                "av_package_types",
+                                erm_package.package_type
+                            )
+                        }}</span>
+                    </li>
+                    <li>
+                        <label>{{ $t("Content type:") }}</label>
+                        <span>{{
+                            get_lib_from_av(
+                                "av_package_content_types",
+                                erm_package.content_type
+                            )
+                        }}</span>
+                    </li>
+                    <li>
+                        <label>{{ $t("Created on:") }}</label>
+                        <span>{{ format_date(erm_package.created_on) }}</span>
+                    </li>
+                </ol>
+            </fieldset>
+            <fieldset class="action">
+                <router-link
+                    to="/cgi-bin/koha/erm/packages"
+                    role="button"
+                    class="cancel"
+                    >{{ $t("Close") }}</router-link
+                >
+            </fieldset>
+        </div>
+    </div>
+</template>
+
+<script>
+import { useVendorStore } from "../../stores/vendors"
+import { useAVStore } from "../../stores/authorised_values"
+import { fetchPackage } from "../../fetch"
+import { storeToRefs } from "pinia"
+
+export default {
+    setup() {
+        const format_date = $date
+
+        const vendorStore = useVendorStore()
+        const { vendors } = storeToRefs(vendorStore)
+
+        const AVStore = useAVStore()
+        const { get_lib_from_av } = AVStore
+
+        return {
+            format_date,
+            get_lib_from_av,
+            vendors,
+        }
+    },
+    data() {
+        return {
+            erm_package: {
+                package_id: null,
+                vendor_id: null,
+                name: '',
+                external_package_id: '',
+                package_type: '',
+                content_type: '',
+                created_on: null,
+            },
+            initialized: false,
+        }
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.getPackage(to.params.package_id)
+        })
+    },
+    beforeRouteUpdate(to, from) {
+        this.erm_package = this.getPackage(to.params.package_id)
+    },
+    methods: {
+        async getPackage(package_id) {
+            const erm_package = await fetchPackage(package_id)
+            this.erm_package = erm_package
+            this.initialized = true
+        },
+    },
+    name: "PackagesShow",
+}
+</script>
+<style scoped>
+.action_links a {
+    padding-left: 0.2em;
+    font-size: 11px;
+}
+</style>
\ No newline at end of file
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesToolbar.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/PackagesToolbar.vue
new file mode 100644 (file)
index 0000000..cb48be3
--- /dev/null
@@ -0,0 +1,12 @@
+<template>
+    <router-link to="/cgi-bin/koha/erm/packages/add" class="btn btn-default"
+        ><font-awesome-icon icon="plus" />
+        {{ $t("New package") }}</router-link
+    >
+</template>
+
+<script>
+export default {
+    name: "PackagesToolbar",
+}
+</script>
index 2c1c551..a321c5b 100644 (file)
@@ -103,3 +103,36 @@ export const fetchVendors = async function () {
         );
     return vendors;
 };
+
+export const fetchPackage = async function (package_id) {
+    if (!package_id) return;
+    const apiUrl = "/api/v1/erm/packages/" + package_id;
+    let erm_package;
+    await fetch(apiUrl)
+        .then((res) => res.json())
+        .then(
+            (result) => {
+                erm_package = result;
+            },
+            (error) => {
+                setError(error);
+            }
+        );
+    return erm_package;
+};
+
+export const fetchPackages = async function () {
+    const apiUrl = "/api/v1/erm/packages";
+    let packages;
+    await fetch(apiUrl)
+        .then((res) => res.json())
+        .then(
+            (result) => {
+                packages = result;
+            },
+            (error) => {
+                setError(error);
+            }
+        );
+    return packages;
+};
index a434720..5c3c910 100644 (file)
@@ -3,6 +3,10 @@ import AgreementsList from "./components/ERM/AgreementsList.vue";
 import AgreementsShow from "./components/ERM/AgreementsShow.vue";
 import AgreementsFormAdd from "./components/ERM/AgreementsFormAdd.vue";
 import AgreementsFormConfirmDelete from "./components/ERM/AgreementsFormConfirmDelete.vue";
+import PackagesList from "./components/ERM/PackagesList.vue";
+import PackagesShow from "./components/ERM/PackagesShow.vue";
+import PackagesFormAdd from "./components/ERM/PackagesFormAdd.vue";
+import PackagesFormConfirmDelete from "./components/ERM/PackagesFormConfirmDelete.vue";
 import LicensesList from "./components/ERM/LicensesList.vue";
 import LicensesShow from "./components/ERM/LicensesShow.vue";
 import LicensesFormAdd from "./components/ERM/LicensesFormAdd.vue";
@@ -16,6 +20,7 @@ const breadcrumbs = {
     },
     agreements: { text: "Agreements", path: "/cgi-bin/koha/erm/agreements" },
     licenses: { text: "Licenses", path: "/cgi-bin/koha/erm/licenses" },
+    packages: { text: "Packages", path: "/cgi-bin/koha/erm/packages" },
 };
 export const routes = [
     { path: "/cgi-bin/koha/mainpage.pl" },
@@ -87,6 +92,66 @@ export const routes = [
         },
     },
     {
+        path: "/cgi-bin/koha/erm/packages",
+        component: PackagesList,
+        meta: {
+            breadcrumb: [
+                breadcrumbs.home,
+                breadcrumbs.erm_home,
+                breadcrumbs.packages,
+            ],
+            view: "list",
+        },
+    },
+    {
+        path: "/cgi-bin/koha/erm/packages/:package_id",
+        component: PackagesShow,
+        meta: {
+            breadcrumb: [
+                breadcrumbs.home,
+                breadcrumbs.erm_home,
+                breadcrumbs.packages,
+            ],
+            view: "show",
+        },
+    },
+    {
+        path: "/cgi-bin/koha/erm/packages/delete/:package_id",
+        component: PackagesFormConfirmDelete,
+        meta: {
+            breadcrumb: [
+                breadcrumbs.home,
+                breadcrumbs.erm_home,
+                breadcrumbs.packages,
+            ],
+            view: "confirm-delete-form",
+        },
+    },
+    {
+        path: "/cgi-bin/koha/erm/packages/add",
+        component: PackagesFormAdd,
+        meta: {
+            breadcrumb: [
+                breadcrumbs.home,
+                breadcrumbs.erm_home,
+                breadcrumbs.packages,
+            ],
+            view: "add",
+        },
+    },
+    {
+        path: "/cgi-bin/koha/erm/packages/edit/:package_id",
+        component: PackagesFormAdd,
+        meta: {
+            breadcrumb: [
+                breadcrumbs.home,
+                breadcrumbs.erm_home,
+                breadcrumbs.packages,
+            ],
+            view: "edit",
+        },
+    },
+    {
         path: "/cgi-bin/koha/erm/licenses",
         component: LicensesList,
         meta: {
index 0a49a2b..d6e6693 100644 (file)
@@ -33,9 +33,19 @@ export const useAVStore = defineStore("authorised_values", {
             { authorised_value: "has_frontfile_in", lib: "has_frontfile_in" },
             { authorised_value: "related_to", lib: "related_to" },
         ],
+        av_package_types: [],
+        av_package_content_types: [],
     }),
     actions: {
         get_lib_from_av(arr_name, av) {
+            if (this[arr_name] === undefined) {
+                console.warn(
+                    "The authorised value category for '%s' is not defined.".format(
+                        arr_name
+                    )
+                );
+                return;
+            }
             let o = this[arr_name].find((e) => e.authorised_value == av);
             return o ? o.lib : "";
         },