aboutsummaryrefslogtreecommitdiffstats
path: root/lib/Reaction/UI/ViewPort/TimeRangeCollection.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Reaction/UI/ViewPort/TimeRangeCollection.pm')
-rw-r--r--lib/Reaction/UI/ViewPort/TimeRangeCollection.pm390
1 files changed, 390 insertions, 0 deletions
diff --git a/lib/Reaction/UI/ViewPort/TimeRangeCollection.pm b/lib/Reaction/UI/ViewPort/TimeRangeCollection.pm
new file mode 100644
index 0000000..eb1b680
--- /dev/null
+++ b/lib/Reaction/UI/ViewPort/TimeRangeCollection.pm
@@ -0,0 +1,390 @@
+package Reaction::UI::ViewPort::TimeRangeCollection;
+
+use Reaction::Class;
+use Reaction::Types::DateTime;
+use Moose::Util::TypeConstraints ();
+use DateTime::Event::Recurrence;
+use aliased 'Reaction::UI::ViewPort::Field::String';
+use aliased 'Reaction::UI::ViewPort::Field::DateTime';
+use aliased 'Reaction::UI::ViewPort::Field::HiddenArray';
+use aliased 'Reaction::UI::ViewPort::Field::TimeRange';
+
+class TimeRangeCollection is 'Reaction::UI::ViewPort', which {
+
+ has '+layout' => (default => 'timerangecollection');
+
+ has '+column_order' => (
+ default => sub{[ qw/ time_from time_to pattern repeat_from repeat_to / ]},
+ );
+
+ has time_from => (
+ isa => 'Reaction::UI::ViewPort::Field::DateTime',
+ is => 'rw', lazy_build => 1,
+ );
+
+ has time_to => (
+ isa => 'Reaction::UI::ViewPort::Field::DateTime',
+ is => 'rw', lazy_build => 1,
+ );
+
+ has repeat_from => (
+ isa => 'Reaction::UI::ViewPort::Field::DateTime',
+ is => 'rw', lazy_build => 1,
+ );
+
+ has repeat_to => (
+ isa => 'Reaction::UI::ViewPort::Field::DateTime',
+ is => 'rw', lazy_build => 1,
+ );
+
+ has pattern => (
+ isa => 'Reaction::UI::ViewPort::Field::String',
+ # valid_values => [ qw/none daily weekly monthly/ ],
+ is => 'rw', lazy_build => 1,
+ );
+
+ has range_vps => (isa => 'ArrayRef', is => 'rw', lazy_build => 1,);
+
+ has max_range_vps => (isa => 'Int', is => 'rw', lazy_build => 1,);
+
+ has error => (
+ isa => 'Str',
+ is => 'rw',
+ required => 0,
+ );
+
+ has field_names => (
+ isa => 'ArrayRef', is => 'rw',
+ lazy_build => 1, clearer => 'clear_field_names',
+ );
+
+ has _field_map => (
+ isa => 'HashRef', is => 'rw', init_arg => 'fields',
+ clearer => '_clear_field_map',
+ predicate => '_has_field_map',
+ set_or_lazy_build('field_map'),
+ );
+
+ has on_next_callback => (
+ isa => 'CodeRef',
+ is => 'rw',
+ predicate => 'has_on_next_callback',
+ );
+
+ implements fields => as { shift->_field_map };
+
+ implements build_range_vps => as { [] };
+
+ implements spanset => as {
+ my ($self) = @_;
+ my $spanset = DateTime::SpanSet->empty_set;
+ $spanset = $spanset->union($_->value) for @{$self->range_vps};
+ return $spanset;
+ };
+
+ implements range_strings => as {
+ my ($self) = @_;
+ return [ map { $_->value_string } @{$self->range_vps} ];
+ };
+
+ implements remove_range_vp => as {
+ my ($self, $to_remove) = @_;
+ $self->range_vps([ grep { $_ != $to_remove } @{$self->range_vps} ]);
+ $self->_clear_field_map;
+ $self->clear_field_names;
+ };
+
+ implements add_range_vp => as {
+ my ($self) = @_;
+ if ($self->can_add) {
+ $self->_clear_field_map;
+ $self->clear_field_names;
+ my @span_info = (
+ $self->time_from->value,
+ $self->time_to->value,
+ (map { $_->has_value ? $_->value : '' }
+ map { $self->$_ } qw/repeat_from repeat_to/),
+ $self->pattern->value,
+ );
+ my $encoded_spanset = join ',', @span_info;
+ my $args = {
+ value_string => $encoded_spanset,
+ parent => $self
+ };
+ my $count = scalar(@{$self->range_vps});
+ my $field = $self->build_simple_field(TimeRange, 'range-'.$count, $args);
+ my $d = DateTime::Format::Duration->new( pattern => '%s' );
+ if ($d->format_duration( $self->spanset->intersection($field->value)->duration ) > 0) {
+ # XXX - Stop using the stash here?
+ $self->ctx->stash->{warning} = 'Warning: Most recent time range overlaps '.
+ 'with existing time range in this booking.';
+ }
+ #warn "encoded spanset = $encoded_spanset\n";
+ #warn "current range = ".join(', ', (@{$self->range_vps}))."\n";
+ push(@{$self->range_vps}, $field);
+ }
+ };
+
+ implements build_field_map => as {
+ my ($self) = @_;
+ my %map;
+ foreach my $field (@{$self->range_vps}) {
+ $map{$field->name} = $field;
+ }
+ foreach my $name (@{$self->column_order}) {
+ $map{$name} = $self->$name;
+ }
+ return \%map;
+ };
+
+ implements build_field_names => as {
+ my ($self) = @_;
+ return [
+ (map { $_->name } @{$self->range_vps}),
+ @{$self->column_order}
+ ];
+ };
+
+ implements can_add => as {
+ my ($self) = @_;
+ my $error;
+ if ($self->time_to->has_value && $self->time_from->has_value) {
+ my $time_to = $self->time_to->value;
+ my $time_from = $self->time_from->value;
+
+ my ($pattern, $repeat_from, $repeat_to) = ('','','');
+ $pattern = $self->pattern->value if $self->pattern->has_value;
+ $repeat_from = $self->repeat_from->value if $self->repeat_from->has_value;
+ $repeat_to = $self->repeat_to->value if $self->repeat_to->has_value;
+
+ my $duration = $time_to - $time_from;
+ if ($time_to < $time_from) {
+ $error = 'Please make sure that the Time To is after the Time From.';
+ } elsif ($time_to == $time_from) {
+ $error = 'Your desired booking slot is too small.';
+ } elsif ($pattern && $pattern ne 'none') {
+ my %pattern = (hourly => [ hours => 1 ],
+ daily => [ days => 1 ],
+ weekly => [ days => 7 ],
+ monthly => [ months => 1 ]);
+ my $pattern_comp = DateTime::Duration->compare(
+ $duration, DateTime::Duration->new( @{$pattern{$pattern}} )
+ );
+ if (!$repeat_to || !$repeat_from) {
+ $error = 'Please make sure that you enter a valid range for the '.
+ 'repetition period.';
+ } elsif ($time_to == $time_from) {
+ $error = 'Your desired repetition period is too short.';
+ } elsif ($repeat_to && ($repeat_to < $repeat_from)) {
+ $error = 'Please make sure that the Repeat To is after the Repeat From.';
+ } elsif ( ( ($pattern eq 'hourly') && ($pattern_comp > 0) ) ||
+ ( ($pattern eq 'daily') && ($pattern_comp > 0) ) ||
+ ( ($pattern eq 'weekly') && ($pattern_comp > 0) ) ||
+ ( ($pattern eq 'monthly') && ($pattern_comp > 0) ) ) {
+ $error = "Your repetition pattern ($pattern) is too short for your ".
+ "desired booking length.";
+ }
+ }
+ } else {
+ $error = 'Please complete both the Time To and Time From fields.';
+ }
+ $self->error($error);
+ return !defined($error);
+ };
+
+ implements build_simple_field => as {
+ my ($self, $class, $name, $args) = @_;
+ return $class->new(
+ name => $name,
+ location => join('-', $self->location, 'field', $name),
+ ctx => $self->ctx,
+ %$args
+ );
+ };
+
+ implements build_time_to => as {
+ my ($self) = @_;
+ return $self->build_simple_field(DateTime, 'time_to', {});
+ };
+
+ implements build_time_from => as {
+ my ($self) = @_;
+ return $self->build_simple_field(DateTime, 'time_from', {});
+ };
+
+ implements build_repeat_to => as {
+ my ($self) = @_;
+ return $self->build_simple_field(DateTime, 'repeat_to', {});
+ };
+
+ implements build_repeat_from => as {
+ my ($self) = @_;
+ return $self->build_simple_field(DateTime, 'repeat_from', {});
+ };
+
+ implements build_pattern => as {
+ my ($self) = @_;
+ return $self->build_simple_field(String, 'pattern', {});
+ };
+
+ implements next => as {
+ $_[0]->on_next_callback->(@_);
+ };
+
+ override accept_events => sub {
+ my $self = shift;
+ ('add_range_vp', ($self->has_on_next_callback ? ('next') : ()), super());
+ };
+
+ override child_event_sinks => sub {
+ my ($self) = @_;
+ return ((grep { ref($_) =~ 'Hidden' } values %{$self->_field_map}),
+ (grep { ref($_) !~ 'Hidden' } values %{$self->_field_map}),
+ super());
+ };
+
+ override apply_events => sub {
+ my ($self, $ctx, $events) = @_;
+
+ # auto-inflate range fields based on number from hidden field
+
+ my $max = $events->{$self->location.':max_range_vps'};
+ my @range_vps = map {
+ TimeRange->new(
+ name => "range-$_",
+ location => join('-', $self->location, 'field', 'range', $_),
+ ctx => $self->ctx,
+ parent => $self,
+ )
+ } ($max ? (0 .. $max - 1) : ());
+ $self->range_vps(\@range_vps);
+ $self->_clear_field_map;
+ $self->clear_field_names;
+
+ # call original event handling
+
+ super();
+
+ # repack range VPs in case of deletion
+
+ my $prev_idx = -1;
+
+ foreach my $vp (@{$self->range_vps}) {
+ my $cur_idx = ($vp->name =~ m/range-(\d+)/);
+ if (($cur_idx - $prev_idx) > 1) {
+ $cur_idx--;
+ my $name = "range-${cur_idx}";
+ $vp->name($name);
+ $vp->location(join('-', $self->location, 'field', $name));
+ }
+ $prev_idx = $cur_idx;
+ }
+ };
+
+};
+
+1;
+
+=head1 NAME
+
+Reaction::UI::ViewPort::TimeRangeCollection
+
+=head1 SYNOPSIS
+
+ my $trc = $self->push_viewport(TimeRangeCollection,
+ layout => 'avail_search_form',
+ on_apply_callback => $search_callback,
+ name => 'TRC',
+ );
+
+=head1 DESCRIPTION
+
+=head1 ATTRIBUTES
+
+=head2 can_add
+
+=head2 column_order
+
+=head2 error
+
+=head2 field_names
+
+=head2 fields
+
+=head2 layout
+
+=head2 pattern
+
+Typically either: none, daily, weekly or monthly
+
+=head2 max_range_vps
+
+=head2 range_vps
+
+=head2 repeat_from
+
+A DateTime field.
+
+=head2 repeat_to
+
+A DateTime field.
+
+=head2 time_from
+
+A DateTime field.
+
+=head2 time_to
+
+A DateTime field.
+
+=head1 METHODS
+
+=head2 spanset
+
+Returns: $spanset consisting of all the TimeRange spans combined
+
+=head2 range_strings
+
+Returns: ArrayRef of Str consisting of the value_strings of all TimeRange
+VPs
+
+=head2 remove_range_vp
+
+Arguments: $to_remove
+
+=head2 add_range_vp
+
+Arguments: $to_add
+
+=head2 build_simple_field
+
+Arguments: $class, $name, $args
+where $class is an object, $name is a scalar and $args is a hashref
+
+=head2 next
+
+=head2 on_next_callback
+
+=head2 clear_field_names
+
+=head2 child_event_sinks
+
+=head1 SEE ALSO
+
+=head2 L<Reaction::UI::ViewPort>
+
+=head2 L<Reaction::UI::ViewPort::Field::TimeRange>
+
+=head2 L<Reaction::UI::ViewPort::Field::DateTime>
+
+=head2 L<DateTime::Event::Recurrence>
+
+=head1 AUTHORS
+
+See L<Reaction::Class> for authors.
+
+=head1 LICENSE
+
+See L<Reaction::Class> for the license.
+
+=cut