It is starting to look like this may actually work after all. Listview is the only...
[catagits/Reaction.git] / lib / Reaction / UI / ViewPort / TimeRangeCollection.pm
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
14   #has '+layout' => (default => 'timerangecollection');
15
16   has '+column_order' => (
17     default => sub{[ qw/ time_from time_to pattern repeat_from repeat_to / ]},
18   );
19
20   has time_from => (
21     isa => 'Reaction::UI::ViewPort::Field::DateTime',
22     is => 'rw', lazy_build => 1,
23   );
24
25   has time_to => (
26     isa => 'Reaction::UI::ViewPort::Field::DateTime',
27     is => 'rw', lazy_build => 1,
28   );
29
30   has repeat_from => (
31     isa => 'Reaction::UI::ViewPort::Field::DateTime',
32     is => 'rw', lazy_build => 1,
33   );
34
35   has repeat_to => (
36     isa => 'Reaction::UI::ViewPort::Field::DateTime',
37     is => 'rw', lazy_build => 1,
38   );
39
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   );
45
46   has range_vps => (isa => 'ArrayRef', is => 'rw', lazy_build => 1,);
47
48   has max_range_vps => (isa => 'Int', is => 'rw', lazy_build => 1,);
49
50   has error => (
51     isa => 'Str',
52     is => 'rw',
53     required => 0,
54   );
55
56   has field_names => (
57     isa => 'ArrayRef', is => 'rw',
58     lazy_build => 1, clearer => 'clear_field_names',
59   );
60
61   has _field_map => (
62     isa => 'HashRef', is => 'rw', init_arg => 'fields',
63     clearer => '_clear_field_map',
64     predicate => '_has_field_map',
65     set_or_lazy_build('field_map'),
66   );
67
68   has on_next_callback => (
69     isa => 'CodeRef',
70     is => 'rw',
71     predicate => 'has_on_next_callback',
72   );
73
74   implements fields => as { shift->_field_map };
75
76   implements build_range_vps => as { [] };
77
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   };
84
85   implements range_strings => as {
86     my ($self) = @_;
87     return [ map { $_->value_string } @{$self->range_vps} ];
88   };
89
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;
95   };
96
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});
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.';
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   };
127
128   implements build_field_map => as {
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   };
139
140   implements build_field_names => as {
141     my ($self) = @_;
142     return [
143       (map { $_->name } @{$self->range_vps}),
144       @{$self->column_order}
145     ];
146   };
147
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;
154
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;
159
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   };
194
195   implements build_simple_field => as {
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   };
204
205   implements build_time_to => as {
206     my ($self) = @_;
207     return $self->build_simple_field(DateTime, 'time_to', {});
208   };
209
210   implements build_time_from => as {
211     my ($self) = @_;
212     return $self->build_simple_field(DateTime, 'time_from', {});
213   };
214
215   implements build_repeat_to => as {
216     my ($self) = @_;
217     return $self->build_simple_field(DateTime, 'repeat_to', {});
218   };
219
220   implements build_repeat_from => as {
221     my ($self) = @_;
222     return $self->build_simple_field(DateTime, 'repeat_from', {});
223   };
224
225   implements build_pattern => as {
226     my ($self) = @_;
227     return $self->build_simple_field(String, 'pattern', {});
228   };
229
230   implements next => as {
231     $_[0]->on_next_callback->(@_);
232   };
233
234   override accept_events => sub {
235     my $self = shift;
236     ('add_range_vp', ($self->has_on_next_callback ? ('next') : ()), super());
237   };
238
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   };
245
246   override apply_events => sub {
247     my ($self, $ctx, $events) = @_;
248
249     # auto-inflate range fields based on number from hidden field
250
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;
263
264     # call original event handling
265
266     super();
267
268     # repack range VPs in case of deletion
269
270     my $prev_idx = -1;
271
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
286 1;
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
350
351 =head2 remove_range_vp
352
353 Arguments: $to_remove
354
355 =head2 add_range_vp
356
357 Arguments: $to_add
358
359 =head2 build_simple_field
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