View | Details | Raw Unified | Return to bug 10190
Collapse All | Expand All

(-)a/Koha/CirculationRules.pm (+4 lines)
Lines 190-195 our $RULE_KINDS = { Link Here
190
        scope        => [ 'branchcode', 'categorycode', 'itemtype' ],
190
        scope        => [ 'branchcode', 'categorycode', 'itemtype' ],
191
        can_be_blank => 0,
191
        can_be_blank => 0,
192
    },
192
    },
193
    overdue_X_has_rules => {
194
        scope        => [ 'branchcode', 'categorycode', 'itemtype' ],
195
        can_be_blank => 0,
196
    },
193
    renewalperiod => {
197
    renewalperiod => {
194
        scope => [ 'branchcode', 'categorycode', 'itemtype' ],
198
        scope => [ 'branchcode', 'categorycode', 'itemtype' ],
195
    },
199
    },
(-)a/admin/circulation_triggers.pl (+43 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2024 PTFS Europe Ltd
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use CGI        qw ( -utf8 );
23
use C4::Auth   qw( get_template_and_user );
24
use C4::Output qw( output_html_with_http_headers );
25
26
my $query = CGI->new;
27
28
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
29
    {
30
        template_name => "admin/circulation_triggers.tt",
31
        query         => $query,
32
        type          => "intranet",
33
    }
34
);
35
36
my $letters = Koha::Notice::Templates->search(
37
    { module  => "circulation", lang => 'default' },
38
    { columns => [ 'module', 'code', 'branchcode', 'name' ] }
39
)->unblessed;
40
41
$template->param( letters => $letters );
42
43
output_html_with_http_headers $query, $cookie, $template->output;
(-)a/debian/templates/apache-shared-intranet.conf (+1 lines)
Lines 27-32 RewriteRule ^/cgi-bin/koha/admin/record_sources(.*)?$ /cgi-bin/koha/admin/record Link Here
27
RewriteCond %{QUERY_STRING} booksellerid=(.*)
27
RewriteCond %{QUERY_STRING} booksellerid=(.*)
28
RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R]
28
RewriteRule ^/cgi-bin/koha/acqui/supplier.pl$ /cgi-bin/koha/acquisition/vendors/%1? [R]
29
RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT]
29
RewriteRule ^/cgi-bin/koha/acquisition/vendors(.*)?$ /cgi-bin/koha/acqui/vendors.pl$1 [PT]
30
RewriteRule ^/cgi-bin/koha/admin/circulation_triggers(.*)?$ /cgi-bin/koha/admin/circulation_triggers.pl$1 [PT]
30
31
31
Alias "/api" "/usr/share/koha/api"
32
Alias "/api" "/usr/share/koha/api"
32
<Directory "/usr/share/koha/api">
33
<Directory "/usr/share/koha/api">
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt (+4 lines)
Lines 96-101 Link Here
96
                        <dt><a href="/cgi-bin/koha/admin/smart-rules.pl">Circulation and fine rules</a></dt>
96
                        <dt><a href="/cgi-bin/koha/admin/smart-rules.pl">Circulation and fine rules</a></dt>
97
                        <dd>Define circulation and fine rules for combinations of libraries, patron categories, and item types</dd>
97
                        <dd>Define circulation and fine rules for combinations of libraries, patron categories, and item types</dd>
98
                    [% END %]
98
                    [% END %]
99
                    [% IF ( CAN_user_tools_edit_notice_status_triggers ) %]
100
                        <dt><a href="/cgi-bin/koha/admin/circulation_triggers">Circulation triggers</a></dt>
101
                        <dd>Define triggers for circulation notices</dd>
102
                    [% END %]
99
                    [% IF ( CAN_user_parameters_manage_patron_attributes ) %]
103
                    [% IF ( CAN_user_parameters_manage_patron_attributes ) %]
100
                        <dt><a href="/cgi-bin/koha/admin/patron-attr-types.pl">Patron attribute types</a></dt>
104
                        <dt><a href="/cgi-bin/koha/admin/patron-attr-types.pl">Patron attribute types</a></dt>
101
                        <dd>Define extended attributes (identifiers and statistical categories) for patron records</dd>
105
                        <dd>Define extended attributes (identifiers and statistical categories) for patron records</dd>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/circulation_triggers.tt (+43 lines)
Line 0 Link Here
1
[% USE raw %]
2
[% USE To %]
3
[% USE Asset %]
4
[% USE Branches %]
5
[% USE KohaDates %]
6
[% USE TablesSettings %]
7
[% USE AuthorisedValues %]
8
[% SET footerjs = 1 %]
9
[% PROCESS 'i18n.inc' %]
10
[% INCLUDE 'doc-head-open.inc' %]
11
<title
12
    >[% FILTER collapse %]
13
        [% t("Circulation triggers") | html %]
14
        &rsaquo; [% t("Administration") | html %] &rsaquo; [% t("Koha") | html %]
15
    [% END %]</title
16
>
17
[% INCLUDE 'doc-head-close.inc' %]
18
</head>
19
20
<body id="circulation_triggers" class="circulation_triggers">
21
[% WRAPPER 'header.inc' %]
22
    [% INCLUDE 'prefs-admin-search.inc' %]
23
[% END %]
24
25
<div id="__app">
26
    <!-- this is closed in intranet-bottom.inc -->
27
28
    [% MACRO jsinclude BLOCK %]
29
        [% INCLUDE 'calendar.inc' %]
30
        <!-- FIXME: this shouldn't be needed -->
31
        [% INCLUDE 'datatables.inc' %]
32
        [% INCLUDE 'js-patron-format.inc' %]
33
        [% INCLUDE 'js-date-format.inc' %]
34
        <script>
35
            const letters = [% To.json(letters) | $raw %];
36
            const default_view = "[%- IF Koha.Preference('DefaultToLoggedInLibraryOverdueTriggers') -%][% Branches.GetLoggedInBranchcode %][%- ELSE -%]*[%- END -%]";
37
            const from_branch = "[% Koha.Preference('OverdueNoticeFrom') %]";
38
        </script>
39
        [% Asset.js("js/vue/dist/admin/circulation_triggers.js") | $raw %]
40
    [% END %]
41
42
    [% INCLUDE 'intranet-bottom.inc' %]</div
43
>
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/circulation-rules-api-client.js (+36 lines)
Line 0 Link Here
1
export class CircRuleAPIClient {
2
    constructor(HttpClient) {
3
        this.httpClient = new HttpClient({
4
            baseURL: "/api/v1/circulation_rules",
5
        });
6
    }
7
8
    get circRules() {
9
        return {
10
            getAll: (query, params, headers) =>
11
                this.httpClient.getAll({
12
                    endpoint: "",
13
                    query,
14
                    params,
15
                    headers,
16
                }),
17
            update: rule =>
18
                this.httpClient.put({
19
                    endpoint: "",
20
                    body: rule,
21
                }),
22
            count: (query = {}) =>
23
                this.httpClient.count({
24
                    endpoint:
25
                        "?" +
26
                        new URLSearchParams({
27
                            _page: 1,
28
                            _per_page: 1,
29
                            ...(query && { q: JSON.stringify(query) }),
30
                        }),
31
                }),
32
        };
33
    }
34
}
35
36
export default CircRuleAPIClient;
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/item-api-client.js (+11 lines)
Lines 16-21 export class ItemAPIClient { Link Here
16
                }),
16
                }),
17
        };
17
        };
18
    }
18
    }
19
    get itemTypes() {
20
        return {
21
            getAll: (query, params, headers) =>
22
                this.httpClient.getAll({
23
                    endpoint: "item_types",
24
                    query,
25
                    params,
26
                    headers,
27
                }),
28
        };
29
    }
19
}
30
}
20
31
21
export default ItemAPIClient;
32
export default ItemAPIClient;
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/library-api-client.js (+25 lines)
Line 0 Link Here
1
export class LibraryAPIClient {
2
    constructor(HttpClient) {
3
        this.httpClient = new HttpClient({
4
            baseURL: "/api/v1/libraries",
5
        });
6
    }
7
8
    get libraries() {
9
        return {
10
            getAll: (query, params, headers) =>
11
                this.httpClient.getAll({
12
                    endpoint: "",
13
                    query,
14
                    params,
15
                    headers,
16
                }),
17
            get: id =>
18
                this.httpClient.get({
19
                    endpoint: `/${id}`,
20
                }),
21
        };
22
    }
23
}
24
25
export default LibraryAPIClient;
(-)a/koha-tmpl/intranet-tmpl/prog/js/fetch/patron-api-client.js (-2 / +14 lines)
Lines 1-7 Link Here
1
export class PatronAPIClient {
1
export class PatronAPIClient {
2
    constructor(HttpClient) {
2
    constructor(HttpClient) {
3
        this.httpClient = new HttpClient({
3
        this.httpClient = new HttpClient({
4
            baseURL: "/api/v1/patrons/",
4
            baseURL: "/api/v1/",
5
        });
5
        });
6
    }
6
    }
7
7
Lines 9-15 export class PatronAPIClient { Link Here
9
        return {
9
        return {
10
            get: id =>
10
            get: id =>
11
                this.httpClient.get({
11
                this.httpClient.get({
12
                    endpoint: id,
12
                    endpoint: "patrons/" + id,
13
                }),
14
        };
15
    }
16
17
    get patronCategories() {
18
        return {
19
            getAll: (query, params, headers) =>
20
                this.httpClient.getAll({
21
                    endpoint: "patron_categories",
22
                    query,
23
                    params,
24
                    headers,
13
                }),
25
                }),
14
        };
26
        };
15
    }
27
    }
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersForm.vue (+44 lines)
Line 0 Link Here
1
<template>
2
    <form
3
        id="circulation-trigger-form"
4
        @submit="submitAction($event)"
5
        class="modal-content"
6
    >
7
        <div class="modal-header">
8
            <h1 class="modal-title">
9
                {{ $__(formTitle) }}
10
            </h1>
11
            <router-link
12
                class="btn-close"
13
                type="button"
14
                :to="{
15
                    name: 'CirculationTriggersList',
16
                }"
17
            ></router-link>
18
        </div>
19
        <div class="modal-body">
20
            <slot></slot>
21
        </div>
22
        <div class="modal-footer">
23
            <ButtonSubmit text="Confirm" />
24
            <router-link
25
                :to="{
26
                    name: 'CirculationTriggersList',
27
                }"
28
                >{{ $__("Cancel") }}
29
            </router-link>
30
        </div>
31
    </form>
32
</template>
33
<script>
34
import ButtonSubmit from "../../ButtonSubmit.vue";
35
export default {
36
    props: ["submitAction", "formTitle"],
37
    components: { ButtonSubmit },
38
};
39
</script>
40
<style scoped>
41
#circulation-trigger-form {
42
    max-height: 90vh;
43
}
44
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue (+1008 lines)
Line 0 Link Here
1
<template>
2
    <form
3
        class="modal-content"
4
        id="circulation-trigger-form-add"
5
        @submit="addCircRule($event)"
6
    >
7
        <div class="modal-header">
8
            <h1 class="modal-title">
9
                {{ $__("Circulation Trigger Configuration") }}
10
            </h1>
11
            <router-link
12
                class="btn-close"
13
                type="button"
14
                :to="{
15
                    name: 'CirculationTriggersList',
16
                }"
17
            ></router-link>
18
        </div>
19
        <div class="modal-body">
20
            <div class="page-section bg-info" v-if="ruleSetInitialized">
21
                <h2>{{ $__("Circulation context") }}</h2>
22
                <TriggerContext :ruleSetInfo="ruleSetInfo" />
23
            </div>
24
            <div v-else-if="editMode !== 'confirmContext'">
25
                <p>{{ $__("Loading rule set information...") }}</p>
26
            </div>
27
            <fieldset class="rows" v-if="contextInitialized">
28
                <legend>{{ $__("Confirm trigger context") }}</legend>
29
                <ol>
30
                    <li>
31
                        <label for="library_id" class="required"
32
                            >{{ $__("Library") }}:</label
33
                        >
34
                        <v-select
35
                            id="library_id"
36
                            v-model="context.library_id"
37
                            label="name"
38
                            :reduce="lib => lib.library_id"
39
                            :options="libraries"
40
                            :disabled="editMode !== 'confirmContext'"
41
                        >
42
                            <template #search="{ attributes, events }">
43
                                <input
44
                                    :required="!context.library_id"
45
                                    class="vs__search"
46
                                    v-bind="attributes"
47
                                    v-on="events"
48
                                />
49
                            </template>
50
                        </v-select>
51
                        <span class="required">{{ $__("Required") }}</span>
52
                    </li>
53
                    <li>
54
                        <label for="patron_category_id" class="required"
55
                            >{{ $__("Patron category") }}:</label
56
                        >
57
                        <v-select
58
                            id="patron_category_id"
59
                            v-model="context.patron_category_id"
60
                            label="name"
61
                            :reduce="cat => cat.patron_category_id"
62
                            :options="patronCategories"
63
                            :disabled="editMode !== 'confirmContext'"
64
                        >
65
                            <template #search="{ attributes, events }">
66
                                <input
67
                                    :required="!context.patron_category_id"
68
                                    class="vs__search"
69
                                    v-bind="attributes"
70
                                    v-on="events"
71
                                />
72
                            </template>
73
                        </v-select>
74
                        <span class="required">{{ $__("Required") }}</span>
75
                    </li>
76
                    <li>
77
                        <label for="item_type_id" class="required"
78
                            >{{ $__("Item type") }}:</label
79
                        >
80
                        <v-select
81
                            id="item_type_id"
82
                            v-model="context.item_type_id"
83
                            label="description"
84
                            :reduce="type => type.item_type_id"
85
                            :options="itemTypes"
86
                            :disabled="editMode !== 'confirmContext'"
87
                        >
88
                            <template #search="{ attributes, events }">
89
                                <input
90
                                    :required="!context.item_type_id"
91
                                    class="vs__search"
92
                                    v-bind="attributes"
93
                                    v-on="events"
94
                                />
95
                            </template>
96
                        </v-select>
97
                        <span class="required">{{ $__("Required") }}</span>
98
                    </li>
99
                </ol>
100
                <div v-if="editMode === 'confirmContext'">
101
                    <router-link
102
                        :to="{
103
                            name: 'CirculationTriggersSelectOrAdd',
104
                            query: context,
105
                        }"
106
                        class="btn btn-default btn-xs"
107
                        ><i class="fa-solid fa-pencil"></i>
108
                        {{ $__("Confirm context") }}</router-link
109
                    >
110
                </div>
111
                <div
112
                    class="page-section"
113
                    v-if="ruleSetInitialized && editMode !== 'confirmContext'"
114
                >
115
                    <TriggersTable
116
                        :triggerNumber="triggerNumber"
117
                        :modal="true"
118
                        :actions="true"
119
                        :ruleSets="effectiveTriggerFilteredRuleSets"
120
                        :ruleSetBeingEdited="currentRuleSet"
121
                        :triggerBeingEdited="triggerBeingEdited"
122
                    />
123
                </div>
124
            </fieldset>
125
            <div v-else>
126
                <p>{{ $__("Loading context...") }}</p>
127
            </div>
128
            <fieldset class="rows" v-if="alertMessage">
129
                <div class="alert alert-info">{{ alertMessage }}</div>
130
            </fieldset>
131
            <fieldset
132
                class="rows"
133
                v-if="
134
                    (ruleSetInitialized && editMode === 'edit') ||
135
                    editMode === 'add'
136
                "
137
            >
138
                <legend v-if="editMode === 'add'">
139
                    {{ $__("Add new trigger") }}
140
                    {{ " " + triggerNumber }}
141
                </legend>
142
                <legend v-else>
143
                    {{ $__("Edit trigger") }} {{ " " + triggerNumber }}
144
                </legend>
145
                <div class="page-section bg-info">
146
                    <p>
147
                        {{
148
                            $__(
149
                                "NOTE: Delay for a given trigger can be pushed forward or backwards only within the bounds of what its two neighbouring triggers allows."
150
                            )
151
                        }}
152
                    </p>
153
                </div>
154
                <ol>
155
                    <li>
156
                        <label for="overdue_delay">{{ $__("Delay") }}: </label>
157
                        <div class="numeric-input-wrapper">
158
                            <div class="input-with-clear">
159
                                <input
160
                                    @input="setAllowSubmission"
161
                                    id="overdue_delay"
162
                                    v-model="
163
                                        ruleSetToSubmit[
164
                                            `overdue_${triggerNumber}_delay`
165
                                        ]
166
                                    "
167
                                    type="number"
168
                                    :placeholder="
169
                                        fallbackRuleSet?.[
170
                                            `overdue_${triggerNumber}_delay`
171
                                        ]
172
                                    "
173
                                    :min="minDelay"
174
                                    :max="maxDelay"
175
                                    class="numeric-input"
176
                                />
177
                                <button
178
                                    v-if="
179
                                        ruleSetToSubmit[
180
                                            `overdue_${triggerNumber}_delay`
181
                                        ] !== null &&
182
                                        ruleSetToSubmit[
183
                                            `overdue_${triggerNumber}_delay`
184
                                        ] !== undefined
185
                                    "
186
                                    type="button"
187
                                    class="clear-btn"
188
                                    @click="handleSetDelayToNull"
189
                                >
190
                                    <svg
191
                                        xmlns="http://www.w3.org/2000/svg"
192
                                        width="10"
193
                                        height="10"
194
                                    >
195
                                        <path
196
                                            d="M6.895455 5l2.842897-2.842898c.348864-.348863.348864-.914488 0-1.263636L9.106534.261648c-.348864-.348864-.914489-.348864-1.263636 0L5 3.104545 2.157102.261648c-.348863-.348864-.914488-.348864-1.263636 0L.261648.893466c-.348864.348864-.348864.914489 0 1.263636L3.104545 5 .261648 7.842898c-.348864.348863-.348864.914488 0 1.263636l.631818.631818c.348864.348864.914773.348864 1.263636 0L5 6.895455l2.842898 2.842897c.348863.348864.914772.348864 1.263636 0l.631818-.631818c.348864-.348864.348864-.914489 0-1.263636L6.895455 5z"
197
                                        ></path>
198
                                    </svg>
199
                                </button>
200
                                <div class="chevron-buttons">
201
                                    <button
202
                                        type="button"
203
                                        class="increment-btn"
204
                                        @click="incrementDelay"
205
                                    >
206
                                        â–´
207
                                    </button>
208
                                    <button
209
                                        type="button"
210
                                        class="decrement-btn"
211
                                        @click="decrementDelay"
212
                                    >
213
                                        â–¾
214
                                    </button>
215
                                </div>
216
                            </div>
217
                        </div>
218
                    </li>
219
                    <li>
220
                        <label for="restricts"
221
                            >{{ $__("Restricts checkouts") }}:</label
222
                        >
223
                        <div>
224
                            <input
225
                                @click="setAllowSubmission"
226
                                type="radio"
227
                                id="restricts-yes"
228
                                v-model="
229
                                    ruleSetToSubmit[
230
                                        `overdue_${triggerNumber}_restrict`
231
                                    ]
232
                                "
233
                                :value="1"
234
                            />
235
                            {{ $__("Yes") }}
236
                            <input
237
                                @click="setAllowSubmission"
238
                                type="radio"
239
                                id="restricts-no"
240
                                v-model="
241
                                    ruleSetToSubmit[
242
                                        `overdue_${triggerNumber}_restrict`
243
                                    ]
244
                                "
245
                                :value="0"
246
                            />
247
                            {{ $__("No") }}
248
                            <input
249
                                @click="setAllowSubmission"
250
                                type="radio"
251
                                id="restricts-fallback"
252
                                v-model="
253
                                    ruleSetToSubmit[
254
                                        `overdue_${triggerNumber}_restrict`
255
                                    ]
256
                                "
257
                                :value="null"
258
                            />
259
                            {{ $__("Fallback to default") }}
260
                            <span
261
                                v-if="
262
                                    fallbackRuleSet?.[
263
                                        `overdue_${triggerNumber}_restrict`
264
                                    ] !== null
265
                                "
266
                            >
267
                                ({{
268
                                    fallbackRuleSet?.[
269
                                        `overdue_${triggerNumber}_restrict`
270
                                    ] === "1"
271
                                        ? $__("Yes")
272
                                        : $__("No")
273
                                }})
274
                            </span>
275
                        </div>
276
                    </li>
277
                </ol>
278
            </fieldset>
279
            <div v-else-if="editMode === 'add' || editMode === 'edit'">
280
                <p>{{ $__("Loading circulation rules...") }}</p>
281
            </div>
282
            <fieldset
283
                class="rows"
284
                v-if="
285
                    (ruleSetInitialized && editMode === 'edit') ||
286
                    editMode === 'add'
287
                "
288
            >
289
                <legend v-if="ruleSetInfo.triggerCount < triggerNumber">
290
                    {{ $__("Notice for trigger") }}
291
                    {{ " " + triggerNumber }}
292
                </legend>
293
                <legend v-else>
294
                    {{ $__("Edit notice for trigger") }}
295
                    {{ " " + triggerNumber }}
296
                </legend>
297
                <ol>
298
                    <li>
299
                        <label for="letter_code">{{ $__("Letter") }}:</label>
300
                        <v-select
301
                            id="letter_code"
302
                            v-model="
303
                                ruleSetToSubmit[
304
                                    `overdue_${triggerNumber}_notice`
305
                                ]
306
                            "
307
                            label="name"
308
                            :reduce="type => type.code"
309
                            :options="filteredLetters"
310
                            @update:modelValue="setAllowSubmission"
311
                        >
312
                            <template #search="{ attributes, events }">
313
                                <input
314
                                    class="vs__search"
315
                                    v-bind="attributes"
316
                                    v-on="events"
317
                                    :placeholder="
318
                                        ruleSetToSubmit[
319
                                            `overdue_${triggerNumber}_notice`
320
                                        ] === null ||
321
                                        ruleSetToSubmit[
322
                                            `overdue_${triggerNumber}_notice`
323
                                        ] === undefined
324
                                            ? letters.find(
325
                                                  letter =>
326
                                                      letter.code ===
327
                                                      fallbackRuleSet?.[
328
                                                          `overdue_${triggerNumber}_notice`
329
                                                      ]
330
                                              )?.name ||
331
                                              fallbackRuleSet?.[
332
                                                  `overdue_${triggerNumber}_notice`
333
                                              ]
334
                                            : ''
335
                                    "
336
                                />
337
                            </template>
338
                        </v-select>
339
                    </li>
340
                    <li
341
                        v-if="
342
                            ruleSetToSubmit[
343
                                `overdue_${triggerNumber}_notice`
344
                            ] !== '' ||
345
                            ((ruleSetToSubmit[
346
                                `overdue_${triggerNumber}_notice`
347
                            ] === null ||
348
                                ruleSetToSubmit[
349
                                    `overdue_${triggerNumber}_notice`
350
                                ] === undefined) &&
351
                                fallbackRuleSet?.[
352
                                    `overdue_${triggerNumber}_notice`
353
                                ] !== '')
354
                        "
355
                    >
356
                        <label for="mtt">{{ $__("Transport type(s)") }}:</label>
357
                        <v-select
358
                            id="mtt"
359
                            v-model="
360
                                ruleSetToSubmit[`overdue_${triggerNumber}_mtt`]
361
                            "
362
                            label="name"
363
                            :reduce="type => type.code"
364
                            :options="transportTypes"
365
                            multiple
366
                            :required="true"
367
                            @update:modelValue="setAllowSubmission"
368
                        >
369
                            <template #search="{ attributes, events }">
370
                                <input
371
                                    class="vs__search"
372
                                    v-bind="attributes"
373
                                    v-on="events"
374
                                    :placeholder="
375
                                        ruleSetToSubmit[
376
                                            `overdue_${triggerNumber}_mtt`
377
                                        ] === null ||
378
                                        ruleSetToSubmit[
379
                                            `overdue_${triggerNumber}_mtt`
380
                                        ] === undefined ||
381
                                        ruleSetToSubmit[
382
                                            `overdue_${triggerNumber}_mtt`
383
                                        ].length === 0
384
                                            ? fallbackRuleSet?.[
385
                                                  `overdue_${triggerNumber}_mtt`
386
                                              ]
387
                                            : ''
388
                                    "
389
                                />
390
                            </template>
391
                        </v-select>
392
                    </li>
393
                </ol>
394
            </fieldset>
395
            <div v-else-if="editMode === 'add' || editMode === 'edit'">
396
                <p>{{ $__("Loading circulation rules...") }}</p>
397
            </div>
398
        </div>
399
        <div class="modal-footer">
400
            <ButtonSubmit
401
                v-if="editMode === 'edit' || editMode === 'add'"
402
                :disabled="!allowSubmission"
403
            />
404
            <router-link
405
                :to="{
406
                    name: 'CirculationTriggersList',
407
                }"
408
                >{{ $__("Cancel") }}</router-link
409
            >
410
        </div>
411
    </form>
412
</template>
413
414
<script>
415
import TriggersTable from "./TriggersTable.vue";
416
import { inject } from "vue";
417
import { storeToRefs } from "pinia";
418
import ButtonSubmit from "../../ButtonSubmit.vue";
419
import TriggerContext from "./TriggerContext.vue";
420
import { isEqual, cloneDeep } from "lodash";
421
422
export default {
423
    setup() {
424
        const circRulesStore = inject("circRulesStore");
425
        const {
426
            updateTriggerCount,
427
            findEffectiveRule,
428
            getSelectedRuleSet,
429
            setEffectiveTriggerFilteredRuleSet,
430
            updateCircRuleSets,
431
            hasConflict,
432
        } = circRulesStore;
433
        const {
434
            letters,
435
            libraries,
436
            itemTypes,
437
            transportTypes,
438
            patronCategories,
439
            triggerCounts,
440
            storeInitialized,
441
        } = storeToRefs(circRulesStore);
442
443
        return {
444
            letters,
445
            itemTypes,
446
            libraries,
447
            transportTypes,
448
            triggerCounts,
449
            patronCategories,
450
            getSelectedRuleSet,
451
            updateTriggerCount,
452
            findEffectiveRule,
453
            setEffectiveTriggerFilteredRuleSet,
454
            updateCircRuleSets,
455
            hasConflict,
456
            storeInitialized,
457
        };
458
    },
459
    data() {
460
        return {
461
            triggerNumber: 1,
462
            context: {
463
                library_id: "*",
464
                item_type_id: "*",
465
                patron_category_id: "*",
466
            },
467
            fallbackRuleSet: null,
468
            ruleSetInfo: {
469
                issuelength: null,
470
                decreaseloanholds: null,
471
                fine: null,
472
                chargeperiod: null,
473
                lengthunit: null,
474
                triggerCount: null,
475
            },
476
            editMode: false,
477
            ruleSetToSubmit: null,
478
            currentRuleSet: null,
479
            triggerBeingEdited: null,
480
            minDelay: 0,
481
            maxDelay: Infinity,
482
            filteredLetters: [],
483
            alertMessage: null,
484
            allowSubmission: false,
485
            effectiveTriggerFilteredRuleSets: [],
486
            contextInitialized: false,
487
            ruleSetInitialized: false,
488
        };
489
    },
490
    beforeMount() {
491
        // handle hard refresh mid-stepper workflow by ensuring store is initialized
492
        if (!this.storeInitialized) {
493
            this.$watch("storeInitialized", newVal => {
494
                if (newVal) {
495
                    this.initializeComponent();
496
                }
497
            });
498
            return;
499
        }
500
        this.initializeComponent();
501
    },
502
    beforeRouteEnter(to, from, next) {
503
        if (!from.name) {
504
            next();
505
            return;
506
        }
507
        next(vm => vm.initializeComponent());
508
    },
509
    methods: {
510
        initializeComponent() {
511
            const { query } = this.$route;
512
            this.setEditMode();
513
            this.setContext(query);
514
            this.contextInitialized = true;
515
            this.setTriggerNumber(query.triggerNumber, this.context.library_id);
516
            if (
517
                ["selectOrAdd", "add", "edit"].some(str =>
518
                    this.$route.fullPath.includes(str)
519
                )
520
            ) {
521
                this.setRuleSets();
522
            }
523
        },
524
        async addCircRule(e) {
525
            e.preventDefault();
526
527
            const ruleSetToSubmit = {
528
                context: this.context,
529
            };
530
531
            // ensure that no property is set to null as this would delete the rule!
532
            if (
533
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] !==
534
                    null &&
535
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] !==
536
                    this.fallbackRuleSet[`overdue_${this.triggerNumber}_delay`]
537
            ) {
538
                ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] =
539
                    cloneDeep(
540
                        this.ruleSetToSubmit[
541
                            `overdue_${this.triggerNumber}_delay`
542
                        ]
543
                    );
544
            }
545
546
            if (
547
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_notice`] !==
548
                    null &&
549
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_notice`] !==
550
                    this.fallbackRuleSet[`overdue_${this.triggerNumber}_notice`]
551
            ) {
552
                ruleSetToSubmit[`overdue_${this.triggerNumber}_notice`] =
553
                    cloneDeep(
554
                        this.ruleSetToSubmit[
555
                            `overdue_${this.triggerNumber}_notice`
556
                        ]
557
                    );
558
            }
559
560
            if (
561
                this.ruleSetToSubmit[
562
                    `overdue_${this.triggerNumber}_restrict`
563
                ] !== null &&
564
                this.ruleSetToSubmit[
565
                    `overdue_${this.triggerNumber}_restrict`
566
                ] !==
567
                    this.fallbackRuleSet[
568
                        `overdue_${this.triggerNumber}_restrict`
569
                    ]
570
            ) {
571
                ruleSetToSubmit[`overdue_${this.triggerNumber}_restrict`] =
572
                    cloneDeep(
573
                        this.ruleSetToSubmit[
574
                            `overdue_${this.triggerNumber}_restrict`
575
                        ]
576
                    );
577
            }
578
579
            if (
580
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_mtt`] !==
581
                    null &&
582
                Array.isArray(
583
                    this.ruleSetToSubmit[`overdue_${this.triggerNumber}_mtt`]
584
                ) &&
585
                !isEqual(
586
                    this.ruleSetToSubmit[`overdue_${this.triggerNumber}_mtt`],
587
                    this.fallbackRuleSet[`overdue_${this.triggerNumber}_mtt`]
588
                )
589
            ) {
590
                ruleSetToSubmit[`overdue_${this.triggerNumber}_mtt`] =
591
                    cloneDeep(
592
                        this.ruleSetToSubmit[
593
                            `overdue_${this.triggerNumber}_mtt`
594
                        ].join(",")
595
                    );
596
            }
597
598
            ruleSetToSubmit[`overdue_${this.triggerNumber}_has_rules`] = true;
599
600
            // in edit mode, check for changes from elsewhere to the rule set being edited
601
            if (this.editMode === "edit") {
602
                const ruleSetInDb = await this.getSelectedRuleSet(this.context);
603
                if (
604
                    this.hasConflict(
605
                        ruleSetInDb,
606
                        this.currentRuleSet,
607
                        this.triggerNumber
608
                    )
609
                ) {
610
                    this.alertMessage =
611
                        "Your changes could not be saved as this circulation trigger was updated elsewhere. Please see the updated trigger below.";
612
                    this.$router.push({
613
                        path: "/cgi-bin/koha/admin/circulation_triggers/edit",
614
                        query: {
615
                            library_id: this.context.library_id,
616
                            patron_category_id: this.context.patron_category_id,
617
                            item_type_id: this.context.item_type_id,
618
                            triggerNumber: this.triggerNumber,
619
                        },
620
                    });
621
                    return;
622
                }
623
            }
624
625
            await this.updateCircRuleSets(ruleSetToSubmit, this.triggerNumber);
626
            await this.$router.push({
627
                name: "CirculationTriggersList",
628
                query: { refresh: Date.now() },
629
            });
630
        },
631
        async setRuleSets() {
632
            this.updateTriggerCount();
633
            await this.setCurrentRuleSet();
634
635
            this.ruleSetToSubmit = cloneDeep(this.currentRuleSet);
636
637
            this.setMinDelay();
638
            if (this.ruleSetToSubmit === null) {
639
                this.ruleSetToSubmit = {
640
                    context: this.context,
641
                    [`overdue_${this.triggerNumber}_delay`]: this.minDelay,
642
                    [`overdue_${this.triggerNumber}_notice`]: null,
643
                    [`overdue_${this.triggerNumber}_mtt`]: null,
644
                    [`overdue_${this.triggerNumber}_restrict`]: null,
645
                };
646
            }
647
            this.setMaxDelay();
648
            this.setRuleSetInfo();
649
            this.effectiveTriggerFilteredRuleSets =
650
                this.setEffectiveTriggerFilteredRuleSet(this.context);
651
            this.setFallbackRuleSet();
652
            this.setFilteredLetters();
653
            this.setAllowSubmission();
654
            this.ruleSetInitialized = true;
655
        },
656
        setContext(query) {
657
            this.context.library_id = query.library_id ?? "*";
658
            this.context.item_type_id = query.item_type_id ?? "*";
659
            this.context.patron_category_id = query.patron_category_id ?? "*";
660
        },
661
        setEditMode() {
662
            this.editMode = this.$route.path.substring(
663
                this.$route.path.lastIndexOf("/") + 1
664
            );
665
        },
666
        setRuleSetInfo() {
667
            this.ruleSetInfo = {
668
                issuelength: this.currentRuleSet.issuelength,
669
                decreaseloanholds: this.currentRuleSet.decreaseloanholds,
670
                fine: this.currentRuleSet.fine,
671
                chargeperiod: this.currentRuleSet.chargeperiod,
672
                lengthunit: this.currentRuleSet.lengthunit,
673
                triggerCount: this.triggerCounts[this.context.library_id],
674
            };
675
        },
676
        async setCurrentRuleSet() {
677
            this.currentRuleSet = await this.getSelectedRuleSet(
678
                this.context,
679
                true
680
            );
681
            // override the context as fetched from the DBto ensure it matches the selected context
682
            // FIXME: consider passing the context to trigger table separately
683
            this.currentRuleSet.context = this.context;
684
        },
685
        setTriggerNumber(triggerNumber, library_id) {
686
            this.triggerNumber =
687
                this.editMode === "edit"
688
                    ? triggerNumber
689
                    : this.triggerCounts[library_id] + 1;
690
        },
691
        setFallbackRuleSet() {
692
            this.fallbackRuleSet = {
693
                [`overdue_${this.triggerNumber}_delay`]: this.findEffectiveRule(
694
                    this.context,
695
                    "delay",
696
                    i
697
                ).value,
698
            };
699
700
            if (this.editMode === "add") {
701
                return;
702
            }
703
            this.fallbackRuleSet = {
704
                [`overdue_${this.triggerNumber}_delay`]: this.findEffectiveRule(
705
                    this.context,
706
                    "delay",
707
                    this.triggerNumber
708
                ).value,
709
                [`overdue_${this.triggerNumber}_notice`]:
710
                    this.findEffectiveRule(
711
                        this.context,
712
                        "notice",
713
                        this.triggerNumber
714
                    ).value,
715
                [`overdue_${this.triggerNumber}_mtt`]: this.findEffectiveRule(
716
                    this.context,
717
                    "mtt",
718
                    this.triggerNumber
719
                ).value,
720
                [`overdue_${this.triggerNumber}_restrict`]:
721
                    this.findEffectiveRule(
722
                        this.context,
723
                        "restrict",
724
                        this.triggerNumber
725
                    ).value,
726
            };
727
        },
728
        setMinDelay() {
729
            if (
730
                this.ruleSetToSubmit === null ||
731
                this.triggerNumber === 0 ||
732
                this.triggerNumber === 1
733
            ) {
734
                this.minDelay = 0;
735
                return;
736
            }
737
            const priorTriggerNumber = this.triggerNumber - 1;
738
            this.minDelay = this.currentRuleSet[
739
                `overdue_${priorTriggerNumber}_delay`
740
            ]
741
                ? parseInt(
742
                      this.currentRuleSet[`overdue_${priorTriggerNumber}_delay`]
743
                  ) + 1
744
                : 0;
745
            this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] =
746
                this.minDelay;
747
        },
748
        setMaxDelay() {
749
            const nextTriggerNumber = parseInt(this.triggerNumber) + 1;
750
            this.maxDelay = this.currentRuleSet?.[
751
                `overdue_${nextTriggerNumber}_delay`
752
            ]
753
                ? parseInt(
754
                      this.currentRuleSet[`overdue_${nextTriggerNumber}_delay`]
755
                  ) - 1
756
                : Infinity;
757
        },
758
        setFilteredLetters() {
759
            let library = this.context.library_id;
760
            const branchcodeMatches = letters.filter(
761
                letter => letter.branchcode === library
762
            );
763
            const emptyBranchcodeMatches = letters.filter(
764
                letter => letter.branchcode === ""
765
            );
766
767
            const uniqueCodes = [
768
                ...new Set(
769
                    [...branchcodeMatches, ...emptyBranchcodeMatches].map(
770
                        letter => letter.code
771
                    )
772
                ),
773
            ];
774
775
            this.filteredLetters = letters.filter(
776
                letter =>
777
                    uniqueCodes.includes(letter.code) &&
778
                    (letter.branchcode === library || letter.branchcode === "")
779
            );
780
        },
781
        incrementDelay() {
782
            // Check for minDelay and maxDelay
783
            const min = this.minDelay !== undefined ? this.minDelay : 1;
784
            const max = this.maxDelay !== undefined ? this.maxDelay : Infinity;
785
786
            // Set to minDelay if it's null or undefined
787
            if (
788
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] ===
789
                    undefined ||
790
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] ===
791
                    null
792
            ) {
793
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] =
794
                    min;
795
            }
796
797
            // Increment within the valid range
798
            else {
799
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] =
800
                    Math.min(
801
                        this.ruleSetToSubmit[
802
                            `overdue_${this.triggerNumber}_delay`
803
                        ] + 1,
804
                        max
805
                    );
806
            }
807
            this.setAllowSubmission();
808
        },
809
        decrementDelay() {
810
            // Check for minDelay
811
            const min = this.minDelay !== undefined ? this.minDelay : 1;
812
            // Decrement only if greater than minDelay
813
            if (
814
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] >
815
                min
816
            ) {
817
                this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`]--;
818
            }
819
            this.setAllowSubmission();
820
        },
821
        setAllowSubmission() {
822
            // if notice is set to "", this translates to 'No letter', for which submisison is allowed, and mtt is redundant
823
            const noticeAllowSubmissionAndBypassMtt =
824
                this.ruleSetToSubmit?.[
825
                    `overdue_${this.triggerNumber}_notice`
826
                ] === "";
827
            if (noticeAllowSubmissionAndBypassMtt) {
828
                this.allowSubmission = true;
829
                return;
830
            }
831
832
            // check specificity required as 0 is a value that submission must be allowed for
833
            const delayAllowsSubmission =
834
                this.ruleSetToSubmit?.[`overdue_${this.triggerNumber}_delay`] !=
835
                null;
836
            if (delayAllowsSubmission) {
837
                this.allowSubmission = true;
838
                return;
839
            }
840
841
            // if notice is set, then ensure at least one transport has been set
842
            const noticeAllowSubmisison =
843
                this.ruleSetToSubmit?.[
844
                    `overdue_${this.triggerNumber}_notice`
845
                ] != null;
846
            const mttHasItems =
847
                this.ruleSetToSubmit?.[`overdue_${this.triggerNumber}_mtt`]
848
                    ?.length;
849
            if (noticeAllowSubmisison && mttHasItems) {
850
                this.allowSubmission = true;
851
                return;
852
            }
853
854
            const restrictAllowsSubmission =
855
                this.ruleSetToSubmit?.[
856
                    `overdue_${this.triggerNumber}_restrict`
857
                ] != null;
858
            if (restrictAllowsSubmission) {
859
                this.allowSubmission = true;
860
                return;
861
            }
862
            this.allowSubmission = false;
863
        },
864
        handleSetDelayToNull() {
865
            this.ruleSetToSubmit[`overdue_${this.triggerNumber}_delay`] = null;
866
            this.setAllowSubmission();
867
        },
868
    },
869
    watch: {
870
        $route: {
871
            immediate: true,
872
            handler: function (newVal, oldVal) {
873
                if (
874
                    oldVal &&
875
                    oldVal.query.triggerNumber &&
876
                    newVal.query.triggerNumber !== oldVal.query.triggerNumber
877
                ) {
878
                    this.$router.go(0);
879
                }
880
            },
881
        },
882
    },
883
    components: { TriggersTable, ButtonSubmit, TriggerContext },
884
};
885
</script>
886
887
<style scoped>
888
#circulation-trigger-form-add {
889
    max-height: 90vh;
890
}
891
892
form li {
893
    display: flex;
894
    align-items: center;
895
}
896
897
.numeric-input-wrapper {
898
    position: relative;
899
    display: inline-block;
900
    width: 30%;
901
}
902
903
.input-with-clear {
904
    position: relative;
905
    display: flex;
906
    align-items: center;
907
    width: 100%;
908
}
909
910
.numeric-input {
911
    padding-right: 40px; /* Adjust to leave space for clear button */
912
    padding-left: 0.25em;
913
    padding-top: 2px;
914
    padding-bottom: 2px;
915
    width: 100%;
916
    border-radius: 4px;
917
    border: 1px solid #ccc;
918
    font-size: 16px;
919
    box-sizing: border-box;
920
    transition: border-color 0.2s ease;
921
}
922
923
.clear-btn {
924
    position: absolute;
925
    right: 22px; /* Adjust positioning */
926
    fill: var(--vs-controls-color);
927
    background-color: transparent;
928
    border: 0;
929
    font-size: 1.2em;
930
    color: #333;
931
    cursor: pointer;
932
    z-index: 2; /* Ensure it is above the input */
933
}
934
935
.button:active:hover,
936
.clear-btn:active:hover {
937
    background-color: #d4d4d4;
938
    border-color: #8c8c8c;
939
}
940
941
/* Chevron buttons container */
942
.chevron-buttons {
943
    display: flex;
944
    flex-direction: column;
945
    position: absolute;
946
    right: 0px;
947
    top: 0;
948
    bottom: 0;
949
    width: 16px;
950
    padding: 0px 5px 0px 2px;
951
    justify-content: center;
952
    z-index: 2;
953
}
954
955
/* Chevron button styles */
956
.increment-btn,
957
.decrement-btn {
958
    background-color: transparent;
959
    border: 0px solid #ccc;
960
    font-size: 10px;
961
    padding: 0px;
962
    cursor: pointer;
963
    color: rgba(60, 60, 60, 0.5);
964
    border-radius: 2px;
965
}
966
967
.increment-btn:hover,
968
.decrement-btn:hover {
969
    background-color: #ddd;
970
}
971
972
/* Hide the native increment/decrement buttons */
973
input[type="number"]::-webkit-inner-spin-button,
974
input[type="number"]::-webkit-outer-spin-button {
975
    -webkit-appearance: none;
976
    margin: 0;
977
}
978
979
input[type="number"] {
980
    -moz-appearance: textfield; /* For Firefox */
981
}
982
983
.numeric-input:focus,
984
.numeric-input:hover {
985
    border-color: #007bff; /* Match focus color of v-select */
986
    outline: none;
987
}
988
989
.dialog.alert
990
    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
991
        .bg-success
992
    ):not(.bg-primary):not(.action),
993
.dialog.error
994
    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
995
        .bg-success
996
    ):not(.bg-primary):not(.action) {
997
    margin: 0;
998
    background-color: rgba(255, 255, 255, 1);
999
}
1000
1001
.router-link-active {
1002
    margin-left: 10px;
1003
}
1004
.modal-header {
1005
    display: flex;
1006
    justify-content: space-between;
1007
}
1008
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormConfirmReset.vue (+487 lines)
Line 0 Link Here
1
<template>
2
    <CirculationTriggersForm
3
        :submitAction="resetCircRules"
4
        formTitle="Confirm circulation rule set reset"
5
    >
6
        <fieldset class="rows">
7
            <div class="page-section bg-info">
8
                <p>
9
                    {{
10
                        $__(
11
                            "Resetting this rule set for the chosen context will have an impact on all the contexts that used to fall back on this rule set."
12
                        )
13
                    }}
14
                </p>
15
                <p>
16
                    {{
17
                        $__(
18
                            "To better understand which contexts will be affected, use 'Display all patron categories and items types.' on the circulations triggers page."
19
                        )
20
                    }}
21
                </p>
22
            </div>
23
            <legend>{{ $__("Trigger context") }}</legend>
24
            <ol v-if="initialized">
25
                <li>
26
                    <p>
27
                        <strong>{{ $__("Library") }}:</strong>
28
                    </p>
29
                    <p id="library_id">
30
                        {{ handleContext(library_id, libraries, "library_id") }}
31
                    </p>
32
                </li>
33
                <li>
34
                    <p>
35
                        <strong>{{ $__("Patron category") }}:</strong>
36
                    </p>
37
                    <p id="patron_category_id">
38
                        {{
39
                            handleContext(
40
                                patron_category_id,
41
                                patronCategories,
42
                                "patron_category_id"
43
                            )
44
                        }}
45
                    </p>
46
                </li>
47
                <li>
48
                    <p>
49
                        <strong>{{ $__("Item type") }}:</strong>
50
                    </p>
51
                    <p id="item_type_id">
52
                        {{
53
                            handleContext(
54
                                item_type_id,
55
                                itemTypes,
56
                                "item_type_id",
57
                                "description"
58
                            )
59
                        }}
60
                    </p>
61
                </li>
62
            </ol>
63
            <div v-else>
64
                <p>{{ $__("Loading circulation context...") }}</p>
65
            </div>
66
        </fieldset>
67
68
        <fieldset class="rows">
69
            <legend>
70
                {{ $__("Rules (overrides) which will be deleted") }}
71
            </legend>
72
            <table>
73
                <thead>
74
                    <th>
75
                        {{ $__("Delay") }}
76
                    </th>
77
                    <th>
78
                        {{ $__("Notice") }}
79
                    </th>
80
                    <th>
81
                        {{ $__("Email") }}
82
                    </th>
83
                    <th>
84
                        {{ $__("Print") }}
85
                    </th>
86
                    <th>
87
                        {{ $__("SMS") }}
88
                    </th>
89
                    <th>
90
                        {{ $__("Restricts checkouts") }}
91
                    </th>
92
                </thead>
93
                <tbody v-if="initialized">
94
                    <tr>
95
                        <!-- Delay -->
96
                        <td>
97
                            <span
98
                                v-if="
99
                                    effectiveRuleSet[
100
                                        `overdue_${triggerNumber}_delay`
101
                                    ]
102
                                "
103
                                :class="{
104
                                    fallback:
105
                                        effectiveRuleSet[
106
                                            `overdue_${triggerNumber}_delay`
107
                                        ].isFallback,
108
                                }"
109
                            >
110
                                {{
111
                                    effectiveRuleSet[
112
                                        `overdue_${triggerNumber}_delay`
113
                                    ].value
114
                                }}
115
                            </span>
116
                        </td>
117
118
                        <!--  Notice -->
119
                        <td>
120
                            <span
121
                                v-if="
122
                                    effectiveRuleSet[
123
                                        `overdue_${triggerNumber}_notice`
124
                                    ]
125
                                "
126
                                :class="{
127
                                    fallback:
128
                                        effectiveRuleSet[
129
                                            `overdue_${triggerNumber}_notice`
130
                                        ].isFallback,
131
                                }"
132
                            >
133
                                {{
134
                                    handleNotice(
135
                                        effectiveRuleSet[
136
                                            `overdue_${triggerNumber}_notice`
137
                                        ].value
138
                                    )
139
                                }}
140
                            </span>
141
                        </td>
142
                        <!-- Email -->
143
                        <td>
144
                            <span
145
                                v-if="
146
                                    effectiveRuleSet[
147
                                        `overdue_${triggerNumber}_mtt`
148
                                    ]?.length
149
                                "
150
                                :class="{
151
                                    fallback:
152
                                        effectiveRuleSet[
153
                                            `overdue_${triggerNumber}_mtt`
154
                                        ].isFallback,
155
                                }"
156
                            >
157
                                {{
158
                                    handleTransport(
159
                                        effectiveRuleSet[
160
                                            `overdue_${triggerNumber}_mtt`
161
                                        ].value,
162
                                        "email",
163
                                        !effectiveRuleSet[
164
                                            `overdue_${modal ? i + 1 : triggerNumber}_notice`
165
                                        ].value
166
                                    )
167
                                }}
168
                            </span>
169
                        </td>
170
                        <!-- Print -->
171
                        <td>
172
                            <span
173
                                v-if="
174
                                    effectiveRuleSet[
175
                                        `overdue_${triggerNumber}_mtt`
176
                                    ]?.length
177
                                "
178
                                :class="{
179
                                    fallback:
180
                                        effectiveRuleSet[
181
                                            `overdue_${triggerNumber}_mtt`
182
                                        ].isFallback,
183
                                }"
184
                            >
185
                                {{
186
                                    handleTransport(
187
                                        effectiveRuleSet[
188
                                            `overdue_${triggerNumber}_mtt`
189
                                        ].value,
190
                                        "print",
191
                                        !effectiveRuleSet[
192
                                            `overdue_${modal ? i + 1 : triggerNumber}_notice`
193
                                        ].value
194
                                    )
195
                                }}
196
                            </span>
197
                        </td>
198
                        <!-- SMS -->
199
                        <td>
200
                            <span
201
                                v-if="
202
                                    effectiveRuleSet[
203
                                        `overdue_${triggerNumber}_mtt`
204
                                    ]?.length
205
                                "
206
                                :class="{
207
                                    fallback:
208
                                        effectiveRuleSet[
209
                                            `overdue_${triggerNumber}_mtt`
210
                                        ].isFallback,
211
                                }"
212
                            >
213
                                {{
214
                                    handleTransport(
215
                                        effectiveRuleSet[
216
                                            `overdue_${triggerNumber}_mtt`
217
                                        ].value,
218
                                        !effectiveRuleSet[
219
                                            `overdue_${modal ? i + 1 : triggerNumber}_notice`
220
                                        ].value
221
                                    )
222
                                }}
223
                            </span>
224
                        </td>
225
                        <!-- Restricts Checkouts -->
226
                        <td>
227
                            <span
228
                                v-if="
229
                                    effectiveRuleSet[
230
                                        `overdue_${triggerNumber}_restrict`
231
                                    ]
232
                                "
233
                                :class="{
234
                                    fallback:
235
                                        effectiveRuleSet[
236
                                            `overdue_${triggerNumber}_restrict`
237
                                        ].isFallback,
238
                                }"
239
                            >
240
                                {{
241
                                    handleRestrictions(
242
                                        effectiveRuleSet[
243
                                            `overdue_${triggerNumber}_restrict`
244
                                        ].value
245
                                    )
246
                                }}
247
                            </span>
248
                        </td>
249
                    </tr>
250
                </tbody>
251
                <tbody v-else>
252
                    <tr>
253
                        {{
254
                            $__("Loading circulation rule set...")
255
                        }}
256
                    </tr>
257
                </tbody>
258
            </table>
259
        </fieldset>
260
261
        <fieldset class="rows" v-if="alertMessage">
262
            <div class="alert alert-info">{{ alertMessage }}</div>
263
        </fieldset>
264
    </CirculationTriggersForm>
265
</template>
266
267
<script>
268
import ButtonSubmit from "../../ButtonSubmit.vue";
269
import CirculationTriggersForm from "./CirculationTriggersForm.vue";
270
import TriggerContext from "./TriggerContext.vue";
271
import { inject } from "vue";
272
import { storeToRefs } from "pinia";
273
274
export default {
275
    setup() {
276
        const circRulesStore = inject("circRulesStore");
277
        const {
278
            handleContext,
279
            handleNotice,
280
            handleTransport,
281
            getSelectedRuleSet,
282
            formatTriggerSpecificRuleSetForDisplay,
283
            deleteRuleSet,
284
            handleRestrictions,
285
        } = circRulesStore;
286
        const { libraries, itemTypes, patronCategories } =
287
            storeToRefs(circRulesStore);
288
        return {
289
            libraries,
290
            itemTypes,
291
            patronCategories,
292
            handleContext,
293
            handleNotice,
294
            handleTransport,
295
            getSelectedRuleSet,
296
            formatTriggerSpecificRuleSetForDisplay,
297
            deleteRuleSet,
298
            handleRestrictions,
299
        };
300
    },
301
    data() {
302
        return {
303
            alertMessage: null,
304
            initialized: false,
305
            library_id: "*",
306
            item_type_id: "*",
307
            patron_category_id: "*",
308
            triggerNumber: null,
309
            ruleSet: null,
310
            currentRuleSet: null,
311
            fallbackRuleSet: null,
312
            effectiveRuleSet: null,
313
        };
314
    },
315
    beforeRouteEnter(to, from, next) {
316
        next(async vm => {
317
            vm.setContext(to.query);
318
            vm.currentRuleSet = await vm.getSelectedRuleSet({
319
                library_id: vm.library_id,
320
                patron_category_id: vm.patron_category_id,
321
                item_type_id: vm.item_type_id,
322
            });
323
            vm.effectiveRuleSet = vm.formatTriggerSpecificRuleSetForDisplay(
324
                vm.currentRuleSet.context,
325
                vm.triggerNumber,
326
                false
327
            );
328
            vm.initialized = true;
329
        });
330
    },
331
    methods: {
332
        async resetCircRules(e) {
333
            e.preventDefault();
334
            try {
335
                await this.deleteRuleSet(
336
                    this.currentRuleSet,
337
                    this.triggerNumber
338
                );
339
            } catch (e) {
340
                this.alertMessage = e;
341
                // reload the form components that have changed, remain in edit mode
342
                this.$router.push({
343
                    path: "/cgi-bin/koha/admin/circulation_triggers/reset",
344
                    query: {
345
                        ...this.currentRuleSet.context,
346
                        triggerNumber: triggerNumber,
347
                    },
348
                });
349
            }
350
            await this.$router.push({
351
                name: "CirculationTriggersList",
352
                query: { refresh: Date.now() },
353
            });
354
        },
355
        setContext(query) {
356
            this.library_id = query.library_id ?? "*";
357
            this.item_type_id = query.item_type_id ?? "*";
358
            this.patron_category_id = query.patron_category_id ?? "*";
359
            this.triggerNumber = query.triggerNumber;
360
        },
361
    },
362
    components: { ButtonSubmit, TriggerContext, CirculationTriggersForm },
363
};
364
</script>
365
366
<style scoped>
367
#circulation-trigger-form-confirm-reset {
368
    max-height: 90vh;
369
}
370
371
form li {
372
    display: flex;
373
    align-items: center;
374
}
375
376
.numeric-input-wrapper {
377
    position: relative;
378
    display: inline-block;
379
    width: 30%;
380
}
381
382
.input-with-clear {
383
    position: relative;
384
    display: flex;
385
    align-items: center;
386
    width: 100%;
387
}
388
389
.numeric-input {
390
    padding-right: 40px; /* Adjust to leave space for clear button */
391
    padding-left: 0.25em;
392
    padding-top: 2px;
393
    padding-bottom: 2px;
394
    width: 100%;
395
    border-radius: 4px;
396
    border: 1px solid #ccc;
397
    font-size: 16px;
398
    box-sizing: border-box;
399
    transition: border-color 0.2s ease;
400
}
401
402
.clear-btn {
403
    position: absolute;
404
    right: 22px; /* Adjust positioning */
405
    fill: var(--vs-controls-color);
406
    background-color: transparent;
407
    border: 0;
408
    font-size: 1.2em;
409
    color: #333;
410
    cursor: pointer;
411
    z-index: 2; /* Ensure it is above the input */
412
}
413
414
.button:active:hover,
415
.clear-btn:active:hover {
416
    background-color: #d4d4d4;
417
    border-color: #8c8c8c;
418
}
419
420
/* Chevron buttons container */
421
.chevron-buttons {
422
    display: flex;
423
    flex-direction: column;
424
    position: absolute;
425
    right: 0px;
426
    top: 0;
427
    bottom: 0;
428
    width: 16px;
429
    padding: 0px 5px 0px 2px;
430
    justify-content: center;
431
    z-index: 2;
432
}
433
434
/* Chevron button styles */
435
.increment-btn,
436
.decrement-btn {
437
    background-color: transparent;
438
    border: 0px solid #ccc;
439
    font-size: 10px;
440
    padding: 0px;
441
    cursor: pointer;
442
    color: rgba(60, 60, 60, 0.5);
443
    border-radius: 2px;
444
}
445
446
.increment-btn:hover,
447
.decrement-btn:hover {
448
    background-color: #ddd;
449
}
450
451
/* Hide the native increment/decrement buttons */
452
input[type="number"]::-webkit-inner-spin-button,
453
input[type="number"]::-webkit-outer-spin-button {
454
    -webkit-appearance: none;
455
    margin: 0;
456
}
457
458
input[type="number"] {
459
    -moz-appearance: textfield; /* For Firefox */
460
}
461
462
.numeric-input:focus,
463
.numeric-input:hover {
464
    border-color: #007bff; /* Match focus color of v-select */
465
    outline: none;
466
}
467
468
.dialog.alert
469
    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
470
        .bg-success
471
    ):not(.bg-primary):not(.action),
472
.dialog.error
473
    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
474
        .bg-success
475
    ):not(.bg-primary):not(.action) {
476
    margin: 0;
477
    background-color: rgba(255, 255, 255, 1);
478
}
479
480
.router-link-active {
481
    margin-left: 10px;
482
}
483
.modal-header {
484
    display: flex;
485
    justify-content: space-between;
486
}
487
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersFormConfirmTriggerDelete.vue (+257 lines)
Line 0 Link Here
1
<template>
2
    <CirculationTriggersForm
3
        :submitAction="deleteTrigger"
4
        formTitle="Confirm circulation trigger deletion"
5
    >
6
        <fieldset class="rows">
7
            <h2 v-if="initialized">
8
                {{
9
                    $__(
10
                        `Delete Trigger ${triggerNumber} for: ${handleContext(library_id, libraries, "library_id")}`
11
                    )
12
                }}
13
            </h2>
14
            <div v-else>
15
                <p>{{ $__("Loading library...") }}</p>
16
            </div>
17
        </fieldset>
18
        <fieldset class="rows">
19
            <TriggersTable
20
                :triggerNumber="triggerNumber"
21
                :modal="false"
22
                :actions="false"
23
                :ruleSets="formattedEffectiveRuleSets"
24
            />
25
        </fieldset>
26
        <fieldset class="rows">
27
            <div class="page-section bg-info">
28
                <p>
29
                    {{
30
                        $__(
31
                            "Deleting this trigger will delete every rule set listed above, and therefore completely remove this trigger for the library selected."
32
                        )
33
                    }}
34
                </p>
35
            </div>
36
            <div class="alert alert-info" v-if="alertMessage">
37
                {{ alertMessage }}
38
            </div>
39
        </fieldset>
40
    </CirculationTriggersForm>
41
</template>
42
<script>
43
import TriggersTable from "./TriggersTable.vue";
44
import TriggerContext from "./TriggerContext.vue";
45
import CirculationTriggersForm from "./CirculationTriggersForm.vue";
46
import { inject } from "vue";
47
import { storeToRefs } from "pinia";
48
49
export default {
50
    setup() {
51
        const circRulesStore = inject("circRulesStore");
52
        const { handleContext, findEffectiveRule, deleteRuleSet } =
53
            circRulesStore;
54
        const { libraries, allCurrentLibraryRawRuleSets, ruleSuffixes } =
55
            storeToRefs(circRulesStore);
56
        return {
57
            libraries,
58
            handleContext,
59
            findEffectiveRule,
60
            allCurrentLibraryRawRuleSets,
61
            ruleSuffixes,
62
            deleteRuleSet,
63
        };
64
    },
65
    data() {
66
        return {
67
            alertMessage: null,
68
            initialized: false,
69
            library_id: "*",
70
            triggerNumber: null,
71
            formattedEffectiveRuleSets: [],
72
        };
73
    },
74
    beforeRouteEnter(to, from, next) {
75
        next(async vm => {
76
            vm.setContext(to.query);
77
            vm.setFormattedEffectiveRuleSets();
78
            vm.initialized = true;
79
        });
80
    },
81
    methods: {
82
        async deleteTrigger(e) {
83
            e.preventDefault();
84
            for (const ruleSet of this.allCurrentLibraryRawRuleSets) {
85
                try {
86
                    await this.deleteRuleSet(ruleSet, this.triggerNumber);
87
                } catch (e) {
88
                    this.alertMessage = e;
89
                    this.$router.push({
90
                        path: "/cgi-bin/koha/admin/circulation_triggers/reset",
91
                        query: {
92
                            ...ruleSet.context,
93
                            triggerNumber: this.triggerNumber,
94
                        },
95
                    });
96
                }
97
            }
98
            await this.$router.push({
99
                name: "CirculationTriggersList",
100
                query: { refresh: Date.now() },
101
            });
102
        },
103
        setFormattedEffectiveRuleSets() {
104
            this.formattedEffectiveRuleSets = [];
105
            this.allCurrentLibraryRawRuleSets.forEach(ruleSet => {
106
                const effectiveRuleSet = {
107
                    context: { ...ruleSet.context },
108
                };
109
110
                if (
111
                    ruleSet[`overdue_${this.triggerNumber}_has_rules`] === null
112
                ) {
113
                    return;
114
                }
115
                this.ruleSuffixes.forEach(ruleSuffix => {
116
                    effectiveRuleSet[
117
                        `overdue_${this.triggerNumber}_${ruleSuffix}`
118
                    ] = this.findEffectiveRule(
119
                        ruleSet.context,
120
                        ruleSuffix,
121
                        this.triggerNumber
122
                    );
123
                });
124
                this.formattedEffectiveRuleSets.push(effectiveRuleSet);
125
            });
126
        },
127
        setContext(query) {
128
            this.library_id = query.library_id ?? "*";
129
            this.triggerNumber = query.triggerNumber;
130
        },
131
    },
132
    components: { TriggerContext, TriggersTable, CirculationTriggersForm },
133
};
134
</script>
135
136
<style scoped>
137
#circulation-trigger-form-confirm-trigger-delete {
138
    max-height: 90vh;
139
}
140
141
form li {
142
    display: flex;
143
    align-items: center;
144
}
145
146
.numeric-input-wrapper {
147
    position: relative;
148
    display: inline-block;
149
    width: 30%;
150
}
151
152
.input-with-clear {
153
    position: relative;
154
    display: flex;
155
    align-items: center;
156
    width: 100%;
157
}
158
159
.numeric-input {
160
    padding-right: 40px; /* Adjust to leave space for clear button */
161
    padding-left: 0.25em;
162
    padding-top: 2px;
163
    padding-bottom: 2px;
164
    width: 100%;
165
    border-radius: 4px;
166
    border: 1px solid #ccc;
167
    font-size: 16px;
168
    box-sizing: border-box;
169
    transition: border-color 0.2s ease;
170
}
171
172
.clear-btn {
173
    position: absolute;
174
    right: 22px; /* Adjust positioning */
175
    fill: var(--vs-controls-color);
176
    background-color: transparent;
177
    border: 0;
178
    font-size: 1.2em;
179
    color: #333;
180
    cursor: pointer;
181
    z-index: 2; /* Ensure it is above the input */
182
}
183
184
.button:active:hover,
185
.clear-btn:active:hover {
186
    background-color: #d4d4d4;
187
    border-color: #8c8c8c;
188
}
189
190
/* Chevron buttons container */
191
.chevron-buttons {
192
    display: flex;
193
    flex-direction: column;
194
    position: absolute;
195
    right: 0px;
196
    top: 0;
197
    bottom: 0;
198
    width: 16px;
199
    padding: 0px 5px 0px 2px;
200
    justify-content: center;
201
    z-index: 2;
202
}
203
204
/* Chevron button styles */
205
.increment-btn,
206
.decrement-btn {
207
    background-color: transparent;
208
    border: 0px solid #ccc;
209
    font-size: 10px;
210
    padding: 0px;
211
    cursor: pointer;
212
    color: rgba(60, 60, 60, 0.5);
213
    border-radius: 2px;
214
}
215
216
.increment-btn:hover,
217
.decrement-btn:hover {
218
    background-color: #ddd;
219
}
220
221
/* Hide the native increment/decrement buttons */
222
input[type="number"]::-webkit-inner-spin-button,
223
input[type="number"]::-webkit-outer-spin-button {
224
    -webkit-appearance: none;
225
    margin: 0;
226
}
227
228
input[type="number"] {
229
    -moz-appearance: textfield; /* For Firefox */
230
}
231
232
.numeric-input:focus,
233
.numeric-input:hover {
234
    border-color: #007bff; /* Match focus color of v-select */
235
    outline: none;
236
}
237
238
.dialog.alert
239
    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
240
        .bg-success
241
    ):not(.bg-primary):not(.action),
242
.dialog.error
243
    fieldset:not(.bg-danger):not(.bg-warning):not(.bg-info):not(
244
        .bg-success
245
    ):not(.bg-primary):not(.action) {
246
    margin: 0;
247
    background-color: rgba(255, 255, 255, 1);
248
}
249
250
.router-link-active {
251
    margin-left: 10px;
252
}
253
.modal-header {
254
    display: flex;
255
    justify-content: space-between;
256
}
257
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/CirculationTriggersList.vue (+503 lines)
Line 0 Link Here
1
<template>
2
    <Toolbar>
3
        <ToolbarButton
4
            :to="{
5
                name: 'CirculationTriggersFormConfirmContext',
6
                query: {
7
                    library_id: currentLibraryId,
8
                    patron_category_id: currentPatronCategoryId,
9
                    item_type_id: currentItemTypeId,
10
                },
11
            }"
12
            icon="plus"
13
            :title="$__('Add new trigger')"
14
        />
15
    </Toolbar>
16
    <div v-if="filtersInitialized">
17
        <h1>{{ $__("Circulation triggers") }}</h1>
18
        <div class="page-section bg-info">
19
            <p>
20
                {{
21
                    $__(
22
                        "Rules are applied from most specific to less specific, using the first found in this order"
23
                    )
24
                }}:
25
            </p>
26
            <ul>
27
                <li>
28
                    {{
29
                        $__(
30
                            "same library, same patron category, same item type"
31
                        )
32
                    }}
33
                </li>
34
                <li>
35
                    {{
36
                        $__(
37
                            "same library, same patron category, all item types"
38
                        )
39
                    }}
40
                </li>
41
                <li>
42
                    {{
43
                        $__(
44
                            "same library, all patron categories, same item type"
45
                        )
46
                    }}
47
                </li>
48
                <li>
49
                    {{
50
                        $__(
51
                            "same library, all patron categories, all item types"
52
                        )
53
                    }}
54
                </li>
55
                <li>
56
                    {{
57
                        $__(
58
                            "default (all libraries), same patron category, same item type"
59
                        )
60
                    }}
61
                </li>
62
                <li>
63
                    {{
64
                        $__(
65
                            "default (all libraries), same patron category, all item types"
66
                        )
67
                    }}
68
                </li>
69
                <li>
70
                    {{
71
                        $__(
72
                            "default (all libraries), all patron categories, same item type"
73
                        )
74
                    }}
75
                </li>
76
                <li>
77
                    {{
78
                        $__(
79
                            "default (all libraries), all patron categories, all item types"
80
                        )
81
                    }}
82
                </li>
83
            </ul>
84
            <p>
85
                {{
86
                    $__(
87
                        "The system is currently set to match based on the %s"
88
                    ).format(from_branch)
89
                }}
90
            </p>
91
        </div>
92
        <div class="page-section" v-if="filtersInitialized">
93
            <legend>
94
                Filter by
95
                <span style="color: blue; font-weight: bold">context</span>
96
            </legend>
97
            <table>
98
                <thead>
99
                    <tr>
100
                        <th>{{ $__("Library") }}</th>
101
                        <th>{{ $__("Category") }}</th>
102
                        <th>{{ $__("Item type") }}</th>
103
                    </tr>
104
                </thead>
105
                <tbody>
106
                    <tr>
107
                        <td>
108
                            <v-select
109
                                id="library_select"
110
                                v-model="currentLibraryId"
111
                                label="name"
112
                                :reduce="lib => lib.library_id"
113
                                :options="libraries"
114
                                @update:modelValue="
115
                                    filterRuleSetsbySearchParam()
116
                                "
117
                                :clearable="false"
118
                                placeholder="Default rules for all libraries"
119
                            >
120
                                <template #search="{ attributes, events }">
121
                                    <input
122
                                        :required="!currentLibraryId"
123
                                        class="vs__search"
124
                                        v-bind="attributes"
125
                                        v-on="events"
126
                                    />
127
                                </template>
128
                            </v-select>
129
                        </td>
130
                        <td>
131
                            <v-select
132
                                id="patron_category_select"
133
                                v-model="currentPatronCategoryId"
134
                                label="name"
135
                                :reduce="cat => cat.patron_category_id"
136
                                :options="patronCategories"
137
                                @update:modelValue="
138
                                    filterRuleSetsbySearchParam()
139
                                "
140
                                placeholder="any"
141
                            >
142
                                <template #search="{ attributes, events }">
143
                                    <input
144
                                        :required="!currentPatronCategoryId"
145
                                        class="vs__search"
146
                                        v-bind="attributes"
147
                                        v-on="events"
148
                                    />
149
                                </template>
150
                            </v-select>
151
                        </td>
152
                        <td>
153
                            <v-select
154
                                id="item_type_select"
155
                                v-model="currentItemTypeId"
156
                                label="description"
157
                                :reduce="itype => itype.item_type_id"
158
                                :options="itemTypes"
159
                                @update:modelValue="
160
                                    filterRuleSetsbySearchParam()
161
                                "
162
                                placeholder="any"
163
                            >
164
                                <template #search="{ attributes, events }">
165
                                    <input
166
                                        :required="!currentItemTypeId"
167
                                        class="vs__search"
168
                                        v-bind="attributes"
169
                                        v-on="events"
170
                                    />
171
                                </template>
172
                            </v-select>
173
                        </td>
174
                    </tr>
175
                </tbody>
176
            </table>
177
            <div class="toggle-view-all-applicable-wrapper">
178
                <label for="filter-rules">{{ $__("Display ") }}</label>
179
                <v-select
180
                    id="filter-rules"
181
                    v-model="displayAllApplicableRules"
182
                    :reduce="opt => opt.value"
183
                    :options="[
184
                        {
185
                            value: 0,
186
                            label: 'explictly set rules.',
187
                        },
188
                        {
189
                            value: 1,
190
                            label: 'all applied rules.',
191
                        },
192
                    ]"
193
                    @update:modelValue="filterRuleSetsbySearchParam()"
194
                >
195
                </v-select>
196
            </div>
197
        </div>
198
    </div>
199
    <div v-if="filtersInitialized && ruleSetInitialized">
200
        <div id="circ_triggers_tabs" class="toptabs numbered">
201
            <ul class="nav nav-tabs" role="tablist">
202
                <li
203
                    v-for="number in triggerCounts[currentLibraryId]"
204
                    class="nav-item"
205
                    role="presentation"
206
                    :key="`noticeTab_${number}`"
207
                >
208
                    <a
209
                        href="#"
210
                        class="nav-link"
211
                        role="tab"
212
                        v-bind:class="
213
                            tabSelected === `Notice ${number}` ? 'active' : ''
214
                        "
215
                        @click="changeTabContent"
216
                        :data-content="`Notice ${number}`"
217
                        >{{ $__("Trigger") + " " + number }}
218
                    </a>
219
                </li>
220
            </ul>
221
        </div>
222
        <div class="tab-content">
223
            <template v-for="number in triggerCounts[currentLibraryId]">
224
                <div
225
                    class="tab-pane"
226
                    role="tabpanel"
227
                    v-bind:class="
228
                        tabSelected === `Notice ${number}` ? 'show active' : ''
229
                    "
230
                    v-if="tabSelected === `Notice ${number}`"
231
                    :key="`noticeTabContent_${number}`"
232
                >
233
                    <div class="page-section">
234
                        <TriggersTable
235
                            :modal="false"
236
                            :actions="true"
237
                            :ruleSets="ruleSets"
238
                            :triggerNumber="number"
239
                        >
240
                            <router-link
241
                                v-if="isLastTrigger(number)"
242
                                :to="{
243
                                    name: 'CirculationTriggersFormConfirmTriggerDelete',
244
                                    query: {
245
                                        triggerNumber: number,
246
                                    },
247
                                }"
248
                                class="btn btn-primary"
249
                            >
250
                                <i class="fa-solid fa-xmark"></i>
251
                                {{ $__("Delete") }}
252
                            </router-link>
253
                            <div
254
                                :class="{
255
                                    'page-section bg-info': true,
256
                                    'inline-block': isLastTrigger(number),
257
                                }"
258
                            >
259
                                {{
260
                                    $__(
261
                                        "Bolid italic values denote fallback values where an override has not been set for the context."
262
                                    )
263
                                }}
264
                            </div>
265
                        </TriggersTable>
266
                    </div>
267
                </div>
268
            </template>
269
        </div>
270
    </div>
271
    <div v-if="showModal" class="modal" role="dialog">
272
        <div
273
            class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable"
274
            role="document"
275
        >
276
            <router-view></router-view>
277
        </div>
278
    </div>
279
</template>
280
281
<script>
282
import Toolbar from "../../Toolbar.vue";
283
import ToolbarButton from "../../ToolbarButton.vue";
284
import TriggersTable from "./TriggersTable.vue";
285
import { inject } from "vue";
286
import { storeToRefs } from "pinia";
287
288
export default {
289
    setup() {
290
        const circRulesStore = inject("circRulesStore");
291
        circRulesStore.init();
292
        const {
293
            updateTriggerCount,
294
            setAllRawRuleSets,
295
            setAllEffectiveRuleSets,
296
            setAllExhaustiveEffectiveRuleSets,
297
            isLastTrigger,
298
        } = circRulesStore;
299
        const {
300
            currentLibraryId,
301
            currentPatronCategoryId,
302
            currentItemTypeId,
303
            itemTypes,
304
            libraries,
305
            patronCategories,
306
            triggerCounts,
307
            allExhaustiveEffectiveRuleSets,
308
            allEffectiveRuleSets,
309
            storeInitialized,
310
        } = storeToRefs(circRulesStore);
311
312
        return {
313
            currentLibraryId,
314
            currentPatronCategoryId,
315
            currentItemTypeId,
316
            itemTypes,
317
            libraries,
318
            triggerCounts,
319
            patronCategories,
320
            updateTriggerCount,
321
            allExhaustiveEffectiveRuleSets,
322
            setAllRawRuleSets,
323
            setAllEffectiveRuleSets,
324
            setAllExhaustiveEffectiveRuleSets,
325
            allEffectiveRuleSets,
326
            isLastTrigger,
327
            storeInitialized,
328
            from_branch,
329
        };
330
    },
331
    data() {
332
        return {
333
            filtersInitialized: false,
334
            ruleSetInitialized: false,
335
            tabSelected: "Notice 1",
336
            showModal: false,
337
            displayAllApplicableRules: 1,
338
        };
339
    },
340
    beforeRouteEnter(to, from, next) {
341
        next(async vm => {
342
            vm.filtersInitialized = true;
343
            await vm.filterRuleSetsbySearchParam();
344
        });
345
    },
346
    methods: {
347
        async loadRuleSets() {
348
            this.ruleSetInitialized = false;
349
            await this.setAllRawRuleSets();
350
            this.updateTriggerCount();
351
            this.setAllEffectiveRuleSets();
352
            this.setAllExhaustiveEffectiveRuleSets();
353
            this.storeInitialized = true;
354
        },
355
        formatSelectedParams() {
356
            const selectedParams = {};
357
            selectedParams.effective = true;
358
            selectedParams.library_id = this.currentLibraryId ?? "*";
359
            if (this.currentPatronCategoryId) {
360
                selectedParams.patron_category_id =
361
                    this.currentPatronCategoryId;
362
            }
363
            if (this.currentItemTypeId) {
364
                selectedParams.item_type_id = this.currentItemTypeId;
365
            }
366
            return selectedParams;
367
        },
368
        async filterRuleSetsbySearchParam() {
369
            await this.loadRuleSets();
370
371
            const selectedParams = this.formatSelectedParams();
372
            const data = this.displayAllApplicableRules
373
                ? this.allExhaustiveEffectiveRuleSets
374
                : this.allEffectiveRuleSets;
375
376
            // handle searches for any patron category and item type combinations
377
            if (
378
                !selectedParams.patron_category_id &&
379
                !selectedParams.item_type_id
380
            ) {
381
                this.ruleSets = data;
382
                this.ruleSetInitialized = true;
383
                return;
384
            }
385
386
            // handle searches where only the item type is specified
387
            if (!selectedParams.patron_category_id) {
388
                this.ruleSets = data.filter(
389
                    ruleSet =>
390
                        ruleSet.context.item_type_id ===
391
                            selectedParams.item_type_id &&
392
                        ruleSet.context.library_id === selectedParams.library_id
393
                );
394
                this.ruleSetInitialized = true;
395
                return;
396
            }
397
398
            // handle searches where only the patron category is specified
399
            if (!selectedParams.item_type_id) {
400
                this.ruleSets = data.filter(
401
                    ruleSet =>
402
                        ruleSet.context.patron_category_id ===
403
                            selectedParams.patron_category_id &&
404
                        ruleSet.context.library_id === selectedParams.library_id
405
                );
406
                this.ruleSetInitialized = true;
407
                return;
408
            }
409
410
            // handle searches where both patron category and item type are specified and one specific rule is retrieved
411
            this.ruleSets = data.filter(
412
                ruleSet =>
413
                    ruleSet.context.item_type_id ===
414
                        selectedParams.item_type_id &&
415
                    ruleSet.context.patron_category_id ===
416
                        selectedParams.patron_category_id &&
417
                    ruleSet.context.library_id === selectedParams.library_id
418
            );
419
            this.ruleSetInitialized = true;
420
            return;
421
        },
422
        changeTabContent(e) {
423
            this.tabSelected = e.target.getAttribute("data-content");
424
        },
425
    },
426
    watch: {
427
        $route: {
428
            immediate: true,
429
            handler: function (newVal, oldVal) {
430
                this.showModal = newVal.meta && newVal.meta.showModal;
431
            },
432
        },
433
        "$route.query.refresh": {
434
            async handler(newVal) {
435
                if (newVal) {
436
                    await this.filterRuleSetsbySearchParam();
437
                }
438
            },
439
            immediate: true,
440
        },
441
    },
442
    components: { TriggersTable, Toolbar, ToolbarButton },
443
};
444
</script>
445
446
<style scoped>
447
.inline-block {
448
    display: inline-block;
449
    width: calc(100% - 90.25px);
450
    margin: 0 0 0 13px;
451
}
452
.page-section table {
453
    width: 100%;
454
    table-layout: fixed;
455
}
456
.page-section th,
457
.page-section td {
458
    width: 33%;
459
}
460
.page-section td {
461
    padding: 0.5em;
462
    vertical-align: top;
463
}
464
.v-select {
465
    display: block;
466
    background-color: white;
467
    margin: 10px;
468
    height: auto;
469
}
470
.vs__search,
471
.v__selected {
472
    display: inline-block;
473
    vertical-align: middle;
474
}
475
.active {
476
    cursor: pointer;
477
}
478
.toptabs {
479
    margin-bottom: 0;
480
}
481
.toggle-view-all-applicable-wrapper {
482
    margin: 10px;
483
}
484
485
.modal {
486
    position: fixed;
487
    z-index: 9998;
488
    display: table;
489
    transition: opacity 0.3s ease;
490
    left: 0px;
491
    top: 0px;
492
    width: 100%;
493
    height: 100%;
494
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
495
    background-color: rgba(0, 0, 0, 0.33);
496
}
497
.modal-dialog,
498
.modal-dialog-centered,
499
.modal-lg {
500
    max-width: 90%;
501
    width: fit-content;
502
}
503
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/Main.vue (+69 lines)
Line 0 Link Here
1
<template>
2
    <div>
3
        <div id="sub-header">
4
            <Breadcrumbs />
5
            <Help />
6
        </div>
7
        <div class="main container-fluid">
8
            <div class="row">
9
                <div class="col-md-10 order-md-2 order-sm-1">
10
                    <main>
11
                        <Dialog />
12
                        <router-view />
13
                    </main>
14
                </div>
15
16
                <div class="col-md-2 order-sm-2 order-md-1"></div>
17
            </div>
18
        </div>
19
    </div>
20
</template>
21
22
<script>
23
import Breadcrumbs from "../../Breadcrumbs.vue";
24
import Help from "../../Help.vue";
25
import Dialog from "../../Dialog.vue";
26
import "vue-select/dist/vue-select.css";
27
import { inject } from "vue";
28
29
export default {
30
    setup() {
31
        const circRulesStore = inject("circRulesStore");
32
        letters.unshift({
33
            name: "No letter",
34
            code: "",
35
            branchcode: "",
36
            value: "",
37
        });
38
        circRulesStore.letters = letters;
39
40
        return {
41
            letters,
42
        };
43
    },
44
    components: {
45
        Breadcrumbs,
46
        Dialog,
47
        Help,
48
    },
49
};
50
</script>
51
52
<style>
53
form .v-select {
54
    display: inline-block;
55
    background-color: white;
56
    width: 50%;
57
}
58
59
.v-select,
60
input:not([type="submit"]):not([type="search"]):not([type="button"]):not(
61
        [type="checkbox"]
62
    ):not([type="radio"]),
63
textarea {
64
    border-color: rgba(60, 60, 60, 0.26);
65
    border-width: 1px;
66
    border-radius: 4px;
67
    min-width: 30%;
68
}
69
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggerContext.vue (+50 lines)
Line 0 Link Here
1
<template>
2
    <div class="page-section">
3
        <table>
4
            <thead>
5
                <th>
6
                    {{ $__("Loan period") }}
7
                </th>
8
                <th>
9
                    {{ $__("Unit") }}
10
                </th>
11
                <th>
12
                    {{ $__("Decreased loan period for high holds") }}
13
                </th>
14
                <th>
15
                    {{ $__("Fine amount") }}
16
                </th>
17
                <th>
18
                    {{ $__("Fine charging interval") }}
19
                </th>
20
            </thead>
21
            <tbody>
22
                <tr>
23
                    <td>
24
                        {{ ruleSetInfo.issuelength }}
25
                    </td>
26
                    <td>
27
                        {{ ruleSetInfo.lengthunit }}
28
                    </td>
29
                    <td>
30
                        {{ ruleSetInfo.decreaseloanholds }}
31
                    </td>
32
                    <td>
33
                        {{ ruleSetInfo.fine }}
34
                    </td>
35
                    <td>
36
                        {{ ruleSetInfo.chargeperiod }}
37
                    </td>
38
                </tr>
39
            </tbody>
40
        </table>
41
    </div>
42
</template>
43
44
<script>
45
export default {
46
    props: ["ruleSetInfo"],
47
};
48
</script>
49
50
<style></style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Admin/CirculationTriggers/TriggersTable.vue (+357 lines)
Line 0 Link Here
1
<template>
2
    <slot></slot>
3
    <table>
4
        <thead>
5
            <th v-if="!modal" class="trigger_context">
6
                {{ $__("Library") }}
7
            </th>
8
            <th v-if="!modal" class="trigger_context">
9
                {{ $__("Patron category") }}
10
            </th>
11
            <th v-if="!modal" class="trigger_context border_right">
12
                {{ $__("Item type") }}
13
            </th>
14
            <th v-if="modal">
15
                {{ $__("Trigger") }}
16
            </th>
17
            <th>
18
                {{ $__("Delay") }}
19
            </th>
20
            <th>
21
                {{ $__("Notice") }}
22
            </th>
23
            <th>
24
                {{ $__("Email") }}
25
            </th>
26
            <th>
27
                {{ $__("Print") }}
28
            </th>
29
            <th>
30
                {{ $__("SMS") }}
31
            </th>
32
            <th>
33
                {{ $__("Restricts checkouts") }}
34
            </th>
35
            <th v-if="actions">
36
                {{ $__("Actions") }}
37
            </th>
38
        </thead>
39
        <tbody>
40
            <template
41
                v-for="(ruleSet, i) in ruleSets"
42
                :key="'ruleSet' + i"
43
                :class="{
44
                    selected_rule_set:
45
                        modal && i + 1 === parseInt(activeTriggerBeingEdited),
46
                }"
47
            >
48
                <tr
49
                    v-if="
50
                        ruleSet[
51
                            `overdue_${modal ? i + 1 : triggerNumber}_has_rules`
52
                        ]
53
                    "
54
                >
55
                    <td v-if="!modal" class="trigger_context">
56
                        {{
57
                            handleContext(
58
                                ruleSet.context.library_id,
59
                                libraries,
60
                                "library_id"
61
                            )
62
                        }}
63
                    </td>
64
                    <td v-if="!modal" class="trigger_context">
65
                        {{
66
                            handleContext(
67
                                ruleSet.context.patron_category_id,
68
                                patronCategories,
69
                                "patron_category_id"
70
                            )
71
                        }}
72
                    </td>
73
                    <td v-if="!modal" class="trigger_context border_right">
74
                        {{
75
                            handleContext(
76
                                ruleSet.context.item_type_id,
77
                                itemTypes,
78
                                "item_type_id",
79
                                "description"
80
                            )
81
                        }}
82
                    </td>
83
                    <td v-if="modal">
84
                        {{ i + 1 }}
85
                    </td>
86
                    <!-- Delay -->
87
                    <td>
88
                        <span
89
                            :class="{
90
                                fallback:
91
                                    ruleSet[
92
                                        `overdue_${modal ? i + 1 : triggerNumber}_delay`
93
                                    ].isFallback,
94
                            }"
95
                        >
96
                            {{
97
                                ruleSet[
98
                                    `overdue_${modal ? i + 1 : triggerNumber}_delay`
99
                                ].value
100
                            }}
101
                        </span>
102
                    </td>
103
                    <!--  Notice -->
104
                    <td>
105
                        <span
106
                            :class="{
107
                                fallback:
108
                                    ruleSet[
109
                                        `overdue_${modal ? i + 1 : triggerNumber}_notice`
110
                                    ].isFallback,
111
                            }"
112
                        >
113
                            {{
114
                                handleNotice(
115
                                    ruleSet[
116
                                        `overdue_${modal ? i + 1 : triggerNumber}_notice`
117
                                    ].value
118
                                )
119
                            }}
120
                        </span>
121
                    </td>
122
                    <!-- Email -->
123
                    <td>
124
                        <span
125
                            :class="{
126
                                fallback:
127
                                    ruleSet[
128
                                        `overdue_${modal ? i + 1 : triggerNumber}_mtt`
129
                                    ].isFallback,
130
                            }"
131
                        >
132
                            {{
133
                                handleTransport(
134
                                    ruleSet[
135
                                        `overdue_${modal ? i + 1 : triggerNumber}_mtt`
136
                                    ].value,
137
                                    "email",
138
                                    !ruleSet[
139
                                        `overdue_${modal ? i + 1 : triggerNumber}_notice`
140
                                    ].value
141
                                )
142
                            }}
143
                        </span>
144
                    </td>
145
                    <!-- Print -->
146
                    <td>
147
                        <span
148
                            :class="{
149
                                fallback:
150
                                    ruleSet[
151
                                        `overdue_${modal ? i + 1 : triggerNumber}_mtt`
152
                                    ].isFallback,
153
                            }"
154
                        >
155
                            {{
156
                                handleTransport(
157
                                    ruleSet[
158
                                        `overdue_${modal ? i + 1 : triggerNumber}_mtt`
159
                                    ].value,
160
                                    "print",
161
                                    !ruleSet[
162
                                        `overdue_${modal ? i + 1 : triggerNumber}_notice`
163
                                    ].value
164
                                )
165
                            }}
166
                        </span>
167
                    </td>
168
                    <!-- SMS -->
169
                    <td>
170
                        <span
171
                            :class="{
172
                                fallback:
173
                                    ruleSet[
174
                                        `overdue_${modal ? i + 1 : triggerNumber}_mtt`
175
                                    ].isFallback,
176
                            }"
177
                        >
178
                            {{
179
                                handleTransport(
180
                                    ruleSet[
181
                                        `overdue_${modal ? i + 1 : triggerNumber}_mtt`
182
                                    ].value,
183
                                    "sms",
184
                                    !ruleSet[
185
                                        `overdue_${modal ? i + 1 : triggerNumber}_notice`
186
                                    ].value
187
                                )
188
                            }}
189
                        </span>
190
                    </td>
191
                    <!-- Restricts Checkouts -->
192
                    <td>
193
                        <span
194
                            :class="{
195
                                fallback:
196
                                    ruleSet[
197
                                        `overdue_${modal ? i + 1 : triggerNumber}_restrict`
198
                                    ].isFallback,
199
                            }"
200
                        >
201
                            {{
202
                                handleRestrictions(
203
                                    ruleSet[
204
                                        `overdue_${modal ? i + 1 : triggerNumber}_restrict`
205
                                    ].value
206
                                )
207
                            }}
208
                        </span>
209
                    </td>
210
                    <td class="actions" v-if="actions">
211
                        <router-link
212
                            :to="{
213
                                name: 'CirculationTriggersFormEdit',
214
                                query: {
215
                                    library_id: ruleSet.context.library_id,
216
                                    item_type_id: ruleSet.context.item_type_id,
217
                                    patron_category_id:
218
                                        ruleSet.context.patron_category_id,
219
                                    triggerNumber: modal
220
                                        ? i + 1
221
                                        : triggerNumber,
222
                                },
223
                            }"
224
                            class="btn btn-default btn-xs"
225
                            ><i class="fa-solid fa-pencil"></i>
226
                            {{ $__("Edit") }}</router-link
227
                        >
228
                        <router-link
229
                            v-if="
230
                                ruleSet[
231
                                    `overdue_${modal ? i + 1 : triggerNumber}_has_rules`
232
                                ].value &&
233
                                !isOnlyRuleSetForTrigger(triggerNumber)
234
                            "
235
                            :to="{
236
                                name: 'CirculationTriggersFormConfirmReset',
237
                                query: {
238
                                    library_id: ruleSet.context.library_id,
239
                                    item_type_id: ruleSet.context.item_type_id,
240
                                    patron_category_id:
241
                                        ruleSet.context.patron_category_id,
242
                                    triggerNumber: triggerNumber,
243
                                },
244
                            }"
245
                            class="btn btn-default btn-xs"
246
                            ><i class="fa-solid fa-eraser"></i>
247
                            {{ $__("Reset") }}</router-link
248
                        >
249
                    </td>
250
                </tr>
251
            </template>
252
            <tr v-if="modal">
253
                <td colspan="7"></td>
254
                <td class="actions">
255
                    <router-link
256
                        :to="{
257
                            name: 'CirculationTriggersFormAdd',
258
                            query: {
259
                                library_id:
260
                                    activeRuleSetBeingEdited.context.library_id,
261
                                item_type_id:
262
                                    activeRuleSetBeingEdited.context
263
                                        .item_type_id,
264
                                patron_category_id:
265
                                    activeRuleSetBeingEdited.context
266
                                        .patron_category_id,
267
                                triggerNumber:
268
                                    triggerCounts[
269
                                        activeRuleSetBeingEdited.context
270
                                            .library_id
271
                                    ] + 1,
272
                            },
273
                        }"
274
                        class="btn btn-default btn-xs"
275
                        ><i class="fa-solid fa-pencil"></i>
276
                        {{ $__("Add") }}</router-link
277
                    >
278
                </td>
279
            </tr>
280
        </tbody>
281
    </table>
282
</template>
283
284
<script>
285
import { inject } from "vue";
286
import { storeToRefs } from "pinia";
287
288
export default {
289
    props: [
290
        "triggerNumber",
291
        "modal",
292
        "actions",
293
        "ruleSetBeingEdited",
294
        "triggerBeingEdited",
295
        "ruleSets",
296
    ],
297
    computed: {
298
        activeRuleSetBeingEdited() {
299
            return this.modal ? this.ruleSetBeingEdited : null;
300
        },
301
        activeTriggerBeingEdited() {
302
            return this.modal ? this.triggerBeingEdited : null;
303
        },
304
    },
305
    setup() {
306
        const circRulesStore = inject("circRulesStore");
307
        const { triggerCounts, patronCategories, itemTypes, libraries } =
308
            storeToRefs(circRulesStore);
309
        const {
310
            handleContext,
311
            handleNotice,
312
            handleTransport,
313
            isOnlyRuleSetForTrigger,
314
            handleRestrictions,
315
        } = circRulesStore;
316
317
        return {
318
            handleContext,
319
            handleNotice,
320
            handleTransport,
321
            triggerCounts,
322
            patronCategories,
323
            itemTypes,
324
            libraries,
325
            isOnlyRuleSetForTrigger,
326
            handleRestrictions,
327
        };
328
    },
329
};
330
</script>
331
332
<style scoped>
333
.selected_rule_set > td {
334
    background-color: yellow !important;
335
}
336
337
.fallback {
338
    font-style: italic;
339
    font-weight: bold;
340
}
341
342
.actions a {
343
    margin-right: 5px;
344
}
345
346
td.trigger_context {
347
    color: black;
348
}
349
350
th.trigger_context {
351
    color: blue;
352
}
353
354
.border_right {
355
    border-right: solid 4px black;
356
}
357
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/api-client.js (+4 lines)
Lines 2-7 import HttpClient from "./http-client"; Link Here
2
2
3
import ERMAPIClient from "@fetch/erm-api-client";
3
import ERMAPIClient from "@fetch/erm-api-client";
4
import PatronAPIClient from "@fetch/patron-api-client";
4
import PatronAPIClient from "@fetch/patron-api-client";
5
import LibraryAPIClient from "@fetch/library-api-client";
5
import AcquisitionAPIClient from "@fetch/acquisition-api-client";
6
import AcquisitionAPIClient from "@fetch/acquisition-api-client";
6
import AdditionalFieldsAPIClient from "@fetch/additional-fields-api-client";
7
import AdditionalFieldsAPIClient from "@fetch/additional-fields-api-client";
7
import AVAPIClient from "@fetch/authorised-values-api-client";
8
import AVAPIClient from "@fetch/authorised-values-api-client";
Lines 9-18 import ItemAPIClient from "@fetch/item-api-client"; Link Here
9
import RecordSourcesAPIClient from "@fetch/record-sources-api-client";
10
import RecordSourcesAPIClient from "@fetch/record-sources-api-client";
10
import SysprefAPIClient from "@fetch/system-preferences-api-client";
11
import SysprefAPIClient from "@fetch/system-preferences-api-client";
11
import PreservationAPIClient from "@fetch/preservation-api-client";
12
import PreservationAPIClient from "@fetch/preservation-api-client";
13
import CircRuleAPIClient from "@fetch/circulation-rules-api-client";
12
14
13
export const APIClient = {
15
export const APIClient = {
14
    erm: new ERMAPIClient(HttpClient),
16
    erm: new ERMAPIClient(HttpClient),
15
    patron: new PatronAPIClient(HttpClient),
17
    patron: new PatronAPIClient(HttpClient),
18
    library: new LibraryAPIClient(HttpClient),
16
    acquisition: new AcquisitionAPIClient(HttpClient),
19
    acquisition: new AcquisitionAPIClient(HttpClient),
17
    additional_fields: new AdditionalFieldsAPIClient(HttpClient),
20
    additional_fields: new AdditionalFieldsAPIClient(HttpClient),
18
    authorised_values: new AVAPIClient(HttpClient),
21
    authorised_values: new AVAPIClient(HttpClient),
Lines 20-23 export const APIClient = { Link Here
20
    sysprefs: new SysprefAPIClient(HttpClient),
23
    sysprefs: new SysprefAPIClient(HttpClient),
21
    preservation: new PreservationAPIClient(HttpClient),
24
    preservation: new PreservationAPIClient(HttpClient),
22
    record_sources: new RecordSourcesAPIClient(HttpClient),
25
    record_sources: new RecordSourcesAPIClient(HttpClient),
26
    circRule: new CircRuleAPIClient(HttpClient),
23
};
27
};
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/circulation_triggers.ts (+57 lines)
Line 0 Link Here
1
import { createApp } from "vue";
2
import { createPinia } from "pinia";
3
import { createWebHistory, createRouter } from "vue-router";
4
5
import { library } from "@fortawesome/fontawesome-svg-core";
6
import {
7
    faPlus,
8
    faMinus,
9
    faPencil,
10
    faTrash,
11
    faSpinner,
12
} from "@fortawesome/free-solid-svg-icons";
13
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
14
import vSelect from "vue-select";
15
import { useNavigationStore } from "../../stores/navigation";
16
import { useMainStore } from "../../stores/main";
17
import { useCircRulesStore } from "../../stores/circulation-rules";
18
import routesDef from "../../routes/admin/circulation_triggers";
19
20
library.add(faPlus, faMinus, faPencil, faTrash, faSpinner);
21
22
const pinia = createPinia();
23
const navigationStore = useNavigationStore(pinia);
24
const mainStore = useMainStore(pinia);
25
const circRulesStore = useCircRulesStore(pinia);
26
const { removeMessages } = mainStore;
27
const { setRoutes } = navigationStore;
28
const routes = setRoutes(routesDef);
29
30
const router = createRouter({
31
    history: createWebHistory(),
32
    linkExactActiveClass: "current",
33
    routes,
34
});
35
36
import App from "../../components/Admin/CirculationTriggers/Main.vue";
37
import i18n from "../../i18n";
38
39
const app = createApp(App);
40
41
const rootComponent = app
42
    .use(i18n)
43
    .use(pinia)
44
    .use(router)
45
    .component("font-awesome-icon", FontAwesomeIcon)
46
    .component("v-select", vSelect);
47
48
app.config.unwrapInjectedRef = true;
49
app.provide("mainStore", mainStore);
50
app.provide("navigationStore", navigationStore);
51
app.provide("circRulesStore", circRulesStore);
52
app.mount("#__app");
53
54
router.beforeEach(to => {
55
    navigationStore.$patch({ current: to.matched, params: to.params || {} });
56
    removeMessages(); // This will actually flag the messages as displayed already
57
});
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/admin/circulation_triggers.js (+91 lines)
Line 0 Link Here
1
import { markRaw } from "vue";
2
import { $__ } from "../../i18n";
3
4
import CirculationTriggersList from "../../components/Admin/CirculationTriggers/CirculationTriggersList.vue";
5
import CirculationTriggersFormAdd from "../../components/Admin/CirculationTriggers/CirculationTriggersFormAdd.vue";
6
import CirculationTriggersFormConfirmReset from "../../components/Admin/CirculationTriggers/CirculationTriggersFormConfirmReset.vue";
7
import CirculationTriggersFormConfirmTriggerDelete from "../../components/Admin/CirculationTriggers/CirculationTriggersFormConfirmTriggerDelete.vue";
8
9
export default {
10
    title: $__("Administration"),
11
    path: "",
12
    href: "/cgi-bin/koha/admin/admin-home.pl",
13
    is_base: true,
14
    is_default: true,
15
    children: [
16
        {
17
            title: $__("Circulation triggers"),
18
            path: "/cgi-bin/koha/admin/circulation_triggers",
19
            children: [
20
                {
21
                    path: "",
22
                    name: "CirculationTriggersList",
23
                    component: markRaw(CirculationTriggersList),
24
                    title: $__("Home"),
25
                    children: [
26
                        {
27
                            path: "confirmContext",
28
                            name: "CirculationTriggersFormConfirmContext",
29
                            component: markRaw(CirculationTriggersFormAdd),
30
                            title: $__("Confirm trigger context"),
31
                            meta: {
32
                                showModal: true,
33
                            },
34
                        },
35
                        {
36
                            path: "selectOrAdd",
37
                            name: "CirculationTriggersSelectOrAdd",
38
                            component: markRaw(CirculationTriggersFormAdd),
39
                            title: $__(
40
                                "Select a trigger to edit or select add"
41
                            ),
42
                            meta: {
43
                                showModal: true,
44
                            },
45
                        },
46
                        {
47
                            path: "add",
48
                            name: "CirculationTriggersFormAdd",
49
                            component: markRaw(CirculationTriggersFormAdd),
50
                            title: $__("Add new rule set"),
51
                            meta: {
52
                                showModal: true,
53
                            },
54
                        },
55
                        {
56
                            path: "edit",
57
                            name: "CirculationTriggersFormEdit",
58
                            component: markRaw(CirculationTriggersFormAdd),
59
                            title: $__("Edit rule set"),
60
                            meta: {
61
                                showModal: true,
62
                            },
63
                        },
64
                        {
65
                            path: "reset",
66
                            name: "CirculationTriggersFormConfirmReset",
67
                            component: markRaw(
68
                                CirculationTriggersFormConfirmReset
69
                            ),
70
                            title: $__("Confirm rule set reset"),
71
                            meta: {
72
                                showModal: true,
73
                            },
74
                        },
75
                        {
76
                            path: "delete",
77
                            name: "CirculationTriggersFormConfirmTriggerDelete",
78
                            component: markRaw(
79
                                CirculationTriggersFormConfirmTriggerDelete
80
                            ),
81
                            title: $__("Confirm trigger deletion"),
82
                            meta: {
83
                                showModal: true,
84
                            },
85
                        },
86
                    ],
87
                },
88
            ],
89
        },
90
    ],
91
};
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/circulation-rules.js (+486 lines)
Line 0 Link Here
1
import { defineStore } from "pinia";
2
import { $__ } from "../i18n";
3
import { APIClient } from "../fetch/api-client.js";
4
import { isEqual } from "lodash";
5
6
export const useCircRulesStore = defineStore("circRules", {
7
    // NOTES ON RULE SETS TYPES
8
    // exhaustive:  includes 'pure fallback' rules sets for contexts that no rules match.
9
    //              Format: [{overdue_X_<rule_name>: {value: mixed: isFallback: bool}}]
10
    // effective:   includes sets only for contexts for which one or more rule exists in the db. These sets will include fallbacks.
11
    //              Format: [{overdue_X_<rule_name>: <value>}}
12
    // raw:         includes only the exact sets as they are found in the db
13
    //              Format: [{overdue_X_<rule_name>: <value>}}]
14
    state: () => ({
15
        // context
16
        currentLibraryId: "*",
17
        currentPatronCategoryId: null,
18
        currentItemTypeId: null,
19
        triggerCounts: { "*": 0 },
20
        // references
21
        itemTypes: [],
22
        libraries: [],
23
        patronCategories: [],
24
        letters: [],
25
        ruleSuffixes: ["delay", "notice", "mtt", "restrict", "has_rules"],
26
        transportTypes: [
27
            { code: "email", name: "Email" },
28
            { code: "sms", name: "SMS" },
29
            { code: "print", name: "Print" },
30
        ],
31
        regex: /overdue_(\d+)_has_rules/,
32
        // rule sets
33
        allDefaultLibraryRawRuleSets: [], // source of truth for default library
34
        allCurrentLibraryRawRuleSets: [], // source of truth for current library
35
        allEffectiveRuleSets: [], // main data set for display explicitly set rules for current library
36
        allExhaustiveEffectiveRuleSets: [], // main data set for display all applied rules for current library
37
        currentAndDefaultRawRuleSets: [], // data set to identify effective rules from (combines allDefaultLibraryRawRuleSets and allCurrentLibraryRawRuleSets)
38
        storeInitialized: false,
39
    }),
40
    actions: {
41
        // controllers
42
        async init() {
43
            await this.getItemTypes();
44
            await this.getLibraries();
45
            await this.getPatronCategories();
46
        },
47
        // utilities
48
        formatTriggerSpecificRuleSetForDisplay(
49
            context,
50
            triggerNumber,
51
            includeFallbacks = true
52
        ) {
53
            const triggerSpecificRuleSet = {
54
                context,
55
            };
56
            this.ruleSuffixes.forEach(ruleSuffix => {
57
                triggerSpecificRuleSet[
58
                    `overdue_${triggerNumber}_${ruleSuffix}`
59
                ] = this.findEffectiveRule(
60
                    context,
61
                    ruleSuffix,
62
                    triggerNumber,
63
                    includeFallbacks
64
                );
65
            });
66
            triggerSpecificRuleSet.context = context;
67
            return triggerSpecificRuleSet;
68
        },
69
        handleContext(value, data, type, displayProperty = "name") {
70
            const item = data.find(item => item[type] === value);
71
            return item[displayProperty];
72
        },
73
        handleNotice(notice) {
74
            const letter = this.letters.find(letter => letter.code === notice);
75
            return letter ? letter.name : notice;
76
        },
77
        handleRestrictions(value) {
78
            if (!value) {
79
                return;
80
            }
81
            return value === "1" ? $__("Yes") : $__("No");
82
        },
83
        handleTransport(value, type, noLetter) {
84
            if (!value || noLetter) {
85
                return "";
86
            }
87
            return value.includes(type) ? $__("Yes") : $__("No");
88
        },
89
        hasConflict(oldRuleSet, newRuleSet, triggerNumber) {
90
            if (!oldRuleSet) {
91
                return false;
92
            }
93
            return (
94
                oldRuleSet.context.library_id !==
95
                    newRuleSet.context.library_id ||
96
                oldRuleSet.context.item_type_id !==
97
                    newRuleSet.context.item_type_id ||
98
                oldRuleSet.context.patron_category_id !==
99
                    newRuleSet.context.patron_category_id ||
100
                oldRuleSet[`overdue_${triggerNumber}_delay`] !==
101
                    newRuleSet[`overdue_${triggerNumber}_delay`] ||
102
                oldRuleSet[`overdue_${triggerNumber}_notice`] !==
103
                    newRuleSet[`overdue_${triggerNumber}_notice`] ||
104
                oldRuleSet[`overdue_${triggerNumber}_restrict`] !==
105
                    newRuleSet[`overdue_${triggerNumber}_restrict`] ||
106
                !isEqual(
107
                    oldRuleSet[`overdue_${triggerNumber}_mtt`],
108
                    newRuleSet[`overdue_${triggerNumber}_mtt`]
109
                )
110
            );
111
        },
112
        isOnlyRuleSetForTrigger(triggerNumber) {
113
            return (
114
                this.allCurrentLibraryRawRuleSets.filter(
115
                    ruleSet => ruleSet[`overdue_${triggerNumber}_has_rules`]
116
                ).length === 1
117
            );
118
        },
119
        isLastTrigger(triggerNumber) {
120
            return (
121
                parseInt(triggerNumber) ===
122
                this.triggerCounts[this.currentLibraryId]
123
            );
124
        },
125
        // services
126
        findEffectiveRule(
127
            context,
128
            ruleSuffix,
129
            triggerNumber,
130
            includeFallbacks = true
131
        ) {
132
            if (
133
                !this.currentAndDefaultRawRuleSets ||
134
                !Array.isArray(this.currentAndDefaultRawRuleSets) ||
135
                this.currentAndDefaultRawRuleSets.length === 0
136
            ) {
137
                return { value: null, isFallback: true };
138
            }
139
            // Check if the current ruleSet's value for the ruleSuffix is undefined
140
            const existingRule = this.currentAndDefaultRawRuleSets.find(
141
                ruleSet =>
142
                    ruleSet[`overdue_${triggerNumber}_${ruleSuffix}`] !==
143
                        undefined &&
144
                    ruleSet[`overdue_${triggerNumber}_${ruleSuffix}`] !==
145
                        null &&
146
                    ruleSet?.context.library_id === context.library_id &&
147
                    ruleSet?.context.patron_category_id ===
148
                        context.patron_category_id &&
149
                    ruleSet?.context.item_type_id === context.item_type_id
150
            );
151
152
            // if handling 'has_rules', stop here
153
            if (ruleSuffix === "has_rules") {
154
                return {
155
                    value: existingRule?.[`overdue_${triggerNumber}_has_rules`],
156
                    isFallback:
157
                        !existingRule?.[`overdue_${triggerNumber}_has_rules`],
158
                };
159
            }
160
161
            // If the current ruleSet's value is not null, use it directly
162
            if (existingRule !== undefined) {
163
                return {
164
                    value: existingRule[
165
                        `overdue_${triggerNumber}_${ruleSuffix}`
166
                    ],
167
                    isFallback:
168
                        !existingRule[`overdue_${triggerNumber}_has_rules`],
169
                };
170
            }
171
172
            // If set to return a raw set, return
173
            if (!includeFallbacks) {
174
                return;
175
            }
176
177
            // Filter ruleSets to only those with non-null values for the specified ruleSuffix
178
            // and that are no excluded from the selected context
179
            const relevantRules = this.currentAndDefaultRawRuleSets.filter(
180
                ruleSet =>
181
                    ruleSet[`overdue_${triggerNumber}_${ruleSuffix}`] !==
182
                        undefined &&
183
                    ruleSet[`overdue_${triggerNumber}_${ruleSuffix}`] !==
184
                        null &&
185
                    (ruleSet.context.library_id === context.library_id ||
186
                        ruleSet.context.library_id === "*") &&
187
                    (ruleSet.context.patron_category_id ===
188
                        context.patron_category_id ||
189
                        ruleSet.context.patron_category_id === "*") &&
190
                    (ruleSet.context.item_type_id === context.item_type_id ||
191
                        ruleSet.context.item_type_id === "*")
192
            );
193
194
            // Function to calculate specificity score
195
            const getSpecificityScore = ruleSetContext => {
196
                let score = 0;
197
                if (
198
                    ruleSetContext.library_id !== "*" &&
199
                    ruleSetContext.library_id === context.library_id
200
                )
201
                    score += 4;
202
                if (
203
                    ruleSetContext.patron_category_id !== "*" &&
204
                    ruleSetContext.patron_category_id ===
205
                        context.patron_category_id
206
                )
207
                    score += 2;
208
                if (
209
                    ruleSetContext.item_type_id !== "*" &&
210
                    ruleSetContext.item_type_id === context.item_type_id
211
                )
212
                    score += 1;
213
                return score;
214
            };
215
216
            // Sort the ruleSets based on specificity score, descending
217
            const sortedRules = relevantRules.sort((a, b) => {
218
                return (
219
                    getSpecificityScore(b.context) -
220
                    getSpecificityScore(a.context)
221
                );
222
            });
223
            // If no ruleSet found, return null
224
            if (sortedRules.length === 0) {
225
                return { value: null, isFallback: true };
226
            }
227
            // Get the value from the most specific ruleSet
228
            const bestRule = sortedRules[0];
229
            return {
230
                value: bestRule[`overdue_${triggerNumber}_${ruleSuffix}`],
231
                isFallback: true,
232
            };
233
        },
234
        setAllExhaustiveEffectiveRuleSets() {
235
            // clear array
236
            this.allExhaustiveEffectiveRuleSets = [];
237
            // generate complete rule set list
238
            this.patronCategories.forEach(category => {
239
                this.itemTypes.forEach(itemType => {
240
                    const effectiveRuleSet = {
241
                        context: {
242
                            library_id: this.currentLibraryId,
243
                            patron_category_id: category.patron_category_id,
244
                            item_type_id: itemType.item_type_id,
245
                        },
246
                    };
247
                    for (
248
                        let i = 1;
249
                        i <= this.triggerCounts[this.currentLibraryId];
250
                        i++
251
                    ) {
252
                        this.ruleSuffixes.forEach(ruleSuffix => {
253
                            effectiveRuleSet[`overdue_${i}_${ruleSuffix}`] =
254
                                this.findEffectiveRule(
255
                                    effectiveRuleSet.context,
256
                                    ruleSuffix,
257
                                    i
258
                                );
259
                        });
260
                    }
261
                    this.allExhaustiveEffectiveRuleSets.push(effectiveRuleSet);
262
                });
263
            });
264
        },
265
        setAllEffectiveRuleSets() {
266
            // clear array
267
            this.allEffectiveRuleSets = [];
268
            // generate complete rule set list
269
            this.allCurrentLibraryRawRuleSets.forEach(ruleSet => {
270
                const effectiveRuleSet = {
271
                    context: { ...ruleSet.context },
272
                };
273
                for (
274
                    let i = 1;
275
                    i <= this.triggerCounts[this.currentLibraryId];
276
                    i++
277
                ) {
278
                    if (ruleSet[`overdue_${i}_has_rules`] === null) {
279
                        continue;
280
                    }
281
                    this.ruleSuffixes.forEach(ruleSuffix => {
282
                        effectiveRuleSet[`overdue_${i}_${ruleSuffix}`] =
283
                            this.findEffectiveRule(
284
                                ruleSet.context,
285
                                ruleSuffix,
286
                                i
287
                            );
288
                    });
289
                }
290
                this.allEffectiveRuleSets.push(effectiveRuleSet);
291
            });
292
        },
293
        setEffectiveTriggerFilteredRuleSet(context) {
294
            const effectiveTriggerFilteredRuleSets = [];
295
            for (
296
                let i = 1;
297
                i <= this.triggerCounts[this.currentLibraryId];
298
                i++
299
            ) {
300
                const triggerSpecificRuleSet =
301
                    this.formatTriggerSpecificRuleSetForDisplay(context, i);
302
                if (!triggerSpecificRuleSet) {
303
                    continue;
304
                }
305
                effectiveTriggerFilteredRuleSets.push(triggerSpecificRuleSet);
306
            }
307
            return effectiveTriggerFilteredRuleSets;
308
        },
309
        updateTriggerCount() {
310
            // Library-specific triggerCounts can fall into the following use cases:
311
            // - No rule sets specific to them exist. Therefore, their triggerCount is the same as default's.
312
            // - Rule sets exists that override default triggers. Therefore, their triggerCount is the same as default's.
313
            // - Rule sets exists for triggers for which there is no default.
314
            //     => such triggers are follow up addition to the existing default sequence.
315
            //     => the triggerCount for this library will be higher than default's, and equal to the highest trigger number for this library for which has_rules is not null.
316
317
            const ruleNames = Object.keys(this.allDefaultLibraryRawRuleSets[0]);
318
            // Set the triggerCount for the default library rule set
319
            if (this.currentLibraryId === "*") {
320
                this.triggerCounts["*"] = ruleNames.filter(
321
                    ruleSuffix =>
322
                        this.regex.test(ruleSuffix) &&
323
                        this.allDefaultLibraryRawRuleSets.some(
324
                            ruleSet => ruleSet[ruleSuffix] !== null
325
                        )
326
                ).length;
327
                return;
328
            }
329
330
            // Set a library-specific trigger count: no rule set exists -> simply use the default trigger count
331
            if (this.allCurrentLibraryRawRuleSets.length === 0) {
332
                this.triggerCounts[this.currentLibraryId] =
333
                    this.triggerCounts["*"];
334
                return;
335
            }
336
337
            // Set a library-specific trigger count: at least one rule set exists -> start from the first trigger for which there is no default rule set
338
            let i = this.triggerCounts["*"] + 1;
339
            while (
340
                ruleNames.includes(`overdue_${i}_has_rules`) &&
341
                this.allCurrentLibraryRawRuleSets.some(
342
                    ruleSet => ruleSet[`overdue_${i}_has_rules`] !== null
343
                )
344
            ) {
345
                i++;
346
            }
347
            this.triggerCounts[this.currentLibraryId] = i - 1;
348
        },
349
        async setAllRawRuleSets() {
350
            const client = APIClient.circRule;
351
            try {
352
                await this.getAllRawRuleSets();
353
            } catch (e) {
354
                //TODO: handle e
355
            }
356
            this.currentAndDefaultRawRuleSets = [
357
                ...this.allCurrentLibraryRawRuleSets,
358
                ...this.allDefaultLibraryRawRuleSets,
359
            ];
360
        },
361
        // repositories
362
        async deleteRuleSet(ruleSet, triggerNumber) {
363
            const ruleSetInDb = await this.getSelectedRuleSet(ruleSet.context);
364
365
            if (this.hasConflict(ruleSet, ruleSetInDb, triggerNumber)) {
366
                throw "The rule set for the selected trigger context could not be reset as it was updated elsewhere. Please see the updated trigger above.";
367
            }
368
369
            const rulesForDeletion = { context: ruleSet.context };
370
371
            if (ruleSet[`overdue_${triggerNumber}_delay`] !== null) {
372
                rulesForDeletion[`overdue_${triggerNumber}_delay`] = null;
373
            }
374
            if (ruleSet[`overdue_${triggerNumber}_notice`] !== null) {
375
                rulesForDeletion[`overdue_${triggerNumber}_notice`] = null;
376
            }
377
            if (ruleSet[`overdue_${triggerNumber}_restrict`] !== null) {
378
                rulesForDeletion[`overdue_${triggerNumber}_restrict`] = null;
379
            }
380
            if (ruleSet[`overdue_${triggerNumber}_mtt`] !== null) {
381
                rulesForDeletion[`overdue_${triggerNumber}_mtt`] = null;
382
            }
383
            rulesForDeletion[`overdue_${triggerNumber}_has_rules`] = null;
384
            this.updateCircRuleSets(rulesForDeletion, triggerNumber);
385
        },
386
        async getAllRawRuleSets() {
387
            const client = APIClient.circRule;
388
            try {
389
                this.allDefaultLibraryRawRuleSets =
390
                    await client.circRules.getAll(
391
                        {},
392
                        { library_id: "*", effective: false }
393
                    );
394
                this.allCurrentLibraryRawRuleSets =
395
                    await client.circRules.getAll(
396
                        {},
397
                        { library_id: this.currentLibraryId, effective: false }
398
                    );
399
            } catch (e) {
400
                throw e;
401
            }
402
        },
403
        async getItemTypes() {
404
            const client = APIClient.item;
405
            let itemTypes = [];
406
            try {
407
                itemTypes = await client.itemTypes.getAll();
408
            } catch (e) {
409
                // TODO: handle e
410
            }
411
            itemTypes.unshift({
412
                item_type_id: "*",
413
                description: $__("Default rule for all item types"),
414
            });
415
            this.itemTypes = itemTypes;
416
        },
417
        async getLibraries() {
418
            const client = APIClient.library;
419
            let libraries = [];
420
            try {
421
                libraries = await client.libraries.getAll();
422
            } catch (e) {
423
                // TODO: handle e
424
            }
425
            libraries.unshift({
426
                library_id: "*",
427
                name: $__("Default rule for all libraries"),
428
            });
429
            this.libraries = libraries;
430
        },
431
        async getPatronCategories() {
432
            const client = APIClient.patron;
433
            let patronCategories = [];
434
            try {
435
                patronCategories = await client.patronCategories.getAll();
436
            } catch (e) {
437
                // TODO: handle e
438
            }
439
            patronCategories.unshift({
440
                patron_category_id: "*",
441
                name: $__("Default rule for all categories"),
442
            });
443
            this.patronCategories = patronCategories;
444
        },
445
        async getSelectedRuleSet(context, effective = false) {
446
            if (context.library_id === null) {
447
                context.library_id = "*";
448
            }
449
            const client = APIClient.circRule;
450
            let result;
451
            try {
452
                result = await client.circRules.getAll(
453
                    {},
454
                    {
455
                        library_id: context.library_id,
456
                        patron_category_id: context.patron_category_id,
457
                        item_type_id: context.item_type_id,
458
                        effective,
459
                    }
460
                );
461
            } catch (e) {
462
                throw e;
463
            }
464
            return result[0] ?? null;
465
        },
466
        async updateCircRuleSets(existingRuleSet, triggerNumber) {
467
            const circRuleSet = { context: existingRuleSet.context };
468
            circRuleSet[`overdue_${triggerNumber}_delay`] =
469
                existingRuleSet[`overdue_${triggerNumber}_delay`];
470
            circRuleSet[`overdue_${triggerNumber}_notice`] =
471
                existingRuleSet[`overdue_${triggerNumber}_notice`];
472
            circRuleSet[`overdue_${triggerNumber}_restrict`] =
473
                existingRuleSet[`overdue_${triggerNumber}_restrict`];
474
            circRuleSet[`overdue_${triggerNumber}_mtt`] =
475
                existingRuleSet[`overdue_${triggerNumber}_mtt`];
476
            circRuleSet[`overdue_${triggerNumber}_has_rules`] =
477
                existingRuleSet[`overdue_${triggerNumber}_has_rules`];
478
            try {
479
                const client = APIClient.circRule;
480
                await client.circRules.update(circRuleSet);
481
            } catch (e) {
482
                //TODO: handle e
483
            }
484
        },
485
    },
486
});
(-)a/rspack.config.js (-2 / +3 lines)
Lines 27-33 module.exports = [ Link Here
27
            acquisitions:
27
            acquisitions:
28
                "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/acquisitions.ts",
28
                "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/acquisitions.ts",
29
            islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts",
29
            islands: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/islands.ts",
30
        },
30
            "admin/circulation_triggers":
31
            "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/admin/circulation_triggers.ts",
32
    },
31
        output: {
33
        output: {
32
            filename: "[name].js",
34
            filename: "[name].js",
33
            path: path.resolve(
35
            path: path.resolve(
34
- 

Return to bug 10190