1 package Reaction::UI::ViewPort::TimeRangeCollection;
4 use Reaction::Types::DateTime;
5 use Moose::Util::TypeConstraints ();
6 use DateTime::Event::Recurrence;
7 use aliased 'Reaction::UI::ViewPort::Field::String';
8 use aliased 'Reaction::UI::ViewPort::Field::DateTime';
9 use aliased 'Reaction::UI::ViewPort::Field::HiddenArray';
10 use aliased 'Reaction::UI::ViewPort::Field::TimeRange';
12 class TimeRangeCollection is 'Reaction::UI::ViewPort', which {
14 #has '+layout' => (default => 'timerangecollection');
16 has '+column_order' => (
17 default => sub{[ qw/ time_from time_to pattern repeat_from repeat_to / ]},
21 isa => 'Reaction::UI::ViewPort::Field::DateTime',
22 is => 'rw', lazy_build => 1,
26 isa => 'Reaction::UI::ViewPort::Field::DateTime',
27 is => 'rw', lazy_build => 1,
31 isa => 'Reaction::UI::ViewPort::Field::DateTime',
32 is => 'rw', lazy_build => 1,
36 isa => 'Reaction::UI::ViewPort::Field::DateTime',
37 is => 'rw', lazy_build => 1,
41 isa => 'Reaction::UI::ViewPort::Field::String',
42 # valid_values => [ qw/none daily weekly monthly/ ],
43 is => 'rw', lazy_build => 1,
46 has range_vps => (isa => 'ArrayRef', is => 'rw', lazy_build => 1,);
48 has max_range_vps => (isa => 'Int', is => 'rw', lazy_build => 1,);
57 isa => 'ArrayRef', is => 'rw',
58 lazy_build => 1, clearer => 'clear_field_names',
62 isa => 'HashRef', is => 'rw', init_arg => 'fields',
63 clearer => '_clear_field_map',
64 predicate => '_has_field_map',
68 has on_next_callback => (
71 predicate => 'has_on_next_callback',
74 implements fields => as { shift->_field_map };
76 implements _build_range_vps => as { [] };
78 implements spanset => as {
80 my $spanset = DateTime::SpanSet->empty_set;
81 $spanset = $spanset->union($_->value) for @{$self->range_vps};
85 implements range_strings => as {
87 return [ map { $_->value_string } @{$self->range_vps} ];
90 implements remove_range_vp => as {
91 my ($self, $to_remove) = @_;
92 $self->range_vps([ grep { $_ != $to_remove } @{$self->range_vps} ]);
93 $self->_clear_field_map;
94 $self->clear_field_names;
97 implements add_range_vp => as {
100 $self->_clear_field_map;
101 $self->clear_field_names;
103 $self->time_from->value,
104 $self->time_to->value,
105 (map { $_->has_value ? $_->value : '' }
106 map { $self->$_ } qw/repeat_from repeat_to/),
107 $self->pattern->value,
109 my $encoded_spanset = join ',', @span_info;
111 value_string => $encoded_spanset,
114 my $count = scalar(@{$self->range_vps});
115 my $field = $self->_build_simple_field(TimeRange, 'range-'.$count, $args);
116 my $d = DateTime::Format::Duration->new( pattern => '%s' );
117 if ($d->format_duration( $self->spanset->intersection($field->value)->duration ) > 0) {
118 # XXX - Stop using the stash here?
119 $self->ctx->stash->{warning} = 'Warning: Most recent time range overlaps '.
120 'with existing time range in this booking.';
122 #warn "encoded spanset = $encoded_spanset\n";
123 #warn "current range = ".join(', ', (@{$self->range_vps}))."\n";
124 push(@{$self->range_vps}, $field);
128 implements _build_field_map => as {
131 foreach my $field (@{$self->range_vps}) {
132 $map{$field->name} = $field;
134 foreach my $name (@{$self->column_order}) {
135 $map{$name} = $self->$name;
140 implements _build_field_names => as {
143 (map { $_->name } @{$self->range_vps}),
144 @{$self->column_order}
148 implements can_add => as {
151 if ($self->time_to->has_value && $self->time_from->has_value) {
152 my $time_to = $self->time_to->value;
153 my $time_from = $self->time_from->value;
155 my ($pattern, $repeat_from, $repeat_to) = ('','','');
156 $pattern = $self->pattern->value if $self->pattern->has_value;
157 $repeat_from = $self->repeat_from->value if $self->repeat_from->has_value;
158 $repeat_to = $self->repeat_to->value if $self->repeat_to->has_value;
160 my $duration = $time_to - $time_from;
161 if ($time_to < $time_from) {
162 $error = 'Please make sure that the Time To is after the Time From.';
163 } elsif ($time_to == $time_from) {
164 $error = 'Your desired booking slot is too small.';
165 } elsif ($pattern && $pattern ne 'none') {
166 my %pattern = (hourly => [ hours => 1 ],
167 daily => [ days => 1 ],
168 weekly => [ days => 7 ],
169 monthly => [ months => 1 ]);
170 my $pattern_comp = DateTime::Duration->compare(
171 $duration, DateTime::Duration->new( @{$pattern{$pattern}} )
173 if (!$repeat_to || !$repeat_from) {
174 $error = 'Please make sure that you enter a valid range for the '.
175 'repetition period.';
176 } elsif ($time_to == $time_from) {
177 $error = 'Your desired repetition period is too short.';
178 } elsif ($repeat_to && ($repeat_to < $repeat_from)) {
179 $error = 'Please make sure that the Repeat To is after the Repeat From.';
180 } elsif ( ( ($pattern eq 'hourly') && ($pattern_comp > 0) ) ||
181 ( ($pattern eq 'daily') && ($pattern_comp > 0) ) ||
182 ( ($pattern eq 'weekly') && ($pattern_comp > 0) ) ||
183 ( ($pattern eq 'monthly') && ($pattern_comp > 0) ) ) {
184 $error = "Your repetition pattern ($pattern) is too short for your ".
185 "desired booking length.";
189 $error = 'Please complete both the Time To and Time From fields.';
191 $self->error($error);
192 return !defined($error);
195 implements _build_simple_field => as {
196 my ($self, $class, $name, $args) = @_;
199 location => join('-', $self->location, 'field', $name),
205 implements _build_time_to => as {
207 return $self->_build_simple_field(DateTime, 'time_to', {});
210 implements _build_time_from => as {
212 return $self->_build_simple_field(DateTime, 'time_from', {});
215 implements _build_repeat_to => as {
217 return $self->_build_simple_field(DateTime, 'repeat_to', {});
220 implements _build_repeat_from => as {
222 return $self->_build_simple_field(DateTime, 'repeat_from', {});
225 implements _build_pattern => as {
227 return $self->_build_simple_field(String, 'pattern', {});
230 implements next => as {
231 $_[0]->on_next_callback->(@_);
234 override accept_events => sub {
236 ('add_range_vp', ($self->has_on_next_callback ? ('next') : ()), super());
239 override child_event_sinks => sub {
241 return ((grep { ref($_) =~ 'Hidden' } values %{$self->_field_map}),
242 (grep { ref($_) !~ 'Hidden' } values %{$self->_field_map}),
246 override apply_events => sub {
247 my ($self, $ctx, $events) = @_;
249 # auto-inflate range fields based on number from hidden field
251 my $max = $events->{$self->location.':max_range_vps'};
252 my @range_vps = map {
255 location => join('-', $self->location, 'field', 'range', $_),
259 } ($max ? (0 .. $max - 1) : ());
260 $self->range_vps(\@range_vps);
261 $self->_clear_field_map;
262 $self->clear_field_names;
264 # call original event handling
268 # repack range VPs in case of deletion
272 foreach my $vp (@{$self->range_vps}) {
273 my $cur_idx = ($vp->name =~ m/range-(\d+)/);
274 if (($cur_idx - $prev_idx) > 1) {
276 my $name = "range-${cur_idx}";
278 $vp->location(join('-', $self->location, 'field', $name));
280 $prev_idx = $cur_idx;
290 Reaction::UI::ViewPort::TimeRangeCollection
294 my $trc = $self->push_viewport(TimeRangeCollection,
295 layout => 'avail_search_form',
296 on_apply_callback => $search_callback,
318 Typically either: none, daily, weekly or monthly
344 Returns: $spanset consisting of all the TimeRange spans combined
348 Returns: ArrayRef of Str consisting of the value_strings of all TimeRange
351 =head2 remove_range_vp
353 Arguments: $to_remove
359 =head2 _build_simple_field
361 Arguments: $class, $name, $args
362 where $class is an object, $name is a scalar and $args is a hashref
366 =head2 on_next_callback
368 =head2 clear_field_names
370 =head2 child_event_sinks
374 =head2 L<Reaction::UI::ViewPort>
376 =head2 L<Reaction::UI::ViewPort::Field::TimeRange>
378 =head2 L<Reaction::UI::ViewPort::Field::DateTime>
380 =head2 L<DateTime::Event::Recurrence>
384 See L<Reaction::Class> for authors.
388 See L<Reaction::Class> for the license.