<field reporter:label="Last Captured Hold" name="last_captured_hold" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Has Holds" name="holds_count" oils_persist:virtual="true" reporter:datatype="link"/>
<field reporter:label="Copy Tags" name="tags" oils_persist:virtual="true" reporter:datatype="link"/>
+ <field reporter:label="Copy Alerts" name="copy_alerts" oils_persist:virtual="true" reporter:datatype="link"/>
</fields>
<links>
<link field="age_protect" reltype="has_a" key="id" map="" class="crahp"/>
<link field="floating" reltype="has_a" key="id" map="" class="cfg"/>
<link field="holds_count" reltype="might_have" key="id" map="" class="hasholdscount"/>
<link field="tags" reltype="has_many" key="copy" map="" class="acptcm"/>
+ <link field="copy_alerts" reltype="has_many" key="copy" map="" class="aca"/>
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
</permacrud>
</class>
+ <class id="ccat" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::copy_alert_type" oils_persist:tablename="config.copy_alert_type" reporter:label="Copy Alert Type" oils_persist:restrict_primary="100">
+ <fields oils_persist:primary="id" oils_persist:sequence="config.copy_alert_type_id_seq">
+ <field reporter:label="Id" name="id" reporter:selector="name" reporter:datatype="id"/>
+ <field reporter:label="Scope Org Unit" name="scope_org" reporter:datatype="org_unit"/>
+ <field reporter:label="Active" name="active" reporter:datatype="bool" />
+ <field reporter:label="Name" name="name" reporter:datatype="text" />
+ <field reporter:label="State" name="state" reporter:datatype="text"/>
+ <field reporter:label="Event" name="event" reporter:datatype="text" />
+ <field reporter:label="During Renewal" name="in_renew" reporter:datatype="bool" />
+ <field reporter:label="Allow At Copy Circ Lib" name="at_circ" reporter:datatype="bool"/>
+ <field reporter:label="Allow At Copy Owning Lib" name="at_owning" reporter:datatype="bool"/>
+ <field reporter:label="Invert allowed locations" name="invert_location" reporter:datatype="bool"/>
+ <field reporter:label="Next Statuses" name="next_status" reporter:datatype="text"/>
+ </fields>
+ <links>
+ <link field="scope_org" reltype="has_a" key="id" map="" class="aou"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="ADMIN_COPY_ALERT_TYPE CREATE_COPY_ALERT_TYPE" context_field="scope_org"/>
+ <retrieve/>
+ <update permission="ADMIN_COPY_ALERT_TYPE UPDATE_COPY_ALERT_TYPE" context_field="scope_org"/>
+ <delete permission="ADMIN_COPY_ALERT_TYPE DELETE_COPY_ALERT_TYPE" context_field="scope_org"/>
+ </actions>
+ </permacrud>
+ </class>
+
+ <class id="acas" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::copy_alert_suppress" oils_persist:tablename="actor.copy_alert_suppress" reporter:label="Copy Alert Suppression">
+ <fields oils_persist:primary="id" oils_persist:sequence="actor.copy_alert_suppress_id_seq">
+ <field reporter:label="Id" name="id" reporter:datatype="id"/>
+ <field reporter:label="Org Unit" name="org" reporter:datatype="org_unit"/>
+ <field reporter:label="Alert Type" name="alert_type" reporter:datatype="link" />
+ </fields>
+ <links>
+ <link field="org" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="alert_type" reltype="has_a" key="id" map="" class="ccat"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="ADMIN_COPY_ALERT_SUPPRESS CREATE_COPY_ALERT_SUPPRESS" context_field="org"/>
+ <retrieve/>
+ <update permission="ADMIN_COPY_ALERT_SUPPRESS UPDATE_COPY_ALERT_SUPPRESS" context_field="org"/>
+ <delete permission="ADMIN_COPY_ALERT_SUPPRESS DELETE_COPY_ALERT_SUPPRESS" context_field="org"/>
+ </actions>
+ </permacrud>
+ </class>
+
+ <class id="aca" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_alert" oils_persist:tablename="asset.copy_alert" reporter:label="Copy Alert">
+ <fields oils_persist:primary="id" oils_persist:sequence="asset.copy_alert_id_seq">
+ <field reporter:label="Id" name="id" reporter:datatype="id"/>
+ <field reporter:label="Alert Type" name="alert_type" reporter:datatype="link" />
+ <field reporter:label="Copy" name="copy" reporter:datatype="link"/>
+ <field reporter:label="Temporary" name="temp" reporter:datatype="bool" />
+ <field reporter:label="Create Date/Time" name="create_time" reporter:datatype="timestamp" />
+ <field reporter:label="Creator" name="create_staff" reporter:datatype="link"/>
+ <field reporter:label="Note" name="note" reporter:datatype="text"/>
+ <field reporter:label="Acknowledge Date/Time" name="ack_time" reporter:datatype="timestamp" />
+ <field reporter:label="Acknowledger" name="ack_staff" reporter:datatype="link"/>
+ </fields>
+ <links>
+ <link field="alert_type" reltype="has_a" key="id" map="" class="ccat"/>
+ <link field="copy" reltype="has_a" key="id" map="" class="acp"/>
+ <link field="create_staff" reltype="has_a" key="id" map="" class="au"/>
+ <link field="ack_staff" reltype="has_a" key="id" map="" class="au"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="ADMIN_COPY_ALERT CREATE_COPY_ALERT" global_required="true"/>
+ <retrieve permission="ADMIN_COPY_ALERT VIEW_COPY_ALERT" global_required="true"/>
+ <update permission="ADMIN_COPY_ALERT UPDATE_COPY_ALERT" global_required="true"/>
+ <delete permission="ADMIN_COPY_ALERT DELETE_COPY_ALERT" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+
+ <class id="aaca" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::active_copy_alert" oils_persist:tablename="asset.active_copy_alert" reporter:label="Active Copy Alert" oils_persist:readonly="true">
+ <fields oils_persist:primary="id">
+ <field reporter:label="Id" name="id" reporter:datatype="id"/>
+ <field reporter:label="Alert Type" name="alert_type" reporter:datatype="link" />
+ <field reporter:label="Copy" name="copy" reporter:datatype="link"/>
+ <field reporter:label="Temporary" name="temp" reporter:datatype="bool" />
+ <field reporter:label="Create Date/Time" name="create_time" reporter:datatype="timestamp" />
+ <field reporter:label="Creator" name="create_staff" reporter:datatype="link"/>
+ <field reporter:label="Note" name="note" reporter:datatype="text"/>
+ <field reporter:label="Acknowledge Date/Time" name="ack_time" reporter:datatype="timestamp" />
+ <field reporter:label="Acknowledger" name="ack_staff" reporter:datatype="link"/>
+ </fields>
+ <links>
+ <link field="alert_type" reltype="has_a" key="id" map="" class="ccat"/>
+ <link field="copy" reltype="has_a" key="id" map="" class="acp"/>
+ <link field="create_staff" reltype="has_a" key="id" map="" class="au"/>
+ <link field="ack_staff" reltype="has_a" key="id" map="" class="au"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="ADMIN_COPY_ALERT CREATE_COPY_ALERT" global_required="true"/>
+ <retrieve permission="ADMIN_COPY_ALERT VIEW_COPY_ALERT" global_required="true"/>
+ <update permission="ADMIN_COPY_ALERT UPDATE_COPY_ALERT" global_required="true"/>
+ <delete permission="ADMIN_COPY_ALERT DELETE_COPY_ALERT" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+
<class id="act" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_template" oils_persist:tablename="asset.copy_template" reporter:label="Asset Copy Template">
<fields oils_persist:primary="id" oils_persist:sequence="asset.copy_template_id_seq">
<field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name" />
);
}
+# 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.
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) = @_;
return undef;
}
-
-
sub update_copy {
my($class, $editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib) = @_;
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;
$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;
}
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(
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 =
$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();
return $data;
} elsif( $api =~ /checkout/ ) {
- $circulator->is_checkout(1);
$circulator->do_checkout();
} elsif( $circulator->is_res_checkin ) {
$circulator->do_checkin();
} elsif( $api =~ /renew/ ) {
- $circulator->is_renewal(1);
$circulator->do_renew();
}
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
}
}
+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
# --------------------------------------------------------------------------
}
},
"where" => {
+ deleted => 'f',
"+bresv" => {
"id" => (ref $self->reservation) ?
$self->reservation->id : $self->reservation
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'))
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)
$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 {
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);
}
} 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;
}
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".
} 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;
}
}
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 ||
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;
--- /dev/null
+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;
--- /dev/null
+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;
+
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+[% ctx.page_title = l("Copy Alert Suppression"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(record)">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 ng-if="creating" class="modal-title">[% l('Create copy alert suppression rule') %]</h4>
+ <h4 ng-if="!creating" class="modal-title">[% l('Update copy alert suppression rule') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="copy-alert-type-selector">[% l('Alert Type') %]</label>
+ <select id="copy-alert-type-selector" class="form-control"
+ ng-model="record.alert_type"
+ ng-options="at.id() as at.name() for at in ccat">
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="select-org-unit">[% l('Org Unit') %]</label>
+ <eg-org-selector selected="record.org"></eg-org-selector>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" ng-disabled="form.$invalid"
+ class="btn btn-primary" value="[% l('Save') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+ </div>
+ </div> <!-- modal-content -->
+</form>
--- /dev/null
+[% ctx.page_title = l("Copy Alert Types"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(record)">
+ <div>
+ <div class="modal-header">
+ <button type="button" class="close"
+ ng-click="cancel()" aria-hidden="true">×</button>
+ <h4 ng-if="creating" class="modal-title">[% l('Create copy alert type') %]</h4>
+ <h4 ng-if="!creating" class="modal-title">[% l('Update copy alert type') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="edit-alert-name">[% l('Name') %]</label>
+ <input type="text" class="form-control" focus-me='focusMe' required
+ id="edit-alert-name" ng-model="record.name" placeholder="[% l('Name...') %]"/>
+ </div>
+ <div class="form-group">
+ <label for="active-selector">[% l('Active') %]</label>
+ <select id="active-selector" class="form-control" ng-model="record.active">
+ <option value="t">[% l('Yes') %]</option>
+ <option value="f">[% l('No') %]</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="state-selector">[% l('State') %]</label>
+ <select id="state-selector" class="form-control" ng-model="record.state">
+ <option value="NORMAL">[% l('Normal') %]</option>
+ <option value="LOST">[% l('Lost') %]</option>
+ <option value="LOST_AND_PAID">[% l('Lost and paid for') %]</option>
+ <option value="LONGOVERDUE">[% l('Long Overdue') %]</option>
+ <option value="MISSING">[% l('Missing') %]</option>
+ <option value="DAMAGED">[% l('Damaged') %]</option>
+ <option value="CLAIMSRETURNED">[% l('Claims returned') %]</option>
+ <option value="CLAIMSNEVERCHECKEDOUT">[% l('Claims never checked out') %]</option>
+ </select>
+ </div>
+ <div class="form-group nullable">
+ <label for="event-selector">[% l('Event') %]</label>
+ <select id="event-selector" class="form-control" ng-model="record.event"
+ ng-init="event_list = [{l:'[% l('Checkin') %]',v:'CHECKIN'},{l:'[% l('Checkout') %]',v:'CHECKOUT'}]"
+ ng-options="e.v as e.l for e in event_list">
+ <option value="">[% l('Any Event') %]</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="select-org-unit">[% l('Scope Org Unit') %]</label>
+ <eg-org-selector selected="record.scope_org"></eg-org-selector>
+ </div>
+ <div class="form-group">
+ <label for="edit-alert-next-statuses">[% l('Next Status') %]</label>
+ <select id="edit-alert-next-statuses" class="form-control" focus-me='focusMe'
+ multiple="multiple" ng-model="record.next_status">
+ <option ng-repeat="s in ccs" value="{{s.id()}}">{{s.name()}}</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="inrenew-selector">[% l('Renewing?') %]</label>
+ <select id="inrenew-selector" class="form-control" ng-model="record.in_renew">
+ <option value="">[% l('Any') %]</option>
+ <option value="t">[% l('Yes') %]</option>
+ <option value="f">[% l('No') %]</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="invert-location-selector">[% l('Invert location?') %]</label>
+ <select id="invert-location-selector" class="form-control" ng-model="record.invert_location">
+ <option value="t">[% l('Yes') %]</option>
+ <option value="f">[% l('No') %]</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="at-circ-selector">[% l('At Circulation Library?') %]</label>
+ <select id="at-circ-selector" class="form-control" ng-model="record.at_circ">
+ <option value="">[% l('Do not care') %]</option>
+ <option value="t">[% l('Yes') %]</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="at-owning-selector">[% l('At Owning Library?') %]</label>
+ <select id="at-owning-selector" class="form-control" ng-model="record.at_owning">
+ <option value="">[% l('Do not care') %]</option>
+ <option value="t">[% l('Yes') %]</option>
+ </select>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <input type="submit" ng-disabled="form.$invalid"
+ class="btn btn-primary" value="[% l('Save') %]"/>
+ <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+ </div>
+ </div> <!-- modal-content -->
+</form>
[% BLOCK APP_JS %]
<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/local/app.js"></script>
<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+ s.REMOVE_ITEM_CONFIRM = '[% l('Delete rows?') %]';
+}]);
+</script
[% END %]
<div ng-view></div>
--- /dev/null
+<eg-grid
+ id-field="id"
+ idl-class="{{baseFmClass}}"
+ features="-multisort,-multiselect"
+ grid-controls="gridControls"
+ auto-fields="true"
+ persist-key="admin.local.config.grideditor.{{baseFmClass}}">
+
+ <eg-grid-menu-item label="[% l('Create') %]" handler="createHandler"></eg-grid-menu-item>
+ <eg-grid-action handler="editHandler" label="[% l('Edit') %]"></eg-grid-action>
+ <eg-grid-action handler="deleteHandler" label="[% l('Delete') %]"></eg-grid-action>
+</eg-grid>
,[ 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" ]
label="[% l('Copies') %]"></eg-grid-action>
<eg-grid-action handler="selectedHoldingsVolCopyAdd" group="[% l('Add') %]"
label="[% l('Volumes and Copies') %]"></eg-grid-action>
+ <eg-grid-action handler="selectedHoldingsCopyAlertsAdd" group="[% l('Add') %]" disabled="vols_not_shown"
+ label="[% l('Copy Alerts') %]"></eg-grid-action>
<eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
label="[% l('Volumes') %]"></eg-grid-action>
label="[% l('Volumes and Copies') %]"></eg-grid-action>
<eg-grid-action handler="replaceBarcodes" group="[% l('Edit') %]"
label="[% l('Replace Barcodes') %]"></eg-grid-action>
+ <eg-grid-action handler="selectedHoldingsCopyAlertsManage" group="[% l('Edit') %]"
+ label="[% l('Manage Copy Alerts') %]"></eg-grid-action>
<eg-grid-action handler="selectedHoldingsEmptyVolCopyDelete" group="[% l('Delete') %]" disabled="vols_not_shown"
label="[% l('Empty Volumes') %]"></eg-grid-action>
<eg-grid-field label="[% l('Holdable') %]" datatype="bool" path="holdable"></eg-grid-field>
<eg-grid-field label="[% l('Age-based Hold Protection') %]" path="age_protect.name"></eg-grid-field>
<eg-grid-field label="[% l('Reference') %]" datatype="bool" path="ref"></eg-grid-field>
+ <eg-grid-field label="[% l('Alerts') %]" path="copy_alert_count" handlers="gridCellHandlers" visible compiled>
+ {{item['copy_alert_count']}}
+ <button ng-disabled="item['copy_alert_count'] <= 0" class="btn btn-sm btn-default" ng-click="col.handlers.copyAlertsEdit(item['id'])">[% l('Manage') %]</button>
+ </eg-grid-field>
</eg-grid>
</div>
label="[% l('Items') %]"></eg-grid-action>
<eg-grid-action handler="selectedHoldingsVolCopyAdd" group="[% l('Add') %]"
label="[% l('Volumes and Items') %]"></eg-grid-action>
+ <eg-grid-action handler="selectedHoldingsCopyAlertsAdd" group="[% l('Add') %]"
+ label="[% l('Copy Alerts') %]"></eg-grid-action>
<eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
label="[% l('Volumes') %]"></eg-grid-action>
label="[% l('Volumes and Items') %]"></eg-grid-action>
<eg-grid-action handler="replaceBarcodes" group="[% l('Edit') %]"
label="[% l('Replace Barcodes') %]"></eg-grid-action>
+ <eg-grid-action handler="selectedHoldingsCopyAlertsEdit" group="[% l('Edit') %]"
+ label="[% l('Manage Copy Alerts') %]"></eg-grid-action>
<eg-grid-action handler="changeItemOwningLib" group="[% l('Transfer') %]"
label="[% l('Items to Previously Marked Library') %]"></eg-grid-action>
</a>
</eg-grid-field>
-
<eg-grid-field label="[% l('Acquisition Cost') %]" path="cost" hidden></eg-grid-field>
<eg-grid-field label="[% l('Age-Based Hold Protection') %]" path="age_protect" hidden></eg-grid-field>
<eg-grid-field label="[% l('Author') %]" path="call_number.record.simple_record.author" hidden></eg-grid-field>
<eg-grid-field label="[% l('TCN') %]" path="call_number.record.tcn_value" hidden></eg-grid-field>
<eg-grid-field label="[% l('TCN Source') %]" path="call_number.record.tcn_source" hidden></eg-grid-field>
<eg-grid-field label="[% l('Transaction Complete') %]" path="_circ.xact_finish" datatype="timestamp" hidden></eg-grid-field>
-</eg-grid>
+ <eg-grid-field label="[% l('Alerts') %]" path="copy_alert_count" handlers="gridCellHandlers" visible compiled>
+ {{item['copy_alert_count']}}
+ <button ng-disabled="item['copy_alert_count'] <= 0" class="btn btn-sm btn-default" ng-click="col.handlers.copyAlertsEdit(item['id'])">[% l('Manage') %]</button>
+ </eg-grid-field>
+
</eg-grid>
<div class="flex-row pad-vert">
</div>
<div class="flex-row">
- <div class="flex-cell">[% l('Alert Message') %]</div>
- <div class="well" style="flex:7">{{copy.alert_message()}}</div>
+ <div class="flex-cell">[% l('Copy Alerts') %]</div>
+ <div id="item-status-alert-msg">
+ <button class="btn btn-default" ng-click="addCopyAlerts(copy.id())" >[% l('Add') %]</button>
+ <button class="btn btn-default" ng-click="manageCopyAlerts(copy.id())" >[% l('Manage') %]</button>
+ </div>
</div>
</div>
type="button">
[% l('Copy Notes') %]
</button>
+ <button
+ class="btn btn-default"
+ ng-disabled="!defaults.copy_alerts"
+ ng-click="copy_alerts_dialog(workingGridControls.selectedItems())"
+ type="button">
+ [% l('Copy Alerts') %]
+ </button>
</div>
</div>
--- /dev/null
+<form ng-submit="ok(copy_alert)" role="form">
+ <div class="modal-header">
+ <button type="button" class="close" ng-click="cancel()"
+ aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('New Copy Alert') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="row">
+ <div class="col-md-6 form-inline">
+ <label for="copy-alert-type-selector"> [% l('Type') %]</label>
+ <select id="copy-alert-type-selector" class="form-control"
+ ng-model="copy_alert.alert_type"
+ ng-options="at.id() as at.name() for at in alert_types">
+ </select>
+ </div>
+ <div class="col-md-3">
+ <label>
+ <input type="checkbox" ng-model="copy_alert.temp"/>
+ [% l('Temporary') %]
+ </label>
+ </div>
+ </div>
+ <div class="row pad-vert">
+ <div class="col-md-12">
+ <textarea class="form-control"
+ ng-model="copy_alert.note" placeholder="[% l('Alert...') %]">
+ </textarea>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <div class="row">
+ <div class="col-md-10 pull-right">
+ <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+ <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+ </div>
+ </div>
+
+ <div class="row pad-vert" ng-if="copy_alert_list.length > 0">
+ <div class="col-md-12">
+ <div class="row">
+ <div class="col-md-12">
+ <hr/>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <h4 class="pull-left">[% l('Existing Copy Alerts') %]</h4>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row" ng-repeat="a in copy_alert_list" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
+ <div class="col-md-12">
+ <div class="row">
+ <div class="col-md-6 form-inline">
+ <label for="copy-alert-type-select-{{a.id()}}">[% l('Type') %]</label>
+ <select id="copy-alert-type-select-{{a.id()}}" class="form-control"
+ ng-model="alert_type"
+ ng-change="a.alert_type(alert_type) && a.ischanged(1)"
+ ng-options="at.id() as at.name() for at in alert_types">
+ </select>
+ </div>
+ <div class="col-md-3">
+ <label>
+ <input type="checkbox" ng-model="temp" ng-change="a.temp(temp ? 't' : 'f') && a.ischanged(1)" ng-disabled="acked"/>
+ [% l('Temporary') %]
+ </label>
+ </div>
+ <div class="col-md-3">
+ <label>
+ <input type="checkbox" ng-model="acked" ng-change="(acked ? a.ack_time('now') : a.ack_time(null)) && a.ischanged(1)"/>
+ [% l('Clear?') %]
+ </label>
+ </div>
+ </div>
+ <div class="row pad-vert">
+ <div class="col-md-12">
+ <textarea class="form-control" ng-change="a.note(note) && a.ischanged(1)"
+ ng-model="note" placeholder="[% l('Alert...') %]" ng-disabled="acked">
+ </textarea>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <hr/>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</form>
</label>
</div>
</div>
+
+ <div class="row">
+ <div class="col-xs-6">
+ <label>
+ <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.copy_alerts"/>
+ [% l('Add/Edit Copy Alerts') %]
+ </label>
+ </div>
+ </div>
</div>
<div class="col-md-4">
handler="abortTransit"
label="[% l('Cancel Transits') %]">
</eg-grid-action>
+ <eg-grid-action
+ handler="addCopyAlerts"
+ label="[% l('Add Copy Alerts') %]">
+ </eg-grid-action>
+ <eg-grid-action
+ handler="manageCopyAlerts"
+ label="[% l('Manage Copy Alerts') %]">
+ </eg-grid-action>
+
<!-- Show Group -->
<eg-grid-action handler="showBibHolds" group="[% l('Show') %]"
label="[% l('Record Holds') %]">
<eg-grid-action handler="printSpineLabels" group="[% l('Print') %]"
label="[% l('Spine Labels') %]">
</eg-grid-action>
- <eg-grid-field label="[% l('Alert Msg') %]"
- path="acp.alert_message"></eg-grid-field>
<eg-grid-field label="[% l('Balance Owed') %]"
path='mbts.balance_owed' comparator="sort_money"></eg-grid-field>
persist-key="circ.patron.checkout"
dateformat="{{$root.egDateAndTimeFormat}}">
- <eg-grid-field label="[% l('Alert Msg') %]"
- path="acp.alert_message"></eg-grid-field>
+ <eg-grid-action
+ handler="addCopyAlerts"
+ label="[% l('Add Copy Alerts') %]">
+ </eg-grid-action>
+ <eg-grid-action
+ handler="manageCopyAlerts"
+ label="[% l('Manage Copy Alerts') %]">
+ </eg-grid-action>
<eg-grid-field label="[% l('Balance Owed') %]"
path='mbts.balance_owed'></eg-grid-field>
<eg-grid-field path="acp.circ_modifier.name" label="[% l('Circulation Modifier') %]"></eg-grid-field>
<eg-grid-field path="acp.circ_lib.shortname" label="[% l('Circulation Library') %]"></eg-grid-field>
<eg-grid-field path="acn.owning_lib.shortname" label="[% l('Owning Library') %]"></eg-grid-field>
+
+ <eg-grid-field label="[% l('Alerts') %]" path="copy_alert_count" handlers="gridCellHandlers" visible compiled>
+ {{item['copy_alert_count']}}
+ <button ng-disabled="item['copy_alert_count'] <= 0" class="btn btn-sm btn-default" ng-click="col.handlers.copyAlertsEdit(item['acp'].id())">[% l('Manage') %]</button>
+ </eg-grid-field>
+
<eg-grid-field path="circ.*" parent-idl-class="circ" hidden></eg-grid-field>
<eg-grid-field path="acp.*" parent-idl-class="acp" hidden></eg-grid-field>
<eg-grid-field path="acn.*" parent-idl-class="acn" hidden></eg-grid-field>
handler="abortTransit"
label="[% l('Cancel Transits') %]">
</eg-grid-action>
+ <eg-grid-action divider="true"></eg-grid-action>
+ <eg-grid-action
+ handler="addCopyAlerts"
+ label="[% l('Add Copy Alerts') %]">
+ </eg-grid-action>
+ <eg-grid-action
+ handler="manageCopyAlerts"
+ label="[% l('Manage Copy Alerts') %]">
+ </eg-grid-action>
- <eg-grid-field label="[% l('Alert Msg') %]"
- path="acp.alert_message"></eg-grid-field>
-
<eg-grid-field label="[% l('Balance Owed') %]"
path='mbts.balance_owed' comparator="sort_money"></eg-grid-field>
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") %]'
+ }
+};
}]);
</script>
/* 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;}
--- /dev/null
+<form ng-submit="ok(copy_alert)" role="form">
+ <div class="modal-header">
+ <button type="button" class="close" ng-click="cancel()"
+ aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Add Copy Alert') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="row">
+ <div class="col-md-6 form-inline">
+ <label for="copy-alert-type-selector"> [% l('Type') %]</label>
+ <select id="copy-alert-type-selector" class="form-control"
+ ng-model="copy_alert.alert_type"
+ ng-options="at.id() as at.name() for at in alert_types">
+ </select>
+ </div>
+ <div class="col-md-3">
+ <label>
+ <input type="checkbox" ng-model="copy_alert.temp"/>
+ [% l('Temporary') %]
+ </label>
+ </div>
+ </div>
+ <div class="row pad-vert">
+ <div class="col-md-12">
+ <textarea class="form-control"
+ ng-model="copy_alert.note" placeholder="[% l('Alert...') %]">
+ </textarea>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <div class="row">
+ <div class="col-md-10 pull-right">
+ <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+ <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+ </div>
+ </div>
+
+ </div>
+</form>
<div class="eg-grid" ng-class="{'eg-grid-as-conf' : showGridConf}">
<!-- import our eg-grid-field defs -->
- <div ng-transclude></div>
+ <div style="display: none;" ng-transclude></div>
<div class="eg-grid-row eg-grid-header-row">
<div class="eg-grid-cell eg-grid-cell-stock" ng-show="showIndex">
<!-- if the cell comes with its own template,
translate that content into HTML and insert it here -->
- <span ng-if="col.template" style="padding-left:5px; padding-right:10px;"
+ <span ng-if="col.template && !col.compiled" style="padding-left:5px; padding-right:10px;"
ng-bind-html="translateCellTemplate(col, item)">
</span>
+ <span ng-if="col.template && col.compiled" style="padding-left:5px; padding-right:10px;"
+ compile="col.template">
+ </span>
+
<!-- otherwise, simply display the item value, which may
pass through datatype-specific filtering. -->
<span ng-if="!col.template" style="padding-left:5px; padding-right:10px;">
--- /dev/null
+<form ng-submit="ok(copy_alert)" role="form">
+ <div class="modal-header">
+ <button type="button" class="close" ng-click="cancel()"
+ aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Manage Copy Alerts') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div class="row" ng-repeat="a in copy_alert_list" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
+ <div class="col-md-12">
+ <div class="row">
+ <div class="col-md-6 form-inline">
+ <label for="copy-alert-type-select-{{a.id()}}">[% l('Type') %]</label>
+ <select id="copy-alert-type-select-{{a.id()}}" class="form-control"
+ ng-model="alert_type"
+ ng-change="a.alert_type(alert_type) && a.ischanged(1)"
+ ng-options="at.id() as at.name() for at in alert_types">
+ </select>
+ </div>
+ <div class="col-md-3">
+ <label>
+ <input type="checkbox" ng-model="temp" ng-change="a.temp(temp ? 't' : 'f') && a.ischanged(1)" ng-disabled="acked"/>
+ [% l('Temporary') %]
+ </label>
+ </div>
+ <div class="col-md-3">
+ <label>
+ <input type="checkbox" ng-model="acked" ng-change="(acked ? a.ack_time('now') : a.ack_time(null)) && a.ischanged(1)"/>
+ [% l('Clear?') %]
+ </label>
+ </div>
+ </div>
+ <div class="row pad-vert">
+ <div class="col-md-12">
+ <textarea class="form-control" ng-change="a.note(note) && a.ischanged(1)"
+ ng-model="note" placeholder="[% l('Alert...') %]" ng-disabled="acked">
+ </textarea>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <hr/>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <div class="row">
+ <div class="col-md-10 pull-right">
+ <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+ <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+ </div>
+ </div>
+ </div>
+</form>
--- /dev/null
+<!--
+ Copy alert manager dialog
+-->
+<div>
+ <div class="modal-header">
+ <h4 class="modal-title alert alert-info">[% l('Copy alerts') %]</h4>
+ </div>
+ <div class="modal-body">
+ <div>
+ <div class="row" ng-repeat="alert in alerts" style="border-bottom: 1px solid grey; margin-top: 3px;">
+ <div class="col-md-2">{{alert.evt}}</div>
+ <div class="col-md-8" ng-class="{ acknowledged: isAcknowledged(alert) }">{{alert.message}}</div>
+ <div class="col-md-2">
+ <button ng-if="canBeAcknowledged(alert)"
+ class="btn btn-xs btn-default"
+ ng-click="alert.acked = !alert.acked" >[% l('Clear') %]</button>
+ <button ng-if="canBeRemoved(alert) && mode == 'manage'"
+ class="btn btn-xs btn-default"
+ ng-click="alert.acked = !alert.acked" >[% l('Remove') %]</button>
+ </div>
+ </div>
+ </div>
+ <div ng-if="mode == 'checkin' && next_statuses.length > 0">
+ <div ng-if="next_statuses.length == 1" class="row">
+ <div class="col-md-8">
+ <b>[% l('Next copy status: ') %]</b> {{next_statuses[0].name()}}
+ </div>
+ </div>
+ <div ng-if="next_statuses.length > 1" class="row">
+ <div class="col-md-4">
+ <label for="select-next-status"><b>[% l('Next copy status') %]<b></label>
+ </div>
+ <div class="col-md-4">
+ <select id="select-next-status" class="form-control"
+ ng-model="params.the_next_status" focus-me="true"
+ ng-options="st.id() as st.name() for st in next_statuses">
+ </select>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ [% dialog_footer %]
+ <input type="submit" class="btn btn-primary"
+ ng-click="ok()" value="[% l('OK/Continue') %]"/>
+ <button class="btn btn-warning"
+ ng-click="cancel()">[% l('Cancel') %]</button>
+ </div>
+</div>
angular.module('egLocalAdmin',
- ['ngRoute', 'ui.bootstrap', 'egCoreMod','egUiMod'])
+ ['ngRoute', 'ui.bootstrap', 'egCoreMod','egUiMod','egGridMod'])
.config(['$routeProvider','$locationProvider','$compileProvider',
function($routeProvider , $locationProvider , $compileProvider) {
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,
$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();
+ });
+ });
+ };
+}])
+
});
}
+ $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();
});
}
+ $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;
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';
* 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';
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');
}
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']
}
}
}
});
+ // 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;
);
};
+ 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'},
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();
statcats : true,
copy_notes : true,
copy_tags : true,
+ copy_alerts : true,
attributes : {
status : true,
loan_duration : true,
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];
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 {
});
}
+ $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 () {
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;
statcats : true,
copy_notes : true,
copy_tags : true,
+ copy_alerts : true,
attributes : {
status : true,
loan_duration : true,
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) {
}
ngToast.create(egCore.strings.VOL_COPY_TEMPLATE_SUCCESS_SAVE);
}
-
+
$scope.templates = {};
$scope.imported_templates = { data : '' };
$scope.template_name = '';
}
$scope.working = {
+ copy_notes: [],
+ copy_alerts: [],
statcats: {},
statcat_filter: undefined
};
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;
});
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?
+ });
+ }
+
}])
// 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;
});
}
+ $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 : []}
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 = {
// 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));
// 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));
// 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));
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 );
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') {
});
}
+ 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
// 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
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']
},
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);
}
// 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) {
angular.forEach(
[
'visible',
+ 'compiled',
'hidden',
'sortable',
'nonsortable',
linkpath : colSpec.linkpath,
template : colSpec.template,
visible : colSpec.visible,
+ compiled : colSpec.compiled,
+ handlers : colSpec.handlers,
hidden : colSpec.hidden,
datatype : colSpec.datatype,
sortable : colSpec.sortable,
};
})
+/* 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);
+ }
+ );
+ };
+}])
+
/**
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',
template:
'<div class="input-group">'+
'<input placeholder="{{placeholder}}" type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()" focus-me="focusMe">'+
- '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
- '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
- '<ul class="dropdown-menu dropdown-menu-right">'+
+ '<div class="input-group-btn" uib-dropdown ng-class="{open:isopen}">'+
+ '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default" uib-dropdown-toggle><span class="caret"></span></button>'+
+ '<ul dropdown-menu class="dropdown-menu-right">'+
'<li ng-repeat="item in list|filter:selected:compare"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
'<li ng-if="complete_list" class="divider"><span></span></li>'+
'<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+