From 0664ac9e9ec9a7d235f7e640e8b25234d12f5dde Mon Sep 17 00:00:00 2001
From: Matt Blenkinsop <matt.blenkinsop@ptfs-europe.com>
Date: Tue, 13 Aug 2024 14:16:33 +0000
Subject: [PATCH] Bug 10190: Add components to list/add and edit circulation
 triggers

These new components are used by the new 'Circulation triggers'
administration page to display existing trigger rules and add the
ability to add new rules and edit existing ones.

Sponsored-by: Glasgow Colleges Library Group <https://library.cityofglasgowcollege.ac.uk>
Signed-off-by: George Harkins <George.Harkins@cityofglasgowcollege.ac.uk>
---
 .../CirculationTriggersFormAdd.vue            | 524 ++++++++++++++++++
 .../CirculationTriggersList.vue               | 258 +++++++++
 .../CirculationTriggers/TriggerContext.vue    |  50 ++
 .../CirculationTriggers/TriggersTable.vue     | 266 +++++++++
 4 files changed, 1098 insertions(+)
 create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue
 create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersList.vue
 create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggerContext.vue
 create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue

diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue
new file mode 100644
index 00000000000..87568766c55
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue
@@ -0,0 +1,524 @@
+<template>
+    <router-link :to="{ name: 'CirculationTriggersList' }" class="close_modal">
+        <i class="fa fa-fw fa-close"></i>
+    </router-link>
+    <div v-if="initialized" class="modal-content">
+        <form @submit="addCircRule($event)">
+            <div class="modal-header">
+                <h1 v-if="!editMode">{{ $__("Add circulation trigger") }}</h1>
+                <h1 v-else>{{ $__("Edit circulation trigger") }}</h1>
+            </div>
+            <div class="modal-body">
+                <div class="page-section bg-info" v-if="circRules.length">
+                    <h2>{{ $__("Trigger context") }}</h2>
+                    <TriggerContext :ruleInfo="ruleInfo" />
+                    <h2>{{ $__("Existing rules") }}</h2>
+                    <p>{{ $__("Notice") }} {{ " " + newTriggerNumber - 1 }}</p>
+                    <TriggersTable
+                        :circRules="circRules"
+                        :triggerNumber="newTriggerNumber - 1"
+                        :modal="true"
+                        :ruleBeingEdited="ruleBeingEdited"
+                    />
+                </div>
+                <fieldset class="rows">
+                    <ol>
+                        <li>
+                            <label for="library_id" class="required"
+                                >{{ $__("Library") }}:</label
+                            >
+                            <v-select
+                                id="library_id"
+                                v-model="circRuleTrigger.library_id"
+                                label="name"
+                                :reduce="lib => lib.library_id"
+                                :options="libraries"
+                                @update:modelValue="handleContextChange($event)"
+                                :disabled="editMode ? true : false"
+                            >
+                                <template #search="{ attributes, events }">
+                                    <input
+                                        :required="!circRuleTrigger.library_id"
+                                        class="vs__search"
+                                        v-bind="attributes"
+                                        v-on="events"
+                                    />
+                                </template>
+                            </v-select>
+                            <span class="required">{{ $__("Required") }}</span>
+                        </li>
+                        <li>
+                            <label for="patron_category_id" class="required"
+                                >{{ $__("Patron category") }}:</label
+                            >
+                            <v-select
+                                id="patron_category_id"
+                                v-model="circRuleTrigger.patron_category_id"
+                                label="name"
+                                :reduce="cat => cat.patron_category_id"
+                                :options="categories"
+                                @update:modelValue="handleContextChange($event)"
+                                :disabled="editMode ? true : false"
+                            >
+                                <template #search="{ attributes, events }">
+                                    <input
+                                        :required="
+                                            !circRuleTrigger.patron_category_id
+                                        "
+                                        class="vs__search"
+                                        v-bind="attributes"
+                                        v-on="events"
+                                    />
+                                </template>
+                            </v-select>
+                            <span class="required">{{ $__("Required") }}</span>
+                        </li>
+                        <li>
+                            <label for="item_type_id" class="required"
+                                >{{ $__("Item type") }}:</label
+                            >
+                            <v-select
+                                id="item_type_id"
+                                v-model="circRuleTrigger.item_type_id"
+                                label="description"
+                                :reduce="type => type.item_type_id"
+                                :options="itemTypes"
+                                @update:modelValue="handleContextChange($event)"
+                                :disabled="editMode ? true : false"
+                            >
+                                <template #search="{ attributes, events }">
+                                    <input
+                                        :required="
+                                            !circRuleTrigger.item_type_id
+                                        "
+                                        class="vs__search"
+                                        v-bind="attributes"
+                                        v-on="events"
+                                    />
+                                </template>
+                            </v-select>
+                            <span class="required">{{ $__("Required") }}</span>
+                        </li>
+                        <li>
+                            <label for="overdue_delay"
+                                >{{ $__("Delay") }}:
+                            </label>
+                            <input
+                                id="overdue_delay"
+                                v-model="circRuleTrigger.delay"
+                                type="number"
+                            />
+                        </li>
+                        <li>
+                            <label for="letter_code"
+                                >{{ $__("Letter") }}:</label
+                            >
+                            <v-select
+                                id="letter_code"
+                                v-model="circRuleTrigger.notice"
+                                label="name"
+                                :reduce="type => type.code"
+                                :options="letters"
+                            >
+                                <template #search="{ attributes, events }">
+                                    <input
+                                        class="vs__search"
+                                        v-bind="attributes"
+                                        v-on="events"
+                                    />
+                                </template>
+                            </v-select>
+                        </li>
+                        <li>
+                            <label for="mtt"
+                                >{{ $__("Transport type(s)") }}:</label
+                            >
+                            <v-select
+                                id="mtt"
+                                v-model="circRuleTrigger.mtt"
+                                label="name"
+                                :reduce="type => type.code"
+                                :options="mtts"
+                                multiple
+                            >
+                                <template #search="{ attributes, events }">
+                                    <input
+                                        class="vs__search"
+                                        v-bind="attributes"
+                                        v-on="events"
+                                    />
+                                </template>
+                            </v-select>
+                        </li>
+                        <li>
+                            <label for="restricts"
+                                >{{ $__("Restricts checkouts") }}:</label
+                            >
+                            <input
+                                type="checkbox"
+                                id="restricts"
+                                :checked="false"
+                                true-value="1"
+                                false-value="0"
+                                v-model="circRuleTrigger.restrict"
+                            />
+                        </li>
+                    </ol>
+                </fieldset>
+            </div>
+            <div class="modal-footer">
+                <ButtonSubmit />
+                <router-link
+                    :to="{
+                        name: 'CirculationTriggersList',
+                    }"
+                    >Cancel</router-link
+                >
+            </div>
+        </form>
+    </div>
+    <div v-else>
+        <p>{{ $__("Loading...") }}</p>
+    </div>
+</template>
+
+<script>
+import { APIClient } from "../../../fetch/api-client.js";
+import TriggersTable from "./TriggersTable.vue";
+import { inject } from "vue";
+import { storeToRefs } from "pinia";
+import ButtonSubmit from "../../ButtonSubmit.vue";
+import TriggerContext from "./TriggerContext.vue";
+
+export default {
+    setup() {
+        const circRulesStore = inject("circRulesStore");
+        const { splitCircRulesByTriggerNumber } = circRulesStore;
+        const { letters } = storeToRefs(circRulesStore);
+
+        return {
+            splitCircRulesByTriggerNumber,
+            letters,
+        };
+    },
+    data() {
+        return {
+            initialized: false,
+            libraries: null,
+            categories: null,
+            itemTypes: null,
+            circRules: [],
+            circRuleTrigger: {
+                item_type_id: "*",
+                library_id: "*",
+                patron_category_id: "*",
+                delay: null,
+                notice: null,
+                mtt: null,
+                restrict: "0",
+            },
+            effectiveRule: {
+                item_type_id: "*",
+                library_id: "*",
+                patron_category_id: "*",
+                delay: null,
+                notice: null,
+                mtt: null,
+                restrict: "0",
+            },
+            newTriggerNumber: 1,
+            mtts: [
+                { code: "email", name: "Email" },
+                { code: "sms", name: "SMS" },
+                { code: "print", name: "Print" },
+            ],
+            ruleInfo: {
+                issuelength: null,
+                decreaseloanholds: null,
+                fine: null,
+                chargeperiod: null,
+                lengthunit: null,
+            },
+            editMode: false,
+            ruleBeingEdited: null,
+        };
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.getLibraries().then(() =>
+                vm.getCategories().then(() =>
+                    vm.getItemTypes().then(() => {
+                        const { query } = to;
+                        vm.checkForExistingRules(query).then(
+                            () => (vm.initialized = true)
+                        );
+                    })
+                )
+            );
+        });
+    },
+    methods: {
+        async addCircRule(e) {
+            e.preventDefault();
+
+            const context = {
+                library_id: this.circRuleTrigger.library_id || "*",
+                item_type_id: this.circRuleTrigger.item_type_id || "*",
+                patron_category_id:
+                    this.circRuleTrigger.patron_category_id || "*",
+            };
+
+            const delay =
+                this.circRuleTrigger.delay === this.effectiveRule.delay
+                    ? null
+                    : this.circRuleTrigger.delay;
+            const notice =
+                this.circRuleTrigger.notice === this.effectiveRule.notice
+                    ? null
+                    : this.circRuleTrigger.notice;
+            const restrict =
+                this.circRuleTrigger.restrict === this.effectiveRule.restrict
+                    ? null
+                    : this.circRuleTrigger.restrict;
+            const mtt =
+                this.circRuleTrigger.mtt.join(",") ===
+                this.effectiveRule.mtt.join(",")
+                    ? null
+                    : this.circRuleTrigger.mtt.join(",");
+
+            const circRule = {
+                context,
+            };
+            circRule[`overdue_${this.newTriggerNumber}_delay`] = delay;
+            circRule[`overdue_${this.newTriggerNumber}_notice`] = notice;
+            circRule[`overdue_${this.newTriggerNumber}_restrict`] = restrict;
+            circRule[`overdue_${this.newTriggerNumber}_mtt`] = mtt;
+
+            if (this.editMode) {
+                Object.keys(circRule).forEach(key => {
+                    if (key === "context") return;
+                    if (!circRule[key]) delete circRule[key];
+                });
+            }
+
+            const client = APIClient.circRule;
+            await client.circRules.update(circRule).then(
+                () => {
+                    this.$router
+                        .push({ name: "CirculationTriggersList" })
+                        .then(() => this.$router.go(0));
+                },
+                error => {}
+            );
+        },
+        async getLibraries() {
+            const client = APIClient.library;
+            await client.libraries.getAll().then(
+                libraries => {
+                    libraries.unshift({
+                        library_id: "*",
+                        name: "All libraries",
+                    });
+                    this.libraries = libraries;
+                },
+                error => {}
+            );
+        },
+        async getCategories() {
+            const client = APIClient.patron;
+            await client.patronCategories.getAll().then(
+                categories => {
+                    categories.unshift({
+                        patron_category_id: "*",
+                        name: "All categories",
+                    });
+                    this.categories = categories;
+                },
+                error => {}
+            );
+        },
+        async getItemTypes() {
+            const client = APIClient.item;
+            await client.itemTypes.getAll().then(
+                types => {
+                    types.unshift({
+                        item_type_id: "*",
+                        description: "All item types",
+                    });
+                    this.itemTypes = types;
+                },
+                error => {}
+            );
+        },
+        async handleContextChange() {
+            await this.checkForExistingRules();
+        },
+        async checkForExistingRules(routeParams) {
+            // We always pass library_id so we need to check for the existence of either item type or patron category
+            const editMode = routeParams && routeParams.triggerNumber;
+            if (editMode) {
+                this.editMode = editMode;
+                this.ruleBeingEdited = routeParams.triggerNumber;
+            }
+            const library_id =
+                routeParams && routeParams.library_id
+                    ? routeParams.library_id
+                    : this.circRuleTrigger.library_id || "*";
+            const item_type_id =
+                routeParams && routeParams.item_type_id
+                    ? routeParams.item_type_id
+                    : this.circRuleTrigger.item_type_id || "*";
+            const patron_category_id =
+                routeParams && routeParams.patron_category_id
+                    ? routeParams.patron_category_id
+                    : this.circRuleTrigger.patron_category_id || "*";
+            const params = {
+                library_id,
+                item_type_id,
+                patron_category_id,
+            };
+
+            const client = APIClient.circRule;
+            await client.circRules.getAll({}, params).then(
+                rules => {
+                    if (rules.length === 0) {
+                        this.newTriggerNumber = 1;
+                    } else {
+                        const { rulesPerTrigger } =
+                            this.splitCircRulesByTriggerNumber(rules);
+                        this.circRules = rulesPerTrigger;
+                        this.ruleInfo = {
+                            issuelength: rules[0].issuelength,
+                            decreaseloanholds: rules[0].decreaseloanholds,
+                            fine: rules[0].fine,
+                            chargeperiod: rules[0].chargeperiod,
+                            lengthunit: rules[0].lengthunit,
+                        };
+                    }
+                },
+                error => {}
+            );
+
+            params.effective = false;
+            await client.circRules.getAll({}, params).then(
+                rules => {
+                    if (rules.length === 0) {
+                        this.newTriggerNumber = 1;
+
+                        this.assignTriggerValues(
+                            "circRuleTrigger",
+                            this.circRules,
+                            this.newTriggerNumber,
+                            params
+                        );
+                        this.assignTriggerValues(
+                            "effectiveRule",
+                            this.circRules,
+                            this.newTriggerNumber,
+                            params
+                        );
+                    } else {
+                        const regex = /overdue_(\d+)_delay/g;
+                        const numberOfTriggers = Object.keys(rules[0]).filter(
+                            key => regex.test(key) && rules[0][key] !== null
+                        ).length;
+                        this.newTriggerNumber = editMode
+                            ? routeParams.triggerNumber
+                            : numberOfTriggers + 1;
+                        const { rulesPerTrigger } =
+                            this.splitCircRulesByTriggerNumber(rules);
+                        const triggerToDisplay = editMode
+                            ? routeParams.triggerNumber
+                            : numberOfTriggers;
+                        this.assignTriggerValues(
+                            "circRuleTrigger",
+                            rulesPerTrigger,
+                            triggerToDisplay
+                        );
+                        this.assignTriggerValues(
+                            "effectiveRule",
+                            rulesPerTrigger,
+                            triggerToDisplay
+                        );
+                    }
+                },
+                error => {}
+            );
+        },
+        assignTriggerValues(prop, rules, triggerNumber, context = null) {
+            this[prop] = {
+                item_type_id: context
+                    ? context.item_type_id
+                    : rules[triggerNumber - 1].context.item_type_id || "*",
+                library_id: context
+                    ? context.library_id
+                    : rules[triggerNumber - 1].context.library_id || "*",
+                patron_category_id: context
+                    ? context.patron_category_id
+                    : rules[triggerNumber - 1].context.patron_category_id ||
+                      "*",
+                delay: rules[triggerNumber - 1][
+                    `overdue_${triggerNumber}_delay`
+                ],
+                notice: rules[triggerNumber - 1][
+                    `overdue_${triggerNumber}_notice`
+                ],
+                mtt: rules[triggerNumber - 1][`overdue_${triggerNumber}_mtt`]
+                    ? rules[triggerNumber - 1][
+                          `overdue_${triggerNumber}_mtt`
+                      ].split(",")
+                    : [],
+                restrict:
+                    rules[triggerNumber - 1][
+                        `overdue_${triggerNumber}_restrict`
+                    ],
+            };
+        },
+    },
+    watch: {
+        $route: {
+            immediate: true,
+            handler: function (newVal, oldVal) {
+                if (
+                    oldVal &&
+                    oldVal.query.triggerNumber &&
+                    newVal.query.triggerNumber !== oldVal.query.triggerNumber
+                ) {
+                    this.$router.go(0);
+                }
+            },
+        },
+    },
+    components: { TriggersTable, ButtonSubmit, TriggerContext },
+};
+</script>
+
+<style scoped>
+.close_modal {
+    position: absolute;
+    top: 0.5rem;
+    right: 0.5rem;
+    font-size: 2rem;
+}
+
+form li {
+    display: flex;
+    align-items: center;
+}
+
+.dialog.alert
+    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
+        .bg-success
+    ):not(.bg-primary):not(.action),
+.dialog.error
+    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
+        .bg-success
+    ):not(.bg-primary):not(.action) {
+    margin: 0;
+    background-color: rgba(255, 255, 255, 1);
+}
+
+.router-link-active {
+    margin-left: 10px;
+}
+</style>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersList.vue
new file mode 100644
index 00000000000..06810ce0879
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersList.vue
@@ -0,0 +1,258 @@
+<template>
+    <Toolbar>
+        <ToolbarButton
+            :to="{
+                name: 'CirculationTriggersFormAdd',
+                query: { library_id: selectedLibrary },
+            }"
+            icon="plus"
+            :title="$__('Add new trigger')"
+        />
+    </Toolbar>
+    <div v-if="initialized">
+        <h1>Circulation triggers</h1>
+        <div class="page-section bg-info">
+            <p>
+                Rules are applied from most specific to less specific, using the
+                first found in this order:
+            </p>
+            <ul>
+                <li>same library, same patron category, same item type</li>
+                <li>same library, same patron category, all item types</li>
+                <li>same library, all patron categories, same item type</li>
+                <li>same library, all patron categories, all item types</li>
+                <li>
+                    default (all libraries), same patron category, same item
+                    type
+                </li>
+                <li>
+                    default (all libraries), same patron category, all item
+                    types
+                </li>
+                <li>
+                    default (all libraries), all patron categories, same item
+                    type
+                </li>
+                <li>
+                    default (all libraries), all patron categories, all item
+                    types
+                </li>
+            </ul>
+        </div>
+        <div class="page-section" v-if="initialized">
+            <label for="library_select">{{ $__("Select a library") }}:</label>
+            <v-select
+                id="library_select"
+                v-model="selectedLibrary"
+                label="name"
+                :reduce="lib => lib.library_id"
+                :options="libraries"
+                @update:modelValue="handleLibrarySelection($event)"
+            >
+                <template #search="{ attributes, events }">
+                    <input
+                        :required="!selectedLibrary"
+                        class="vs__search"
+                        v-bind="attributes"
+                        v-on="events"
+                    />
+                </template>
+            </v-select>
+        </div>
+    </div>
+    <div v-if="initialized">
+        <div id="circ_triggers_tabs" class="toptabs numbered">
+            <ul class="nav nav-tabs" role="tablist">
+                <li
+                    v-for="(number, i) in numberOfTabs"
+                    role="presentation"
+                    v-bind:class="
+                        tabSelected === `Notice ${number}` ? 'active' : ''
+                    "
+                    :key="`noticeTab${i}`"
+                >
+                    <a
+                        href="#"
+                        role="tab"
+                        @click="changeTabContent"
+                        :data-content="`Notice ${number}`"
+                        >{{ $__("Trigger") + " " + number }}</a
+                    >
+                </li>
+            </ul>
+        </div>
+        <div class="tab_content">
+            <template v-for="(number, i) in numberOfTabs">
+                <div
+                    v-if="tabSelected === `Notice ${number}`"
+                    :key="`noticeTabContent${i}`"
+                >
+                    <TriggersTable
+                        :circRules="circRules"
+                        :triggerNumber="number"
+                        :categories="categories"
+                        :itemTypes="itemTypes"
+                    />
+                </div>
+            </template>
+        </div>
+    </div>
+    <div v-if="showModal" class="modal_centered">
+        <div
+            class="modal-dialog modal-dialog-centered modal-lg"
+            role="document"
+        >
+            <router-view></router-view>
+        </div>
+    </div>
+</template>
+
+<script>
+import Toolbar from "../../Toolbar.vue";
+import ToolbarButton from "../../ToolbarButton.vue";
+import { APIClient } from "../../../fetch/api-client.js";
+import TriggersTable from "./TriggersTable.vue";
+import { inject } from "vue";
+
+export default {
+    setup() {
+        const { splitCircRulesByTriggerNumber } = inject("circRulesStore");
+
+        return {
+            splitCircRulesByTriggerNumber,
+        };
+    },
+    data() {
+        return {
+            initialized: false,
+            libraries: null,
+            selectedLibrary: "*",
+            circRules: null,
+            numberOfTabs: [1],
+            tabSelected: "Notice 1",
+            showModal: false,
+        };
+    },
+    beforeRouteEnter(to, from, next) {
+        next(vm => {
+            vm.getLibraries().then(() =>
+                vm
+                    .getCategories()
+                    .then(() =>
+                        vm
+                            .getItemTypes()
+                            .then(() =>
+                                vm
+                                    .getCircRules({}, true)
+                                    .then(() => (vm.initialized = true))
+                            )
+                    )
+            );
+        });
+    },
+    methods: {
+        async getLibraries() {
+            const libClient = APIClient.library;
+            await libClient.libraries.getAll().then(
+                libraries => {
+                    libraries.unshift({
+                        library_id: "*",
+                        name: "All libraries",
+                    });
+                    this.libraries = libraries;
+                },
+                error => {}
+            );
+        },
+        async getCategories() {
+            const client = APIClient.patron;
+            await client.patronCategories.getAll().then(
+                categories => {
+                    categories.unshift({
+                        patron_category_id: "*",
+                        name: "All",
+                    });
+                    this.categories = categories;
+                },
+                error => {}
+            );
+        },
+        async getItemTypes() {
+            const client = APIClient.item;
+            await client.itemTypes.getAll().then(
+                types => {
+                    types.unshift({
+                        item_type_id: "*",
+                        description: "All",
+                    });
+                    this.itemTypes = types;
+                },
+                error => {}
+            );
+        },
+        async getCircRules(params = {}, pageLoad) {
+            params.effective = false;
+            if (pageLoad) {
+                params.library_id = "*";
+            }
+            const client = APIClient.circRule;
+            await client.circRules.getAll({}, params).then(
+                rules => {
+                    const { numberOfTabs, rulesPerTrigger: circRules } =
+                        this.splitCircRulesByTriggerNumber(rules);
+                    this.numberOfTabs = numberOfTabs;
+                    this.circRules = circRules;
+                },
+                error => {}
+            );
+        },
+        async handleLibrarySelection(e) {
+            if (!e) e = "";
+            await this.getCircRules({ library_id: e });
+        },
+        changeTabContent(e) {
+            this.tabSelected = e.target.getAttribute("data-content");
+        },
+    },
+    watch: {
+        $route: {
+            immediate: true,
+            handler: function (newVal, oldVal) {
+                this.showModal = newVal.meta && newVal.meta.showModal;
+            },
+        },
+    },
+    components: { TriggersTable, Toolbar, ToolbarButton },
+};
+</script>
+
+<style scoped>
+.v-select {
+    display: inline-block;
+    background-color: white;
+    width: 30%;
+    margin-left: 10px;
+}
+.active {
+    cursor: pointer;
+}
+.toptabs {
+    margin-bottom: 0;
+}
+
+.modal_centered {
+    position: fixed;
+    z-index: 9998;
+    display: table;
+    transition: opacity 0.3s ease;
+    left: 0px;
+    top: 0px;
+    width: 100%;
+    height: 100%;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
+}
+.modal-dialog {
+    overflow: auto;
+    height: 90%;
+}
+</style>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggerContext.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggerContext.vue
new file mode 100644
index 00000000000..1e9704dedbe
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggerContext.vue
@@ -0,0 +1,50 @@
+<template>
+    <div class="page-section">
+        <table>
+            <thead>
+                <th>
+                    {{ $__("Loan period") }}
+                </th>
+                <th>
+                    {{ $__("Unit") }}
+                </th>
+                <th>
+                    {{ $__("Decreased loan period for high holds") }}
+                </th>
+                <th>
+                    {{ $__("Fine amount") }}
+                </th>
+                <th>
+                    {{ $__("Fine charging interval") }}
+                </th>
+            </thead>
+            <tbody>
+                <tr>
+                    <td>
+                        {{ ruleInfo.issuelength }}
+                    </td>
+                    <td>
+                        {{ ruleInfo.lengthunit }}
+                    </td>
+                    <td>
+                        {{ ruleInfo.decreaseloanholds }}
+                    </td>
+                    <td>
+                        {{ ruleInfo.fine }}
+                    </td>
+                    <td>
+                        {{ ruleInfo.chargeperiod }}
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</template>
+
+<script>
+export default {
+    props: ["ruleInfo"],
+};
+</script>
+
+<style></style>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue
new file mode 100644
index 00000000000..798aab18452
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue
@@ -0,0 +1,266 @@
+<template>
+    <div class="page-section">
+        <table>
+            <thead>
+                <th v-if="!modal">
+                    {{ $__("Patron category") }}
+                </th>
+                <th v-if="!modal">
+                    {{ $__("Item type") }}
+                </th>
+                <th v-if="modal">
+                    {{ $__("Notice") }}
+                </th>
+                <th>
+                    {{ $__("Delay") }}
+                </th>
+                <th>
+                    {{ $__("Notice") }}
+                </th>
+                <th>
+                    {{ $__("Email") }}
+                </th>
+                <th>
+                    {{ $__("Print") }}
+                </th>
+                <th>
+                    {{ $__("SMS") }}
+                </th>
+                <th>
+                    {{ $__("Restricts checkouts") }}
+                </th>
+                <th v-if="!modal">
+                    {{ $__("Actions") }}
+                </th>
+            </thead>
+            <tbody>
+                <tr
+                    v-for="(rule, i) in filterCircRulesByTabNumber(
+                        triggerNumber
+                    )"
+                    v-bind:key="'rule' + i"
+                >
+                    <td
+                        v-if="!modal"
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            handleContext(
+                                rule.context.patron_category_id,
+                                categories,
+                                "patron_category_id"
+                            )
+                        }}
+                    </td>
+                    <td
+                        v-if="!modal"
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            handleContext(
+                                rule.context.item_type_id,
+                                itemTypes,
+                                "item_type_id",
+                                "description"
+                            )
+                        }}
+                    </td>
+                    <td
+                        v-if="modal"
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{ i + 1 }}
+                    </td>
+                    <td
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            rule[
+                                "overdue_" +
+                                    (modal ? i + 1 : triggerNumber) +
+                                    "_delay"
+                            ]
+                                ? rule[
+                                      "overdue_" +
+                                          (modal ? i + 1 : triggerNumber) +
+                                          "_delay"
+                                  ] +
+                                  " " +
+                                  $__("days")
+                                : 0 + " " + $__("days")
+                        }}
+                    </td>
+                    <td
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            rule[
+                                "overdue_" +
+                                    (modal ? i + 1 : triggerNumber) +
+                                    "_notice"
+                            ]
+                        }}
+                    </td>
+                    <td
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            handleTransport(
+                                rule[
+                                    "overdue_" +
+                                        (modal ? i + 1 : triggerNumber) +
+                                        "_mtt"
+                                ],
+                                "email"
+                            )
+                        }}
+                    </td>
+                    <td
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            handleTransport(
+                                rule[
+                                    "overdue_" +
+                                        (modal ? i + 1 : triggerNumber) +
+                                        "_mtt"
+                                ],
+                                "print"
+                            )
+                        }}
+                    </td>
+                    <td
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            handleTransport(
+                                rule[
+                                    "overdue_" +
+                                        (modal ? i + 1 : triggerNumber) +
+                                        "_mtt"
+                                ],
+                                "sms"
+                            )
+                        }}
+                    </td>
+                    <td
+                        :class="
+                            modal && i + 1 === parseInt(ruleBeingEdited)
+                                ? 'selected_rule'
+                                : ''
+                        "
+                    >
+                        {{
+                            handleRestrictions(
+                                rule[
+                                    "overdue_" +
+                                        (modal ? i + 1 : triggerNumber) +
+                                        "_restrict"
+                                ]
+                            )
+                        }}
+                    </td>
+                    <td v-if="!modal" class="actions">
+                        <router-link
+                            :to="{
+                                name: 'CirculationTriggersFormEdit',
+                                query: {
+                                    library_id: rule.context.library_id,
+                                    item_type_id: rule.context.item_type_id,
+                                    patron_category_id:
+                                        rule.context.patron_category_id,
+                                    triggerNumber,
+                                },
+                            }"
+                            class="btn btn-default btn-xs"
+                            ><i class="fa-solid fa-pencil"></i>
+                            Edit</router-link
+                        >
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</template>
+
+<script>
+export default {
+    props: [
+        "circRules",
+        "triggerNumber",
+        "modal",
+        "ruleBeingEdited",
+        "categories",
+        "itemTypes",
+    ],
+    methods: {
+        handleContext(value, data, type, displayProperty = "name") {
+            const item = data.find(item => item[type] === value);
+            return item[displayProperty];
+        },
+        handleTransport(value, type) {
+            return value
+                ? value.includes(type)
+                    ? this.$__("Yes")
+                    : this.$__("No")
+                : this.$__("Default");
+        },
+        handleRestrictions(value) {
+            return value === "1" ? this.$__("Yes") : this.$__("No");
+        },
+        filterCircRulesByTabNumber(number) {
+            if (this.modal) return this.circRules;
+            return this.circRules.filter(
+                rule =>
+                    rule.triggerNumber === number &&
+                    (rule[`overdue_${number}_delay`] ||
+                        rule[`overdue_${number}_notice`] ||
+                        rule[`overdue_${number}_mtt`] ||
+                        rule[`overdue_${number}_restrict`])
+            );
+        },
+    },
+};
+</script>
+
+<style scoped>
+.selected_rule {
+    background-color: yellow !important;
+}
+
+.actions a {
+    margin-right: 5px;
+}
+</style>
-- 
2.48.1