From 6f1f2a4faf180bd7861bfd0ac349c85d774d4562 Mon Sep 17 00:00:00 2001 From: Mike Rylander Date: Tue, 20 Oct 2015 10:10:28 -0400 Subject: [PATCH] LP#1676608: copy alert and suppression matrix The Copy Alerts feature allows library staff to add customized alert messages to copies. The copy alerts will appear when a specific event takes place, such as when the copy is checked in, checked out, or renewed. Alerts can be temporary or persistent: temporary alerts will be disabled after the initial alert and acknowledgement from staff, while persistent alerts will display each time the alert event takes place. Copy Alerts can be configured to display at the circulating or owning library only or, alternatively, when the library at which the alert event takes place is not the circulating or owning library. Copy Alerts can also be configured to provide options for the next copy status that should be applied to an item. Library administrators will have the ability to create and customize Copy Alert Types and to suppress copy alerts at specific org units. Copy alerts can be added via the volume/creator and the check in, check out, and renew pages. Copy alerts can also be managed at the item status page. Copy alert types can be managed via the Copy Alert Types page in Local Administration, and suppression of them can be adminstered via the Copy Alert Suppression page under Local Administration. Co-authored-by: Galen Charlton Signed-off-by: Mike Rylander Signed-off-by: Galen Charlton Signed-off-by: Mike Rylander --- Open-ILS/examples/fm_IDL.xml | 105 ++++++++ .../perlmods/lib/OpenILS/Application/AppUtils.pm | 8 + .../lib/OpenILS/Application/Cat/AssetCommon.pm | 42 ++- .../lib/OpenILS/Application/Circ/Circulate.pm | 298 ++++++++++++++++++++- Open-ILS/src/sql/Pg/040.schema.asset.sql | 92 +++++++ Open-ILS/src/sql/Pg/live_t/copy_state.pg | 11 + .../src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql | 96 +++++++ .../upgrade/YYYY.data.stock_copy_alert_types.sql | 62 +++++ ...ta.yaous_for_open_circ_exists_fine_handling.sql | 35 +++ .../staff/admin/local/autoGridEditor/acas.tt2 | 30 +++ .../staff/admin/local/autoGridEditor/ccat.tt2 | 92 +++++++ Open-ILS/src/templates/staff/admin/local/index.tt2 | 6 + .../templates/staff/admin/local/t_grid_editor.tt2 | 12 + .../src/templates/staff/admin/local/t_splash.tt2 | 2 + .../src/templates/staff/cat/catalog/t_holdings.tt2 | 8 + Open-ILS/src/templates/staff/cat/item/t_list.tt2 | 11 +- .../templates/staff/cat/item/t_summary_pane.tt2 | 7 +- .../templates/staff/cat/volcopy/t_attr_edit.tt2 | 7 + .../templates/staff/cat/volcopy/t_copy_alerts.tt2 | 94 +++++++ .../src/templates/staff/cat/volcopy/t_defaults.tt2 | 9 + .../staff/circ/checkin/t_checkin_table.tt2 | 11 +- .../src/templates/staff/circ/patron/t_checkout.tt2 | 16 +- .../src/templates/staff/circ/renew/t_renew.tt2 | 12 +- .../templates/staff/circ/share/circ_strings.tt2 | 24 +- Open-ILS/src/templates/staff/css/style.css.tt2 | 4 + .../staff/share/t_add_copy_alert_dialog.tt2 | 40 +++ Open-ILS/src/templates/staff/share/t_autogrid.tt2 | 8 +- .../staff/share/t_copy_alert_editor_dialog.tt2 | 55 ++++ .../staff/share/t_copy_alert_manager_dialog.tt2 | 49 ++++ .../web/js/ui/default/staff/admin/local/app.js | 160 ++++++++++- .../web/js/ui/default/staff/cat/catalog/app.js | 18 ++ Open-ILS/web/js/ui/default/staff/cat/item/app.js | 42 ++- .../js/ui/default/staff/cat/services/holdings.js | 7 +- .../web/js/ui/default/staff/cat/volcopy/app.js | 291 +++++++++++++++++++- .../web/js/ui/default/staff/circ/checkin/app.js | 21 ++ .../js/ui/default/staff/circ/patron/checkout.js | 38 +++ Open-ILS/web/js/ui/default/staff/circ/renew/app.js | 20 ++ .../web/js/ui/default/staff/circ/services/circ.js | 85 ++++-- .../web/js/ui/default/staff/circ/services/item.js | 6 +- Open-ILS/web/js/ui/default/staff/services/grid.js | 35 ++- Open-ILS/web/js/ui/default/staff/services/ui.js | 256 +++++++++++++++++- 41 files changed, 2165 insertions(+), 60 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/live_t/copy_state.pg create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql create mode 100644 Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2 create mode 100644 Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2 create mode 100644 Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2 create mode 100644 Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2 create mode 100644 Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2 create mode 100644 Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2 create mode 100644 Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2 diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 19bce3d532..04f3420596 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -7204,6 +7204,7 @@ SELECT usr, + @@ -7230,6 +7231,7 @@ SELECT usr, + @@ -7247,6 +7249,109 @@ SELECT usr, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm index 4c40d65d69..5ed0e25436 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm @@ -2279,6 +2279,14 @@ sub unique_unnested_numbers { ); } +# Given a list of numbers, turn them into a PG array, skipping undef's +sub intarray2pgarray { + my $class = shift; + no warnings 'numeric'; + + return '{' . join( ',', map(int, grep { defined && /^\d+$/ } @_) ) . '}'; +} + # Check if a transaction should be left open or closed. Close the # transaction if it should be closed or open it otherwise. Returns # undef on success or a failure event. diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm index 14a52d4b63..e52d94f341 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm @@ -258,6 +258,39 @@ sub update_copy_notes { return undef; } +sub update_copy_alerts { + my($class, $editor, $copy) = @_; + + return undef if $copy->isdeleted; + + my $evt; + my $incoming_copy_alerts = $copy->copy_alerts; + + for my $incoming_copy_alert (@$incoming_copy_alerts) { + next unless $incoming_copy_alert; + + if ($incoming_copy_alert->isnew) { + next if ($incoming_copy_alert->isdeleted); # if it was added and deleted in the same session + + my $new_copy_alert = Fieldmapper::asset::copy_alert->new(); + $new_copy_alert->copy( $copy->id ); + $new_copy_alert->temp( $incoming_copy_alert->temp ); + $new_copy_alert->ack_time( $incoming_copy_alert->ack_time ); + $new_copy_alert->note( $incoming_copy_alert->note ); + $new_copy_alert->alert_type( $incoming_copy_alert->alert_type ); + $new_copy_alert->create_staff( $incoming_copy_alert->create_staff || $editor->requestor->id ); + $incoming_copy_alert = $editor->create_asset_copy_alert($new_copy_alert) + or return $editor->event; + } elsif ($incoming_copy_alert->ischanged) { + $incoming_copy_alert = $editor->update_asset_copy_alert($incoming_copy_alert) + } elsif ($incoming_copy_alert->isdeleted) { + $incoming_copy_alert = $editor->delete_asset_copy_alert($incoming_copy_alert->id) + } + + } + + return undef; +} sub update_copy_tags { my($class, $editor, $copy) = @_; @@ -303,8 +336,6 @@ sub update_copy_tags { return undef; } - - sub update_copy { my($class, $editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib) = @_; @@ -415,9 +446,13 @@ sub update_fleshed_copies { my $notes = $copy->notes; $copy->clear_notes; + my $tags = $copy->tags; $copy->clear_tags; + my $copy_alerts = $copy->copy_alerts; + $copy->clear_copy_alerts; + if( $copy->isdeleted ) { $evt = $class->delete_copy($editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib); return $evt if $evt; @@ -445,6 +480,9 @@ sub update_fleshed_copies { $copy->tags( $tags ); $evt = $class->update_copy_tags($editor, $copy); + $copy->copy_alerts( $copy_alerts ); + $evt = $class->update_copy_alerts($editor, $copy); + return $evt if $evt; } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm index ce05bc0c2f..927e3f059b 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm @@ -40,6 +40,18 @@ my $MK_ENV_FLESH = { flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']} }; +# table of cases where suppressing a system-generated copy alerts +# should generate an override of an old-style event +my %COPY_ALERT_OVERRIDES = ( + "CLAIMSRETURNED\tCHECKOUT" => ['CIRC_CLAIMS_RETURNED'], + "CLAIMSRETURNED\tCHECKIN" => ['CIRC_CLAIMS_RETURNED'], + "LOST\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'], + "LONGOVERDUE\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'], + "MISSING\tCHECKOUT" => ['COPY_STATUS_MISSING'], + "DAMAGED\tCHECKOUT" => ['COPY_NOT_AVAILABLE'], + "LOST_AND_PAID\tCHECKOUT" => ['COPY_NOT_AVAILABLE', 'OPEN_CIRCULATION_EXISTS'] +); + sub initialize {} __PACKAGE__->register_method( @@ -155,6 +167,7 @@ sub run_method { my( $self, $conn, $auth, $args ) = @_; translate_legacy_args($args); $args->{override_args} = { all => 1 } unless defined $args->{override_args}; + $args->{new_copy_alerts} ||= $self->api_level > 1 ? 1 : 0; my $api = $self->api_name; my $circulator = @@ -227,13 +240,13 @@ sub run_method { $circulator->is_renewal(1) if $api =~ /renew/; $circulator->is_checkin(1) if $api =~ /checkin/; + $circulator->is_checkout(1) if $api =~ /checkout/; + $circulator->override(1) if $api =~ /override/o; $circulator->mk_env(); $circulator->noop(1) if $circulator->claims_never_checked_out; return circ_events($circulator) if $circulator->bail_out; - - $circulator->override(1) if $api =~ /override/o; if( $api =~ /checkout\.permit/ ) { $circulator->do_permit(); @@ -260,7 +273,6 @@ sub run_method { return $data; } elsif( $api =~ /checkout/ ) { - $circulator->is_checkout(1); $circulator->do_checkout(); } elsif( $circulator->is_res_checkin ) { @@ -270,7 +282,6 @@ sub run_method { $circulator->do_checkin(); } elsif( $api =~ /renew/ ) { - $circulator->is_renewal(1); $circulator->do_renew(); } @@ -404,6 +415,12 @@ my @AUTOLOAD_FIELDS = qw/ copy copy_id copy_barcode + new_copy_alerts + user_copy_alerts + system_copy_alerts + overrides_per_copy_alerts + next_copy_status + copy_state patron patron_id patron_barcode @@ -640,10 +657,233 @@ sub save_trimmed_copy { } } +sub collect_user_copy_alerts { + my $self = shift; + my $e = $self->editor; + + if($self->copy) { + my $alerts = $e->search_asset_copy_alert([ + {copy => $self->copy->id, ack_time => undef}, + {flesh => 1, flesh_fields => { aca => [ qw/ alert_type / ] }} + ]); + if (ref $alerts eq "ARRAY") { + $logger->info("circulator: found " . scalar(@$alerts) . " alerts for copy " . + $self->copy->id); + $self->user_copy_alerts($alerts); + } + } +} + +sub filter_user_copy_alerts { + my $self = shift; + + my $e = $self->editor; + + if(my $alerts = $self->user_copy_alerts) { + + my $suppress_orgs = $U->get_org_full_path($self->circ_lib); + my $suppressions = $e->search_actor_copy_alert_suppress( + {org => $suppress_orgs} + ); + + my @final_alerts; + foreach my $a (@$alerts) { + # filter on event type + if (defined $a->alert_type) { + next if ($a->alert_type->event eq 'CHECKIN' && !$self->is_checkin && !$self->is_renewal); + next if ($a->alert_type->event eq 'CHECKOUT' && !$self->is_checkout && !$self->is_renewal); + next if (defined $a->alert_type->in_renew && $U->is_true($a->alert_type->in_renew) && !$self->is_renewal); + next if (defined $a->alert_type->in_renew && !$U->is_true($a->alert_type->in_renew) && $self->is_renewal); + } + + # filter on suppression + next if (grep { $a->alert_type->id == $_->alert_type} @$suppressions); + + # filter on "only at circ lib" + if (defined $a->alert_type->at_circ) { + my $copy_circ_lib = (ref $self->copy->circ_lib) ? + $self->copy->circ_lib->id : $self->copy->circ_lib; + my $orgs = $U->get_org_descendants($copy_circ_lib); + + if ($U->is_true($a->alert_type->invert_location)) { + next if (grep {$_ == $self->circ_lib} @$orgs); + } else { + next unless (grep {$_ == $self->circ_lib} @$orgs); + } + } + + # filter on "only at owning lib" + if (defined $a->alert_type->at_owning) { + my $copy_owning_lib = (ref $self->volume->owning_lib) ? + $self->volume->owning_lib->id : $self->volume->owning_lib; + my $orgs = $U->get_org_descendants($copy_owning_lib); + + if ($U->is_true($a->alert_type->invert_location)) { + next if (grep {$_ == $self->circ_lib} @$orgs); + } else { + next unless (grep {$_ == $self->circ_lib} @$orgs); + } + } + + $a->alert_type->next_status([$U->unique_unnested_numbers($a->alert_type->next_status)]); + + push @final_alerts, $a; + } + + $self->user_copy_alerts(\@final_alerts); + } +} + +sub generate_system_copy_alerts { + my $self = shift; + return unless($self->copy); + + # don't create system copy alerts if the copy + # is in a normal state; we're assuming that there's + # never a need to generate a popup for each and every + # checkin or checkout of normal items. If this assumption + # proves false, then we'll need to add a way to explicitly specify + # that a copy alert type should never generate a system copy alert + return if $self->copy_state eq 'NORMAL'; + + my $e = $self->editor; + + my $suppress_orgs = $U->get_org_full_path($self->circ_lib); + my $suppressions = $e->search_actor_copy_alert_suppress( + {org => $suppress_orgs} + ); + + # events we care about ... + my $event = []; + push(@$event, 'CHECKIN') if $self->is_checkin; + push(@$event, 'CHECKOUT') if $self->is_checkout; + return unless scalar(@$event); + + my $alert_orgs = $U->get_org_ancestors($self->circ_lib); + my $alert_types = $e->search_config_copy_alert_type({ + active => 't', + scope_org => $alert_orgs, + event => $event, + state => $self->copy_state, + '-or' => [ { in_renew => $self->is_renewal }, { in_renew => undef } ], + }); + + my @final_types; + foreach my $a (@$alert_types) { + # filter on "only at circ lib" + if (defined $a->at_circ) { + my $copy_circ_lib = (ref $self->copy->circ_lib) ? + $self->copy->circ_lib->id : $self->copy->circ_lib; + my $orgs = $U->get_org_descendants($copy_circ_lib); + + if ($U->is_true($a->invert_location)) { + next if (grep {$_ == $self->circ_lib} @$orgs); + } else { + next unless (grep {$_ == $self->circ_lib} @$orgs); + } + } + + # filter on "only at owning lib" + if (defined $a->at_owning) { + my $copy_owning_lib = (ref $self->volume->owning_lib) ? + $self->volume->owning_lib->id : $self->volume->owning_lib; + my $orgs = $U->get_org_descendants($copy_owning_lib); + + if ($U->is_true($a->invert_location)) { + next if (grep {$_ == $self->circ_lib} @$orgs); + } else { + next unless (grep {$_ == $self->circ_lib} @$orgs); + } + } + + push @final_types, $a; + } + + if (@final_types) { + $logger->info("circulator: found " . scalar(@final_types) . " system alert types for copy" . + $self->copy->id); + } + + my @alerts; + + # keep track of conditions corresponding to suppressed + # system alerts, as these may be used to overridee + # certain old-style-events + my %auto_override_conditions = (); + foreach my $t (@final_types) { + if ($t->next_status) { + if (grep { $t->id == $_->alert_type } @$suppressions) { + $t->next_status([]); + } else { + $t->next_status([$U->unique_unnested_numbers($t->next_status)]); + } + } + + my $alert = new Fieldmapper::asset::copy_alert (); + $alert->alert_type($t->id); + $alert->copy($self->copy->id); + $alert->temp(1); + $alert->create_staff($e->requestor->id); + $alert->create_time('now'); + $alert->ack_staff($e->requestor->id); + $alert->ack_time('now'); + + $alert = $e->create_asset_copy_alert($alert); + + next unless $alert; + + $alert->alert_type($t->clone); + + push(@{$self->next_copy_status}, @{$t->next_status}) if ($t->next_status); + if (grep {$_->alert_type == $t->id} @$suppressions) { + $auto_override_conditions{join("\t", $t->state, $t->event)} = 1; + } + push(@alerts, $alert) unless (grep {$_->alert_type == $t->id} @$suppressions); + } + + $self->system_copy_alerts(\@alerts); + $self->overrides_per_copy_alerts(\%auto_override_conditions); +} + +sub add_overrides_from_system_copy_alerts { + my $self = shift; + my $e = $self->editor; + + foreach my $condition (keys %{$self->overrides_per_copy_alerts()}) { + if (exists $COPY_ALERT_OVERRIDES{$condition}) { + $self->override(1); + push @{$self->override_args->{events}}, @{ $COPY_ALERT_OVERRIDES{$condition} }; + # special handling for long-overdue and lost checkouts + if (grep { $_ eq 'OPEN_CIRCULATION_EXISTS' } @{ $COPY_ALERT_OVERRIDES{$condition} }) { + my $state = (split /\t/, $condition, -1)[0]; + my $setting; + if ($state eq 'LOST' or $state eq 'LOST_AND_PAID') { + $setting = 'circ.copy_alerts.forgive_fines_on_lost_checkin'; + } elsif ($state eq 'LONGOVERDUE') { + $setting = 'circ.copy_alerts.forgive_fines_on_long_overdue_checkin'; + } else { + next; + } + my $forgive = $U->ou_ancestor_setting_value( + $self->circ_lib, $setting, $e + ); + if ($U->is_true($forgive)) { + $self->void_overdues(1); + } + $self->noop(1); # do not attempt transits, just check it in + $self->do_checkin(); + } + } + } +} + sub mk_env { my $self = shift; my $e = $self->editor; + $self->next_copy_status([]) unless (defined $self->next_copy_status); + $self->overrides_per_copy_alerts({}) unless (defined $self->overrides_per_copy_alerts); + # -------------------------------------------------------------------------- # Grab the fleshed copy # -------------------------------------------------------------------------- @@ -676,6 +916,7 @@ sub mk_env { } }, "where" => { + deleted => 'f', "+bresv" => { "id" => (ref $self->reservation) ? $self->reservation->id : $self->reservation @@ -692,6 +933,19 @@ sub mk_env { if($copy) { $self->save_trimmed_copy($copy); + + # alerts! + $self->copy_state( + $e->json_query( + {from => ['asset.copy_state', $copy->id]} + )->[0]{'asset.copy_state'} + ); + + $self->generate_system_copy_alerts; + $self->add_overrides_from_system_copy_alerts; + $self->collect_user_copy_alerts; + $self->filter_user_copy_alerts; + } else { # We can't renew if there is no copy return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND')) @@ -1207,6 +1461,21 @@ sub run_copy_permit_scripts { sub check_copy_alert { my $self = shift; + + if ($self->new_copy_alerts) { + my @alerts; + push @alerts, @{$self->user_copy_alerts} # we have preexisting alerts + if ($self->user_copy_alerts && @{$self->user_copy_alerts}); + + push @alerts, @{$self->system_copy_alerts} # we have new dynamic alerts + if ($self->system_copy_alerts && @{$self->system_copy_alerts}); + + if (@alerts) { + $self->bail_out(1) if (!$self->override); + return OpenILS::Event->new( 'COPY_ALERT_MESSAGE', payload => \@alerts); + } + } + return undef if $self->is_renewal; return OpenILS::Event->new( 'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message) @@ -2640,7 +2909,8 @@ sub do_checkin { $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) { # the item was not supposed to be checked out to the user and should now be marked as missing - $self->copy->status(OILS_COPY_STATUS_MISSING); + my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_MISSING; + $self->copy->status($next_status); $self->update_copy; } else { @@ -2716,13 +2986,15 @@ sub reshelve_copy { my $stat = $U->copy_status($copy->status)->id; + my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING; + if($force || ( $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and $stat != OILS_COPY_STATUS_CATALOGING and $stat != OILS_COPY_STATUS_IN_TRANSIT and - $stat != OILS_COPY_STATUS_RESHELVING )) { + $stat != $next_status )) { - $copy->status( OILS_COPY_STATUS_RESHELVING ); + $copy->status( $next_status ); $self->update_copy; $self->checkin_changed(1); } @@ -3333,7 +3605,8 @@ sub checkin_handle_circ_start { } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) { $logger->info("circulator: not updating copy status on checkin because copy is missing"); } else { - $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING)); + my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING; + $self->copy->status($U->copy_status($next_status)); $self->update_copy; } @@ -3521,7 +3794,8 @@ sub checkin_handle_lost_or_longoverdue { if ($immediately_available) { # item status does not need to be retained, so give it a # reshelving status as if it were a normal checkin - $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING)); + my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING; + $self->copy->status($U->copy_status($next_status)); $self->update_copy; } else { $logger->info("circulator: leaving lost/longoverdue copy". @@ -3530,7 +3804,8 @@ sub checkin_handle_lost_or_longoverdue { } else { # lost/longoverdue item is home and processed, treat like a normal # checkin from this point on - $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING)); + my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING; + $self->copy->status($U->copy_status($next_status)); $self->update_copy; } } @@ -3571,7 +3846,8 @@ sub check_checkin_copy_status { my $status = $U->copy_status($copy->status)->id; return undef - if( $status == OILS_COPY_STATUS_AVAILABLE || + if( $self->new_copy_alerts || + $status == OILS_COPY_STATUS_AVAILABLE || $status == OILS_COPY_STATUS_CHECKED_OUT || $status == OILS_COPY_STATUS_IN_PROCESS || $status == OILS_COPY_STATUS_ON_HOLDS_SHELF || diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql index f9948cb747..d17dcfaa9c 100644 --- a/Open-ILS/src/sql/Pg/040.schema.asset.sql +++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql @@ -987,5 +987,97 @@ CREATE INDEX asset_copy_tag_copy_map_copy_idx CREATE INDEX asset_copy_tag_copy_map_tag_idx ON asset.copy_tag_copy_map (tag); +CREATE OR REPLACE FUNCTION asset.copy_state (cid BIGINT) RETURNS TEXT AS $$ +DECLARE + last_circ_stop TEXT; + the_copy asset.copy%ROWTYPE; +BEGIN + + SELECT * INTO the_copy FROM asset.copy WHERE id = cid; + IF NOT FOUND THEN RETURN NULL; END IF; + + IF the_copy.status = 3 THEN -- Lost + RETURN 'LOST'; + ELSIF the_copy.status = 4 THEN -- Missing + RETURN 'MISSING'; + ELSIF the_copy.status = 14 THEN -- Damaged + RETURN 'DAMAGED'; + ELSIF the_copy.status = 17 THEN -- Lost and paid + RETURN 'LOST_AND_PAID'; + END IF; + + SELECT stop_fines INTO last_circ_stop + FROM action.circulation + WHERE target_copy = cid + ORDER BY xact_start DESC LIMIT 1; + + IF FOUND THEN + IF last_circ_stop IN ( + 'CLAIMSNEVERCHECKEDOUT', + 'CLAIMSRETURNED', + 'LONGOVERDUE' + ) THEN + RETURN last_circ_stop; + END IF; + END IF; + + RETURN 'NORMAL'; +END; +$$ LANGUAGE PLPGSQL; + +CREATE TYPE config.copy_alert_type_state AS ENUM ( + 'NORMAL', + 'LOST', + 'LOST_AND_PAID', + 'MISSING', + 'DAMAGED', + 'CLAIMSRETURNED', + 'LONGOVERDUE', + 'CLAIMSNEVERCHECKEDOUT' +); + +CREATE TYPE config.copy_alert_type_event AS ENUM ( + 'CHECKIN', + 'CHECKOUT' +); + +CREATE TABLE config.copy_alert_type ( + id serial primary key, -- reserve 1-100 for system + scope_org int not null references actor.org_unit (id) on delete cascade, + active bool not null default true, + name text not null unique, + state config.copy_alert_type_state, + event config.copy_alert_type_event, + in_renew bool, + invert_location bool not null default false, + at_circ bool, + at_owning bool, + next_status int[] +); +SELECT SETVAL('config.copy_alert_type_id_seq'::TEXT, 100); + +CREATE TABLE actor.copy_alert_suppress ( + id serial primary key, + org int not null references actor.org_unit (id) on delete cascade, + alert_type int not null references config.copy_alert_type (id) on delete cascade +); + +CREATE TABLE asset.copy_alert ( + id bigserial primary key, + alert_type int not null references config.copy_alert_type (id) on delete cascade, + copy bigint not null references asset.copy (id) on delete cascade, + temp bool not null default false, + create_time timestamptz not null default now(), + create_staff bigint not null references actor.usr (id) on delete set null, + note text, + ack_time timestamptz, + ack_staff bigint references actor.usr (id) on delete set null +); + +CREATE VIEW asset.active_copy_alert AS + SELECT * + FROM asset.copy_alert + WHERE ack_time IS NULL; + COMMIT; diff --git a/Open-ILS/src/sql/Pg/live_t/copy_state.pg b/Open-ILS/src/sql/Pg/live_t/copy_state.pg new file mode 100644 index 0000000000..e09e62b136 --- /dev/null +++ b/Open-ILS/src/sql/Pg/live_t/copy_state.pg @@ -0,0 +1,11 @@ +BEGIN; + +SELECT plan(1); + +\set copy_id 245 + +UPDATE asset.copy SET status = 4 WHERE id = :copy_id; + +SELECT is(asset.copy_state(:copy_id), 'MISSING', 'asset.copy_state detects missing state'); + +ROLLBACK; diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql new file mode 100644 index 0000000000..db4cd25cc0 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql @@ -0,0 +1,96 @@ +BEGIN; + +CREATE OR REPLACE FUNCTION asset.copy_state (cid BIGINT) RETURNS TEXT AS $$ +DECLARE + last_circ_stop TEXT; + the_copy asset.copy%ROWTYPE; +BEGIN + + SELECT * INTO the_copy FROM asset.copy WHERE id = cid; + IF NOT FOUND THEN RETURN NULL; END IF; + + IF the_copy.status = 3 THEN -- Lost + RETURN 'LOST'; + ELSIF the_copy.status = 4 THEN -- Missing + RETURN 'MISSING'; + ELSIF the_copy.status = 14 THEN -- Damaged + RETURN 'DAMAGED'; + ELSIF the_copy.status = 17 THEN -- Lost and paid + RETURN 'LOST_AND_PAID'; + END IF; + + SELECT stop_fines INTO last_circ_stop + FROM action.circulation + WHERE target_copy = cid + ORDER BY xact_start DESC LIMIT 1; + + IF FOUND THEN + IF last_circ_stop IN ( + 'CLAIMSNEVERCHECKEDOUT', + 'CLAIMSRETURNED', + 'LONGOVERDUE' + ) THEN + RETURN last_circ_stop; + END IF; + END IF; + + RETURN 'NORMAL'; +END; +$$ LANGUAGE PLPGSQL; + +CREATE TYPE config.copy_alert_type_state AS ENUM ( + 'NORMAL', + 'LOST', + 'LOST_AND_PAID', + 'MISSING', + 'DAMAGED', + 'CLAIMSRETURNED', + 'LONGOVERDUE', + 'CLAIMSNEVERCHECKEDOUT' +); + +CREATE TYPE config.copy_alert_type_event AS ENUM ( + 'CHECKIN', + 'CHECKOUT' +); + +CREATE TABLE config.copy_alert_type ( + id serial primary key, -- reserve 1-100 for system + scope_org int not null references actor.org_unit (id) on delete cascade, + active bool not null default true, + name text not null unique, + state config.copy_alert_type_state, + event config.copy_alert_type_event, + in_renew bool, + invert_location bool not null default false, + at_circ bool, + at_owning bool, + next_status int[] +); +SELECT SETVAL('config.copy_alert_type_id_seq'::TEXT, 100); + +CREATE TABLE actor.copy_alert_suppress ( + id serial primary key, + org int not null references actor.org_unit (id) on delete cascade, + alert_type int not null references config.copy_alert_type (id) on delete cascade +); + +CREATE TABLE asset.copy_alert ( + id bigserial primary key, + alert_type int not null references config.copy_alert_type (id) on delete cascade, + copy bigint not null references asset.copy (id) on delete cascade, + temp bool not null default false, + create_time timestamptz not null default now(), + create_staff bigint not null references actor.usr (id) on delete set null, + note text, + ack_time timestamptz, + ack_staff bigint references actor.usr (id) on delete set null +); + +CREATE VIEW asset.active_copy_alert AS + SELECT * + FROM asset.copy_alert + WHERE ack_time IS NULL; + +COMMIT; + diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql new file mode 100644 index 0000000000..cc09016d9c --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql @@ -0,0 +1,62 @@ +BEGIN; + +-- staff-usable alert types with no location awareness +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew) +VALUES (1, 1, FALSE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew) +VALUES (2, 1, FALSE, 'Normal checkin', 'NORMAL', 'CHECKIN', FALSE); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew) +VALUES (3, 1, FALSE, 'Normal renewal', 'NORMAL', 'CHECKIN', TRUE); + +-- copy alerts upon checkin or renewal of exceptional copy statuses are not active by +-- default; they're meant to be turned once a site is ready to fully +-- commit to using the webstaff client for circulation +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (4, 1, FALSE, 'Checkin of lost copy', 'LOST', 'CHECKIN'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (5, 1, FALSE, 'Checkin of missing copy', 'MISSING', 'CHECKIN'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (6, 1, FALSE, 'Checkin of lost-and-paid copy', 'LOST_AND_PAID', 'CHECKIN'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (7, 1, FALSE, 'Checkin of damaged copy', 'DAMAGED', 'CHECKIN'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (8, 1, FALSE, 'Checkin of claims-returned copy', 'CLAIMSRETURNED', 'CHECKIN'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (9, 1, FALSE, 'Checkin of long overdue copy', 'LONGOVERDUE', 'CHECKIN'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (10, 1, FALSE, 'Checkin of claims-never-checked-out copy', 'CLAIMSNEVERCHECKEDOUT', 'CHECKIN'); + +-- copy alerts upon checkout of exceptional copy statuses are not active by +-- default; they're meant to be turned once a site is ready to fully +-- commit to using the webstaff client for circulation +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (11, 1, FALSE, 'Checkout of lost copy', 'LOST', 'CHECKOUT'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (12, 1, FALSE, 'Checkout of missing copy', 'MISSING', 'CHECKOUT'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (13, 1, FALSE, 'Checkout of lost-and-paid copy', 'LOST_AND_PAID', 'CHECKOUT'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (14, 1, FALSE, 'Checkout of damaged copy', 'DAMAGED', 'CHECKOUT'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (15, 1, FALSE, 'Checkout of claims-returned copy', 'CLAIMSRETURNED', 'CHECKOUT'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (16, 1, FALSE, 'Checkout of long overdue copy', 'LONGOVERDUE', 'CHECKOUT'); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event) +VALUES (17, 1, FALSE, 'Checkout of claims-never-checked-out copy', 'CLAIMSNEVERCHECKEDOUT', 'CHECKOUT'); + +-- staff-usable alert types based on location +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ) +VALUES (18, 1, FALSE, 'Normal checkout at circ lib', 'NORMAL', 'CHECKOUT', FALSE, TRUE); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ) +VALUES (19, 1, FALSE, 'Normal checkin at circ lib', 'NORMAL', 'CHECKIN', FALSE, TRUE); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ) +VALUES (20, 1, FALSE, 'Normal renewal at circ lib', 'NORMAL', 'CHECKIN', TRUE, TRUE); + +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning) +VALUES (21, 1, FALSE, 'Normal checkout at owning lib', 'NORMAL', 'CHECKOUT', FALSE, TRUE); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning) +VALUES (22, 1, FALSE, 'Normal checkin at owning lib', 'NORMAL', 'CHECKIN', FALSE, TRUE); +INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning) +VALUES (23, 1, FALSE, 'Normal renewal at owning lib', 'NORMAL', 'CHECKIN', TRUE, TRUE); + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql new file mode 100644 index 0000000000..4235527112 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql @@ -0,0 +1,35 @@ +BEGIN; + +--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version); + +INSERT INTO config.org_unit_setting_type + (name, grp, label, description, datatype) + VALUES + ('circ.copy_alerts.forgive_fines_on_lost_checkin', + 'circ', + oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin', + 'Forgive fines when checking out a lost item and copy alert is suppressed?', + 'coust', 'label'), + oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin', + 'Controls whether fines are automatically forgiven when checking out an '|| + 'item that has been marked as lost, and the corresponding copy alert has been '|| + 'suppressed.', + 'coust', 'description'), + 'bool'); + +INSERT INTO config.org_unit_setting_type + (name, grp, label, description, datatype) + VALUES + ('circ.copy_alerts.forgive_fines_on_long_overdue_checkin', + 'circ', + oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_long_overdue_checkin', + 'Forgive fines when checking out a long-overdue item and copy alert is suppressed?', + 'coust', 'label'), + oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin', + 'Controls whether fines are automatically forgiven when checking out an '|| + 'item that has been marked as lost, and the corresponding copy alert has been '|| + 'suppressed.', + 'coust', 'description'), + 'bool'); + +COMMIT; diff --git a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2 b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2 new file mode 100644 index 0000000000..eab1609f70 --- /dev/null +++ b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2 @@ -0,0 +1,30 @@ +[% ctx.page_title = l("Copy Alert Suppression"); %] + +
+
+ + + +
+
diff --git a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2 b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2 new file mode 100644 index 0000000000..727bfd9e5b --- /dev/null +++ b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2 @@ -0,0 +1,92 @@ +[% ctx.page_title = l("Copy Alert Types"); %] + +
+
+ + + +
+
diff --git a/Open-ILS/src/templates/staff/admin/local/index.tt2 b/Open-ILS/src/templates/staff/admin/local/index.tt2 index 02d604f887..6200f6efc9 100644 --- a/Open-ILS/src/templates/staff/admin/local/index.tt2 +++ b/Open-ILS/src/templates/staff/admin/local/index.tt2 @@ -7,8 +7,14 @@ [% BLOCK APP_JS %] + + diff --git a/Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2 b/Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2 new file mode 100644 index 0000000000..54f3da031d --- /dev/null +++ b/Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2 @@ -0,0 +1,12 @@ + + + + + + diff --git a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2 index 82599b34eb..151f2bb414 100644 --- a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2 +++ b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2 @@ -17,6 +17,8 @@ ,[ l('Circ Limit Sets'), "./admin/local/config/circ_limit_set" ] ,[ l('Circulation Policies'), "./admin/local/config/circ_matrix_matchpoint" ] ,[ l('Closed Dates Editor'), "./admin/local/actor/closed_dates" ] + ,[ l('Copy Alert Types'), "./admin/local/config/copy_alert_types" ] + ,[ l('Copy Alert Suppression'), "./admin/local/actor/copy_alert_suppress" ] ,[ l('Copy Location Groups'), "./admin/local/asset/copy_location_group" ] ,[ l('Copy Location Order'), "./admin/local/asset/copy_location_order" ] ,[ l('Copy Locations Editor'), "./admin/local/asset/copy_locations" ] diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 index b506f3f405..c6bf4e07ae 100644 --- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 +++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 @@ -71,6 +71,8 @@ label="[% l('Copies') %]"> + @@ -80,6 +82,8 @@ label="[% l('Volumes and Copies') %]"> + @@ -118,6 +122,10 @@ + + {{item['copy_alert_count']}} + + diff --git a/Open-ILS/src/templates/staff/cat/item/t_list.tt2 b/Open-ILS/src/templates/staff/cat/item/t_list.tt2 index eb584c95e5..5a5d5bedf8 100644 --- a/Open-ILS/src/templates/staff/cat/item/t_list.tt2 +++ b/Open-ILS/src/templates/staff/cat/item/t_list.tt2 @@ -46,6 +46,8 @@ label="[% l('Items') %]"> + @@ -55,6 +57,8 @@ label="[% l('Volumes and Items') %]"> + @@ -75,7 +79,6 @@ - @@ -127,7 +130,11 @@ - + + {{item['copy_alert_count']}} + + +
diff --git a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 index 4b9abe3f3f..9e3793939d 100644 --- a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 +++ b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 @@ -177,8 +177,11 @@
-
[% l('Alert Message') %]
-
{{copy.alert_message()}}
+
[% l('Copy Alerts') %]
+
+ + +
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 index 5b9181e5d5..3d60493369 100644 --- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 +++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 @@ -384,6 +384,13 @@ type="button"> [% l('Copy Notes') %] + diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2 new file mode 100644 index 0000000000..b86fef363f --- /dev/null +++ b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2 @@ -0,0 +1,94 @@ +
+ + + +
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 index 89e4ebee9b..77316e6cfa 100644 --- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 +++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 @@ -310,6 +310,15 @@ + +
+
+ +
+
diff --git a/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 b/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 index 8ff4853001..6c7aab0435 100644 --- a/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 +++ b/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 @@ -31,6 +31,15 @@ handler="abortTransit" label="[% l('Cancel Transits') %]"> + + + + + @@ -47,8 +56,6 @@ - diff --git a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 index ebcfb44bce..f52451d7ec 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 @@ -93,8 +93,14 @@ persist-key="circ.patron.checkout" dateformat="{{$root.egDateAndTimeFormat}}"> - + + + + @@ -138,6 +144,12 @@ + + + {{item['copy_alert_count']}} + + + diff --git a/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 b/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 index 344e851b8f..41866f41f8 100644 --- a/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 +++ b/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 @@ -66,11 +66,17 @@ handler="abortTransit" label="[% l('Cancel Transits') %]"> + + + + + - - diff --git a/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2 b/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2 index 6ad5c783d1..6675b99c12 100644 --- a/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2 +++ b/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2 @@ -34,7 +34,29 @@ s.COPY_IN_TRANSIT = '[% l("Copy is In-Transit") %]'; s.TOO_MANY_CLAIMS_RETURNED = '[% l("Patron exceeds claims returned count. Force this action?") %]'; s.MARK_NEVER_CHECKED_OUT = - '[% l("Mark Never Checked Out: [_1]", "{{barcodes.toString()}}") %]' + '[% l("Mark Never Checked Out: [_1]", "{{barcodes.toString()}}") %]'; +s.ON_DEMAND_COPY_ALERT = { + 'CHECKIN': { + 'NORMAL' : '[% l("Normal checkin") %]', + 'LOST' : '[% l("Copy was marked lost") %]', + 'LOST_AND_PAID' : '[% l("Copy was marked lost and paid for") %]', + 'MISSING' : '[% l("Copy was marked missing") %]', + 'DAMAGED' : '[% l("Copy was marked damaged") %]', + 'CLAIMSRETURNED' : '[% l("Copy was marked claims returned") %]', + 'LONGOVERDUE' : '[% l("Copy was marked long overdue") %]', + 'CLAIMSNEVERCHECKEDOUT' : '[% l("Copy was marked claims never checked out") %]' + }, + 'CHECKOUT': { + 'NORMAL' : '[% l("Normal checkout") %]', + 'LOST' : '[% l("Copy was marked lost") %]', + 'LOST_AND_PAID' : '[% l("Copy was marked lost and paid for") %]', + 'MISSING' : '[% l("Copy was marked missing") %]', + 'DAMAGED' : '[% l("Copy was marked damaged") %]', + 'CLAIMSRETURNED' : '[% l("Copy was marked claims returned") %]', + 'LONGOVERDUE' : '[% l("Copy was marked long overdue") %]', + 'CLAIMSNEVERCHECKEDOUT' : '[% l("Copy was marked claims never checked out") %]' + } +}; }]); diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2 index b20f720244..7d322755f0 100644 --- a/Open-ILS/src/templates/staff/css/style.css.tt2 +++ b/Open-ILS/src/templates/staff/css/style.css.tt2 @@ -179,6 +179,10 @@ table.list tr.selected td { /* deprecated? */ /* barcode inputs are everywhere. Let's have a consistent style. */ .barcode { width: 16em !important; } +/* use strike-through to mark something that has been acknowledged, + e.g., a copy alert */ +.acknowledged { text-decoration: line-through; } + /* bootstrap alerts are heavily padded. use this to reduce */ .alert-less-pad {padding: 5px;} diff --git a/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2 new file mode 100644 index 0000000000..0b97a3fd9e --- /dev/null +++ b/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2 @@ -0,0 +1,40 @@ +
+ + + +
diff --git a/Open-ILS/src/templates/staff/share/t_autogrid.tt2 b/Open-ILS/src/templates/staff/share/t_autogrid.tt2 index c6b45d9ddc..4a91d0ccef 100644 --- a/Open-ILS/src/templates/staff/share/t_autogrid.tt2 +++ b/Open-ILS/src/templates/staff/share/t_autogrid.tt2 @@ -215,7 +215,7 @@
-
+
@@ -315,10 +315,14 @@ - + + + diff --git a/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2 new file mode 100644 index 0000000000..aad1a6b383 --- /dev/null +++ b/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2 @@ -0,0 +1,55 @@ +
+ + + +
diff --git a/Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2 new file mode 100644 index 0000000000..843ea0d7c7 --- /dev/null +++ b/Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2 @@ -0,0 +1,49 @@ + +
+ + diff --git a/Open-ILS/web/js/ui/default/staff/admin/local/app.js b/Open-ILS/web/js/ui/default/staff/admin/local/app.js index f2b286f621..875825702c 100644 --- a/Open-ILS/web/js/ui/default/staff/admin/local/app.js +++ b/Open-ILS/web/js/ui/default/staff/admin/local/app.js @@ -1,5 +1,5 @@ angular.module('egLocalAdmin', - ['ngRoute', 'ui.bootstrap', 'egCoreMod','egUiMod']) + ['ngRoute', 'ui.bootstrap', 'egCoreMod','egUiMod','egGridMod']) .config(['$routeProvider','$locationProvider','$compileProvider', function($routeProvider , $locationProvider , $compileProvider) { @@ -52,6 +52,29 @@ angular.module('egLocalAdmin', resolve : resolver }); + $routeProvider.when('/admin/local/config/copy_alert_types', { + templateUrl: './admin/local/t_grid_editor', + controller: 'AutoGridEditorCtl', + fmBase: 'ccat', + createEditPrefetch: { + ccs : { id : {'!=' : null} } + }, + createDefaults : { 'in_renew' : 'f', 'next_status' : [] }, + createEditOrgExpand: ['scope_org'], + createEditIntarray: ['next_status'], + createEditNullableBool : ['in_renew', 'at_circ', 'at_owning'] + }); + + $routeProvider.when('/admin/local/actor/copy_alert_suppress', { + templateUrl: './admin/local/t_grid_editor', + controller: 'AutoGridEditorCtl', + fmBase: 'acas', + createEditPrefetch: { + ccat : { active: 't' } + }, + createEditOrgExpand: ['org'] + }); + // Conify page handler $routeProvider.when('/admin/local/:schema/:page', { template: eframe_template, @@ -106,3 +129,138 @@ function($scope , $location , egCore , $timeout) { $scope.local_admin_url = $location.absUrl().replace(/\/.*/, url); }]) +.controller('AutoGridEditorCtl', + ['$scope','$route','$location','egCore','$timeout','egConfirmDialog','$uibModal', +function($scope , $route , $location , egCore , $timeout , egConfirmDialog , $uibModal) { + + $scope.funcs = {}; + + $scope.baseFmClass = $route.current.$$route.fmBase; + $scope.createEditPrefetch = $route.current.$$route.createEditPrefetch || {}; + $scope.createEditOrgExpand = $route.current.$$route.createEditOrgExpand || []; + $scope.createEditNullableBool = $route.current.$$route.createEditNullableBool || []; + $scope.createEditIntarray = $route.current.$$route.createEditIntarray || []; + $scope.createDefaults = $route.current.$$route.createDefaults || {}; + $scope.gridControls = { + setQuery : function(q) { + if (q) query = q; + return query; + }, + activateItem : function (item) { + $scope.editHandler([item]) + } + }; + $scope.gridControls.setQuery({id : {'!=' : null}}); + + function openCreateEditDialog(id) { + return $uibModal.open({ + templateUrl : './admin/local/autoGridEditor/' + $scope.baseFmClass, + scope : $scope, + controller : + ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) { + $scope.creating = id ? false : true; + angular.forEach($scope.$parent.createEditPrefetch, function(where, fmClass) { + egCore.pcrud.search( + fmClass, where, {}, + {atomic : true, authoritative : true} + ).then(function(vals) { + $scope[fmClass] = vals; + }); + }); + if ($scope.creating) { + $scope.record = $scope.createDefaults; + } else { + egCore.pcrud.retrieve($scope.baseFmClass, id).then(function(to_edit) { + $scope.record = egCore.idl.toHash(to_edit); + angular.forEach($scope.createEditOrgExpand, function(ou_field) { + $scope.record[ou_field] = egCore.org.get($scope.record[ou_field]); + }); + angular.forEach($scope.createEditIntarray, function(intarray_field) { + if (!($scope.record[intarray_field] == null) && $scope.record[intarray_field] != "") { + $scope.record[intarray_field] = $scope.record[intarray_field] + .replace('{', '') + .replace('}', '') + .split(','); + } else { + $scope.record[intarray_field] = []; + } + }); + }); + } + $scope.ok = function(record) { $uibModalInstance.close(record) }; + $scope.cancel = function () { $uibModalInstance.dismiss() } + }] + }); + } + + $scope.createHandler = function() { + openCreateEditDialog().result.then(function(record) { + var newRec = new egCore.idl[$scope.baseFmClass](); + angular.forEach(record, function(val, key) { + if (typeof(val) === 'object' && !angular.isArray(val)) { + newRec[key](val.id()); + } else { + newRec[key](val); + } + }); + angular.forEach($scope.createEditNullableBool, function(nb_field) { + if (!(record[nb_field] == null) && record[nb_field] == "") + newRec[nb_field](null); + }); + angular.forEach($scope.createEditIntarray, function(intarray_field) { + if (newRec[intarray_field]().length > 0) { + newRec[intarray_field]('{' + newRec[intarray_field]().join(',') + '}'); + } else { + newRec[intarray_field](null); + } + }); + return egCore.pcrud.create(newRec); + }).then(function(){ + $scope.gridControls.refresh(); + }); + }; + $scope.editHandler = function(items) { + openCreateEditDialog(items[0].id).result.then(function(record) { + var editedRec = new egCore.idl[$scope.baseFmClass](); + angular.forEach(record, function(val, key) { + if (angular.isObject(val) && !angular.isArray(val)) { + editedRec[key](val.id()); + } else { + editedRec[key](val); + } + }); + angular.forEach($scope.createEditNullableBool, function(nb_field) { + if (!(record[nb_field] == null) && record[nb_field] == "") + editedRec[nb_field](null); + }); + angular.forEach($scope.createEditIntarray, function(intarray_field) { + if (editedRec[intarray_field]().length > 0) { + editedRec[intarray_field]('{' + editedRec[intarray_field]().join(',') + '}'); + } else { + editedRec[intarray_field](null); + } + }); + return egCore.pcrud.update(editedRec); + }).then(function(){ + $scope.gridControls.refresh(); + }); + }; + $scope.deleteHandler = function(items) { + egConfirmDialog.open( + egCore.strings.REMOVE_ITEM_CONFIRM, + '', + {} + ).result.then(function() { + var ids = items.map(function(s){ return s.id }); + egCore.pcrud.search( + $scope.baseFmClass, {id : ids}, {}, + {atomic : true, authoritative : true} + ).then(function(to_delete) { + return egCore.pcrud.remove(to_delete); + }).then(function() { + $scope.gridControls.refresh(); + }); + }); + }; +}]) + diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js index 63b9c04659..4b3d186975 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js @@ -1477,6 +1477,13 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e }); } + $scope.gridCellHandlers = {}; + $scope.gridCellHandlers.copyAlertsEdit = function(id) { + egCirc.manage_copy_alerts([id]).then(function() { + // update grid items? + }); + }; + $scope.transferItems = function (){ var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target'); var copy_ids = gatherSelectedHoldingsIds(); @@ -1586,6 +1593,17 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e }); } + $scope.selectedHoldingsCopyAlertsAdd = function() { + egCirc.add_copy_alerts(gatherSelectedHoldingsIds()).then(function() { + // no need to refresh grid + }); + } + $scope.selectedHoldingsCopyAlertsManage = function() { + egCirc.manage_copy_alerts(gatherSelectedHoldingsIds()).then(function() { + // no need to refresh grid + }); + } + $scope.attach_to_peer_bib = function() { var copy_list = gatherSelectedHoldingsIds(); if (copy_list.length == 0) return; diff --git a/Open-ILS/web/js/ui/default/staff/cat/item/app.js b/Open-ILS/web/js/ui/default/staff/cat/item/app.js index be8e797028..2a3d7a71f1 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/item/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/item/app.js @@ -455,6 +455,33 @@ function($scope , $q , $routeParams , $location , $timeout , $window , egCore , itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),false,true); } + $scope.selectedHoldingsCopyAlertsAdd = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.id) copy_ids.push(item.id); + }); + egCirc.add_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + + $scope.selectedHoldingsCopyAlertsEdit = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.id) copy_ids.push(item.id); + }); + egCirc.manage_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + + $scope.gridCellHandlers = {}; + $scope.gridCellHandlers.copyAlertsEdit = function(id) { + egCirc.manage_copy_alerts([id]).then(function() { + // update grid items? + }); + }; + $scope.showBibHolds = function () { angular.forEach(gatherSelectedRecordIds(), function (r) { var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds'; @@ -522,8 +549,8 @@ function($scope , $q , $routeParams , $location , $timeout , $window , egCore , * Detail view -- shows one copy */ .controller('ViewCtrl', - ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling', -function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) { + ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling','egCirc', +function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling , egCirc) { var copyId = $routeParams.id; $scope.args.copyId = copyId; $scope.tab = $routeParams.tab || 'summary'; @@ -940,6 +967,17 @@ function($scope , $q , $location , $routeParams , $timeout , $window , egCore , return; } + $scope.addCopyAlerts = function(copy_id) { + egCirc.add_copy_alerts([copy_id]).then(function() { + // update grid items? + }); + } + $scope.manageCopyAlerts = function(copy_id) { + egCirc.manage_copy_alerts([copy_id]).then(function() { + // update grid items? + }); + } + $scope.context.toggleDisplay = function() { $location.path('/cat/item/search'); } diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js b/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js index f0e0185c78..e6cc1457be 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js +++ b/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js @@ -15,7 +15,7 @@ function(egCore , $q) { service.prototype.flesh = { flesh : 2, flesh_fields : { - acp : ['status','location','circ_lib','parts','age_protect'], + acp : ['status','location','circ_lib','parts','age_protect','copy_alerts'], acn : ['prefix','suffix','copies'] } } @@ -117,6 +117,11 @@ function(egCore , $q) { } }); + // create virtual field for copy alert count + angular.forEach(svc.copies, function (cp) { + cp.copy_alert_count = cp.copy_alerts.length; + }); + // create a label using just the unique part of the owner list var index = 0; var prev_owner_list; diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js index 39e4aafc77..03cde64549 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js @@ -136,6 +136,21 @@ function(egCore , $q) { ); }; + service.get_copy_alert_types = function(orgs) { + return egCore.pcrud.search('ccat', + { active : 't' }, + {}, + { atomic : true } + ); + }; + + service.get_copy_alerts = function(copy_id) { + return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null }, + { flesh : 1, flesh_fields : { aca : ['alert_type'] } }, + { atomic : true } + ); + }; + service.get_locations = function(orgs) { return egCore.pcrud.search('acpl', {owning_lib : orgs, deleted : 'f'}, @@ -374,6 +389,10 @@ function(egCore , $q) { if (!cp.parts()) cp.parts([]); // just in case... + service.get_copy_alerts(cp.id()).then(function(aca) { + cp.copy_alerts(aca); + }); + var lib = cp.call_number().owning_lib(); var cn = cp.call_number().id(); @@ -907,6 +926,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , statcats : true, copy_notes : true, copy_tags : true, + copy_alerts : true, attributes : { status : true, loan_duration : true, @@ -1101,6 +1121,58 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , statcat_filter: undefined }; + $scope.copyAlertUpdate = function (alerts) { + if (!$scope.in_item_select && + $scope.workingGridControls && + $scope.workingGridControls.selectedItems) { + itemSvc.get_copy_alert_types().then(function(ccat) { + var ccat_map = {}; + $scope.alert_types = ccat; + angular.forEach(ccat, function(t) { + ccat_map[t.id()] = t; + }); + angular.forEach( + $scope.workingGridControls.selectedItems(), + function (cp) { + $scope.dirty = true; + angular.forEach(alerts, function(alrt) { + var a = egCore.idl.fromHash('aca', alrt); + a.isnew(1); + a.create_staff(egCore.auth.user().id()); + a.alert_type(ccat_map[a.alert_type()]); + a.ack_time(null); + a.copy(cp.id()); + cp.copy_alerts().push( a ); + }); + cp.ischanged(1); + } + ); + }); + } + }; + + $scope.copyNoteUpdate = function (notes) { + if (!$scope.in_item_select && + $scope.workingGridControls && + $scope.workingGridControls.selectedItems) { + angular.forEach( + $scope.workingGridControls.selectedItems(), + function (cp) { + $scope.dirty = true; + angular.forEach(notes, function(note) { + var n = egCore.idl.fromHash('acpn', note); + n.isnew(1); + n.creator(egCore.auth.user().id()); + n.owning_copy(cp.id()); + cp.notes().push( n ); + }); + cp.ischanged(1); + } + ); + + } + } + $scope.statcatUpdate = function (id) { var newval = $scope.working.statcats[id]; @@ -1184,6 +1256,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , angular.forEach($scope.templates[n], function (v,k) { if (k == 'circ_lib') { $scope.working[k] = egCore.org.get(v); + } else if (k == 'copy_notes' && v.length) { + $scope.copyNoteUpdate(v); + } else if (k == 'copy_alerts' && v.length) { + $scope.copyAlertUpdate(v); } else if (!angular.isObject(v)) { $scope.working[k] = angular.copy(v); } else { @@ -1936,6 +2012,75 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , }); } + $scope.copy_alerts_dialog = function(copy_list) { + if (!angular.isArray(copy_list)) copy_list = [copy_list]; + + return $uibModal.open({ + templateUrl: './cat/volcopy/t_copy_alerts', + animation: true, + controller: + ['$scope','$uibModalInstance', + function($scope , $uibModalInstance) { + + itemSvc.get_copy_alert_types().then(function(ccat) { + $scope.alert_types = ccat; + }); + + $scope.focusNote = true; + $scope.copy_alert = { + create_staff : egCore.auth.user().id(), + note : '', + temp : false + }; + + egCore.hatch.getItem('cat.copy.alerts.last_type').then(function(t) { + if (t) $scope.copy_alert.alert_type = t; + }); + + if (copy_list.length == 1) { + $scope.copy_alert_list = copy_list[0].copy_alerts(); + } + + $scope.ok = function(copy_alert) { + + if (typeof(copy_alert.note) != 'undefined' && + copy_alert.note != '') { + angular.forEach(copy_list, function (cp) { + var a = new egCore.idl.aca(); + a.isnew(1); + a.create_staff(copy_alert.create_staff); + a.note(copy_alert.note); + a.temp(copy_alert.temp ? 't' : 'f'); + a.copy(cp.id()); + a.ack_time(null); + a.alert_type( + $scope.alert_types.filter(function(at) { + return at.id() == copy_alert.alert_type; + })[0] + ); + cp.copy_alerts().push( a ); + }); + + if (copy_alert.alert_type) { + egCore.hatch.setItem( + 'cat.copy.alerts.last_type', + copy_alert.alert_type + ); + } + + } + + $uibModalInstance.close(); + } + + $scope.cancel = function($event) { + $uibModalInstance.dismiss(); + $event.preventDefault(); + } + }] + }); + } + }]) .directive("egVolTemplate", function () { @@ -1946,8 +2091,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , scope: { editTemplates: '=', }, - controller : ['$scope','$window','itemSvc','egCore','ngToast', - function ( $scope , $window , itemSvc , egCore , ngToast) { + controller : ['$scope','$window','itemSvc','egCore','ngToast','$uibModal', + function ( $scope , $window , itemSvc , egCore , ngToast , $uibModal) { $scope.i18n = egCore.i18n; @@ -1957,6 +2102,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , statcats : true, copy_notes : true, copy_tags : true, + copy_alerts : true, attributes : { status : true, loan_duration : true, @@ -2029,7 +2175,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , angular.forEach($scope.templates[n], function (v,k) { if (k == 'circ_lib') { $scope.working[k] = egCore.org.get(v); - } else if (!angular.isObject(v)) { + } else if (angular.isArray(v) || !angular.isObject(v)) { $scope.working[k] = angular.copy(v); } else { angular.forEach(v, function (sv,sk) { @@ -2080,7 +2226,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , } ngToast.create(egCore.strings.VOL_COPY_TEMPLATE_SUCCESS_SAVE); } - + $scope.templates = {}; $scope.imported_templates = { data : '' }; $scope.template_name = ''; @@ -2134,6 +2280,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , } $scope.working = { + copy_notes: [], + copy_alerts: [], statcats: {}, statcat_filter: undefined }; @@ -2213,7 +2361,142 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore , createStatcatUpdateWatcher(s.id()); }); }); + + $scope.copy_notes_dialog = function() { + var default_pub = Boolean($scope.defaults.copy_notes_pub); + var working = $scope.working; + + return $uibModal.open({ + templateUrl: './cat/volcopy/t_copy_notes', + animation: true, + controller: + ['$scope','$uibModalInstance', + function($scope , $uibModalInstance) { + $scope.focusNote = true; + $scope.note = { + title : '', + value : '', + pub : default_pub, + }; + + $scope.require_initials = false; + egCore.org.settings([ + 'ui.staff.require_initials.copy_notes' + ]).then(function(set) { + $scope.require_initials = Boolean(set['ui.staff.require_initials.copy_notes']); + }); + + $scope.note_list = []; + angular.forEach(working.copy_notes, function(note) { + var acpn = egCore.idl.fromHash('acpn', note); + $scope.note_list.push(acpn); + }); + + $scope.ok = function(note) { + + if (!working.copy_notes) { + working.copy_notes = []; + } + + // clear slate + working.copy_notes.length = 0; + angular.forEach($scope.note_list, function(existing_note) { + if (!existing_note.isdeleted()) { + working.copy_notes.push({ + pub : existing_note.pub() ? 't' : 'f', + title : existing_note.title(), + value : existing_note.value() + }); + } + }); + + // add new note, if any + if (note.initials) note.value += ' [' + note.initials + ']'; + note.pub = note.pub ? 't' : 'f'; + if (note.title.length && note.value.length) { + working.copy_notes.push(note); + } + + $uibModalInstance.close(); + } + + $scope.cancel = function($event) { + $uibModalInstance.dismiss(); + $event.preventDefault(); + } + }] + }); + } + + $scope.copy_alerts_dialog = function() { + var working = $scope.working; + + return $uibModal.open({ + templateUrl: './cat/volcopy/t_copy_alerts', + animation: true, + controller: + ['$scope','$uibModalInstance', + function($scope , $uibModalInstance) { + + itemSvc.get_copy_alert_types().then(function(ccat) { + var ccat_map = {}; + $scope.alert_types = ccat; + angular.forEach(ccat, function(t) { + ccat_map[t.id()] = t; + }); + $scope.copy_alert_list = []; + angular.forEach(working.copy_alerts, function (alrt) { + var aca = egCore.idl.fromHash('aca', alrt); + aca.alert_type(ccat_map[alrt.alert_type]); + aca.ack_time(null); + $scope.copy_alert_list.push(aca); + }); + }); + + $scope.focusNote = true; + $scope.copy_alert = { + note : '', + temp : false + }; + + $scope.ok = function(copy_alert) { + if (!working.copy_alerts) { + working.copy_alerts = []; + } + // clear slate + working.copy_alerts.length = 0; + + angular.forEach($scope.copy_alert_list, function(alrt) { + if (alrt.ack_time() == null) { + working.copy_alerts.push({ + note : alrt.note(), + temp : alrt.temp(), + alert_type : alrt.alert_type().id() + }); + } + }); + + if (typeof(copy_alert.note) != 'undefined' && + copy_alert.note != '') { + working.copy_alerts.push({ + note : copy_alert.note, + temp : copy_alert.temp ? 't' : 'f', + alert_type : copy_alert.alert_type + }); + } + + $uibModalInstance.close(); + } + + $scope.cancel = function($event) { + $uibModalInstance.dismiss(); + $event.preventDefault(); + } + }] + }); + } + $scope.status_list = []; itemSvc.get_magic_statuses().then(function(list){ $scope.magic_status_list = list; diff --git a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js index 0a71c3f6a4..24e4a3baa9 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js +++ b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js @@ -387,5 +387,26 @@ function($scope , $q , $window , $location , $timeout , egCore , checkinSvc , eg }); itemSvc.print_spine_labels(copy_ids); } + + $scope.addCopyAlerts = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.acp) copy_ids.push(item.acp.id()); + }); + egCirc.add_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + + $scope.manageCopyAlerts = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.acp) copy_ids.push(item.acp.id()); + }); + egCirc.manage_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + }]) diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js b/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js index 10a97e4273..99b001cdaf 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js @@ -252,9 +252,47 @@ function($scope , $q , $routeParams , egCore , egUser , patronSvc , // Non-cat circs don't return the full list of circs. // Refresh the list of non-cat circs from the server. patronSvc.getUserNonCats(patronSvc.current.id()); + row_item.copy_alert_count = 0; + } else { + row_item.copy_alert_count = 0; + egCore.pcrud.search( + 'aca', + { copy : co_resp.data.acp.id(), ack_time : null }, + null, + { atomic : true } + ).then(function(list) { + row_item.copy_alert_count = list.length; + }); } } + $scope.addCopyAlerts = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.acp) copy_ids.push(item.acp.id()); + }); + egCirc.add_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + + $scope.manageCopyAlerts = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.acp) copy_ids.push(item.acp.id()); + }); + egCirc.manage_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + + $scope.gridCellHandlers = {}; + $scope.gridCellHandlers.copyAlertsEdit = function(id) { + egCirc.manage_copy_alerts([id]).then(function() { + // update grid items? + }); + }; + $scope.print_receipt = function() { var print_data = {circulations : []}; var cusr = patronSvc.current; diff --git a/Open-ILS/web/js/ui/default/staff/circ/renew/app.js b/Open-ILS/web/js/ui/default/staff/circ/renew/app.js index 2c907bd750..2c6ba639ea 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/renew/app.js +++ b/Open-ILS/web/js/ui/default/staff/circ/renew/app.js @@ -201,6 +201,26 @@ function($scope , $window , $location , egCore , egGridDataProvider , egCirc) { }); } + $scope.addCopyAlerts = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.acp) copy_ids.push(item.acp.id()); + }); + egCirc.add_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + + $scope.manageCopyAlerts = function(items) { + var copy_ids = []; + angular.forEach(items, function(item) { + if (item.acp) copy_ids.push(item.acp.id()); + }); + egCirc.manage_copy_alerts(copy_ids).then(function() { + // update grid items? + }); + } + $scope.print_receipt = function() { var print_data = {circulations : []} diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js index d43ad41526..91ad8fb9b4 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js +++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js @@ -5,9 +5,10 @@ angular.module('egCoreMod') .factory('egCirc', - ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog', + + ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog','egAddCopyAlertDialog','egCopyAlertManagerDialog','egCopyAlertEditorDialog', 'egWorkLog', -function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, +function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAlertDialog , egCopyAlertManagerDialog, egCopyAlertEditorDialog , egWorkLog) { var service = { @@ -123,6 +124,8 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, // options : non-parameter controls. e.g. "override", "check_barcode" service.checkout = function(params, options) { if (!options) options = {}; + params.new_copy_alerts = 1; + console.debug('egCirc.checkout() : ' + js2JSON(params) + ' : ' + js2JSON(options)); @@ -177,6 +180,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, // Rejected if the renewal cannot be completed. service.renew = function(params, options) { if (!options) options = {}; + params.new_copy_alerts = 1; console.debug('egCirc.renew() : ' + js2JSON(params) + ' : ' + js2JSON(options)); @@ -224,6 +228,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, // Rejected if the checkin cannot be completed. service.checkin = function(params, options) { if (!options) options = {}; + params.new_copy_alerts = 1; console.debug('egCirc.checkin() : ' + js2JSON(params) + ' : ' + js2JSON(options)); @@ -947,12 +952,24 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, if (angular.isArray(evt)) evt = evt[0]; if (!evt.payload.old_circ) { - return egCore.pcrud.search('circ', - {target_copy : evt.payload.copy.id(), checkin_time : null}, - {limit : 1} // should only ever be 1 - ).then(function(old_circ) { - evt.payload.old_circ = old_circ; - return service.circ_exists_dialog_impl(evt, params, options); + return egCore.net.request( + 'open-ils.search', + 'open-ils.search.asset.copy.fleshed2.find_by_barcode', + params.copy_barcode + ).then(function(resp){ + console.log(resp); + if (egCore.evt.parse(resp)) { + console.error(egCore.evt.parse(resp)); + } else { + return egCore.net.request( + 'open-ils.circ', + 'open-ils.circ.copy_checkout_history.retrieve', + egCore.auth.token(), resp.id(), 1 + ).then( function (circs) { + evt.payload.old_circ = circs[0]; + return service.circ_exists_dialog_impl( evt, params, options ); + }); + } }); } else { return service.circ_exists_dialog_impl( evt, params, options ); @@ -983,14 +1000,12 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, function(args) { if (sameUser) { params.void_overdues = args.forgive_fines; - options.override = true; return service.renew(params, options); } return service.checkin({ barcode : params.copy_barcode, noop : true, - override : true, void_overdues : args.forgive_fines }).then(function(checkin_resp) { if (checkin_resp.evt[0].textcode == 'SUCCESS') { @@ -1344,7 +1359,21 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, }); } + service.add_copy_alerts = function(item_ids) { + return egAddCopyAlertDialog.open({ + copy_ids : item_ids, + ok : function() { }, + cancel : function() {} + }).result.then(function() { }); + } + service.manage_copy_alerts = function(item_ids) { + return egCopyAlertEditorDialog.open({ + copy_id : item_ids[0], + ok : function() { }, + cancel : function() {} + }).result.then(function() { }); + } // alert when copy location alert_message is set. // This does not affect processing, it only produces a click-through @@ -1617,17 +1646,33 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, // action == what action to take if the user confirms the alert service.copy_alert_dialog = function(evt, params, options, action) { if (angular.isArray(evt)) evt = evt[0]; - return egConfirmDialog.open( - egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, - evt.payload, // payload == alert message text - { copy_barcode : params.copy_barcode, - ok : function() {}, + if (!angular.isArray(evt.payload)) { + return egConfirmDialog.open( + egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, + evt.payload, // payload == alert message text + { copy_barcode : params.copy_barcode, + ok : function() {}, + cancel : function() {} + } + ).result.then(function() { + options.override = true; + return service[action](params, options); + }); + } else { // we got a list of copy alert objects ... + return egCopyAlertManagerDialog.open({ + alerts : evt.payload, + mode : action, + ok : function(the_next_status) { + if (the_next_status !== null) { + params.next_copy_status = [ the_next_status ]; + } + }, cancel : function() {} - } - ).result.then(function() { - options.override = true; - return service[action](params, options); - }); + }).result.then(function() { + options.override = true; + return service[action](params, options); + }); + } } // action == what action to take if the user confirms the alert diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/item.js b/Open-ILS/web/js/ui/default/staff/circ/services/item.js index fa9f4408f9..02a58d95e5 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/services/item.js +++ b/Open-ILS/web/js/ui/default/staff/circ/services/item.js @@ -16,7 +16,7 @@ function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog flesh : 3, flesh_fields : { acp : ['call_number','location','status','location','floating','circ_modifier', - 'age_protect','circ_lib'], + 'age_protect','circ_lib','copy_alerts'], acn : ['record','prefix','suffix','label_class'], bre : ['simple_record','creator','editor'] }, @@ -152,6 +152,10 @@ function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog flatCopy._duration = copyData.circ.duration(); } flatCopy.index = service.index++; + flatCopy.copy_alert_count = copyData.copy.copy_alerts().filter(function(aca) { + return !aca.ack_time(); + }).length; + service.copies.unshift(flatCopy); } diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js index b2d760af53..ed21ae0aa0 100644 --- a/Open-ILS/web/js/ui/default/staff/services/grid.js +++ b/Open-ILS/web/js/ui/default/staff/services/grid.js @@ -1263,7 +1263,12 @@ angular.module('egGridMod', // optional: for non-IDL columns, specifying a datatype // lets the caller control which display filter is used. // datatype should match the standard IDL datatypes. - datatype : '@' + datatype : '@', + + // optional hash of functions that can be imported into + // the directive's scope; meant for cases where the "compiled" + // attribute is set + handlers : '=' }, link : function(scope, element, attrs, egGridCtrl) { @@ -1271,6 +1276,7 @@ angular.module('egGridMod', angular.forEach( [ 'visible', + 'compiled', 'hidden', 'sortable', 'nonsortable', @@ -1538,6 +1544,8 @@ angular.module('egGridMod', linkpath : colSpec.linkpath, template : colSpec.template, visible : colSpec.visible, + compiled : colSpec.compiled, + handlers : colSpec.handlers, hidden : colSpec.hidden, datatype : colSpec.datatype, sortable : colSpec.sortable, @@ -2076,6 +2084,31 @@ angular.module('egGridMod', }; }) +/* https://stackoverflow.com/questions/17343696/adding-an-ng-click-event-inside-a-filter/17344875#17344875 */ +.directive('compile', ['$compile', function ($compile) { + return function(scope, element, attrs) { + // pass through column defs from grid cell's scope + scope.col = scope.$parent.col; + scope.$watch( + function(scope) { + // watch the 'compile' expression for changes + return scope.$eval(attrs.compile); + }, + function(value) { + // when the 'compile' expression changes + // assign it into the current DOM + element.html(value); + + // compile the new DOM and link it to the current + // scope. + // NOTE: we only compile .childNodes so that + // we don't get into infinite loop compiling ourselves + $compile(element.contents())(scope); + } + ); + }; +}]) + /** diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js index 47632ed696..1a3f8220ed 100644 --- a/Open-ILS/web/js/ui/default/staff/services/ui.js +++ b/Open-ILS/web/js/ui/default/staff/services/ui.js @@ -634,6 +634,256 @@ function($window , egStrings) { return service; }]) +/** + * egAddCopyAlertDialog - manage copy alerts + */ +.factory('egAddCopyAlertDialog', + ['$uibModal','$interpolate','egCore', +function($uibModal , $interpolate , egCore) { + var service = {}; + + service.open = function(args) { + return $uibModal.open({ + templateUrl: './share/t_add_copy_alert_dialog', + controller: ['$scope','$q','$uibModalInstance', + function( $scope , $q , $uibModalInstance) { + + $scope.copy_ids = args.copy_ids; + egCore.pcrud.search('ccat', + { active : 't' }, + {}, + { atomic : true } + ).then(function (ccat) { + $scope.alert_types = ccat; + }); + + $scope.copy_alert = { + create_staff : egCore.auth.user().id(), + note : '', + temp : false + }; + + $scope.ok = function(copy_alert) { + if (typeof(copy_alert.note) != 'undefined' && + copy_alert.note != '') { + copy_alerts = []; + angular.forEach($scope.copy_ids, function (cp_id) { + var a = new egCore.idl.aca(); + a.isnew(1); + a.create_staff(copy_alert.create_staff); + a.note(copy_alert.note); + a.temp(copy_alert.temp ? 't' : 'f'); + a.copy(cp_id); + a.ack_time(null); + a.alert_type( + $scope.alert_types.filter(function(at) { + return at.id() == copy_alert.alert_type; + })[0] + ); + copy_alerts.push( a ); + }); + if (copy_alerts.length > 0) { + egCore.pcrud.apply(copy_alerts); + } + } + if (args.ok) args.ok(); + $uibModalInstance.close() + } + $scope.cancel = function() { + if (args.cancel) args.cancel(); + $uibModalInstance.dismiss(); + } + } + ] + }) + } + + return service; +}]) + +/** + * egCopyAlertManagerDialog - manage copy alerts + */ +.factory('egCopyAlertManagerDialog', + ['$uibModal','$interpolate','egCore', +function($uibModal , $interpolate , egCore) { + var service = {}; + + service.get_user_copy_alerts = function(copy_id) { + return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null }, + { flesh : 1, flesh_fields : { aca : ['alert_type'] } }, + { atomic : true } + ); + } + + service.open = function(args) { + return $uibModal.open({ + templateUrl: './share/t_copy_alert_manager_dialog', + controller: ['$scope','$q','$uibModalInstance', + function( $scope , $q , $uibModalInstance) { + + function init(args) { + var defer = $q.defer(); + if (args.copy_id) { + service.get_user_copy_alerts(args.copy_id).then(function(aca) { + defer.resolve(aca); + }); + } else { + defer.resolve(args.alerts); + } + return defer.promise; + } + + // returns a promise resolved with the list of circ statuses + $scope.get_copy_statuses = function() { + if (egCore.env.ccs) + return $q.when(egCore.env.ccs.list); + + return egCore.pcrud.retrieveAll('ccs', null, {atomic : true}) + .then(function(list) { + egCore.env.absorbList(list, 'ccs'); + return list; + }); + }; + + $scope.mode = args.mode || 'checkin'; + + var next_statuses = []; + var seen_statuses = {}; + $scope.next_statuses = []; + $scope.params = { + 'the_next_status' : null + } + init(args).then(function(copy_alerts) { + $scope.alerts = copy_alerts; + angular.forEach($scope.alerts, function(copy_alert) { + var state = copy_alert.alert_type().state(); + copy_alert.evt = copy_alert.alert_type().event(); + + copy_alert.message = copy_alert.note() || + egCore.strings.ON_DEMAND_COPY_ALERT[copy_alert.evt][state]; + + if (copy_alert.temp() == 't') { + angular.forEach(copy_alert.alert_type().next_status(), function (st) { + if (!seen_statuses[st]) { + seen_statuses[st] = true; + next_statuses.push(st); + } + }); + } + }); + if ($scope.mode == 'checkin' && next_statuses.length > 0) { + $scope.get_copy_statuses().then(function() { + angular.forEach(next_statuses, function(st) { + if (egCore.env.ccs.map[st]) + $scope.next_statuses.push(egCore.env.ccs.map[st]); + }); + $scope.params.the_next_status = $scope.next_statuses[0].id(); + }); + } + }); + + $scope.isAcknowledged = function(copy_alert) { + return (copy_alert.acked); + }; + $scope.canBeAcknowledged = function(copy_alert) { + return (!copy_alert.ack_time() && copy_alert.temp() == 't'); + }; + $scope.canBeRemoved = function(copy_alert) { + return (!copy_alert.ack_time() && copy_alert.temp() == 'f'); + }; + + $scope.ok = function() { + var acks = []; + angular.forEach($scope.alerts, function (copy_alert) { + if (copy_alert.acked) { + copy_alert.ack_time('now'); + copy_alert.ack_staff(egCore.auth.user().id()); + copy_alert.ischanged(true); + acks.push(copy_alert); + } + }); + if (acks.length > 0) { + egCore.pcrud.apply(acks); + } + if (args.ok) args.ok($scope.params.the_next_status); + $uibModalInstance.close() + } + $scope.cancel = function() { + if (args.cancel) args.cancel(); + $uibModalInstance.dismiss(); + } + } + ] + }) + } + + return service; +}]) + +/** + * egCopyAlertEditorDialog - manage copy alerts + */ +.factory('egCopyAlertEditorDialog', + ['$uibModal','$interpolate','egCore', +function($uibModal , $interpolate , egCore) { + var service = {}; + + service.get_user_copy_alerts = function(copy_id) { + return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null }, + { flesh : 1, flesh_fields : { aca : ['alert_type'] } }, + { atomic : true } + ); + } + + service.get_copy_alert_types = function() { + return egCore.pcrud.search('ccat', + { active : 't' }, + {}, + { atomic : true } + ); + }; + + service.open = function(args) { + return $uibModal.open({ + templateUrl: './share/t_copy_alert_editor_dialog', + controller: ['$scope','$q','$uibModalInstance', + function( $scope , $q , $uibModalInstance) { + + function init(args) { + var defer = $q.defer(); + if (args.copy_id) { + service.get_user_copy_alerts(args.copy_id).then(function(aca) { + defer.resolve(aca); + }); + } else { + defer.resolve(args.alerts); + } + return defer.promise; + } + + init(args).then(function(copy_alerts) { + $scope.copy_alert_list = copy_alerts; + }); + service.get_copy_alert_types().then(function(ccat) { + $scope.alert_types = ccat; + }); + + $scope.ok = function() { + egCore.pcrud.apply($scope.copy_alert_list); + $uibModalInstance.close() + } + $scope.cancel = function() { + if (args.cancel) args.cancel(); + $uibModalInstance.dismiss(); + } + } + ] + }) + } + + return service; +}]) .directive('aDisabled', function() { return { restrict : 'A', @@ -676,9 +926,9 @@ function($window , egStrings) { template: '
'+ ''+ - '
'+ - ''+ - '