Commit | Line | Data |
7adfd53f |
1 | package Reaction::UI::ViewPort::TimeRangeCollection; |
2 | |
3 | use Reaction::Class; |
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'; |
11 | |
12 | class TimeRangeCollection is 'Reaction::UI::ViewPort', which { |
13 | |
6ab43711 |
14 | #has '+layout' => (default => 'timerangecollection'); |
15 | |
7adfd53f |
16 | has '+column_order' => ( |
17 | default => sub{[ qw/ time_from time_to pattern repeat_from repeat_to / ]}, |
18 | ); |
6ab43711 |
19 | |
7adfd53f |
20 | has time_from => ( |
21 | isa => 'Reaction::UI::ViewPort::Field::DateTime', |
22 | is => 'rw', lazy_build => 1, |
23 | ); |
6ab43711 |
24 | |
7adfd53f |
25 | has time_to => ( |
26 | isa => 'Reaction::UI::ViewPort::Field::DateTime', |
27 | is => 'rw', lazy_build => 1, |
28 | ); |
6ab43711 |
29 | |
7adfd53f |
30 | has repeat_from => ( |
31 | isa => 'Reaction::UI::ViewPort::Field::DateTime', |
32 | is => 'rw', lazy_build => 1, |
33 | ); |
6ab43711 |
34 | |
7adfd53f |
35 | has repeat_to => ( |
36 | isa => 'Reaction::UI::ViewPort::Field::DateTime', |
37 | is => 'rw', lazy_build => 1, |
38 | ); |
6ab43711 |
39 | |
7adfd53f |
40 | has pattern => ( |
41 | isa => 'Reaction::UI::ViewPort::Field::String', |
42 | # valid_values => [ qw/none daily weekly monthly/ ], |
43 | is => 'rw', lazy_build => 1, |
44 | ); |
6ab43711 |
45 | |
7adfd53f |
46 | has range_vps => (isa => 'ArrayRef', is => 'rw', lazy_build => 1,); |
6ab43711 |
47 | |
7adfd53f |
48 | has max_range_vps => (isa => 'Int', is => 'rw', lazy_build => 1,); |
6ab43711 |
49 | |
7adfd53f |
50 | has error => ( |
51 | isa => 'Str', |
52 | is => 'rw', |
53 | required => 0, |
54 | ); |
6ab43711 |
55 | |
7adfd53f |
56 | has field_names => ( |
57 | isa => 'ArrayRef', is => 'rw', |
58 | lazy_build => 1, clearer => 'clear_field_names', |
59 | ); |
6ab43711 |
60 | |
7adfd53f |
61 | has _field_map => ( |
62 | isa => 'HashRef', is => 'rw', init_arg => 'fields', |
63 | clearer => '_clear_field_map', |
64 | predicate => '_has_field_map', |
89939ff9 |
65 | lazy_build => 1, |
7adfd53f |
66 | ); |
6ab43711 |
67 | |
7adfd53f |
68 | has on_next_callback => ( |
69 | isa => 'CodeRef', |
70 | is => 'rw', |
71 | predicate => 'has_on_next_callback', |
72 | ); |
6ab43711 |
73 | |
7adfd53f |
74 | implements fields => as { shift->_field_map }; |
6ab43711 |
75 | |
89939ff9 |
76 | implements _build_range_vps => as { [] }; |
6ab43711 |
77 | |
7adfd53f |
78 | implements spanset => as { |
79 | my ($self) = @_; |
80 | my $spanset = DateTime::SpanSet->empty_set; |
81 | $spanset = $spanset->union($_->value) for @{$self->range_vps}; |
82 | return $spanset; |
83 | }; |
6ab43711 |
84 | |
7adfd53f |
85 | implements range_strings => as { |
86 | my ($self) = @_; |
87 | return [ map { $_->value_string } @{$self->range_vps} ]; |
88 | }; |
6ab43711 |
89 | |
7adfd53f |
90 | implements remove_range_vp => as { |
6ab43711 |
91 | my ($self, $to_remove) = @_; |
7adfd53f |
92 | $self->range_vps([ grep { $_ != $to_remove } @{$self->range_vps} ]); |
93 | $self->_clear_field_map; |
94 | $self->clear_field_names; |
95 | }; |
6ab43711 |
96 | |
7adfd53f |
97 | implements add_range_vp => as { |
98 | my ($self) = @_; |
99 | if ($self->can_add) { |
100 | $self->_clear_field_map; |
101 | $self->clear_field_names; |
102 | my @span_info = ( |
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, |
108 | ); |
109 | my $encoded_spanset = join ',', @span_info; |
110 | my $args = { |
111 | value_string => $encoded_spanset, |
112 | parent => $self |
113 | }; |
114 | my $count = scalar(@{$self->range_vps}); |
89939ff9 |
115 | my $field = $self->_build_simple_field(TimeRange, 'range-'.$count, $args); |
7adfd53f |
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.'; |
121 | } |
122 | #warn "encoded spanset = $encoded_spanset\n"; |
123 | #warn "current range = ".join(', ', (@{$self->range_vps}))."\n"; |
124 | push(@{$self->range_vps}, $field); |
125 | } |
126 | }; |
6ab43711 |
127 | |
89939ff9 |
128 | implements _build_field_map => as { |
7adfd53f |
129 | my ($self) = @_; |
130 | my %map; |
131 | foreach my $field (@{$self->range_vps}) { |
132 | $map{$field->name} = $field; |
133 | } |
134 | foreach my $name (@{$self->column_order}) { |
135 | $map{$name} = $self->$name; |
136 | } |
137 | return \%map; |
138 | }; |
6ab43711 |
139 | |
89939ff9 |
140 | implements _build_field_names => as { |
7adfd53f |
141 | my ($self) = @_; |
142 | return [ |
143 | (map { $_->name } @{$self->range_vps}), |
144 | @{$self->column_order} |
145 | ]; |
146 | }; |
6ab43711 |
147 | |
7adfd53f |
148 | implements can_add => as { |
149 | my ($self) = @_; |
150 | my $error; |
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; |
6ab43711 |
154 | |
7adfd53f |
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; |
6ab43711 |
159 | |
7adfd53f |
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}} ) |
172 | ); |
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."; |
186 | } |
187 | } |
188 | } else { |
189 | $error = 'Please complete both the Time To and Time From fields.'; |
190 | } |
191 | $self->error($error); |
192 | return !defined($error); |
193 | }; |
6ab43711 |
194 | |
89939ff9 |
195 | implements _build_simple_field => as { |
7adfd53f |
196 | my ($self, $class, $name, $args) = @_; |
197 | return $class->new( |
198 | name => $name, |
199 | location => join('-', $self->location, 'field', $name), |
200 | ctx => $self->ctx, |
201 | %$args |
202 | ); |
203 | }; |
6ab43711 |
204 | |
89939ff9 |
205 | implements _build_time_to => as { |
7adfd53f |
206 | my ($self) = @_; |
89939ff9 |
207 | return $self->_build_simple_field(DateTime, 'time_to', {}); |
7adfd53f |
208 | }; |
6ab43711 |
209 | |
89939ff9 |
210 | implements _build_time_from => as { |
7adfd53f |
211 | my ($self) = @_; |
89939ff9 |
212 | return $self->_build_simple_field(DateTime, 'time_from', {}); |
7adfd53f |
213 | }; |
6ab43711 |
214 | |
89939ff9 |
215 | implements _build_repeat_to => as { |
7adfd53f |
216 | my ($self) = @_; |
89939ff9 |
217 | return $self->_build_simple_field(DateTime, 'repeat_to', {}); |
7adfd53f |
218 | }; |
6ab43711 |
219 | |
89939ff9 |
220 | implements _build_repeat_from => as { |
7adfd53f |
221 | my ($self) = @_; |
89939ff9 |
222 | return $self->_build_simple_field(DateTime, 'repeat_from', {}); |
7adfd53f |
223 | }; |
6ab43711 |
224 | |
89939ff9 |
225 | implements _build_pattern => as { |
7adfd53f |
226 | my ($self) = @_; |
89939ff9 |
227 | return $self->_build_simple_field(String, 'pattern', {}); |
7adfd53f |
228 | }; |
6ab43711 |
229 | |
7adfd53f |
230 | implements next => as { |
231 | $_[0]->on_next_callback->(@_); |
232 | }; |
6ab43711 |
233 | |
7adfd53f |
234 | override accept_events => sub { |
235 | my $self = shift; |
236 | ('add_range_vp', ($self->has_on_next_callback ? ('next') : ()), super()); |
237 | }; |
6ab43711 |
238 | |
7adfd53f |
239 | override child_event_sinks => sub { |
240 | my ($self) = @_; |
241 | return ((grep { ref($_) =~ 'Hidden' } values %{$self->_field_map}), |
242 | (grep { ref($_) !~ 'Hidden' } values %{$self->_field_map}), |
243 | super()); |
244 | }; |
6ab43711 |
245 | |
7adfd53f |
246 | override apply_events => sub { |
247 | my ($self, $ctx, $events) = @_; |
6ab43711 |
248 | |
7adfd53f |
249 | # auto-inflate range fields based on number from hidden field |
6ab43711 |
250 | |
7adfd53f |
251 | my $max = $events->{$self->location.':max_range_vps'}; |
252 | my @range_vps = map { |
253 | TimeRange->new( |
254 | name => "range-$_", |
255 | location => join('-', $self->location, 'field', 'range', $_), |
256 | ctx => $self->ctx, |
257 | parent => $self, |
258 | ) |
259 | } ($max ? (0 .. $max - 1) : ()); |
260 | $self->range_vps(\@range_vps); |
261 | $self->_clear_field_map; |
262 | $self->clear_field_names; |
6ab43711 |
263 | |
7adfd53f |
264 | # call original event handling |
6ab43711 |
265 | |
7adfd53f |
266 | super(); |
6ab43711 |
267 | |
268 | # repack range VPs in case of deletion |
269 | |
7adfd53f |
270 | my $prev_idx = -1; |
6ab43711 |
271 | |
7adfd53f |
272 | foreach my $vp (@{$self->range_vps}) { |
273 | my $cur_idx = ($vp->name =~ m/range-(\d+)/); |
274 | if (($cur_idx - $prev_idx) > 1) { |
275 | $cur_idx--; |
276 | my $name = "range-${cur_idx}"; |
277 | $vp->name($name); |
278 | $vp->location(join('-', $self->location, 'field', $name)); |
279 | } |
280 | $prev_idx = $cur_idx; |
281 | } |
282 | }; |
283 | |
284 | }; |
285 | |
6ab43711 |
286 | 1; |
7adfd53f |
287 | |
288 | =head1 NAME |
289 | |
290 | Reaction::UI::ViewPort::TimeRangeCollection |
291 | |
292 | =head1 SYNOPSIS |
293 | |
294 | my $trc = $self->push_viewport(TimeRangeCollection, |
295 | layout => 'avail_search_form', |
296 | on_apply_callback => $search_callback, |
297 | name => 'TRC', |
298 | ); |
299 | |
300 | =head1 DESCRIPTION |
301 | |
302 | =head1 ATTRIBUTES |
303 | |
304 | =head2 can_add |
305 | |
306 | =head2 column_order |
307 | |
308 | =head2 error |
309 | |
310 | =head2 field_names |
311 | |
312 | =head2 fields |
313 | |
314 | =head2 layout |
315 | |
316 | =head2 pattern |
317 | |
318 | Typically either: none, daily, weekly or monthly |
319 | |
320 | =head2 max_range_vps |
321 | |
322 | =head2 range_vps |
323 | |
324 | =head2 repeat_from |
325 | |
326 | A DateTime field. |
327 | |
328 | =head2 repeat_to |
329 | |
330 | A DateTime field. |
331 | |
332 | =head2 time_from |
333 | |
334 | A DateTime field. |
335 | |
336 | =head2 time_to |
337 | |
338 | A DateTime field. |
339 | |
340 | =head1 METHODS |
341 | |
342 | =head2 spanset |
343 | |
344 | Returns: $spanset consisting of all the TimeRange spans combined |
345 | |
346 | =head2 range_strings |
347 | |
348 | Returns: ArrayRef of Str consisting of the value_strings of all TimeRange |
349 | VPs |
6ab43711 |
350 | |
7adfd53f |
351 | =head2 remove_range_vp |
352 | |
353 | Arguments: $to_remove |
6ab43711 |
354 | |
7adfd53f |
355 | =head2 add_range_vp |
356 | |
357 | Arguments: $to_add |
358 | |
89939ff9 |
359 | =head2 _build_simple_field |
7adfd53f |
360 | |
361 | Arguments: $class, $name, $args |
362 | where $class is an object, $name is a scalar and $args is a hashref |
363 | |
364 | =head2 next |
365 | |
366 | =head2 on_next_callback |
367 | |
368 | =head2 clear_field_names |
369 | |
370 | =head2 child_event_sinks |
371 | |
372 | =head1 SEE ALSO |
373 | |
374 | =head2 L<Reaction::UI::ViewPort> |
375 | |
376 | =head2 L<Reaction::UI::ViewPort::Field::TimeRange> |
377 | |
378 | =head2 L<Reaction::UI::ViewPort::Field::DateTime> |
379 | |
380 | =head2 L<DateTime::Event::Recurrence> |
381 | |
382 | =head1 AUTHORS |
383 | |
384 | See L<Reaction::Class> for authors. |
385 | |
386 | =head1 LICENSE |
387 | |
388 | See L<Reaction::Class> for the license. |
389 | |
390 | =cut |