diff --git a/ISSUES b/ISSUES index 138d621886..c44c8fe307 100644 --- a/ISSUES +++ b/ISSUES @@ -1,2 +1,4 @@ - Modal needs more flexible width. It breaks when making screen narrower than 816px, and if no DataTable field is there, it can be narrower also. + +- check what is displayed in time sets of subscriber preferences when more mappings are active at diff --git a/lib/NGCP/Panel/Controller/Subscriber.pm b/lib/NGCP/Panel/Controller/Subscriber.pm index 171eb5c84a..445c42d266 100644 --- a/lib/NGCP/Panel/Controller/Subscriber.pm +++ b/lib/NGCP/Panel/Controller/Subscriber.pm @@ -7,7 +7,8 @@ use NGCP::Panel::Form::Subscriber; use NGCP::Panel::Form::SubscriberCFSimple; use NGCP::Panel::Form::SubscriberCFTSimple; use NGCP::Panel::Form::SubscriberCFAdvanced; -#use NGCP::Panel::Form::SubscriberCFTAdvanced; +use NGCP::Panel::Form::SubscriberCFTAdvanced; +use NGCP::Panel::Form::DestinationSet; use UUID; use Data::Printer; @@ -36,6 +37,7 @@ sub sub_list :Chained('/') :PathPart('subscriber') :CaptureArgs(0) { $c->stash( template => 'subscriber/list.tt', ); + #NGCP::Panel::Utils::check_redirect_chain(c => $c); } @@ -306,10 +308,10 @@ sub preferences_callforward :Chained('base') :PathPart('preferences/callforward' my $cf_desc; given($cf_type) { - when("cfu") { $cf_desc = "Unconditional" } - when("cfb") { $cf_desc = "Busy" } - when("cft") { $cf_desc = "Timeout" } - when("cfna") { $cf_desc = "Unavailable" } + when("cfu") { $cf_desc = "Call Forward Unconditional" } + when("cfb") { $cf_desc = "Call Forward Busy" } + when("cft") { $cf_desc = "Call Forward Timeout" } + when("cfna") { $cf_desc = "Call Forward Unavailable" } default { $c->log->error("Invalid call-forward type '$cf_type'"); $c->flash(messages => [{type => 'error', text => 'Invalid Call Forward type'}]); @@ -335,6 +337,18 @@ sub preferences_callforward :Chained('base') :PathPart('preferences/callforward' $cf_mapping->destination_set && $cf_mapping->destination_set->voip_cf_destinations->first) { + # there are more than one destinations or a time set, so + # which can only be handled in advanced mode + if($cf_mapping->destination_set->voip_cf_destinations->count > 1 || + $cf_mapping->time_set) { + + $c->response->redirect( + $c->uri_for_action('/subscriber/preferences_callforward_advanced', + [$c->req->captures->[0]], $cf_type, 'advanced' + ) + ); + return; + } $destination = $cf_mapping->destination_set->voip_cf_destinations->first; } @@ -352,7 +366,10 @@ sub preferences_callforward :Chained('base') :PathPart('preferences/callforward' } if($posted) { $params = $c->request->params; - if(!defined($c->request->params->{submitid})) { + if(length($params->{destination}) && + (!$c->request->params->{submitid} || + $c->request->params->{submitid} eq "cf_actions.save") + ) { if($params->{destination} !~ /\@/) { $params->{destination} .= '@'.$billing_subscriber->domain->domain; } @@ -472,6 +489,7 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal say ">>>>>>>>>>>>>>>>>>>>>> preferences_callforward_advanced"; + # TODO bail out of $advanced ne "advanced" if(defined $advanced && $advanced eq 'advanced') { $advanced = 1; } else { @@ -480,10 +498,10 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal my $cf_desc; given($cf_type) { - when("cfu") { $cf_desc = "Unconditional" } - when("cfb") { $cf_desc = "Busy" } - when("cft") { $cf_desc = "Timeout" } - when("cfna") { $cf_desc = "Unavailable" } + when("cfu") { $cf_desc = "Call Forward Unconditional" } + when("cfb") { $cf_desc = "Call Forward Busy" } + when("cft") { $cf_desc = "Call Forward Timeout" } + when("cfna") { $cf_desc = "Call Forward Unavailable" } default { $c->log->error("Invalid call-forward type '$cf_type'"); $c->flash(messages => [{type => 'error', text => 'Invalid Call Forward type'}]); @@ -494,40 +512,35 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal my $billing_subscriber = $c->stash->{subscriber}; my $prov_subscriber = $billing_subscriber->provisioning_voip_subscriber; - my $cf_mapping = $prov_subscriber->voip_cf_mappings->find({ type => $cf_type }); - if($cf_mapping) { - $c->stash->{cf_active_destination_set} = $cf_mapping->destination_set - if($cf_mapping->destination_set); - $c->stash->{cf_destination_sets} = $prov_subscriber->voip_cf_destination_sets; - $c->stash->{cf_active_time_set} = $cf_mapping->time_set - if($cf_mapping->time_set); - $c->stash->{cf_time_sets} = $prov_subscriber->voip_cf_time_sets; - } - - my $destination; - if($cf_mapping && - $cf_mapping->destination_set && - $cf_mapping->destination_set->voip_cf_destinations->first) { - - $destination = $cf_mapping->destination_set->voip_cf_destinations->first; + my $cf_mapping = $prov_subscriber->voip_cf_mappings->search_rs({ type => $cf_type }); + + # TODO: we can have more than one active, no? + if($cf_mapping->count) { + $c->stash->{cf_active_destination_set} = $cf_mapping->first->destination_set + if($cf_mapping->first->destination_set); + $c->stash->{cf_active_time_set} = $cf_mapping->first->time_set + if($cf_mapping->first->time_set); } - + $c->stash->{cf_destination_sets} = $prov_subscriber->voip_cf_destination_sets; + $c->stash->{cf_time_sets} = $prov_subscriber->voip_cf_time_sets; my $posted = ($c->request->method eq 'POST'); my $cf_form; # my $params = {}; -# if($cf_type eq "cft") { -# $cf_form = NGCP::Panel::Form::SubscriberCFTAdvanced->new; -# } else { + if($cf_type eq "cft") { + $cf_form = NGCP::Panel::Form::SubscriberCFTAdvanced->new(ctx => $c); + } else { $cf_form = NGCP::Panel::Form::SubscriberCFAdvanced->new(ctx => $c); -# } + } $cf_form->process( params => $posted ? $c->request->params : {} ); - say ">>>>>>>>>>>>>>>>>>>>>>>> check_form_buttons"; + my $back_uri = $c->uri_for_action('/subscriber/preferences_callforward_advanced', + [$c->req->captures->[0]], $cf_type, 'advanced'); + say ">>>>>>>>>>>>>>>>>>>>>>>> check_form_buttons, back_uri=$back_uri"; return if NGCP::Panel::Utils::check_form_buttons( c => $c, form => $cf_form, fields => { @@ -535,72 +548,52 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal $c->uri_for_action('/subscriber/preferences_callforward', [$c->req->captures->[0], $cf_type], ), + 'cf_actions.edit_destination_sets' => + $c->uri_for_action('/subscriber/preferences_callforward_destinationset', + [$c->req->captures->[0]], + ), +# 'cf_actions.edit_time_sets' => +# $c->uri_for_action('/subscriber/preferences_callforward_timeset', +# [$c->req->captures->[0]], +# ), }, - back_uri => $c->uri_for($c->action, $c->req->captures) + back_uri => $c->uri_for_action('/subscriber/preferences_callforward_advanced', + [$c->req->captures->[0]], $cf_type, 'advanced'), ); say ">>>>>>>>>>>>>>>>>>>>>>>> after check_form_buttons"; -=pod if($posted && $cf_form->validated) { try { $c->model('DB')->schema->txn_do( sub { - my $dest_set = $c->model('DB')->resultset('voip_cf_destination_sets')->find({ - subscriber_id => $prov_subscriber->id, - name => 'quickset_'.$cf_type, - }); - unless($dest_set) { - $dest_set = $c->model('DB')->resultset('voip_cf_destination_sets')->create({ - name => 'quickset_'.$cf_type, - subscriber_id => $prov_subscriber->id, - }); - } else { - my @all = $dest_set->voip_cf_destinations->all; - foreach my $dest(@all) { - $dest->delete; + my @active = $cf_form->field('active_callforward')->fields; + if($cf_mapping->count) { + foreach my $map($cf_mapping->all) { + $map->delete; + } + unless(@active) { + $c->flash(messages => [{type => 'success', text => 'Successfully cleared Call Forward'}]); + $c->response->redirect( + $c->uri_for_action('/subscriber/preferences', + [$c->req->captures->[0]]) + ); + return; } } - my $dest = $dest_set->voip_cf_destinations->create({ - priority => 1, - timeout => 300, - destination => $c->request->params->{destination}, - }); - - unless(defined $cf_mapping) { - $cf_mapping = $prov_subscriber->voip_cf_mappings->create({ + foreach my $map(@active) { + $cf_mapping->create({ type => $cf_type, - # subscriber_id => $prov_subscriber->id, - destination_set_id => $dest_set->id, - time_set_id => undef, #$time_set_id, - }); - } - my $cf_preference_row = $cf_preference->find({ - subscriber_id => $prov_subscriber->id - }); - if($cf_preference_row) { - $cf_preference_row->update({ value => $cf_mapping->id }); - } else { - $cf_preference->create({ - subscriber_id => $prov_subscriber->id, - value => $cf_mapping->id, + destination_set_id => $map->field('destination_set')->value, + time_set_id => $map->field('time_set')->value, }); } - if($cf_type eq 'cft') { - my $ringtimeout_preference_row = $ringtimeout_preference->find({ - subscriber_id => $prov_subscriber->id - }); - if($ringtimeout_preference_row) { - $ringtimeout_preference_row->update({ - value => $c->request->params->{ringtimeout} - }); - } else { - $ringtimeout_preference->create({ - subscriber_id => $prov_subscriber->id, - value => $c->request->params->{ringtimeout}, - }); - } - } + $c->flash(messages => [{type => 'success', text => 'Successfully saved Call Forward'}]); + $c->response->redirect( + $c->uri_for_action('/subscriber/preferences', + [$c->req->captures->[0]]) + ); + return; }); } catch($e) { $c->log->error("failed to save call-forward: $e"); @@ -608,13 +601,8 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal $c->response->redirect($c->uri_for_action('/subscriber/preferences', [$c->req->captures->[0]])); return; } - - $c->flash(messages => [{type => 'success', text => 'Successfully saved Call Forward'}]); - $c->response->redirect($c->uri_for_action('/subscriber/preferences', [$c->req->captures->[0]])); - return; } -=cut $self->load_preference_list($c); $c->stash(template => 'subscriber/preferences.tt'); @@ -625,6 +613,148 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal ); } +sub preferences_callforward_destinationset :Chained('base') :PathPart('preferences/destinationset') :Args(0) { + my ($self, $c, $type) = @_; + + my $billing_subscriber = $c->stash->{subscriber}; + my $prov_subscriber = $billing_subscriber->provisioning_voip_subscriber; + + my @sets; + if($prov_subscriber->voip_cf_destination_sets) { + foreach my $set($prov_subscriber->voip_cf_destination_sets->all) { + if($set->voip_cf_destinations) { + my @dests = (); + foreach my $dest($set->voip_cf_destinations->search({}, + { order_by => { -asc => 'priority' }})->all) { + my %cols = $dest->get_columns; + push @dests, \%cols; + } + push @sets, { name => $set->name, id => $set->id, destinations => \@dests }; + } + } + } + $c->stash->{cf_sets} = \@sets; + + my $cf_form = undef; + + $c->stash(template => 'subscriber/preferences.tt'); + $c->stash( + edit_cfset_flag => 1, + cf_description => "Destination Sets", + cf_form => $cf_form, + ); +} + +sub preferences_callforward_destinationset_base :Chained('base') :PathPart('preferences/destinationset') :CaptureArgs(1) { + my ($self, $c, $set_id) = @_; + + $c->stash(destination_set => $c->stash->{subscriber} + ->provisioning_voip_subscriber + ->voip_cf_destination_sets + ->find($set_id)); + + $c->stash(template => 'subscriber/preferences.tt'); +} + +sub preferences_callforward_destinationset_edit :Chained('preferences_callforward_destinationset_base') :PathPart('edit') :Args(0) { + my ($self, $c) = @_; + + my $form = NGCP::Panel::Form::DestinationSet->new; + + my $posted = ($c->request->method eq 'POST'); + + my $set = $c->stash->{destination_set}; + my $params; + unless($posted) { + p $set; + $params->{name} = $set->name; + my @destinations; + for my $dest($set->voip_cf_destinations->all) { + push @destinations, { + destination => $dest->destination, + timeout => $dest->timeout, + priority => $dest->priority, + id => $dest->id, + }; + } + $params->{destination} = \@destinations; + } + + $form->process( + params => $posted ? $c->req->params : $params + ); + + if($posted && $form->validated) { + say ">>>>>>>>>>>>>>>>>>>> dset form validated"; + + try { + my $schema = $c->model('DB'); + $schema->txn_do(sub { + # delete whole set and mapping if empty + my @fields = $form->field('destination')->fields; + unless(@fields) { + foreach my $mapping($set->voip_cf_mappings) { + $mapping->delete; + } + $set->delete; + + $c->response->redirect( + $c->uri_for_action('/subscriber/preferences_callforward_destinationset', + [$c->req->captures->[0]]) + ); + return; + } + if($form->field('name')->value ne $set->name) { + $set->update({name => $form->field('name')->value}); + } + foreach my $dest($set->voip_cf_destinations->all) { + $dest->delete; + } + foreach my $dest($form->field('destination')->fields) { + my $d = $dest->field('destination')->value; + if($d !~ /\@/) { + $d .= '@'.$c->stash->{subscriber}->domain->domain; + } + if($d !~ /^sip:/) { + $d = 'sip:' . $d; + } + $set->voip_cf_destinations->create({ + destination => $d, + timeout => $dest->field('timeout')->value, + priority => $dest->field('priority')->value, + }); + } + $c->response->redirect( + $c->uri_for_action('/subscriber/preferences_callforward_destinationset', + [$c->req->captures->[0]]) + ); + return; + }); + } catch($e) { + $c->log->error("failed to update destination set: $e"); + $c->response->redirect( + $c->uri_for_action('/subscriber/preferences_callforward_destinationset', + [$c->req->captures->[0]]) + ); + return; + } + } + + $c->stash( + edit_cf_flag => 1, + cf_description => "Destination Set", + cf_form => $form, + ); + +} + +sub preferences_callforward_destinationset_delete :Chained('preferences_callforward_destinationset_base') :PathPart('delete') :Args(0) { + my ($self, $c) = @_; + + + +} + sub preferences_callforward_delete :Chained('base') :PathPart('preferences/callforward/delete') :Args(1) { my ($self, $c, $cfmap_id) = @_; diff --git a/lib/NGCP/Panel/Field/SubscriberDestinationSet.pm b/lib/NGCP/Panel/Field/SubscriberDestinationSet.pm index ad4cd1a7f0..0cd2bc68a2 100644 --- a/lib/NGCP/Panel/Field/SubscriberDestinationSet.pm +++ b/lib/NGCP/Panel/Field/SubscriberDestinationSet.pm @@ -12,13 +12,17 @@ sub build_options { my $destination_sets = $form->ctx->stash->{cf_destination_sets}; my @all; + return \@all unless($destination_sets); + push @all, { label => '', value => undef} + unless($active_destination_set); foreach my $set($destination_sets->all) { my $entry = {}; $entry->{label} = $set->name; $entry->{value} = $set->id; + if($active_destination_set && $set->id == $active_destination_set->id) { - $entry->{active} = 1; + $entry->{selected} = 1; } push @all, $entry; } diff --git a/lib/NGCP/Panel/Field/SubscriberTimeSet.pm b/lib/NGCP/Panel/Field/SubscriberTimeSet.pm index db01ecf28a..d56a2c7bcb 100644 --- a/lib/NGCP/Panel/Field/SubscriberTimeSet.pm +++ b/lib/NGCP/Panel/Field/SubscriberTimeSet.pm @@ -12,6 +12,7 @@ sub build_options { my $time_sets = $form->ctx->stash->{cf_time_sets}; my @all = ({label => '<always>', value => undef}); + return \@all unless($time_sets); foreach my $set($time_sets->all) { my $entry = {}; $entry->{label} = $set->name; diff --git a/lib/NGCP/Panel/Form/DestinationSet.pm b/lib/NGCP/Panel/Form/DestinationSet.pm new file mode 100644 index 0000000000..cbdcacc853 --- /dev/null +++ b/lib/NGCP/Panel/Form/DestinationSet.pm @@ -0,0 +1,111 @@ +package NGCP::Panel::Form::DestinationSet; +use HTML::FormHandler::Moose; +use HTML::FormHandler::Widget::Block::Bootstrap; +use Moose::Util::TypeConstraints; +extends 'HTML::FormHandler'; + +with 'NGCP::Panel::Render::RepeatableJs'; + +has '+widget_wrapper' => (default => 'Bootstrap'); + +has_field 'submitid' => ( + type => 'Hidden', +); + +has_field 'name' => ( + type => 'Text', + label => 'Name', + wrapper_class => [qw/hfh-rep-field/], + required => 1, +); + +has_field 'destination' => ( + type => 'Repeatable', + setup_for_js => 1, + do_wrapper => 1, + do_label => 0, + tags => { + controls_div => 1, + }, + wrapper_class => [qw/hfh-rep/], +); + +has_field 'destination.id' => ( + type => 'Hidden', +); +has_field 'destination.destination' => ( + type => 'Text', + label => 'Destination', + wrapper_class => [qw/hfh-rep-field/], + required => 1, +); +has_field 'destination.timeout' => ( + type => 'PosInteger', + label => 'Ring for (sec)', + wrapper_class => [qw/hfh-rep-field/], + default => 300, +); +has_field 'destination.priority' => ( + type => 'PosInteger', + label => 'Priority', + wrapper_class => [qw/hfh-rep-field/], + default => 1, +); + +has_field 'destination.rm' => ( + type => 'RmElement', + value => 'Remove', + element_class => [qw/btn btn-primary pull-right/], +# tags => { +# "data-confirm" => "Delete", +# }, +); + + +has_field 'destination_add' => ( + type => 'AddElement', + repeatable => 'destination', + value => 'Add another destination', + element_class => [qw/btn btn-primary pull-right/], +); + +has_block 'fields' => ( + tag => 'div', + class => [qw(modal-body)], + render_list => [qw(submitid name destination destination_add)], +); + +has_field 'save' => ( + type => 'Submit', + do_label => 0, + value => 'Save', + element_class => [qw(btn btn-primary)], +); + +has_block 'actions' => ( + tag => 'div', + class => [qw(modal-footer)], + render_list => [qw(save)], +); + +sub build_render_list { + return [qw(fields actions)]; +} + +sub build_form_element_class { + return [qw(form-horizontal)]; +} + +#sub validate_destination { +# my ($self, $field) = @_; +# +# # TODO: proper SIP URI check! +# if($field->value !~ /^sip:.+\@.+$/) { +# my $err_msg = 'Destination must be a valid SIP URI in format "sip:user@domain"'; +# $field->add_error($err_msg); +# } +#} + +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/SubscriberCFTAdvanced.pm b/lib/NGCP/Panel/Form/SubscriberCFTAdvanced.pm new file mode 100644 index 0000000000..3db59f90ce --- /dev/null +++ b/lib/NGCP/Panel/Form/SubscriberCFTAdvanced.pm @@ -0,0 +1,34 @@ +package NGCP::Panel::Form::SubscriberCFTAdvanced; +use HTML::FormHandler::Moose; +use HTML::FormHandler::Widget::Block::Bootstrap; +use Moose::Util::TypeConstraints; +extends 'NGCP::Panel::Form::SubscriberCFAdvanced'; + +has_field 'ringtimeout' => ( + type => 'PosInteger', + required => 1, + label => 'after ring timeout', + element_attr => { + rel => ['tooltip'], + title => ['Seconds to wait for pick-up until engaging Call Forward (e.g. “10”)'] + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw(modal-body)], + render_list => [qw(submitid active_callforward callforward_controls_add ringtimeout)], +); + + +sub validate_ringtimeout { + my ($self, $field) = @_; + + if($field->value < 1) { + my $err_msg = 'Ring Timeout must be greater than 0'; + $field->add_error($err_msg); + } +} + +1; + diff --git a/lib/NGCP/Panel/Form/SubscriberCFTSimple.pm b/lib/NGCP/Panel/Form/SubscriberCFTSimple.pm index f1a038f985..f803fd940a 100644 --- a/lib/NGCP/Panel/Form/SubscriberCFTSimple.pm +++ b/lib/NGCP/Panel/Form/SubscriberCFTSimple.pm @@ -13,10 +13,11 @@ has_field 'ringtimeout' => ( title => ['Seconds to wait for pick-up until engaging Call Forward (e.g. “10”)'] }, ); + has_block 'fields' => ( tag => 'div', class => [qw(modal-body)], - render_list => [qw(destination ringtimeout)], + render_list => [qw(submitid destination ringtimeout)], ); sub validate_ringtimeout { diff --git a/share/templates/subscriber/preferences.tt b/share/templates/subscriber/preferences.tt index 81ed525167..91242938ac 100644 --- a/share/templates/subscriber/preferences.tt +++ b/share/templates/subscriber/preferences.tt @@ -91,7 +91,7 @@ [% IF edit_cf_flag -%] [% PROCESS "helpers/modal.tt"; - modal_header(m.name = "Call Forward " _ cf_description); + modal_header(m.name = cf_description); -%] [% IF cf_form.has_for_js -%] @@ -99,6 +99,52 @@ [% END -%] [% cf_form.render %] +[% + modal_footer(); + modal_script(m.close_target = c.uri_for_action('/subscriber/preferences', [c.req.captures.0])); +-%] +[% ELSIF edit_cfset_flag -%] +[% + PROCESS "helpers/modal.tt"; + modal_header(m.name = cf_description); +-%] + + <div class="modal-body"> + [% IF cf_sets -%] + <table class="table table-bordered table-striped table-highlight table-hover"> + <thead> + <tr> + <th>Name</th> + <th>Values</th> + <th></th> + </tr> + </thead> + <tbody> + [% FOREACH set IN cf_sets -%] + <tr class="sw_action_row"> + <td>[% set.name %]</td> + <td> + [% FOREACH d IN set.destinations -%] + [% d.destination %] <span class="pull-right">after [% d.timeout %]s</span><br/> + [% END -%] + </td> + <td class="ngcp-actions-column"> + <div class="sw_actions pull-right"> + <a class="btn btn-small btn-primary" href="[% c.uri_for_action('/subscriber/preferences_callforward_destinationset_edit', [ c.req.captures.0, set.id ]) %]"> + <i class="icon-edit"></i> Edit + </a> + <a class="btn btn-small btn-secondary" data-confirm="Delete" href="[% c.uri_for_action('/subscriber/preferences_callforward_destinationset_delete', [ c.req.captures.0, set.id ]) %]"> + <i class="icon-trash"></i> Delete + </a> + </div> + </td> + </tr> + [% END -%] + </tbody> + </table> + [% END -%] + </div> + [% modal_footer(); modal_script(m.close_target = c.uri_for_action('/subscriber/preferences', [c.req.captures.0]));