Implement destination set handling.

agranig/1_0_subfix
Andreas Granig 12 years ago
parent 79118bd3d7
commit c5942e83c9

@ -1,2 +1,4 @@
- Modal needs more flexible width. It breaks when making screen narrower - 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. 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

@ -7,7 +7,8 @@ use NGCP::Panel::Form::Subscriber;
use NGCP::Panel::Form::SubscriberCFSimple; use NGCP::Panel::Form::SubscriberCFSimple;
use NGCP::Panel::Form::SubscriberCFTSimple; use NGCP::Panel::Form::SubscriberCFTSimple;
use NGCP::Panel::Form::SubscriberCFAdvanced; use NGCP::Panel::Form::SubscriberCFAdvanced;
#use NGCP::Panel::Form::SubscriberCFTAdvanced; use NGCP::Panel::Form::SubscriberCFTAdvanced;
use NGCP::Panel::Form::DestinationSet;
use UUID; use UUID;
use Data::Printer; use Data::Printer;
@ -36,6 +37,7 @@ sub sub_list :Chained('/') :PathPart('subscriber') :CaptureArgs(0) {
$c->stash( $c->stash(
template => 'subscriber/list.tt', 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; my $cf_desc;
given($cf_type) { given($cf_type) {
when("cfu") { $cf_desc = "Unconditional" } when("cfu") { $cf_desc = "Call Forward Unconditional" }
when("cfb") { $cf_desc = "Busy" } when("cfb") { $cf_desc = "Call Forward Busy" }
when("cft") { $cf_desc = "Timeout" } when("cft") { $cf_desc = "Call Forward Timeout" }
when("cfna") { $cf_desc = "Unavailable" } when("cfna") { $cf_desc = "Call Forward Unavailable" }
default { default {
$c->log->error("Invalid call-forward type '$cf_type'"); $c->log->error("Invalid call-forward type '$cf_type'");
$c->flash(messages => [{type => 'error', text => 'Invalid Call Forward 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 &&
$cf_mapping->destination_set->voip_cf_destinations->first) { $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; $destination = $cf_mapping->destination_set->voip_cf_destinations->first;
} }
@ -352,7 +366,10 @@ sub preferences_callforward :Chained('base') :PathPart('preferences/callforward'
} }
if($posted) { if($posted) {
$params = $c->request->params; $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} !~ /\@/) { if($params->{destination} !~ /\@/) {
$params->{destination} .= '@'.$billing_subscriber->domain->domain; $params->{destination} .= '@'.$billing_subscriber->domain->domain;
} }
@ -472,6 +489,7 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal
say ">>>>>>>>>>>>>>>>>>>>>> preferences_callforward_advanced"; say ">>>>>>>>>>>>>>>>>>>>>> preferences_callforward_advanced";
# TODO bail out of $advanced ne "advanced"
if(defined $advanced && $advanced eq 'advanced') { if(defined $advanced && $advanced eq 'advanced') {
$advanced = 1; $advanced = 1;
} else { } else {
@ -480,10 +498,10 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal
my $cf_desc; my $cf_desc;
given($cf_type) { given($cf_type) {
when("cfu") { $cf_desc = "Unconditional" } when("cfu") { $cf_desc = "Call Forward Unconditional" }
when("cfb") { $cf_desc = "Busy" } when("cfb") { $cf_desc = "Call Forward Busy" }
when("cft") { $cf_desc = "Timeout" } when("cft") { $cf_desc = "Call Forward Timeout" }
when("cfna") { $cf_desc = "Unavailable" } when("cfna") { $cf_desc = "Call Forward Unavailable" }
default { default {
$c->log->error("Invalid call-forward type '$cf_type'"); $c->log->error("Invalid call-forward type '$cf_type'");
$c->flash(messages => [{type => 'error', text => 'Invalid Call Forward 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 $billing_subscriber = $c->stash->{subscriber};
my $prov_subscriber = $billing_subscriber->provisioning_voip_subscriber; my $prov_subscriber = $billing_subscriber->provisioning_voip_subscriber;
my $cf_mapping = $prov_subscriber->voip_cf_mappings->find({ type => $cf_type }); my $cf_mapping = $prov_subscriber->voip_cf_mappings->search_rs({ type => $cf_type });
if($cf_mapping) {
$c->stash->{cf_active_destination_set} = $cf_mapping->destination_set # TODO: we can have more than one active, no?
if($cf_mapping->destination_set); if($cf_mapping->count) {
$c->stash->{cf_destination_sets} = $prov_subscriber->voip_cf_destination_sets; $c->stash->{cf_active_destination_set} = $cf_mapping->first->destination_set
$c->stash->{cf_active_time_set} = $cf_mapping->time_set if($cf_mapping->first->destination_set);
if($cf_mapping->time_set); $c->stash->{cf_active_time_set} = $cf_mapping->first->time_set
$c->stash->{cf_time_sets} = $prov_subscriber->voip_cf_time_sets; if($cf_mapping->first->time_set);
}
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;
} }
$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 $posted = ($c->request->method eq 'POST');
my $cf_form; my $cf_form;
# my $params = {}; # my $params = {};
# if($cf_type eq "cft") { if($cf_type eq "cft") {
# $cf_form = NGCP::Panel::Form::SubscriberCFTAdvanced->new; $cf_form = NGCP::Panel::Form::SubscriberCFTAdvanced->new(ctx => $c);
# } else { } else {
$cf_form = NGCP::Panel::Form::SubscriberCFAdvanced->new(ctx => $c); $cf_form = NGCP::Panel::Form::SubscriberCFAdvanced->new(ctx => $c);
# } }
$cf_form->process( $cf_form->process(
params => $posted ? $c->request->params : {} 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( return if NGCP::Panel::Utils::check_form_buttons(
c => $c, form => $cf_form, c => $c, form => $cf_form,
fields => { fields => {
@ -535,72 +548,52 @@ sub preferences_callforward_advanced :Chained('base') :PathPart('preferences/cal
$c->uri_for_action('/subscriber/preferences_callforward', $c->uri_for_action('/subscriber/preferences_callforward',
[$c->req->captures->[0], $cf_type], [$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"; say ">>>>>>>>>>>>>>>>>>>>>>>> after check_form_buttons";
=pod
if($posted && $cf_form->validated) { if($posted && $cf_form->validated) {
try { try {
$c->model('DB')->schema->txn_do( sub { $c->model('DB')->schema->txn_do( sub {
my $dest_set = $c->model('DB')->resultset('voip_cf_destination_sets')->find({ my @active = $cf_form->field('active_callforward')->fields;
subscriber_id => $prov_subscriber->id, if($cf_mapping->count) {
name => 'quickset_'.$cf_type, foreach my $map($cf_mapping->all) {
}); $map->delete;
unless($dest_set) { }
$dest_set = $c->model('DB')->resultset('voip_cf_destination_sets')->create({ unless(@active) {
name => 'quickset_'.$cf_type, $c->flash(messages => [{type => 'success', text => 'Successfully cleared Call Forward'}]);
subscriber_id => $prov_subscriber->id, $c->response->redirect(
}); $c->uri_for_action('/subscriber/preferences',
} else { [$c->req->captures->[0]])
my @all = $dest_set->voip_cf_destinations->all; );
foreach my $dest(@all) { return;
$dest->delete;
} }
} }
my $dest = $dest_set->voip_cf_destinations->create({ foreach my $map(@active) {
priority => 1, $cf_mapping->create({
timeout => 300,
destination => $c->request->params->{destination},
});
unless(defined $cf_mapping) {
$cf_mapping = $prov_subscriber->voip_cf_mappings->create({
type => $cf_type, type => $cf_type,
# subscriber_id => $prov_subscriber->id, destination_set_id => $map->field('destination_set')->value,
destination_set_id => $dest_set->id, time_set_id => $map->field('time_set')->value,
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,
}); });
} }
if($cf_type eq 'cft') { $c->flash(messages => [{type => 'success', text => 'Successfully saved Call Forward'}]);
my $ringtimeout_preference_row = $ringtimeout_preference->find({ $c->response->redirect(
subscriber_id => $prov_subscriber->id $c->uri_for_action('/subscriber/preferences',
}); [$c->req->captures->[0]])
if($ringtimeout_preference_row) { );
$ringtimeout_preference_row->update({ return;
value => $c->request->params->{ringtimeout}
});
} else {
$ringtimeout_preference->create({
subscriber_id => $prov_subscriber->id,
value => $c->request->params->{ringtimeout},
});
}
}
}); });
} catch($e) { } catch($e) {
$c->log->error("failed to save call-forward: $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]])); $c->response->redirect($c->uri_for_action('/subscriber/preferences', [$c->req->captures->[0]]));
return; 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); $self->load_preference_list($c);
$c->stash(template => 'subscriber/preferences.tt'); $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) { sub preferences_callforward_delete :Chained('base') :PathPart('preferences/callforward/delete') :Args(1) {
my ($self, $c, $cfmap_id) = @_; my ($self, $c, $cfmap_id) = @_;

@ -12,13 +12,17 @@ sub build_options {
my $destination_sets = $form->ctx->stash->{cf_destination_sets}; my $destination_sets = $form->ctx->stash->{cf_destination_sets};
my @all; my @all;
return \@all unless($destination_sets);
push @all, { label => '', value => undef}
unless($active_destination_set);
foreach my $set($destination_sets->all) { foreach my $set($destination_sets->all) {
my $entry = {}; my $entry = {};
$entry->{label} = $set->name; $entry->{label} = $set->name;
$entry->{value} = $set->id; $entry->{value} = $set->id;
if($active_destination_set && if($active_destination_set &&
$set->id == $active_destination_set->id) { $set->id == $active_destination_set->id) {
$entry->{active} = 1; $entry->{selected} = 1;
} }
push @all, $entry; push @all, $entry;
} }

@ -12,6 +12,7 @@ sub build_options {
my $time_sets = $form->ctx->stash->{cf_time_sets}; my $time_sets = $form->ctx->stash->{cf_time_sets};
my @all = ({label => '<always>', value => undef}); my @all = ({label => '<always>', value => undef});
return \@all unless($time_sets);
foreach my $set($time_sets->all) { foreach my $set($time_sets->all) {
my $entry = {}; my $entry = {};
$entry->{label} = $set->name; $entry->{label} = $set->name;

@ -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:

@ -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. &ldquo;10&rdquo;)']
},
);
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;

@ -13,10 +13,11 @@ has_field 'ringtimeout' => (
title => ['Seconds to wait for pick-up until engaging Call Forward (e.g. &ldquo;10&rdquo;)'] title => ['Seconds to wait for pick-up until engaging Call Forward (e.g. &ldquo;10&rdquo;)']
}, },
); );
has_block 'fields' => ( has_block 'fields' => (
tag => 'div', tag => 'div',
class => [qw(modal-body)], class => [qw(modal-body)],
render_list => [qw(destination ringtimeout)], render_list => [qw(submitid destination ringtimeout)],
); );
sub validate_ringtimeout { sub validate_ringtimeout {

@ -91,7 +91,7 @@
[% IF edit_cf_flag -%] [% IF edit_cf_flag -%]
[% [%
PROCESS "helpers/modal.tt"; PROCESS "helpers/modal.tt";
modal_header(m.name = "Call Forward " _ cf_description); modal_header(m.name = cf_description);
-%] -%]
[% IF cf_form.has_for_js -%] [% IF cf_form.has_for_js -%]
@ -99,6 +99,52 @@
[% END -%] [% END -%]
[% cf_form.render %] [% 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_footer();
modal_script(m.close_target = c.uri_for_action('/subscriber/preferences', [c.req.captures.0])); modal_script(m.close_target = c.uri_for_action('/subscriber/preferences', [c.req.captures.0]));

Loading…
Cancel
Save