+package App::Web::Comstock;
+
+use 5.010000;
+use strict;
+use warnings;
+our $VERSION = '0.000_001';
+
+use DateTime;
+use DBIx::Simple;
+use HTML::TreeBuilder;
+use HTML::Element::Library;
+use Plack::Builder;
+use Plack::Request;
+use POSIX qw/strftime/;
+
+sub HTML::Element::iter3 {
+ my ($self, $data, $code) = @_;
+ my $orig = $self;
+ my $prev = $orig;
+ for my $el (@$data) {
+ my $current = $orig->clone;
+ $code->($el, $current);
+ $prev->postinsert($current);
+ $prev = $current;
+ }
+ $orig->detach;
+}
+
+sub HTML::Element::fid { shift->look_down(id => shift) }
+sub HTML::Element::fclass { shift->look_down(class => qr/\b$_[0]\b/) }
+
+##################################################
+
+my ($index);
+
+{
+ sub parse_html {
+ my $builder = HTML::TreeBuilder->new;
+ $builder->ignore_unknown(0);
+ $builder->parse_file("tmpl/$_[0].html");
+ $builder
+ }
+
+ $index = parse_html 'index';
+}
+
+sub db : lvalue {
+ shift->{'comstock.db'}
+}
+
+sub nav_li {
+ my ($data, $li) = @_;
+ $li->find('a')->replace_content($data->{title});
+ $li->find('a')->attr(href => '?item='.$data->{item});
+}
+
+sub nav_ul {
+ my ($data, $ul) = @_;
+ $ul->find('li')->iter3($data, \&nav_li);
+}
+
+sub display_app {
+ my ($env) = @_;
+ my $req = Plack::Request->new($env);
+ my $tree = $index->clone;
+ my @items = db($env)->select(items => '*')->hashes;
+ my %items;
+
+ for my $item (@items) {
+ $items{$item->{category}} //= [];
+ push @{$items{$item->{category}}}, $item
+ }
+
+ my @data = sort { $a->[0]{category} cmp $b->[0]{category} } values %items; #map { $items{$_} } sort keys %items;
+ $tree->fid('comstock_nav')->find('ul')->iter3(\@data, \&nav_ul);
+ my $item = $req->param('item');
+ if ($item) {
+ $tree->look_down(name => 'item')->attr(value => $item);
+ my ($begin, $end) = db($env)->select(items => [qw/begin_hour end_hour/], {item => $item})->list;
+ for my $name (qw/begin_hour end_hour/) {
+ my $select = $tree->look_down(name => $name);
+ $select->iter($select->find('option') => $begin .. $end)
+ }
+ } else {
+ $tree->fid('book_div')->detach
+ }
+ $tree
+}
+
+sub error {
+ 'Error: ' . $_[0]
+}
+
+sub book_app {
+ my ($env) = @_;
+ my $req = Plack::Request->new($env);
+ my ($begin_year, $begin_month, $begin_day) = split '/', $req->param('begin');
+ my ($end_year, $end_month, $end_day) = split '/', $req->param('end');
+ my $begin_hour = $req->param('begin_hour');
+ my $end_hour = $req->param('end_hour');
+ my $begin = DateTime->new(year => $begin_year, month => $begin_month, day => $begin_day, hour => $begin_hour)->epoch;
+ my $end = DateTime->new(year => $end_year, month => $end_month, day => $end_day, hour => $end_hour)->epoch;
+ my $item = 0+$req->param('item');
+ my ($begin_range, $end_range, $min_hours, $max_hours) = db($env)->select(items => [qw/begin_hour end_hour min_hours max_hours/], {item => $item})->list or return error 'No such item';
+
+ return error 'End time is not later than begin time' if $end <= $begin;
+ return error 'Begin/end hour not in allowed range' if $begin_hour < $begin_range || $begin_hour > $end_range || $end_hour < $begin_range || $end_hour > $end_range;
+ return error 'Bookings must last for at least $min_hours hours' if (($end - $begin) / 3600 < $min_hours);
+ return error 'Bookings must last for at most $max_hours hours' if (($end - $begin) / 3600 > $max_hours);
+ return error 'Item is not available for the selected period' if db($env)->query('SELECT item FROM bookings WHERE item = ? AND (end_time - begin_time + ? - ?) > GREATEST(end_time, ?) - LEAST(begin_time, ?)', $item, $end, $begin, $end, $begin)->list;
+ db($env)->insert(bookings => {
+ item => $item,
+ name => scalar $req->param('name'),
+ begin_time => $begin,
+ end_time => $end,
+ });
+ return [200, ['Content-Type' => 'text/plain'], ['Booking was successful']];
+}
+
+sub view_app {
+ my ($env) = @_;
+ my $req = Plack::Request->new($env);
+ my $item = $req->param('item');
+ my $time = time;
+ $time -= $time % 86400;
+ my @bookings = db($env)->select(bookings => '*', {item => $item, begin_time => {'>', $time}}, 'begin_time')->hashes;
+ my $ans;
+ for my $booking (@bookings) {
+ $booking->{name} =~ y/\n//d;
+ $ans .= sprintf "%s -> %s %s\n", strftime ('%c', gmtime $booking->{begin_time}), strftime ('%c', gmtime $booking->{end_time}), $booking->{name};
+ }
+ [200, ['Content-type' => 'text/plain'], [$ans]]
+}
+
+sub app {
+ builder {
+ enable 'ContentLength';
+ enable sub {
+ my $app = shift;
+ my $db = DBIx::Simple->connect($ENV{COMSTOCK_DSN} // 'dbi:Pg:');
+ sub {
+ my ($env) = @_;
+ db($env) = $db;
+ my $res = $app->($env);
+ return $res if ref $res eq 'ARRAY';
+ return [200, ['Content-type' => 'text/html; charset=utf-8'], [$res->as_HTML]]
+ if ref $res;
+ return [500, ['Content-type' => 'text/plain'], ["$res"]]
+ }
+ };
+ mount '/book' => \&book_app;
+ mount '/view' => \&view_app;
+ mount '/' => \&display_app;
+ }
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+App::Web::Comstock - Website for managing bookings of generic items
+
+=head1 SYNOPSIS
+
+ use App::Web::Comstock;
+ App::Web::Comstock->app
+
+=head1 DESCRIPTION
+
+Comstock is an unfinished website for managing bookings of generic
+items. Users will be able to see the availability of existing items,
+book them for some periods and view existing bookings.
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2016 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.22.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut