Bug 32030: ERM - Vue version
authorJonathan Druart <jonathan.druart@bugs.koha-community.org>
Wed, 16 Mar 2022 15:49:00 +0000 (16:49 +0100)
committerTomas Cohen Arazi <tomascohen@theke.io>
Tue, 8 Nov 2022 12:43:38 +0000 (09:43 -0300)
First, `yarn install`.
Then use `yarn build_js` or `yarn watch_js` to regenerate the dist/main.js file

Signed-off-by: Jonathan Field <jonathan.field@ptfs-europe.com>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
19 files changed:
Koha/ERM/Agreement.pm
erm/agreements.pl
koha-tmpl/intranet-tmpl/prog/en/includes/calendar.inc
koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc
koha-tmpl/intranet-tmpl/prog/en/includes/patron-search.inc
koha-tmpl/intranet-tmpl/prog/en/modules/erm/agreements.tt
koha-tmpl/intranet-tmpl/prog/js/vue/Agreements.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementUserRoles.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonDelete.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonEdit.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsFormAdd.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsFormConfirmDelete.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsToolbar.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/main-erm.ts [new file with mode: 0644]
package.json
tsconfig.json [new file with mode: 0644]
webpack.config.js [new file with mode: 0644]

index 2b8789b..5ca06e3 100644 (file)
@@ -41,7 +41,20 @@ Returns the periods for this agreement
 =cut
 
 sub periods {
-    my ( $self ) = @_;
+    my ( $self, $periods ) = @_;
+
+    if ( $periods ) {
+        my $schema = $self->_result->result_source->schema;
+        $schema->txn_do(
+            sub {
+                $self->periods->delete;
+
+                for my $period (@$periods) {
+                    $self->_result->add_to_erm_agreement_periods($period);
+                }
+            }
+        );
+    }
 
     my $periods_rs = $self->_result->erm_agreement_periods;
     return Koha::ERM::Agreement::Periods->_new_from_dbic($periods_rs);
@@ -54,8 +67,20 @@ Returns the user roles for this agreement
 =cut
 
 sub user_roles {
-    my ( $self ) = @_;
-
+    my ( $self, $user_roles ) = @_;
+
+    if ( $user_roles ) {
+        my $schema = $self->_result->result_source->schema;
+        $schema->txn_do(
+            sub {
+                $self->user_roles->delete;
+
+                for my $user_role (@$user_roles) {
+                    $self->_result->add_to_erm_agreement_user_roles($user_role);
+                }
+            }
+        );
+    }
     my $user_roles_rs = $self->_result->erm_agreement_user_roles;
     return Koha::ERM::Agreement::UserRoles->_new_from_dbic($user_roles_rs);
 }
index 88dfa1a..99485df 100755 (executable)
@@ -26,9 +26,6 @@ use Koha::Acquisition::Booksellers;
 use Koha::ERM::Agreements;
 
 my $input        = CGI->new;
-my $agreement_id = $input->param('agreement_id');
-my $op           = $input->param('op') || 'list';
-my @messages;
 
 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
     {
@@ -39,142 +36,8 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
     }
 );
 
-my $dbh = C4::Context->dbh;
-if ( $op eq 'add_form' ) {
-    my $agreement;
-    if ($agreement_id) {
-        $agreement = Koha::ERM::Agreements->find($agreement_id);
-    }
-
-    $template->param( agreement => $agreement, );
-}
-elsif ( $op eq 'add_validate' ) {
-    my $vendor_id        = $input->param('vendor_id');
-    my $name             = $input->param('name');
-    my $description      = $input->param('description');
-    my $status           = $input->param('status');
-    my $closure_reason   = $input->param('closure_reason');
-    my $is_perpetual     = $input->param('is_perpetual');
-    my $renewal_priority = $input->param('renewal_priority');
-    my $license_info     = $input->param('license_info');
-
-    my $schema = Koha::Database->new->schema;
-    $schema->txn_do(sub{
-        my ( $stored, $agreement );
-        if ($agreement_id) {
-            $agreement = Koha::ERM::Agreements->find($agreement_id);
-            $agreement->vendor_id($vendor_id);
-            $agreement->name($name);
-            $agreement->description($description);
-            $agreement->status($status);
-            $agreement->closure_reason($closure_reason);
-            $agreement->is_perpetual($is_perpetual);
-            $agreement->renewal_priority($renewal_priority);
-            $agreement->license_info($license_info);
-
-            eval { $agreement->store; };
-            if ($@) {
-                push @messages, { type => 'error', code => 'error_on_update' };
-            }
-            else {
-                $stored = 1;
-                push @messages, { type => 'message', code => 'success_on_update' };
-            }
-        }
-        else {
-            $agreement = Koha::ERM::Agreement->new(
-                {
-                    vendor_id        => $vendor_id,
-                    name             => $name,
-                    description      => $description,
-                    status           => $status,
-                    closure_reason   => $closure_reason,
-                    is_perpetual     => $is_perpetual,
-                    renewal_priority => $renewal_priority,
-                    license_info     => $license_info,
-                }
-            );
-            eval { $agreement->store; };
-            if ($@) {
-                push @messages, { type => 'error', code => 'error_on_insert' };
-            }
-            else {
-                $stored = 1;
-                push @messages, { type => 'message', code => 'success_on_insert' };
-            }
-        }
-
-        if ( $stored ) {
-            if ( $agreement_id ) {
-                $agreement->periods->delete;
-                $agreement->user_roles->delete;
-            }
-            for my $unique_id ( $input->multi_param('period_unique_id') ) {
-                my $started_on = $input->param( 'started_on_' . $unique_id );
-                next unless $started_on;
-                my $ended_on = $input->param( 'ended_on_' . $unique_id );
-                my $cancellation_deadline = $input->param( 'cancellation_deadline_' . $unique_id );
-                my $notes = $input->param( 'notes_' . $unique_id );
-
-                $started_on = dt_from_string($started_on);
-                $ended_on &&= dt_from_string($ended_on);
-                $cancellation_deadline &&= dt_from_string($cancellation_deadline);
-
-                Koha::ERM::Agreement::Period->new(
-                    {
-                        agreement_id          => $agreement->agreement_id,
-                        started_on            => $started_on,
-                        ended_on              => $ended_on,
-                        cancellation_deadline => $cancellation_deadline,
-                        notes                 => $notes,
-                    }
-                )->store;
-            }
-
-            for my $unique_id ( $input->multi_param('user_unique_id') ) {
-                my $user_id = $input->param('user_id_' . $unique_id);
-                next unless $user_id;
-                my $role = $input->param('user_role_' . $unique_id);
-                Koha::ERM::Agreement::UserRole->new(
-                    {
-                        agreement_id => $agreement->agreement_id,
-                        user_id      => $user_id,
-                        role         => $role,
-                    }
-                )->store;
-            }
-
-        }
-    });
-    $op = 'list';
-}
-elsif ( $op eq 'delete_confirm' ) {
-    my $agreement = Koha::ERM::Agreements->find($agreement_id);
-    $template->param( agreement => $agreement, );
-}
-elsif ( $op eq 'delete_confirmed' ) {
-    my $agreement = Koha::ERM::Agreements->find($agreement_id);
-    my $deleted   = eval { $agreement->delete; };
-
-    if ( $@ or not $deleted ) {
-        push @messages, { type => 'error', code => 'error_on_delete' };
-    }
-    else {
-        push @messages, { type => 'message', code => 'success_on_delete' };
-    }
-    $op = 'list';
-}
-
-if ( $op eq 'list' ) {
-    $template->param(
-        agreements_count => Koha::ERM::Agreements->search->count );
-}
-
 $template->param(
-    vendors      => Koha::Acquisition::Booksellers->search,
-    agreement_id => $agreement_id,
-    messages     => \@messages,
-    op           => $op,
+    vendors => Koha::Acquisition::Booksellers->search,
 );
 
 output_html_with_http_headers $input, $cookie, $template->output;
index 9cdc8b8..eefc245 100644 (file)
@@ -33,7 +33,7 @@
 <script>
     flatpickr.l10ns.default.weekdays = flatpickr_weekdays;
     flatpickr.l10ns.default.months   = flatpickr_months;
-    flatpickr.setDefaults({
+    let flatpickr_defaults = {
         allowInput: true,
         dateFormat: "Y-m-d",
         altInput: true,
             }
           })
         ]
-    });
+    };
+
+    flatpickr.setDefaults(flatpickr_defaults);
+
     $(document).ready(function(){
         $(".flatpickr").each(function(){
             let options = {};
index 92445ec..6b30216 100644 (file)
             return m.format(get_time_pattern(timeformat)+(dateformat=='rfc3339'?':ss'+(!m.isUTC()?'Z':''):''))+(dateformat=='rfc3339' && m.isUTC()?'Z':'');
         }
 
+        window.$date_to_rfc3339 = function(value, options) {
+            var dateformat = (options&&options.dateformat)||def_date_format;
+            let m = moment(value, get_date_pattern(dateformat));
+            return m.format("YYYY-MM-DD");
+        }
+
     })();
 </script>
 <!-- / js-date-format.inc -->
index cb97afa..901fda0 100644 (file)
         }
         function select_user(borrowernumber, data) {
             var p = window.opener;
-            [%  IF callback %]
-                p.[% callback | html %](borrowernumber, data);
-            [%  ELSE %]
-                p.select_user(borrowernumber, data);
-            [%  END %]
+            if ( p.document.getElementById("selected_patron_id") ) {
+                p.document.getElementById("selected_patron_id").value = borrowernumber;
+            } else {
+                [%  IF callback %]
+                    p.[% callback | html %](borrowernumber, data);
+                [%  ELSE %]
+                    p.select_user(borrowernumber, data);
+                [% END %]
+            }
             window.close();
         }
     </script>
index 092b61e..76c217a 100644 (file)
             <a href="/cgi-bin/koha/erm/erm-home.pl">Electronic resources management</a>
         </li>
 
-        [% IF op == 'add_form' %]
-            <li>
-                <a href="/cgi-bin/koha/erm/agreements.pl">Agreements</a>
-            </li>
-            <li>
-                <a href="#" aria-current="page">
-                    [% IF agreement.agreement_id %]
-                        Modify
-                    [% ELSE %]
-                        New
-                    [% END %] Agreement
-                </a>
-            </li>
-
-        [% ELSIF op == 'delete_confirm' %]
-            <li>
-                <a href="/cgi-bin/koha/erm/agreements.pl">Agreements</a>
-            </li>
-            <li>
-                <a href="#" aria-current="page">
-                    Confirm deletion of agreement
-                </a>
-            </li>
-
-        [% ELSE %]
-            <li>
-                <a href="#" aria-current="page">
-                    Agreements
-                </a>
-            </li>
-        [% END %]
+        <li>
+            <a href="/cgi-bin/koha/erm/agreements.pl">Agreements</a>
+        </li>
     </ol>
 </nav>
 
         <div class="col-sm-10 col-sm-push-2">
             <main>
 
-[% FOR m IN messages %]
-    <div class="dialog [% m.type | html %]">
-        [% SWITCH m.code %]
-        [% CASE 'error_on_update' %]
-            An error occurred when updating this agreement. Perhaps it already exists.
-        [% CASE 'error_on_insert' %]
-            An error occurred when adding this agreement. The agreement id might already exist.
-        [% CASE 'error_on_delete' %]
-            An error occurred when deleting this agreement. Check the logs.
-        [% CASE 'success_on_update' %]
-            Agreement updated successfully.
-        [% CASE 'success_on_insert' %]
-            Agreement added successfully.
-        [% CASE 'success_on_delete' %]
-            Agreement deleted successfully.
-        [% CASE 'already_exists' %]
-            This agreement already exists.
-        [% CASE %]
-            [% m.code | html %]
-        [% END %]
-    </div>
-[% END %]
-
-[% IF op == 'add_form' %]
-    [% IF agreement %]
-        <h1>Modify a agreement</h1>
-    [% ELSE %]
-        <h1>New agreement</h1>
-    [% END %]
-
-    <form action="/cgi-bin/koha/erm/agreements.pl" name="Aform" method="post" class="validated">
-        <input type="hidden" name="op" value="add_validate" />
-        <input type="hidden" name="agreement_id" value="[% agreement.agreement_id | html %]" />
-
-        <fieldset class="rows">
-            <ol>
-                [% IF agreement %]
-                    <li><span class="label">Agreement ID: </span>[% agreement.agreement_id | html %]</li>
-                [% END %]
-
-                <li>
-                    <label for="vendor_id">Vendor: </label>
-                    <select name="vendor_id">
-                        <option value=""></option>
-                        [% FOR v IN vendors %]
-                            [% IF v.id == agreement.vendor_id %]
-                                <option value="[% v.id %]" selected="selected">[% v.name %]</option>
-                            [% ELSE %]
-                                <option value="[% v.id %]">[% v.name %]</option>
-                            [% END %]
-                        [% END %]
-                    </select>
-                </li>
-                <li>
-                    <label for="name" class="required">Name: </label>
-                    <input type="text" name="name" id="name" size="80" value="[% agreement.name | html %]" required="required" class="required" /> <span class="required">Required</span>
-                <li>
-                    <label for="description">Description: </label>
-                    <input type="text" name="description" id="description" size="80" value="[% agreement.description | html %]" />
-                <li>
-                    <label for="status" class="required">Status: </label>
-                    <select name="status" required="required" required="required">
-                        <option value=""></option>
-                        [% PROCESS options_for_authorised_values authorised_values => AuthorisedValues.GetAuthValueDropbox( 'ERM_AGREEMENT_STATUS' ), selected_av => agreement.status %]
-                    </select>
-                    <span class="required">Required</span>
-                </li>
-                <li>
-                    <label for="closure_reason">Closure reason: </label>
-                    <select name="closure_reason">
-                        <option value=""></option>
-                        [% PROCESS options_for_authorised_values authorised_values => AuthorisedValues.GetAuthValueDropbox( 'ERM_AGREEMENT_CLOSURE_REASON' ), selected_av => agreement.closure_reason %]
-                    </select>
-                </li>
-                <li>
-                    <label for="is_perpetual">Is perpetual: </label>
-                    <label for="is_perpetual_yes"> Yes
-                        [% IF agreement.is_perpetual %]
-                            <input type="radio" id="is_perpetual_yes" name="is_perpetual" value="1" checked="checked">
-                        [% ELSE %]
-                            <input type="radio" id="is_perpetual_yes" name="is_perpetual" value="1">
-                        [% END %]
-                    </label>
-                    <label for="is_perpetual_no"> No
-                        [% IF agreement.is_perpetual %]
-                            <input type="radio" id="is_perpetual_no" name="is_perpetual" value="0">
-                        [% ELSE %]
-                            <input type="radio" id="is_perpetual_no" name="is_perpetual" value="0" checked="checked">
-                        [% END %]
-                    </label>
-                </li>
-                <li>
-                    <label for="renewal_priority">Renewal priority: </label>
-                    <select name="renewal_priority">
-                        <option value=""></option>
-                        [% PROCESS options_for_authorised_values authorised_values => AuthorisedValues.GetAuthValueDropbox( 'ERM_AGREEMENT_RENEWAL_PRIORITY' ), selected_av => agreement.renewal_priority %]
-                    </select>
-                <li>
-                    <label for="license_info">License info: </label>
-                    <input type="text" name="license_info" id="license_info" size="80" value="[% agreement.license_info | html %]" />
-                </li>
-            </ol>
-        </fieldset>
-
-[% BLOCK agreement_period %]
-    <fieldset class="agreement_period">
-    <legend>
-        Agreement period <span class="period_count">[% id | html %]</span>
-        <a href="#" class="remove_period"><i class="fa fa-trash"></i> Remove this period</a>
-    </legend>
-    <input type="hidden" name="period_unique_id" value="[% id | html %]" />
-    <ol>
-        <li>
-            <label for="started_on" class="required">Start date: </label>
-            <input type="text" id="started_on_[% id | html %]" class="started_on" name="started_on_[% id | html %]" value="[% p.started_on | $KohaDates %]" class="flatpickr" data-date_to="ended_on_[% id | html %]" />
-            <span class="required">Required</span>
-            <div class="hint">[% INCLUDE 'date-format.inc' %]</div>
-        </li>
-        <li>
-            <label for="ended_on">End date: </label>
-            <input type="text" id="ended_on_[% id | html %]" class="ended_on" name="ended_on_[% id | html %]" value="[% p.ended_on | $KohaDates %]" class="flatpickr" />
-            <div class="hint">[% INCLUDE 'date-format.inc' %]</div>
-        </li>
-        <li>
-            <label>Cancellation deadline: </label>
-            <input type="text" class="cancellation_deadline" name="cancellation_deadline_[% id | html %]" value="[% p.cancellation_deadline | $KohaDates %]" class="flatpickr" />
-            <div class="hint">[% INCLUDE 'date-format.inc' %]</div>
-        </li>
-        <li>
-            <label>Notes: </label>
-            <input type="text" class="notes" name="notes_[% id | html %]" value="[% p.notes | html %]" />
-        </li>
-    </ol>
-    </fieldset>
-[% END %]
-
-        <fieldset class="rows" id="agreement_periods">
-            <legend>Periods</legend>
-            [% IF agreement.periods.count %]
-                [% FOR p IN agreement.periods %]
-                    [% PROCESS agreement_period period => p, id => loop.count %]
-                [% END %]
-            [% ELSE %]
-                [% PROCESS agreement_period id => 1 %]
-            [% END %]
-            <button class="add_new_period" type="button" class="btn btn-primary"><i class="fa fa-plus" aria-hidden="true"></i> Add new period</button>
-        </fieldset>
-
-[% BLOCK agreement_user %]
-    <fieldset class="agreement_user">
-        <legend>
-            User <span class="user_count">[% id | html %]</span>
-            <a href="#" class="remove_user"><i class="fa fa-trash"></i> Remove this user</a>
-        </legend>
-        <input type="hidden" name="user_unique_id" value="[% id | html %]" />
-
-        <ol>
-            <li>
-                <label for="user" class="required">User: </label>
-                <span class="user">
-                    [% IF u %]
-                        <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% u.user_id| uri %]">
-                            [% INCLUDE 'patron-title.inc' patron = u.patron %]
-                        </a>
-                        <input type="hidden" class="user_id" name="user_id_[% id | html %]" value="[% u.user_id %]" />
-                    [% END %]
-                </span>
-                (<a href="#" class="pick_user" class="btn btn-default">Select user</a>)
-            </li>
-
-            <li>
-                <label>Role: </label>
-                <select class="user_role" name="user_role_[% id | html %]">
-                    <option value=""></option>
-                    [% PROCESS options_for_authorised_values authorised_values => AuthorisedValues.GetAuthValueDropbox( 'ERM_AGREEMENT_USER_ROLES' ), selected_av => u.role %]
-                </select>
-            </li>
-        </ol>
-    </fieldset>
-[% END %]
-
-        <fieldset class="rows">
-            <legend>Users</legend>
-            [% IF agreement.user_roles.count %]
-                [% FOR u IN agreement.user_roles %]
-                    [% PROCESS agreement_user user => u, id => loop.count %]
-                [% END %]
-            [% ELSE %]
-                [% PROCESS agreement_user, id => 1 %]
-            [% END %]
-
-            <button class="add_new_user_block" type="button" class="btn btn-primary"><i class="fa fa-plus" aria-hidden="true"></i> Add new user</button>
-        </fieldset>
-
-        <fieldset class="action">
-            <input type="submit" value="Submit" />
-            <a class="cancel" href="/cgi-bin/koha/erm/agreements.pl">Cancel</a>
-        </fieldset>
-    </form>
-[% END %]
-
-[% IF op == 'delete_confirm' %]
-    <div class="dialog alert">
-        <h3>Delete agreement "[% agreement.agreement_id | html %]?"</h3>
-        <table>
-            <tr><th>Agreement id</th>
-                <td>[% agreement.agreement_id| html %]</td>
-            </tr>
-            <tr><th>Vendor</th>
-                <td>[% agreement.vendor_id| html %]</td>
-            </tr>
-            <tr><th>Name</th>
-                <td>[% agreement.name| html %]</td>
-            </tr>
-            <tr><th>Description</th>
-                <td>[% agreement.description| html %]</td>
-            </tr>
-            <tr><th>Status</th>
-                <td>[% agreement.status| html %]</td>
-            </tr>
-            <tr><th>Closure_reason</th>
-                <td>[% agreement.closure_reason| html %]</td>
-            </tr>
-            <tr><th>Is perpetual</th>
-                <td>[% agreement.is_perpetual| html %]</td>
-            </tr>
-            <tr><th>Renewal priority</th>
-                <td>[% agreement.renewal_priority| html %]</td>
-            </tr>
-            <tr><th>License info</th>
-                <td>[% agreement.license_info| html %]</td>
-            </tr>
-        </table>
-        <form action="/cgi-bin/koha/erm/agreements.pl" method="post">
-            <input type="hidden" name="op" value="delete_confirmed" />
-            <input type="hidden" name="agreement_id" value="[% agreement.agreement_id | html %]" />
-            <button type="submit" class="approve"><i class="fa fa-fw fa-check"></i> Yes, delete</button>
-        </form>
-        <form action="/cgi-bin/koha/erm/agreements.pl" method="get">
-            <button type="submit" class="deny"><i class="fa fa-fw fa-remove"></i> No, do not delete</button>
-        </form>
-    </div>
-[% END %]
-
-[% IF op == 'list' %]
-
-    <div id="toolbar" class="btn-toolbar">
-        <a class="btn btn-default" id="newagreement" href="/cgi-bin/koha/erm/agreements.pl?op=add_form"><i class="fa fa-plus"></i> New agreement</a>
-    </div>
-
-    <h2>Agreements</h2>
-    [% IF agreement_name_filter %]
-        Searching: [% agreement_name_filter | html %]
-    [% END %]
-
-    [% IF agreements_count > 0 %]
-        <div class="table_agreements_table_controls"></div>
-        <table id="table_agreements">
-            <thead>
-                <tr>
-                    <th>ID</th>
-                    <th>Vendor</th>
-                    <th>Name</th>
-                    <th>Description</th>
-                    <th>Status</th>
-                    <th>Closure reason</th>
-                    <th>Is perpetual</th>
-                    <th>Renewal priority</th>
-                    <th data-class-name="actions noExport">Actions</th>
-                </tr>
-            </thead>
-        </table>
-    [% ELSE %]
-        <div class="dialog message">
-            There are no agreements defined. <a href="/cgi-bin/koha/erm/agreements.pl?op=add_form">Create a new agreement</a>.
-        </div>
-    [% END %]
-[% END %]
+                <div id="agreements"></div>
 
             </main>
         </div> <!-- /.col-sm-10.col-sm-push-2 -->
     [% INCLUDE 'datatables.inc' %]
     [% INCLUDE 'columns_settings.inc' %]
     [% INCLUDE 'js-patron-format.inc' %]
+    [% INCLUDE 'js-date-format.inc' %]
+
     <script>
 
         const agreement_statuses = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_STATUS')) | $raw %];
-        let agreement_statuses_map = agreement_statuses.reduce((map, e) => {
-            map[e.authorised_value] = e;
-            return map;
-        }, {});
+
         const agreement_closure_reasons = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_CLOSURE_REASON')) | $raw %];
-        let agreement_closure_reasons_map = agreement_closure_reasons.reduce((map, e) => {
-            map[e.authorised_value] = e;
-            return map;
-        }, {});
         const agreement_renewal_priorities = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_RENEWAL_PRIORITY')) | $raw %];
-        let agreement_renewal_priorities_map = agreement_renewal_priorities.reduce((map, e) => {
-            map[e.authorised_value] = e;
-            return map;
-        }, {});
-
+        const agreement_user_roles = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_USER_ROLES')) | $raw %];
 
-        let current_user_node;
         var columns_settings = [% TablesSettings.GetColumns( 'erm', 'agreements', 'table_agreements', 'json' ) | $raw %];
-        $(document).ready(function() {
-            var agreements_table_url = '/api/v1/erm/agreements?';
+        var agreements_table_url = '/api/v1/erm/agreements?';
 
         [% IF agreement_name_filter %]
             var agreement_name_filter = {
             };
             agreements_table_url += 'q='+ encodeURIComponent(JSON.stringify(agreement_name_filter));
         [% END %]
+    </script>
 
-            var agreements_table = $("#table_agreements").kohaTable({
-                "ajax": {
-                    "url": agreements_table_url
-                },
-                "order": [[ 1, "asc" ]],
-                "columns": [
-                    {
-                        "data": "agreement_id",
-                        "searchable": true,
-                        "orderable": true
-                    },
-                    {
-                        "data": "vendor_id",
-                        "searchable": true,
-                        "orderable": true
-                    },
-                    {
-                        "data": "name",
-                        "searchable": true,
-                        "orderable": true
-                    },
-                    {
-                        "data": "description",
-                        "searchable": true,
-                        "orderable": true
-                    },
-                    {
-                        "data": "status",
-                        "searchable": true,
-                        "orderable": true,
-                        "render": function( data, type, row, meta ) {
-                            return escape_str(agreement_statuses_map[row.status].lib);
-                        }
-                    },
-                    {
-                        "data": "closure_reason",
-                        "searchable": true,
-                        "orderable": true,
-                        "render": function( data, type, row, meta ) {
-                            return row.closure_reason != undefined && row.closure_reason != "" ? escape_str(agreement_closure_reasons_map[row.closure_reason].lib) : "";
-                        }
-                    },
-                    {
-                        "data": "is_perpetual",
-                        "searchable": true,
-                        "orderable": true,
-                        "render": function( data, type, row, meta ) {
-                            return escape_str(row.is_perpetual ? _("Yes") : _("No"));
-                        }
-                    },
-                    {
-                        "data": "renewal_priority",
-                        "searchable": true,
-                        "orderable": true,
-                        "render": function( data, type, row, meta ) {
-                            return row.renewal_priority != undefined && row.renewal_priority != "" ? escape_str(agreement_renewal_priorities_map[row.renewal_priority].lib) : "";
-                        }
-                    },
-                    {
-                        "data": function( row, type, val, meta ) {
-
-                            var result = '<a class="btn btn-default btn-xs" role="button" href="/cgi-bin/koha/erm/agreements.pl?op=add_form&amp;agreement_id='+ encodeURIComponent(row.agreement_id) +'"><i class="fa fa-pencil" aria-hidden="true"></i> '+_("Edit")+'</a>'+"\n";
-                            result += '<a class="btn btn-default btn-xs" role="button" href="/cgi-bin/koha/erm/agreements.pl?op=delete_confirm&amp;agreement_id='+ encodeURIComponent(row.agreement_id) +'"><i class="fa fa-trash" aria-hidden="true"></i> '+_("Delete")+'</a>';
-                            return result;
-
-                        },
-                        "searchable": false,
-                        "orderable": false
-                    }
-                ]
-            }, columns_settings, 1);
-
-            $(".add_new_period").on("click", function(e){
-                e.preventDefault();
-                let first_period_block = $("fieldset.agreement_period:first");
-                if ( first_period_block.is(":visible") ) {
-                    let new_period_block = first_period_block.clone(1);
-                    new_period_block.find("input").val("");
-                    $(new_period_block).insertBefore(this);
-                } else {
-                    first_period_block.show();
-                }
-                update_period_count();
-            });
-            $(".remove_period").on("click", function(e){
-                e.preventDefault();
-                let fieldset = $(this).parent().parent();
-                if ( $("fieldset.agreement_period").length == 1 ) {
-                    clear_block(fieldset);
-                    fieldset.hide();
-                } else {
-                    fieldset.remove();
-                }
-                update_period_count();
-            });
-
-            $(".add_new_user_block").on("click",function(e){
-                e.preventDefault();
-                let first_user_block = $("fieldset.agreement_user:first");
-                if ( first_user_block.is(":visible") ) {
-                    let new_user_block = $("fieldset.agreement_user:first").clone(1);
-                    new_user_block.find("span.user").empty();
-                    clear_block(new_user_block);
-                    $(new_user_block).insertBefore(this);
-                } else {
-                    first_user_block.show();
-                }
-
-                update_user_count();
-            });
-            $(".remove_user").on("click", function(e){
-                e.preventDefault();
-                let fieldset = $(this).parent().parent();
-                if ( $("fieldset.agreement_user").length == 1 ) {
-                    fieldset.find("span.user").empty();
-                    clear_block(fieldset);
-                    fieldset.hide();
-                } else {
-                    fieldset.remove();
-                }
-
-                update_user_count();
-            });
-
-            $(".pick_user").on("click", function(e){
-                e.preventDefault();
-                current_user_node = $(this).closest("fieldset");
-                window.open("/cgi-bin/koha/members/search.pl?columns=cardnumber,name,category,branch,action&selection_type=select&filter=erm_users",
-                    'PatronPopup',
-                    'width=740,height=450,location=yes,toolbar=no,'
-                    + 'scrollbars=yes,resize=yes'
-                );
-            });
-
-            update_period_count();
-            update_user_count();
-        });
-
-        function clear_block(block){
-            $(block).find('input').val("");
-            $(block).find("select option:first-child").attr("selected", "selected");
-        }
-        function update_period_count(){
-            $("fieldset.agreement_period").each(function(i, period){
-                let id = i + 1;
-                $(period).find(".period_count").text(id);
-                $(period).find("input[name='period_unique_id']").val(id);
-
-                let ended_on_input = $(period).find("input.ended_on");
-                $(ended_on_input).attr("id", "ended_on_" + id);
-                $(ended_on_input).attr("name", "ended_on_" + id);
-                $(ended_on_input).flatpickr();
-
-                let started_on_input = $(period).find("input.started_on");
-                $(started_on_input).attr("name", "started_on_" + id);
-                $(started_on_input).data("date_to", "ended_on_" + id);
-                $(started_on_input).flatpickr();
-
-                let cancellation_deadline_input = $(period).find("input.cancellation_deadline");
-                $(cancellation_deadline_input).attr("name", "cancellation_deadline_" + id);
-                $(cancellation_deadline_input).flatpickr();
-
-                $(period).find(".notes").attr("name", "notes_" + id);
-                $(period).attr('id', 'agreement_period_' + id);
-            });
-        }
-
-        function update_user_count(){
-            $("fieldset.agreement_user").each(function(i, user){
-                let id = i + 1;
-                let remove_user_link = $(this).find(".remove_user");
-                $(user).find(".user_count").text(id);
-                $(user).find("input[name='user_unique_id']").val(id);
-
-                $(user).find(".user_id").attr("name", "user_id_" + id);
-                $(user).find(".user_role").attr("name", "user_role_" + id);
-            });
-        }
-
-        function select_user(borrowernumber, patron) {
-            patron['patron_id'] = borrowernumber;
-            let unique_id = $(current_user_node).find("input[name='user_unique_id']").val();
-            let a = '<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber='
-                + borrowernumber + '">' + $patron_to_html(patron) + '</a> '
-                + '<input type="hidden" class="user_id" name="user_id_' + unique_id + '" value="' + borrowernumber + '" />';
-            $(current_user_node).find("span.user").html(a);
-        }
-
+    [% Asset.js("js/vue/dist/main.js") %]
+    [% Asset.js("js/vue/dist/runtime.js") %]
 
-    </script>
 [% END %]
 [% INCLUDE 'intranet-bottom.inc' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/Agreements.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/Agreements.vue
new file mode 100644 (file)
index 0000000..813b769
--- /dev/null
@@ -0,0 +1,116 @@
+<template>
+    <Toolbar v-if="op == 'list'" @switch-view="switchView" />
+    <b-alert v-if="message" variant="success" show>{{ message }}</b-alert>
+    <b-alert v-if="error" variant="warning" show>{{ error }}</b-alert>
+    <List
+        v-if="op == 'list'"
+        :vendors="vendors"
+        :av_statuses="statuses"
+        :av_closure_reasons="closure_reasons"
+        :av_renewal_priorities="renewal_priorities"
+        @set-current-agreement-id="setCurrentAgreementID"
+        @switch-view="switchView"
+        @set-error="setError"
+    />
+    <AddForm
+        v-if="op == 'add-form'"
+        :agreement_id="agreement_id"
+        :vendors="vendors"
+        :av_statuses="statuses"
+        :av_closure_reasons="closure_reasons"
+        :av_renewal_priorities="renewal_priorities"
+        :av_user_roles="user_roles"
+        @agreement-created="agreementCreated"
+        @agreement-updated="agreementUpdated"
+        @set-error="setError"
+        @switch-view="switchView"
+    />
+    <ConfirmDeleteForm
+        v-if="op == 'confirm-delete-form'"
+        :agreement_id="agreement_id"
+        @agreement-deleted="agreementDeleted"
+        @set-error="setError"
+        @switch-view="switchView"
+    />
+</template>
+
+<script>
+import Toolbar from "./components/ERM/AgreementsToolbar.vue"
+import List from "./components/ERM/AgreementsList.vue"
+import AddForm from "./components/ERM/AgreementsFormAdd.vue"
+import ConfirmDeleteForm from "./components/ERM/AgreementsFormConfirmDelete.vue"
+
+import { reactive, computed } from "vue"
+
+export default {
+    data() {
+        return {
+            agreement_id: null,
+            op: "list",
+            message: null,
+            error: null,
+            vendors: [],
+            statuses: agreement_statuses,
+            closure_reasons: agreement_closure_reasons,
+            renewal_priorities: agreement_renewal_priorities,
+            user_roles: agreement_user_roles,
+        }
+    },
+    beforeCreate() {
+        // FIXME it's not only called on setup, but setup() does not have 'this'.
+        const apiUrl = "/api/v1/acquisitions/vendors"
+
+        fetch(apiUrl)
+            .then((res) => res.json())
+            .then(
+                (result) => {
+                    this.vendors = result
+            }).catch(
+                (error) => {
+                    this.$emit('set-error', error)
+                }
+            )
+
+    },
+    methods: {
+        switchView(view) {
+            this.message = null
+            this.error = null
+            this.op = view
+            if (view == "list") this.agreement_id = null
+        },
+        agreementCreated() {
+            this.message = "Agreement created"
+            this.error = null
+            this.agreement_id = null
+            this.op = "list"
+        },
+        agreementUpdated() {
+            this.message = "Agreement updated"
+            this.error = null
+            this.agreement_id = null
+            this.op = "list"
+        },
+        agreementDeleted() {
+            this.message = "Agreement deleted"
+            this.error = null
+            this.agreement_id = null
+            this.op = "list"
+        },
+        setCurrentAgreementID(agreement_id) {
+            this.agreement_id = agreement_id
+        },
+        setError(error) {
+            this.message = null
+            this.error = "Something went wrong: " + error
+        },
+    },
+    components: {
+        Toolbar,
+        List,
+        AddForm,
+        ConfirmDeleteForm,
+    },
+    emits: ["set-error"],
+};
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementPeriods.vue
new file mode 100644 (file)
index 0000000..1d914dc
--- /dev/null
@@ -0,0 +1,98 @@
+<template>
+    <fieldset class="rows" id="agreement_periods">
+        <legend>Periods</legend>
+        <fieldset
+            class="rows"
+            v-for="(period, counter) in periods"
+            v-bind:key="counter"
+        >
+            <legend>
+                Agreement period {{ counter + 1 }}
+                <a href="#" @click.prevent="deletePeriod(counter)"
+                    ><i class="fa fa-trash"></i> Remove this period</a
+                >
+            </legend>
+            <ol>
+                <li>
+                    <label :for="`started_on_${counter}`" class="required"
+                        >Start date:
+                    </label>
+                    <flat-pickr
+                        v-model="period.started_on"
+                        required
+                        :config="fp_config"
+                        :data-date_to="`ended_on_${counter}`"
+                    />
+                    <span class="required">Required</span>
+                </li>
+                <li>
+                    <label :for="`ended_on_${counter}`">End date: </label>
+                    <flat-pickr
+                        :id="`ended_on_${counter}`"
+                        v-model="period.ended_on"
+                        :config="fp_config"
+                    />
+                </li>
+                <li>
+                    <label :for="`cancellation_deadline_${counter}`"
+                        >Cancellation deadline:
+                    </label>
+                    <flat-pickr
+                        :id="`cancellation_deadline_${counter}`"
+                        v-model="period.cancellation_deadline"
+                        :config="fp_config"
+                    />
+                </li>
+                <li>
+                    <label :for="`notes_${counter}`">Notes: </label>
+                    <input
+                        type="text"
+                        class="notes"
+                        :name="`notes_${counter}`"
+                        v-model="period.notes"
+                    />
+                </li>
+            </ol>
+        </fieldset>
+        <button @click="addPeriod" type="button" class="btn btn-primary">
+            <i class="fa fa-plus" aria-hidden="true"></i> Add new period
+        </button>
+    </fieldset>
+</template>
+
+<script>
+import flatPickr from 'vue-flatpickr-component'
+export default {
+    name: 'AgreementPeriods',
+    data() {
+        return { fp_config: flatpickr_defaults, dates_fixed: 0 }
+    },
+    props: {
+        periods: Array
+    },
+    beforeUpdate() {
+        if (!this.dates_fixed) {
+            this.periods.forEach(p => {
+                p.started_on = $date(p.started_on)
+            })
+            this.dates_fixed = 1
+        }
+    },
+    methods: {
+        addPeriod() {
+            this.periods.push({
+                started_on: null,
+                ended_on: null,
+                cancellation_deadline: null,
+                notes: null,
+            })
+        },
+        deletePeriod(counter) {
+            this.periods.splice(counter, 1)
+        }
+    },
+    components: {
+        flatPickr
+    }
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementUserRoles.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementUserRoles.vue
new file mode 100644 (file)
index 0000000..6571f52
--- /dev/null
@@ -0,0 +1,112 @@
+<template>
+    <fieldset class="rows" id="agreement_user_roles">
+        <legend>Users</legend>
+        <fieldset
+            class="rows"
+            v-for="(user_role, counter) in user_roles"
+            v-bind:key="counter"
+        >
+            <legend>
+                Agreement user {{ counter + 1 }}
+                <a href="#" @click.prevent="deleteUser(counter)"
+                    ><i class="fa fa-trash"></i> Remove this user</a
+                >
+            </legend>
+            <ol>
+                <li>
+                    <label :for="`user_id_${counter}`">User: </label>
+                    <span class="user">
+                        {{ user_role.patron_str }}
+                    </span>
+                    (<a
+                        href="#"
+                        @click="selectUser(counter)"
+                        class="btn btn-default"
+                        >Select user</a
+                    >)
+                </li>
+                <li>
+                    <label :for="`user_role_${counter}`">Role: </label>
+                    <b-form-select v-model="user_role.role" required>
+                        <b-form-select-option value=""></b-form-select-option>
+                        <b-form-select-option
+                            v-for="r in av_user_roles"
+                            :key="r.authorised_values"
+                            :value="r.authorised_value"
+                            :selected="
+                                r.authorised_value == user_role.role
+                                    ? true
+                                    : false
+                            "
+                            >{{ r.lib }}</b-form-select-option
+                        >
+                    </b-form-select>
+                    <span class="required">Required</span>
+                </li>
+            </ol>
+        </fieldset>
+        <input
+            type="hidden"
+            name="selected_patron_id"
+            id="selected_patron_id"
+        />
+        <button @click="addUser" type="button" class="btn btn-primary">
+            <i class="fa fa-plus" aria-hidden="true"></i> Add new user
+        </button>
+    </fieldset>
+</template>
+
+<script>
+export default {
+    name: 'AgreementUserRoles',
+    props: {
+        av_user_roles: Array,
+        user_roles: Array,
+    },
+    beforeUpdate() {
+        this.user_roles.forEach(u => {
+            u.patron_str = $patron_to_html(u.patron)
+        })
+    },
+    methods: {
+        addUser() {
+            this.user_roles.push({
+                user_id: null,
+                role: null,
+                patron_str: '',
+            })
+        },
+        deleteUser(counter) {
+            this.user_roles.splice(counter, 1)
+        },
+        selectUser(counter) {
+            let select_user_window = window.open("/cgi-bin/koha/members/search.pl?columns=cardnumber,name,category,branch,action&selection_type=select&filter=erm_users",
+                'PatronPopup',
+                'width=740,height=450,location=yes,toolbar=no,'
+                + 'scrollbars=yes,resize=yes'
+            )
+            // This is a bit dirty, the "select user" window should be rewritten and be a Vue component
+            // but that's not for now...
+            select_user_window.addEventListener('beforeunload', this.newUserSelected, false)
+            select_user_window.counter = counter
+        },
+        newUserSelected(e) {
+            let c = e.currentTarget.counter
+            let selected_patron_id = document.getElementById("selected_patron_id").value
+            fetch('/api/v1/patrons/' + selected_patron_id)
+                .then(res => res.json())
+                .then(
+                    (result) => {
+                        this.user_roles[c].patron = result
+                        this.user_roles[c].patron_str = $patron_to_html(result)
+                        this.user_roles[c].user_id = result.patron_id
+                    }).catch(
+                        (error) => {
+                            this.$emit('set-error', error)
+                        }
+                )
+
+        }
+    },
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonDelete.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonDelete.vue
new file mode 100644 (file)
index 0000000..6f82ae9
--- /dev/null
@@ -0,0 +1,9 @@
+<template>
+    <button variant="default" size="sm">Delete</button>
+</template>
+
+<script>
+export default {
+    name: "AgreementsButtonDelete"
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonEdit.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonEdit.vue
new file mode 100644 (file)
index 0000000..ea7f6a6
--- /dev/null
@@ -0,0 +1,9 @@
+<template>
+    <button variant="default" size="sm">Edit</button>
+</template>
+
+<script>
+export default {
+    name: "AgreementsButtonEdit",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsFormAdd.vue
new file mode 100644 (file)
index 0000000..0f81913
--- /dev/null
@@ -0,0 +1,283 @@
+<template>
+    <h2 v-if="agreement.agreement_id">Edit agreement</h2>
+    <h2 v-else>New agreement</h2>
+    <div>
+        <b-form @submit="onSubmit">
+            <b-form-group
+                id="agreement_name"
+                label="Agreement name:"
+                label-for="agreement_name"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-input
+                    id="agreement_name"
+                    v-model="agreement.name"
+                    placeholder="Agreement name"
+                    required
+                ></b-form-input>
+                <span class="required">Required</span>
+            </b-form-group>
+            <b-form-group
+                id="agreement_vendor_id"
+                label="Vendor:"
+                label-for="agreement_vendor_id"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-select v-model="agreement.vendor_id">
+                    <b-form-select-option value=""></b-form-select-option>
+                    <b-form-select-option
+                        v-for="vendor in vendors"
+                        :key="vendor.vendor_id"
+                        :value="vendor.id"
+                        :selected="
+                            vendor.id == agreement.vendor_id ? true : false
+                        "
+                        >{{ vendor.name }}</b-form-select-option
+                    >
+                </b-form-select>
+            </b-form-group>
+            <b-form-group
+                id="agreement_description"
+                label="Description"
+                label-for="agreement_description"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-input
+                    id="agreement_description"
+                    v-model="agreement.description"
+                    placeholder="Description"
+                    required
+                ></b-form-input>
+                <span class="required">Required</span>
+            </b-form-group>
+            <b-form-group
+                id="agreement_status"
+                label="Status:"
+                label-for="agreement_status"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-select
+                    id="agreement_status"
+                    v-model="agreement.status"
+                    @change="onStatusChange($event)"
+                    required
+                >
+                    <b-form-select-option value=""></b-form-select-option>
+                    <b-form-select-option
+                        v-for="status in av_statuses"
+                        :key="status.authorised_values"
+                        :value="status.authorised_value"
+                        :selected="
+                            status.authorised_value == agreement.status
+                                ? true
+                                : false
+                        "
+                        >{{ status.lib }}</b-form-select-option
+                    >
+                </b-form-select>
+                <span class="required">Required</span>
+            </b-form-group>
+            <b-form-group
+                id="agreement_closure_reason"
+                label="Closure reason:"
+                label-for="agreement_closure_reason"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-select
+                    id="agreement_closure_reason"
+                    v-model="agreement.closure_reason"
+                    :disabled="agreement.status == 'closed' ? true : false"
+                >
+                    <b-form-select-option value=""></b-form-select-option>
+                    <b-form-select-option
+                        v-for="r in av_closure_reasons"
+                        :key="r.authorised_values"
+                        :value="r.authorised_value"
+                        :selected="
+                            r.authorised_value == agreement.closure_reason
+                                ? true
+                                : false
+                        "
+                        >{{ r.lib }}</b-form-select-option
+                    >
+                </b-form-select>
+            </b-form-group>
+            <b-form-group
+                label="Is perpetual:"
+                label-for="agreement_is_perpetual"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-radio-group
+                    id="agreement_is_perpetual"
+                    label="Is perpetual:"
+                    label-for="agreement_is_perpetual"
+                    label-cols="4"
+                    label-cols-lg="2"
+                    v-model="agreement.is_perpetual"
+                    :options="is_perpetual_options"
+                >
+                </b-form-radio-group>
+            </b-form-group>
+            <b-form-group
+                id="agreement_renewal_priority"
+                label="Renewal priority:"
+                label-for="agreement_renewal_priority"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-select v-model="agreement.renewal_priority">
+                    <b-form-select-option value=""></b-form-select-option>
+                    <b-form-select-option
+                        v-for="p in av_renewal_priorities"
+                        :key="p.authorised_values"
+                        :value="p.authorised_value"
+                        :selected="
+                            p.authorised_value == agreement.renewal_priority
+                                ? true
+                                : false
+                        "
+                        >{{ p.lib }}</b-form-select-option
+                    >
+                </b-form-select>
+            </b-form-group>
+            <b-form-group
+                id="agreement_license_info"
+                label="License info:"
+                label-for="agreement_license_info"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                <b-form-input
+                    id="agreement_license_info"
+                    v-model="agreement.license_info"
+                    placeholder="License info"
+                ></b-form-input>
+            </b-form-group>
+
+            <AgreementPeriods :periods="agreement.periods" />
+            <AgreementUserRoles
+                :user_roles="agreement.user_roles"
+                :av_user_roles="av_user_roles"
+            />
+
+            <b-button type="submit" variant="primary">Submit</b-button>
+            <a href="#" @click="$emit('switch-view', 'list')">Cancel</a>
+        </b-form>
+    </div>
+</template>
+
+<script>
+import AgreementPeriods from './AgreementPeriods.vue'
+import AgreementUserRoles from './AgreementUserRoles.vue'
+
+export default {
+    data() {
+        return {
+            is_perpetual_options: [{ text: "Yes", value: true }, { text: "No", value: false }],
+            agreement: {
+                agreement_id: null,
+                name: '',
+                vendor_id: null,
+                description: '',
+                status: '',
+                closure_reason: '',
+                is_perpetual: false,
+                renewal_priority: '',
+                license_info: '',
+                periods: [],
+                user_roles: [],
+            }
+        }
+    },
+    created() {
+        if (!this.agreement_id) return
+        const apiUrl = '/api/v1/erm/agreements/' + this.agreement_id
+
+        fetch(apiUrl, {
+            headers: {
+                'x-koha-embed': 'periods,user_roles,user_roles.patron'
+            }
+        })
+            .then(res => res.json())
+            .then(
+                (result) => {
+                    this.agreement = result
+                }
+            )
+    },
+    methods: {
+        onSubmit() {
+
+            //let agreement= Object.assign( {} ,this.agreement); // copy
+            let agreement = JSON.parse(JSON.stringify(this.agreement)) // copy
+            let apiUrl = '/api/v1/erm/agreements'
+
+            const myHeaders = new Headers()
+            myHeaders.append('Content-Type', 'application/json')
+
+            let method = 'POST'
+            if (agreement.agreement_id) {
+                method = 'PUT'
+                apiUrl += '/' + agreement.agreement_id
+            }
+            delete agreement.agreement_id
+
+            agreement.periods.forEach(p => {
+                p.started_on = $date_to_rfc3339(p.started_on)
+                p.ended_on = p.ended_on ? $date_to_rfc3339(p.ended_on) : null
+                p.cancellation_deadline = p.cancellation_deadline ? $date_to_rfc3339(p.cancellation_deadline) : null
+            })
+
+            agreement.periods = agreement.periods.map(({ agreement_id, agreement_period_id, ...keepAttrs }) => keepAttrs)
+
+            agreement.user_roles = agreement.user_roles.map(({ patron, patron_str, ...keepAttrs }) => keepAttrs)
+
+            const options = {
+                method: method,
+                body: JSON.stringify(agreement),
+                myHeaders
+            }
+
+            fetch(apiUrl, options)
+                .then(response => {
+                    if (response.status == 200) {
+                        this.$emit('agreement-updated')
+                    } else if (response.status == 201) {
+                        this.$emit('agreement-created')
+                    } else {
+                        this.$emit('set-error', response.message || response.statusText)
+                    }
+                }).catch(
+                    (error) => {
+                        this.$emit('set-error', error)
+                    }
+                )
+        },
+        onStatusChange(status) {
+            if (status == 'closed') {
+               this.agreement.closure_reason = ''
+            }
+        }
+    },
+    emits: ['agreement-created', 'agreement-updated', 'set-error', 'switch-view'],
+    props: {
+        agreement_id: Number,
+        vendors: Array,
+        av_statuses: Array,
+        av_closure_reasons: Array,
+        av_renewal_priorities: Array,
+        av_user_roles: Array,
+    },
+    components: {
+        AgreementPeriods,
+        AgreementUserRoles
+    },
+    name: "AgreementsFormAdd",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsFormConfirmDelete.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsFormConfirmDelete.vue
new file mode 100644 (file)
index 0000000..36737cd
--- /dev/null
@@ -0,0 +1,96 @@
+<template>
+    <h2>Delete agreement</h2>
+    <div>
+        <b-form @submit="onSubmit">
+            <b-form-group
+                id="agreement_name"
+                label="Agreement name:"
+                label-for="agreement_name"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                {{ agreement.name }}
+            </b-form-group>
+            <b-form-group
+                id="agreement_vendor"
+                label="Vendor:"
+                label-for="agreement_vendor"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                {{ agreement.vendor_id }}
+            </b-form-group>
+            <b-form-group
+                id="agreement_description"
+                label="Description:"
+                label-for="agreement_description"
+                label-cols="4"
+                label-cols-lg="2"
+            >
+                {{ agreement.description }}
+            </b-form-group>
+            <b-button type="submit" variant="primary">Submit</b-button>
+            <a href="#" @click="$emit('switch-view', 'list')">Cancel</a>
+        </b-form>
+    </div>
+</template>
+
+<script>
+
+export default {
+    data() {
+        return {
+            agreement: {},
+        }
+    },
+    created() {
+        const apiUrl = '/api/v1/erm/agreements/' + this.agreement_id
+
+        fetch(apiUrl)
+            .then(res => res.json())
+            .then(
+                (result) => {
+                    this.agreement = result
+                },
+            ).catch(
+                (error) => {
+                    this.$emit('set-error', error)
+                }
+            )
+    },
+    methods: {
+        onSubmit() {
+
+            let apiUrl = '/api/v1/erm/agreements/' + this.agreement_id
+
+            const myHeaders = new Headers()
+            myHeaders.append('Content-Type', 'application/json')
+
+            const options = {
+                method: 'DELETE',
+                myHeaders
+            }
+
+            fetch(apiUrl, options)
+                .then(
+                    (response) => {
+                        if (response.status == 204) {
+                            this.$emit('agreement-deleted')
+                        } else {
+                            this.$emit('set-error', response.message || response.statusText)
+                        }
+                    }
+                ).catch(
+                    (error) => {
+                        this.$emit('set-error', error)
+                    }
+                )
+        }
+    },
+    emits: ['agreement-deleted', 'set-error', 'switch-view'],
+    props: {
+        agreement_id: Number
+    },
+    name: "AgreementsFormConfirmDelete",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue
new file mode 100644 (file)
index 0000000..329ac46
--- /dev/null
@@ -0,0 +1,192 @@
+<template>
+    <div>
+        <table v-if="agreements.length" id="my_table"></table>
+        <div v-else-if="this.initialized" class="dialog message">
+            There are no agreements defined.
+        </div>
+        <div v-else>Loading...</div>
+    </div>
+</template>
+
+<script>
+import AgreementsButtonEdit from "./AgreementsButtonEdit.vue"
+import AgreementsButtonDelete from "./AgreementsButtonDelete.vue"
+import { createVNode, defineComponent, render, resolveComponent } from 'vue'
+export default {
+    created() {
+        const apiUrl = '/api/v1/erm/agreements'
+
+        fetch(apiUrl)
+            .then(res => res.json())
+            .then(
+                (result) => {
+                    this.agreements = result
+                    this.initialized = true
+                },
+            ).catch(
+                (error) => {
+                    this.$emit('set-error', error)
+                }
+            )
+    },
+    updated() {
+        let edit_agreement = this.edit_agreement
+        let delete_agreement = this.delete_agreement
+        let vendors_map = this.vendors.reduce((map, e) => {
+            map[e.id] = e
+            return map
+        }, {})
+        let statuses_map = this.av_statuses.reduce((map, e) => {
+            map[e.authorised_value] = e
+            return map
+        }, {})
+        let closure_reasons_map = this.av_closure_reasons.reduce((map, e) => {
+            map[e.authorised_value] = e
+            return map
+        }, {})
+        let renewal_priorities_map = this.av_renewal_priorities.reduce((map, e) => {
+            map[e.authorised_value] = e
+            return map
+        }, {})
+
+        $('#my_table').kohaTable({
+            "ajax": {
+                "url": agreements_table_url,
+            },
+            "order": [[1, "asc"]],
+            "columnDefs": [{
+                "targets": [0, 1, 2, 3, 4],
+                "render": function (data, type, row, meta) {
+                    if (type == 'display') {
+                        return escape_str(data)
+                    }
+                    return data
+                }
+            }],
+            "columns": [
+                {
+                    "title": __("Agrement ID"),
+                    "data": "agreement_id",
+                    "searchable": true,
+                    "orderable": true
+                },
+                {
+                    "title": __("Vendor"),
+                    "data": "vendor_id",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return row.vendor_id != undefined ? escape_str(vendors_map[row.vendor_id].name) : ""
+                    }
+                },
+                {
+                    "title": __("Name"),
+                    "data": "name",
+                    "searchable": true,
+                    "orderable": true
+                },
+                {
+                    "title": __("description"),
+                    "data": "description",
+                    "searchable": true,
+                    "orderable": true
+                },
+                {
+                    "title": __("Status"),
+                    "data": "status",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return escape_str(statuses_map[row.status].lib)
+                    }
+                },
+                {
+                    "title": __("Closure reason"),
+                    "data": "closure_reason",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return row.closure_reason != undefined && row.closure_reason != "" ? escape_str(closure_reasons_map[row.closure_reason].lib) : ""
+                    }
+                },
+                {
+                    "title": __("Is perpetual"),
+                    "data": "is_perpetual",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return escape_str(row.is_perpetual ? _("Yes") : _("No"))
+                    }
+                },
+                {
+                    "title": __("Renewal priority"),
+                    "data": "renewal_priority",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return row.renewal_priority != undefined && row.renewal_priority != "" ? escape_str(renewal_priorities_map[row.renewal_priority].lib) : ""
+                    }
+                },
+                {
+                    "title": __("Actions"),
+                    "data": function (row, type, val, meta) {
+                        return '<div class="actions" data-agreement_id="' + row.agreement_id + '"></div>'
+                    },
+                    "searchable": false,
+                    "orderable": false
+                }
+            ],
+            drawCallback: function (settings) {
+                $.each($(this).find(".actions"), function (index, e) {
+                    let agreement_id = $(e).data('agreement_id')
+                    let editButton = createVNode(AgreementsButtonEdit, {
+                        onClick: () => {
+                            edit_agreement(agreement_id)
+                        }
+                    })
+                    let deleteButton = createVNode(AgreementsButtonDelete, {
+                        onClick: () => {
+                            delete_agreement(agreement_id)
+                        }
+                    })
+                    let n = createVNode('span', {}, [editButton, deleteButton])
+                    render(n, e)
+                })
+            },
+            preDrawCallback: function(settings){
+                var table_id = settings.nTable.id
+                $("#"+table_id).find("thead th").eq(1).attr('data-filter', 'vendors');
+            }
+
+        }, columns_settings, 1)
+    },
+    beforeUnmount() {
+        $('#my_table')
+            .DataTable()
+            .destroy(true)
+    },
+    data: function () {
+        return {
+            agreements: [],
+            initialized: false,
+        }
+    },
+    methods: {
+        edit_agreement: function (agreement_id) {
+            this.$emit('set-current-agreement-id', agreement_id)
+            this.$emit('switch-view', 'add-form')
+        },
+        delete_agreement: function (agreement_id) {
+            this.$emit('set-current-agreement-id', agreement_id)
+            this.$emit('switch-view', 'confirm-delete-form')
+        },
+    },
+    props: {
+        vendors: Array,
+        av_statuses: Array,
+        av_closure_reasons: Array,
+        av_renewal_priorities: Array,
+    },
+    name: "AgreementsList",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsToolbar.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsToolbar.vue
new file mode 100644 (file)
index 0000000..61198d9
--- /dev/null
@@ -0,0 +1,15 @@
+<template>
+    <b-button
+        @click="$emit('switch-view', 'add-form')"
+        id="new_agreement"
+        variant="default"
+        ><font-awesome-icon icon="plus" /> New agreement</b-button
+    >
+</template>
+
+<script>
+export default {
+    name: "AgreementsToolbar",
+    emits: ['switch-view'],
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/main-erm.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/main-erm.ts
new file mode 100644 (file)
index 0000000..ecb0e1c
--- /dev/null
@@ -0,0 +1,18 @@
+import {createApp} from 'vue'
+import BootstrapVue3 from 'bootstrap-vue-3'
+
+//import 'bootstrap/dist/css/bootstrap.css'
+import 'bootstrap-vue-3/dist/bootstrap-vue-3.css'
+
+import {library} from "@fortawesome/fontawesome-svg-core"
+import {faPlus, faPencil, faTrash} from "@fortawesome/free-solid-svg-icons"
+import {FontAwesomeIcon} from "@fortawesome/vue-fontawesome"
+
+library.add(faPlus, faPencil, faTrash)
+
+import App from './Agreements.vue'
+
+createApp(App)
+    .component("font-awesome-icon", FontAwesomeIcon)
+    .use(BootstrapVue3)
+    .mount('#agreements')
index 163f22a..209a2cb 100644 (file)
@@ -7,7 +7,14 @@
     "test": "test"
   },
   "dependencies": {
-    "bootstrap": "^4.5.2",
+    "@fortawesome/fontawesome-svg-core": "^6.1.0",
+    "@fortawesome/free-solid-svg-icons": "^6.0.0",
+    "@fortawesome/vue-fontawesome": "^3.0.0-5",
+    "@popperjs/core": "^2.11.2",
+    "babel-core": "^7.0.0-beta.3",
+    "bootstrap": "^5.1.3",
+    "bootstrap-vue-3": "^0.1.7",
+    "css-loader": "^6.6.0",
     "gulp": "^4.0.2",
     "gulp-autoprefixer": "^4.0.0",
     "gulp-concat-po": "^1.0.0",
     "js-yaml": "^3.13.1",
     "lodash": "^4.17.12",
     "merge-stream": "^2.0.0",
-    "minimist": "^1.2.5"
+    "minimist": "^1.2.5",
+    "style-loader": "^3.3.1",
+    "vue": "^3.2.31",
+    "vue-flatpickr-component": "^9"
   },
   "scripts": {
     "build": "node_modules/.bin/gulp build",
+    "build_js": "webpack --mode production",
+    "watch_js": "webpack --mode development --watch",
     "css": "node_modules/.bin/gulp css",
     "watch": "node_modules/.bin/gulp watch"
   },
     "postcss": "^8.4.14",
     "stylelint-config-standard-scss": "^5.0.0",
     "stylelint-order": "^5.0.0",
-    "stylelint": "^14.9.1"
+    "stylelint": "^14.9.1",
+    "@babel/core": "^7.17.5",
+    "@babel/preset-env": "^7.16.11",
+    "@vue/compiler-sfc": "^3.2.31",
+    "babel-loader": "^8.2.3",
+    "babelify": "^10.0.0",
+    "browserify": "^17.0.0",
+    "clean-webpack-plugin": "^4.0.0",
+    "gulp-tap": "^1.0.1",
+    "html-webpack-plugin": "^5.5.0",
+    "ts-loader": "^9.2.7",
+    "typescript": "^4.6.2",
+    "vinyl-source-stream": "^2.0.0",
+    "vue-loader": "^17.0.0",
+    "watchify": "^4.0.0",
+    "webpack": "^5.69.1",
+    "webpack-cli": "^4.9.2",
+    "webpack-dev-server": "^4.7.4"
   }
 }
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644 (file)
index 0000000..88ccffe
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  "compilerOptions": {
+  }
+}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644 (file)
index 0000000..efa980d
--- /dev/null
@@ -0,0 +1,36 @@
+const { VueLoaderPlugin } = require("vue-loader");
+const autoprefixer = require("autoprefixer");
+const path = require("path");
+
+module.exports = {
+  entry: {
+    main: "./koha-tmpl/intranet-tmpl/prog/js/vue/main-erm.ts",
+  },
+  output: {
+    filename: "[name].js",
+    path: path.resolve(__dirname, "koha-tmpl/intranet-tmpl/prog/js/vue/dist/"),
+    chunkFilename: "[name].js",
+  },
+  module: {
+    rules: [
+      {
+        test: /\.vue$/,
+        loader: "vue-loader",
+      },
+      {
+        test: /\.ts$/,
+        loader: 'ts-loader',
+        options: {
+          appendTsSuffixTo: [/\.vue$/]
+        }
+      },
+      {
+        test: /\.css$/,
+        use: ['style-loader', 'css-loader']
+      }
+    ],
+  },
+  plugins: [
+    new VueLoaderPlugin(),
+  ],
+};