From 08a2678ea37a59fba6c7ad64ec6618c975f0540a Mon Sep 17 00:00:00 2001 From: mehdi <mehdi@inlibro.com> Date: Thu, 12 Jan 2017 11:57:33 -0500 Subject: [PATCH] Bug 17015 - DiscreteCalendar UI, Back-End and necessary scripts --- Koha/DiscreteCalendar.pm | 968 +++++++++++++++++++++ .../bug_17015_koha_discrete_calendar.sql | 14 + .../atomicupdate/generate_discrete_calendar.perl | 165 ++++ .../prog/en/modules/tools/discrete_calendar.tt | 617 +++++++++++++ misc/cronjobs/add_days_discrete_calendar.pl | 113 +++ tools/discrete_calendar.pl | 157 ++++ 6 files changed, 2034 insertions(+) create mode 100644 Koha/DiscreteCalendar.pm create mode 100644 installer/data/mysql/atomicupdate/bug_17015_koha_discrete_calendar.sql create mode 100755 installer/data/mysql/atomicupdate/generate_discrete_calendar.perl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/discrete_calendar.tt create mode 100755 misc/cronjobs/add_days_discrete_calendar.pl create mode 100755 tools/discrete_calendar.pl diff --git a/Koha/DiscreteCalendar.pm b/Koha/DiscreteCalendar.pm new file mode 100644 index 0000000..7a21653 --- /dev/null +++ b/Koha/DiscreteCalendar.pm @@ -0,0 +1,968 @@ +package Koha::DiscreteCalendar; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see <http://www.gnu.org/licenses>. + +#####Sets holiday periods for each branch. Datedues will be extended if branch is closed -TG +use strict; +use warnings; + +use CGI qw ( -utf8 ); +use Carp; +use DateTime; +use DateTime::Format::Strptime; + +use C4::Context; +use C4::Output; +use Koha::Database; +use Koha::DateUtils; + +sub new { + my ( $classname, %options ) = @_; + my $self = {}; + bless $self, $classname; + for my $o_name ( keys %options ) { + my $o = lc $o_name; + $self->{$o} = $options{$o_name}; + } + if ( !defined $self->{branchcode} ) { + croak 'No branchcode argument passed to Koha::DiscreteCalendar->new'; + } + $self->_init(); + + return $self; +} + +sub _init { + my $self = shift; + $self->{days_mode} = C4::Context->preference('useDaysMode'); + #If the branchcode doesn't exist we use the default calendar. + my $schema = Koha::Database->new->schema; + my $branchcode = $self->{branchcode}; + my $dtf = $schema->storage->datetime_parser; + my $today = $dtf->format_datetime(DateTime->today); + my $rs = $schema->resultset('DiscreteCalendar')->single( + { + branchcode => $branchcode, + date => $today + } + ); + #use default if no calendar is found + if (!$rs){ + $self->{branchcode} = 'DFLT'; + } + +} + +sub getDatesInfo { + my $self = shift; + my $branchcode = $self->{branchcode}; + my @datesInfos =(); + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode + }, + { + select => [ 'date', { DATE => 'date' } ], + as => [qw/ date date /], + columns =>[ qw/ holidaytype openhour closehour note/] + }, + ); + + while (my $date = $rs->next()){ + my $outputdate = dt_from_string( $date->date(), 'iso'); + $outputdate = output_pref( { dt => $outputdate, dateonly => 1 } ); + push @datesInfos, { + date => $date->date(), + outputdate => $outputdate, + holidaytype => $date->holidaytype() , + openhour => $date->openhour(), + closehour => $date->closehour(), + note => $date->note() + }; + } + + return @datesInfos; +} +#This methode will copy everything from a given branch found to the new branch +sub add_new_branch { + my ($self, $copyBranch, $newBranch) = @_; + $copyBranch = 'DFLT' unless $copyBranch; + my $schema = Koha::Database->new->schema; + + my $branch_rs = $schema->resultset('DiscreteCalendar')->search({ + branchcode => $copyBranch + }); + + while(my $row = $branch_rs->next()){ + $schema->resultset('DiscreteCalendar')->create({ + date => $row->date(), + branchcode => $newBranch, + isopened => $row->isopened(), + holidaytype => $row->holidaytype(), + openhour => $row->openhour(), + closehour => $row->closehour(), + }); + } + +} +#DiscreteCalendar data transfer object (DTO) +sub get_date_info { + my ($self, $date) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + #String dates for Database usage + my $date_string = $dtf->format_datetime($date); + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + select => [ 'date', { DATE => 'date' } ], + as => [qw/ date date /], + where => \['DATE(?) = date', $date_string ], + columns =>[ qw/ branchcode holidaytype openhour closehour note/] + }, + ); + my $dateDTO; + while (my $date = $rs->next()){ + $dateDTO = { + date => $date->date(), + branchcode => $date->branchcode(), + holidaytype => $date->holidaytype() , + openhour => $date->openhour(), + closehour => $date->closehour(), + note => $date->note() + }; + } + + return $dateDTO; +} + + +sub getMaxDate { + my $self = shift; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode + }, + { + select => [{ MAX => 'date' } ], + as => [qw/ max /], + } + ); + + return $rs->next()->get_column('max'); +} + +sub getMinDate { + my $self = shift; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode + }, + { + select => [{ MIN => 'date' } ], + as => [qw/ min /], + } + ); + + return $rs->next()->get_column('min'); +} + +sub get_single_holidays { + my $self = shift; + my $branchcode = $self->{branchcode}; + my @single_holidays; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + holidaytype => 'E' + }, + { + select => [{ DATE => 'date' }, 'note' ], + as => [qw/ date note/], + } + ); + while (my $date = $rs->next()){ + my $outputdate = dt_from_string($date->date(), 'iso'); + $outputdate = output_pref( { dt => $outputdate, dateonly => 1 } ); + push @single_holidays, { + date => $date->date(), + outputdate => $outputdate, + note => $date->note() + } + } + + return @single_holidays; +} + +sub get_float_holidays { + my $self = shift; + my $branchcode = $self->{branchcode}; + my @float_holidays; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + holidaytype => 'F' + }, + { + select => [{ DATE => 'date' }, 'note' ], + as => [qw/ date note/], + } + ); + while (my $date = $rs->next()){ + my $outputdate = dt_from_string($date->date(), 'iso'); + $outputdate = output_pref( { dt => $outputdate, dateonly => 1 } ); + push @float_holidays, { + date => $date->date(), + outputdate => $outputdate, + note => $date->note() + } + } + + return @float_holidays; +} + +sub get_need_valdation_holidays { + my $self = shift; + my $branchcode = $self->{branchcode}; + my @need_validation_holidays; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + holidaytype => 'N' + }, + { + select => [{ DATE => 'date' }, 'note' ], + as => [qw/ date note/], + } + ); + while (my $date = $rs->next()){ + my $outputdate = dt_from_string($date->date(), 'iso'); + $outputdate = output_pref( { dt => $outputdate, dateonly => 1 } ); + push @need_validation_holidays, { + date => $date->date(), + outputdate => $outputdate, + note => $date->note() + } + } + + return @need_validation_holidays; +} + +sub get_day_month_holidays { + my $self = shift; + my $branchcode = $self->{branchcode}; + my @repeatable_holidays; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + holidaytype => 'R', + + }, + { + select => \[ 'distinct DAY(date), MONTH(date), note'], + as => [qw/ day month note/], + } + ); + + while (my $date = $rs->next()){ + push @repeatable_holidays, { + day=> $date->get_column('day'), + month => $date->get_column('month'), + note => $date->note() + }; + } + + return @repeatable_holidays; +} + +sub get_week_days_holidays { + my $self = shift; + my $branchcode = $self->{branchcode}; + my @week_days; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + holidaytype => 'W', + branchcode => $branchcode, + }, + { + select => [{ DAYOFWEEK => 'date'}, 'note'], + as => [qw/ weekday note /], + distinct => 1, + } + ); + + while (my $date = $rs->next()){ + push @week_days, { + weekday => ($date->get_column('weekday') -1), + note => $date->note() + }; + } + + return @week_days; +} +=head1 edit_holiday + +Modifies a date or a range of dates + +C<$title> Is the title to be modified for the holiday formed by $year/$month/$day. + +C<$weekday> Is the day of week for the holiday + +C<$holidaytype> Is the type of the holiday : + E : Exception holiday, single day. + F : Floating holiday, different day each year. + N : Needs validation, copied float holiday from the past + R : Fixed holiday, repeated on same date. + W : Weekly holiday, same day of the week. + +C<$openHour> Is the opening hour. +C<$closeHour> Is the closing hour. +C<$startDate> Is the start of the range of dates. +C<$endDate> Is the end of the range of dates. +=back +=cut + +sub edit_holiday { + my ($self, $title, $weekday, $holidaytype, $openHour, $closeHour, $startDate, $endDate, $deleteType) = @_; + my $branchcode = $self->{branchcode}; + my $today = DateTime->today; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + #String dates for Database usage + my $startDate_String = $dtf->format_datetime($startDate); + my $endDate_String = $dtf->format_datetime($endDate); + $today = $dtf->format_datetime($today); + + my %updateValues = ( + isopened => 0, + note => $title, + holidaytype => $holidaytype, + ); + $updateValues{openhour} = $openHour if $openHour ne ''; + $updateValues{closehour} = $closeHour if $closeHour ne ''; + + if($holidaytype eq 'W') { + #Insert/Update weekly holidays + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \[ 'DAYOFWEEK(date) = ? and date >= ?', $weekday, $today], + } + ); + + while (my $date = $rs->next()){ + $date->update(\%updateValues); + } + }elsif ($holidaytype eq 'E' || $holidaytype eq 'F' || $holidaytype eq 'N') { + #Insert/Update Exception Float and Needs Validation holidays + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \['date between DATE(?) and DATE(?) and date >= ?',$startDate_String, $endDate_String, $today] + } + ); + while (my $date = $rs->next()){ + $date->update(\%updateValues); + } + + }elsif ($holidaytype eq 'R') { + #Insert/Update repeatable holidays + my $parser = DateTime::Format::Strptime->new( + pattern => '%m-%d', + on_error => 'croak', + ); + #Format the dates to have only month-day ex: 01-04 for January 4th + $startDate = $parser->format_datetime($startDate); + $endDate = $parser->format_datetime($endDate); + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \["(DATE_FORMAT(date,'\%m-\%d') BETWEEN ? AND ? ) AND date >= ?", $startDate, $endDate, $today], + } + ); + while (my $date = $rs->next()){ + $date->update(\%updateValues); + } + + }else { + #Delete/Update date(s) + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \['date between DATE(?) and DATE(?) and date >= ?',$startDate_String, $endDate_String, $today], + } + ); + #If none, the date(s) will be normal days, else, + if($holidaytype eq 'none'){ + $updateValues{holidaytype} =''; + $updateValues{isopened} =1; + }else{ + delete $updateValues{holidaytype}; + } + while (my $date = $rs->next()){ + if($deleteType){ + if($date->holidaytype() eq 'W' && $startDate_String eq $endDate_String){ + $self->remove_weekly_holidays($weekday, \%updateValues, $today); + }elsif($date->holidaytype() eq 'R'){ + $self->remove_fixed_holidays($startDate, $endDate, \%updateValues, $today); + } + }else{ + $date->update(\%updateValues); + } + } + } +} + +sub remove_weekly_holidays { + my ($self, $weekday, $updateValues, $today) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 0, + holidaytype => 'W' + }, + { + where => \["DAYOFWEEK(date) = ? and date >= ?", $weekday,$today], + } + ); + + while (my $date = $rs->next()){ + $date->update($updateValues); + } +} + +sub remove_fixed_holidays { + my ($self, $startDate, $endDate, $updateValues, $today) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $parser = DateTime::Format::Strptime->new( + pattern => '%m-%d', + on_error => 'croak', + ); + #Format the dates to have only month-day ex: 01-04 for January 4th + $startDate = $parser->format_datetime($startDate); + $endDate = $parser->format_datetime($endDate); + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 0, + holidaytype => 'R', + }, + { + where => \["(DATE_FORMAT(date,'\%m-\%d') BETWEEN ? AND ? ) AND date >= ?", $startDate, $endDate, $today], + } + ); + + while (my $date = $rs->next()){ + $date->update($updateValues); + } +} + +sub copyToBranch { + my ($self,$newBranch) =@_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + + my $copyFrom = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode + }, + { + columns => [ qw/ date isopened note holidaytype openhour closehour /] + } + ); + while (my $copyDate = $copyFrom->next()){ + my $copyTo = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $newBranch, + date => $copyDate->date(), + }, + { + columns => [ qw/ date branchcode isopened note holidaytype openhour closehour /] + } + ); + $copyTo->next()->update({ + isopened => $copyDate->isopened(), + holidaytype => $copyDate->holidaytype(), + note => $copyDate->note(), + openhour => $copyDate->openhour(), + closehour => $copyDate->closehour() + }); + } +} + +sub isOpened { + my($self, $date) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + $date= $dtf->format_datetime($date); + #if the date is not found + my $isOpened = -1; + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \['date = DATE(?)', $date] + } + ); + $isOpened = $rs->next()->isopened() if $rs->count() != 0; + + return $isOpened; +} + +sub is_holiday { + my($self, $date) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + $date= $dtf->format_datetime($date); + #if the date is not found + my $isHoliday = -1; + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \['date = DATE(?)', $date] + } + ); + + if($rs->count() != 0){ + $isHoliday = 0 if $rs->first()->isopened(); + $isHoliday = 1 if !$rs->first()->isopened(); + } + + return $isHoliday; +} + +sub copyHoliday { + my ($self, $from_startDate, $from_endDate, $to_startDate, $to_endDate, $daysnumber) = @_; + my $branchcode = $self->{branchcode}; + my $copyFromType = $from_startDate && $from_endDate eq '' ? 'oneDay': 'range'; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + + if ($copyFromType eq 'oneDay'){ + my $where; + $to_startDate = $dtf->format_datetime($to_startDate); + if ($to_startDate && $to_endDate) { + $to_endDate = $dtf->format_datetime($to_endDate); + $where = \["date between ? and ?", $to_startDate, $to_endDate]; + } else { + $where = \['date = ?', $to_startDate]; + } + + $from_startDate = $dtf->format_datetime($from_startDate); + my $fromDate = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + date => $from_startDate + } + ); + my $toDates = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => $where + } + ); + + my $copyDate = $fromDate->next(); + while (my $date = $toDates->next()){ + $date->update({ + isopened => $copyDate->isopened(), + holidaytype => $copyDate->holidaytype(), + note => $copyDate->note(), + openhour => $copyDate->openhour(), + closehour => $copyDate->closehour() + }) + } + + }else{ + my $endDate = dt_from_string($from_endDate); + $to_startDate = $dtf->format_datetime($to_startDate); + $to_endDate = $dtf->format_datetime($to_endDate); + if($daysnumber ==6){ + for (my $tempDate = $from_startDate->clone(); $tempDate <= $endDate;$tempDate->add(days => 1)){ + my $formatedDate = $dtf->format_datetime($tempDate); + my $fromDate = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + date => $formatedDate, + }, + { + select => [{ DAYOFWEEK => 'date' }], + as => [qw/ weekday /], + columns =>[ qw/ holidaytype note openhour closehour note/] + } + ); + my $copyDate = $fromDate->next(); + my $weekday = $copyDate->get_column('weekday'); + + my $toDate = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + + }, + { + where => \['date between ? and ? and DAYOFWEEK(date) = ?',$to_startDate, $to_endDate, $weekday] + } + ); + my $copyToDate = $toDate->next(); + $copyToDate->update({ + isopened => $copyDate->isopened(), + holidaytype => $copyDate->holidaytype(), + note => $copyDate->note(), + openhour => $copyDate->openhour(), + closehour => $copyDate->closehour() + }); + + } + }else{ + my $to_startDate = dt_from_string($to_startDate); + my $to_endDate = dt_from_string($to_endDate); + for (my $tempDate = $from_startDate->clone(); $tempDate <= $endDate;$tempDate->add(days => 1)){ + my $from_formatedDate = $dtf->format_datetime($tempDate); + my $fromDate = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + date => $from_formatedDate, + }, + { + order_by => { -asc => 'date' } + } + ); + my $to_formatedDate = $dtf->format_datetime($to_startDate); + my $toDate = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + date => $to_formatedDate + }, + { + order_by => { -asc => 'date' } + } + ); + my $copyDate = $fromDate->next(); + $toDate->next()->update({ + isopened => $copyDate->isopened(), + holidaytype => $copyDate->holidaytype(), + note => $copyDate->note(), + openhour => $copyDate->openhour(), + closehour => $copyDate->closehour() + }); + $to_startDate->add(days =>1); + } + } + + + } +} + +sub days_between { + my ($self, $start_date, $end_date, ) = @_; + my $branchcode = $self->{branchcode}; + + if ( $start_date->compare($end_date) > 0 ) { + # swap dates + my $int_dt = $end_date; + $end_date = $start_date; + $start_date = $int_dt; + } + + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + $start_date = $dtf->format_datetime($start_date); + $end_date = $dtf->format_datetime($end_date); + + my $days_between = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 1, + }, + { + where => \['date >= date(?) and date < date(?)',$start_date, $end_date] + } + ); + + return DateTime::Duration->new( days => $days_between->count()); +} + +sub next_open_day { + my ( $self, $date ) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + $date = $dtf->format_datetime($date); + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 1, + }, + { + where => \['date > date(?)', $date], + order_by => { -asc => 'date' }, + rows => 1 + } + ); + return dt_from_string( $rs->next()->date(), 'iso'); +} + +sub prev_open_day { + my ( $self, $date ) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + $date = $dtf->format_datetime($date); + + my $rs = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 1, + }, + { + where => \['date < date(?)', $date], + order_by => { -desc => 'date' }, + rows => 1 + } + ); + return dt_from_string( $rs->next()->date(), 'iso'); +} + +sub hours_between { + my ($self, $start_dt, $end_dt) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + my $start_date = $start_dt->clone(); + my $end_date = $end_dt->clone(); + my $duration = $end_date->delta_ms($start_date); + $start_date->truncate( to => 'day' ); + $end_date->truncate( to => 'day' ); + + # NB this is a kludge in that it assumes all days are 24 hours + # However for hourly loans the logic should be expanded to + # take into account open/close times then it would be a duration + # of library open hours + my $skipped_days = 0; + $start_date = $dtf->format_datetime($start_date); + $end_date = $dtf->format_datetime($end_date); + my $hours_between = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 0 + }, + { + where => \[ 'date between ? and ?', $start_date, $end_date], + } + ); + + if ($skipped_days = $hours_between->count()) { + $duration->subtract_duration(DateTime::Duration->new( hours => 24 * $skipped_days)); + } + + return $duration; +} + +sub open_hours_between { + my ($self, $start_date, $end_date) = @_; + my $branchcode = $self->{branchcode}; + my $schema = Koha::Database->new->schema; + my $dtf = $schema->storage->datetime_parser; + $start_date = $dtf->format_datetime($start_date); + $end_date = $dtf->format_datetime($end_date); + + my $working_hours_between = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 1, + }, + { + select => \['sum(time_to_sec(timediff(closehour, openhour)) / 3600)'], + as => [qw /hours_between/], + where => \['date BETWEEN DATE(?) AND DATE(?)', $start_date, $end_date] + } + ); + + my $loan_day = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \['date = DATE(?)', $start_date], + } + ); + + my $return_day = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + }, + { + where => \['date = DATE(?)', $end_date], + } + ); + + #Capture the time portion of the date + $start_date =~ /\s(.*)/; + my $loan_date_time = $1; + $end_date =~ /\s(.*)/; + my $return_date_time = $1; + + my $not_used_hours = $schema->resultset('DiscreteCalendar')->search( + { + branchcode => $branchcode, + isopened => 1, + }, + { + select => \[ '(time_to_sec(timediff(?, ?)) + time_to_sec(timediff(?, ?)) ) / 3600', $return_day->next()->closehour(), $return_date_time, $loan_date_time, $loan_day->next()->openhour()], + as => [qw /not_used_hours/], + } + ); + + return ($working_hours_between->next()->get_column('hours_between') - $not_used_hours->next()->get_column('not_used_hours')); +} +sub addDate { + my ( $self, $startdate, $add_duration, $unit ) = @_; + + # Default to days duration (legacy support I guess) + if ( ref $add_duration ne 'DateTime::Duration' ) { + $add_duration = DateTime::Duration->new( days => $add_duration ); + } + + $unit ||= 'days'; # default days ? + my $dt; + + if ( $unit eq 'hours' ) { + # Fixed for legacy support. Should be set as a branch parameter + my $return_by_hour = 10; + + $dt = $self->addHours($startdate, $add_duration, $return_by_hour); + } else { + # days + $dt = $self->addDays($startdate, $add_duration); + } + + return $dt; +} + +sub addHours { + my ( $self, $startdate, $hours_duration, $return_by_hour ) = @_; + my $base_date = $startdate->clone(); + + $base_date->add_duration($hours_duration); + + # If we are using the calendar behave for now as if Datedue + # was the chosen option (current intended behaviour) + + if ( $self->{days_mode} ne 'Days' && + $self->is_holiday($base_date) ) { + + if ( $hours_duration->is_negative() ) { + $base_date = $self->prev_open_day($base_date); + } else { + $base_date = $self->next_open_day($base_date); + } + + $base_date->set_hour($return_by_hour); + + } + + return $base_date; +} + +sub addDays { + my ( $self, $startdate, $days_duration ) = @_; + my $base_date = $startdate->clone(); + + $self->{days_mode} ||= q{}; + + if ( $self->{days_mode} eq 'Calendar' ) { + # use the calendar to skip all days the library is closed + # when adding + my $days = abs $days_duration->in_units('days'); + + if ( $days_duration->is_negative() ) { + while ($days) { + $base_date = $self->prev_open_day($base_date); + --$days; + } + } else { + while ($days) { + $base_date = $self->next_open_day($base_date); + --$days; + } + } + + } else { # Days or Datedue + # use straight days, then use calendar to push + # the date to the next open day if Datedue + $base_date->add_duration($days_duration); + + if ( $self->{days_mode} eq 'Datedue' ) { + # Datedue, then use the calendar to push + # the date to the next open day if holiday + if (!$self->isOpened($base_date) ) { + + if ( $days_duration->is_negative() ) { + $base_date = $self->prev_open_day($base_date); + } else { + $base_date = $self->next_open_day($base_date); + } + } + } + } + + return $base_date; +} + +1; diff --git a/installer/data/mysql/atomicupdate/bug_17015_koha_discrete_calendar.sql b/installer/data/mysql/atomicupdate/bug_17015_koha_discrete_calendar.sql new file mode 100644 index 0000000..28399e1 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_17015_koha_discrete_calendar.sql @@ -0,0 +1,14 @@ +-- Bugzilla 17015 +-- New koha calendar +-- Create discrete_calendar table to keep track of library day's information + +CREATE TABLE `discrete_calendar` ( + `date` datetime NOT NULL, + `branchcode` varchar(10) NOT NULL, + `isopened` tinyint(1) DEFAULT 1, + `holidaytype` varchar(1) DEFAULT '', + `note` varchar(30) DEFAULT '', + `openhour` time NOT NULL, + `closehour` time NOT NULL, + PRIMARY KEY (`branchcode`,`date`) +); diff --git a/installer/data/mysql/atomicupdate/generate_discrete_calendar.perl b/installer/data/mysql/atomicupdate/generate_discrete_calendar.perl new file mode 100755 index 0000000..9af613b --- /dev/null +++ b/installer/data/mysql/atomicupdate/generate_discrete_calendar.perl @@ -0,0 +1,165 @@ +#!/usr/bin/perl + +# +# Script that fills the discrete_calendar table with dates, using the other date-related tables +# +use strict; +use warnings; +use DateTime; +use DateTime::Format::Strptime; +use Getopt::Long; +use C4::Context; + +# Options +my $help = 0; +my $daysInFuture = 365; +GetOptions ( + 'days|?|d=i' => \$daysInFuture, + 'help|?|h' => \$help); +my $usage = << 'ENDUSAGE'; + +Script that manages the discrete_calendar table. + +This script has the following parameters : + --days --d : how many days in the future will be created, by default it's 365 + -h --help: this message + --generate: fills discrete_calendar table with dates from the last two years and the next one + +ENDUSAGE + +if ($help) { + print $usage; + exit; +} +my $dbh = C4::Context->dbh; + +my $currentDate = DateTime->today; + +# two years ago +my $startDate = DateTime->new( +day => $currentDate->day(), +month => $currentDate->month(), +year => $currentDate->year()-2, +time_zone => C4::Context->tz() +)->truncate( to => 'day' ); + +# a year into the future +my $endDate = DateTime->new( +day => $currentDate->day(), +month => $currentDate->month(), +year => $currentDate->year(), +time_zone => C4::Context->tz() +)->truncate( to => 'day' ); +$endDate->add(days => $daysInFuture); + +#Added a default (standard) branch. +my $add_default_branch = 'INSERT INTO branches (branchname, branchcode) VALUES(?,?)'; +my $add_Branch_Sth = $dbh->prepare($add_default_branch); +$add_Branch_Sth->execute('Default', 'DFLT'); +# finds branches; +my $selectBranchesSt = 'SELECT branchcode FROM branches'; +my $selectBranchesSth = $dbh->prepare($selectBranchesSt); +$selectBranchesSth->execute(); +my @branches = (); +while ( my $branchCode = $selectBranchesSth->fetchrow_array ) { + + push @branches,$branchCode; +} + +# finds what days are closed for each branch +my %repeatableHolidaysPerBranch = (); +my %specialHolidaysPerBranch = (); +my $selectWeeklySt; +my $selectWeeklySth; + + +foreach my $branch (@branches){ + + $selectWeeklySt = 'SELECT weekday, title, day, month FROM repeatable_holidays WHERE branchcode = ?'; + $selectWeeklySth = $dbh->prepare($selectWeeklySt); + $selectWeeklySth->execute($branch); + + my @weeklyHolidays = (); + + while ( my ($weekDay, $title, $day, $month) = $selectWeeklySth->fetchrow_array ) { + push @weeklyHolidays,{weekday => $weekDay, title => $title, day => $day, month => $month}; + + } + + $repeatableHolidaysPerBranch{$branch} = \@weeklyHolidays; + + my $selectSpecialHolidayDateSt = 'SELECT day,month,year,title FROM special_holidays WHERE branchcode = ? AND isexception = 0'; + my $specialHolidayDatesSth = $dbh->prepare($selectSpecialHolidayDateSt); + $specialHolidayDatesSth -> execute($branch); + # Tranforms dates from specialHolidays table in DateTime for our new table + my @specialHolidayDates = (); + while ( my ($day, $month, $year, $title) = $specialHolidayDatesSth->fetchrow_array ) { + + my $specialHolidayDate = DateTime->new( + day => $day, + month => $month, + year => $year, + time_zone => C4::Context->tz() + )->truncate( to => 'day' ); + push @specialHolidayDates,{date=>$specialHolidayDate, title=> $title}; + } + + $specialHolidaysPerBranch{$branch} = \@specialHolidayDates; +} +# Fills table with dates and sets 'isopened' according to the branch's weekly restrictions (repeatable_holidays) +my $insertDateSt; +my $insertDateSth; + +# Loop that does everything in the world +for (my $tempDate = $startDate->clone(); $tempDate <= $endDate;$tempDate->add(days => 1)){ + foreach my $branch (@branches){ + my $dayOfWeek = $tempDate->day_of_week; + # Representation fix + # DateTime object dow (1-7) where Monday is 1 + # Arrays are 0-based where 0 = Sunday, not 7. + $dayOfWeek -=1 unless $dayOfWeek ==7; + $dayOfWeek =0 if $dayOfWeek ==7; + + my $openhour = "09:00:00"; + my $closehour = "17:00:00"; + + # Finds closed days + my $isOpened =1; + my $specialDescription = ""; + my $holidaytype =''; + $dayOfWeek = $tempDate->day_of_week; + foreach my $holidayWeekDay (@{$repeatableHolidaysPerBranch{$branch}}){ + if($holidayWeekDay->{weekday} && $dayOfWeek == $holidayWeekDay->{weekday}){ + $isOpened = 0; + $specialDescription = $holidayWeekDay->{title}; + $holidaytype = 'W'; + }elsif($holidayWeekDay->{day} && $holidayWeekDay->{month}){ + my $date = DateTime->new( + day => $holidayWeekDay->{day}, + month => $holidayWeekDay->{month}, + year => $tempDate->year(), + time_zone => C4::Context->tz() + )->truncate( to => 'day' ); + + if ($tempDate == $date) { + $isOpened = 0; + $specialDescription = $holidayWeekDay->{title}; + $holidaytype = 'R'; + } + } + } + + foreach my $specialDate (@{$specialHolidaysPerBranch{$branch}}){ + if($tempDate->datetime() eq $specialDate->{date}->datetime() ){ + $isOpened = 0; + $specialDescription = $specialDate->{title}; + $holidaytype = 'E'; + } + } + #final insert statement + + $insertDateSt = 'INSERT INTO discrete_calendar (date,branchcode,isopened,holidaytype,note,openhour,closehour) VALUES (?,?,?,?,?,?,?)'; + $insertDateSth = $dbh->prepare($insertDateSt); + $insertDateSth->execute($tempDate,$branch,$isOpened,$holidaytype,$specialDescription,$openhour,$closehour); + } +} diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/discrete_calendar.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/discrete_calendar.tt new file mode 100644 index 0000000..a3e309e --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/discrete_calendar.tt @@ -0,0 +1,617 @@ +[% USE Branches %] +[% INCLUDE 'doc-head-open.inc' %] +<title>Koha › Tools › [% Branches.GetName( branch ) %] calendar</title> +[% INCLUDE 'doc-head-close.inc' %] +[% INCLUDE 'calendar.inc' %] +<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/datatables.css" /> +<script type="text/javascript" src="[% interface %]/lib/jquery/plugins/jquery-ui-timepicker-addon.min.js"></script> +[% INCLUDE 'datatables.inc' %] +<script type="text/javascript"> +//<![CDATA[ + var weekdays = new Array(_("Sundays"),_("Mondays"),_("Tuesdays"),_("Wednesdays"),_("Thursdays"),_("Fridays"),_("Saturdays")); + /* Creates all the structures to deal with all different kinds of holidays */ + var datesInfos = new Array(); + [% FOREACH date IN datesInfos %] + datesInfos["[% date.date %]"] = {title : "[% date.note %]", outputdate : "[% date.outputdate %]", holidaytype:"[% date.holidaytype %]", openhour: "[% date.openhour %]", closehour: "[% date.closehour %]"}; + [% END %] + + function holidayOperation(formObject, opType) { + var op = document.getElementsByName('Operation'); + op[0].value = opType; + formObject.submit(); + } + + // This function shows the "Show Holiday" panel + function showHoliday (dayName, day, month, year, weekDay, title, holidayType) { + $("#newHoliday").slideDown("fast"); + $("#copyHoliday").slideUp("fast"); + $('#newDaynameOutput').html(dayName); + $('#newDayname').val(dayName); + $('#newBranchNameOutput').html($("#branch :selected").text()); + $(".newHoliday ,#BranchName").val($('#branch').val()); + $('#newDayOutput').html(day); + $(".newHoliday #Day").val(day); + $(".newHoliday #Month").val(month); + $(".newHoliday #Year").val(year); + $("#newMonthOutput").html(month); + $("#newYearOutput").html(year); + $(".newHoliday, #Weekday").val(weekDay); + + $('.newHoliday #title').val(title); + $('#HolidayType').val(holidayType); + + //You can't delete holiday based on type for single, floating, need validation holidays. This will show/hide the option based on type + if(holidayType !='weekday' && holidayType != 'daymonth'){ + $('#deleteTypeOption').hide(); + }else{ + $('#deleteTypeOption').show(); + } + + if(holidayType == 'weekday') { + // $("#showOperationDelLabel").html(_("Delete this holiday.")); + $("#holtype").attr("class","key repeatableweekly").html(_("Holiday repeating weekly")); + } else if(holidayType == 'daymonth') { + // $("#showOperationDelLabel").html(_("Delete this holiday.")); + $("#holtype").attr("class","key repeatableyearly").html(_("Holiday repeating yearly")); + } else if(holidayType == 'float') { + // $("#showOperationDelLabel").html(_("Delete this holiday.")); + $("#holtype").attr("class","key float").html(_("Floating holiday")); + } else if(holidayType == 'N') { + // $("#showOperationDelLabel").html(_("Delete this holiday.")); + $("#holtype").attr("class","key exception").html(_("Needs validation")); + } else if(holidayType == 'ymd') { + // $("#showOperationDelLabel").html(_("Delete this holiday.")); + $("#holtype").attr("class","key holiday").html(_("Unique holiday")); + } else{ + $("#holtype").attr("class","key normalday").html(_("Working day ")); + } + } + + function hidePanel(aPanelName) { + $("#"+aPanelName).slideUp("fast"); + } + + function changeBranch () { + var branch = $("#branch option:selected").val(); + location.href='/cgi-bin/koha/tools/discrete_calendar.pl?branch=' + branch + '&calendardate=' + "[% calendardate %]"; + } + + function Help() { + newin=window.open("/cgi-bin/koha/help.pl","KohaHelp",'width=600,height=600,toolbar=false,scrollbars=yes'); + } + + // This function gives css clases to each kind of day + function dateStatusHandler(date) { + date = getSeparetedDate(date); + var day = date.day; + var month = date.month; + var year = date.year; + var weekDay = date.weekDay; + var dayName = weekdays[weekDay]; + var dayMonth = date.dayMonth; + var dateString = date.dateString; + + if (datesInfos[dateString] && datesInfos[dateString].holidaytype =='W'){ + return [true, "repeatableweekly", _("Weekly holiday: %s").format(datesInfos[dateString].title)]; + } else if (datesInfos[dateString] && datesInfos[dateString].holidaytype == 'R') { + return [true, "repeatableyearly", _("Yearly holiday: %s").format(datesInfos[dateString].title)]; + } else if (datesInfos[dateString] && datesInfos[dateString].holidaytype == 'N') { + return [true, "exception", _("Need validation: %s").format(datesInfos[dateString].title)]; + } else if (datesInfos[dateString] && datesInfos[dateString].holidaytype == 'F') { + return [true, "float", _("Floating holiday: %s").format(datesInfos[dateString].title)]; + } else if (datesInfos[dateString] && datesInfos[dateString].holidaytype == 'E') { + return [true, "holiday", _("Single holiday: %s").format(datesInfos[dateString].title)]; + } else { + return [true, "normalday", _("Normal day")]; + } + } + + /* This function is in charge of showing the correct panel considering the kind of holiday */ + function dateChanged(text, calendar) { + calendar = getSeparetedDate(calendar); + var day = calendar.day; + var month = calendar.month; + var year = calendar.year; + var weekDay = calendar.weekDay; + var dayName = weekdays[weekDay]; + var dayMonth = calendar.dayMonth; + var dateString = calendar.dateString; + //set value of form hidden field + $('#from_copyFrom').val(text); + if (datesInfos[dateString].holidaytype =='E') { + showHoliday(dayName, day, month, year, weekDay, datesInfos[dateString].title,'ymd'); + showDateInfo(dateString); + } else if (datesInfos[dateString].holidaytype =='W') { + showHoliday(dayName, day, month, year, weekDay, datesInfos[dateString].title, 'weekday'); + showDateInfo(dateString); + } else if (datesInfos[dateString].holidaytype =='R') { + showHoliday(dayName, day, month, year, weekDay, datesInfos[dateString].title, 'daymonth'); + showDateInfo(dateString); + } else if (datesInfos[dateString].holidaytype =='F') { + showHoliday(dayName, day, month, year, weekDay, datesInfos[dateString].title, 'float'); + showDateInfo(dateString); + } else if (datesInfos[dateString].holidaytype =='N') { + showHoliday(dayName, day, month, year, weekDay, datesInfos[dateString].title, 'N'); + showDateInfo(dateString); + }else { + showHoliday(dayName, day, month, year, weekDay, '', 'normalday'); + showDateInfo(dateString); + } + } + + function showDateInfo (date){ + $("#dateInfo").css('visibility', 'visible'); + $('#openHour').val(datesInfos[date].openhour); + $('#closeHour').val(datesInfos[date].closehour); + $('#openHour').timepicker({ + showOn : 'focus', + timeFormat: 'HH:mm:ss', + defaultTime: datesInfos[date].openhour, + showSecond: false, + stepMinute: 5, + }); + $('#closeHour').timepicker({ + showOn : 'focus', + timeFormat: 'HH:mm:ss', + defaultTime: datesInfos[date].closehour, + showSecond: false, + stepMinute: 5, + }); + $('#closeHour').val(datesInfos[date].closehour); + $('#date').html(datesInfos[date].outputdate); + + if (datesInfos[date].holidaytype !=''){ + var type = datesInfos[date].holidaytype; + $('#holidayType option[value="'+ type +'"]').attr('selected', true) + }else{ + $('#holidayType option[value="none"]').attr('selected', true) + } + $(".dateDetailsPanel ,#BranchName").val($('#branch').val()); + if(datesInfos[date].holidaytype == 'W' || datesInfos[date].holidaytype == 'R'){ + $('#deleteType').show("fast"); + }else{ + $('#deleteType').hide("fast"); + } + + } + function getSeparetedDate(date){ + var mydate = new Array(); + var day = (date.getDate() < 10 ? '0' : '') + date.getDate(); + var month = ((date.getMonth()+1) < 10 ? '0' : '') + (date.getMonth() +1); + var year = date.getFullYear(); + var weekDay = date.getDay(); + var dayMonth = (date.getMonth()+1) + '/' + date.getDate(); + var dateString = year + '-' + month + '-' + day; + mydate = {dateObj : date, date : date ,dateString : dateString, dayMonth: dayMonth, weekDay: weekDay, year: year, month: month, day: day}; + + return mydate; + } + + function validateForm(form){ + + if(form =='newHoliday' && $('#from_copyToDatePicker').val() && $('#CopyRadioButton').is(':checked')){ + var from_DateFrom = new Date($("#jcalendar-container").datepicker("getDate")); + var from_DateTo = new Date($('#from_copyToDatePicker').datepicker("getDate")); + var to_DateFrom = new Date($('#to_copyFromDatePicker').datepicker("getDate")); + var to_DateTo = new Date($('#to_copyToDatePicker').datepicker("getDate")); + + var from_start = Math.round( from_DateFrom.getTime() / (3600*24*1000)); //days as integer from.. + var from_end = Math.round( from_DateTo.getTime() / (3600*24*1000)); + var to_start = Math.round( to_DateFrom.getTime() / (3600*24*1000)); + var to_end = Math.round( to_DateTo.getTime() / (3600*24*1000)); + + var from_daysDiff = from_end - from_start; + var to_daysDiff = to_end - to_start; + + if(from_daysDiff == to_daysDiff || from_daysDiff ==1){ + $('#daysnumber').val(to_daysDiff) + return true; + }else{ + alert("You have to pick the same number of days if you choose 2 ranges"); + return false; + } + }else { + return 1; + } + } + + //Check if date range have the same opening, closing hours and holiday type if there's one. + function checkRange(date){ + date = new Date(date); + $('#toDate').val(getSeparetedDate(date).dateString); + var fromDate = new Date($("#jcalendar-container").datepicker("getDate")); + var sameHoliday =true; + var sameOpenHours =true; + var sameCloseHours =true; + + for (var i = fromDate; i <= date ; i.setDate(i.getDate() + 1)) { + var myDate1 = getSeparetedDate(i); + var date1 = myDate1.dateString; + var holidayType1 = datesInfos[date1].holidaytype; + var openhours1 = datesInfos[date1].openhour; + var closehours1 = datesInfos[date1].closehour; + for (var j = fromDate; j <= date ; j.setDate(j.getDate() + 1)) { + var myDate2 = getSeparetedDate(j); + var date2 = myDate2.dateString; + var holidayType2 = datesInfos[date2].holidaytype; + var openhours2 = datesInfos[date2].openhour; + var closehours2 = datesInfos[date2].closehour; + + if (sameHoliday && holidayType1 != holidayType2){ + $('#holidayType option[value="empty"]').attr('selected', true) + sameHoliday=false; + } + if(sameOpenHours && (openhours1 != openhours2)){ + $('#openHour').val(''); + sameOpenHours=false; + } + if(sameCloseHours && (closehours1 != closehours2)){ + $('#closeHour').val(''); + sameCloseHours=false; + } + } + if (!sameOpenHours && !sameCloseHours && !sameHoliday){ + return false; + } + } + return true; +} + $(document).ready(function() { + $(".hint").hide(); + $("#branch").change(function(){ + changeBranch(); + }); + $("#holidayweeklyrepeatable>tbody>tr").each(function(){ + var first_td = $(this).find('td').first(); + first_td.html(weekdays[first_td.html()]); + }); + $("a.helptext").click(function(){ + $(this).parent().find(".hint").toggle(); return false; + }); + $("#from_copyFrom").each(function () { this.value = "" }); + $("#copyToDp").each(function () { this.value = "" }); + $("#to_copyFrom").each(function () { this.value = "" }); + $("#to_copyTo").each(function () { this.value = "" }); + $("#jcalendar-container").datepicker({ + beforeShowDay: function(thedate) { + return dateStatusHandler(thedate); + }, + onSelect: function(dateText, inst) { + dateChanged(dateText, $(this).datepicker("getDate")); + }, + defaultDate: new Date("[% keydate %]"), + minDate: new Date("[% minDate %]"), + maxDate: new Date("[% maxDate %]") + }); + + $("#from_copyToDatePicker").datepicker(); + $("#from_copyToDatePicker").change(function(){ + checkRange($(this).datepicker("getDate")); + $('#from_copyTo').val(($(this).val())); + }); + $('#to_copyFromDatePicker').datepicker(); + $("#to_copyFromDatePicker").change(function(){ + $('#to_copyFrom').val(($(this).val())); + }); + $('#to_copyToDatePicker').datepicker(); + $("#to_copyToDatePicker").change(function(){ + $('#to_copyTo').val(($(this).val())); + }); + $('.newHoliday input[type="radio"]').click(function () { + if ($(this).attr("id") == "CopyRadioButton") { + $(".CopyToBranchPanel").hide('fast'); + $(".CopyDatePanel").show('fast'); + } else if ($(this).attr("id") == "CopyToBranchRadioButton"){ + $(".CopyDatePanel").hide('fast'); + $(".CopyToBranchPanel").show('fast'); + } else{ + $(".CopyDatePanel").hide('fast'); + $(".CopyToBranchPanel").hide('fast'); + } + }); + $(".hidePanel").on("click",function(){ + if( $(this).hasClass("showHoliday") ){ + hidePanel("showHoliday"); + }if ($(this).hasClass('newHoliday')) { + hidePanel("newHoliday"); + }else { + hidePanel("copyHoliday"); + } + }) + }); +//]]> +</script> +<!-- Datepicker colors --> +<style type="text/css"> .key { padding : 3px; white-space:nowrap; line-height:230%; } +.ui-datepicker { font-size : 150%; } +#jcalendar-container .ui-datepicker { font-size : 185%; } +.ui-datepicker th, .ui-datepicker .ui-datepicker-title select { font-size : 80%; } +.ui-datepicker td a { padding : .5em; } +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { font-size : 80%; } +.key { padding : 3px; white-space:nowrap; line-height:230%; } +.normalday { background-color : #EDEDED; color : Black; border : 1px solid #BCBCBC; } +.ui-datepicker-unselectable { padding :.5em; white-space:nowrap;} +.ui-state-disabled { padding :.5em; white-space:nowrap;} +.exception { background-color : #b3d4ff; color : Black; border : 1px solid #BCBCBC; } +.float { background-color : #66ff33; color : Black; border : 1px solid #BCBCBC; } +.holiday { background-color : #ffaeae; color : Black; border : 1px solid #BCBCBC; } +.repeatableweekly { background-color : #FFFF99; color : Black; border : 1px solid #BCBCBC; } +.repeatableyearly { background-color : #FFCC66; color : Black; border : 1px solid #BCBCBC; } +td.exception a.ui-state-default { background: #b3d4ff none; color : Black; border : 1px solid #BCBCBC; } +td.float a.ui-state-default { background: #66ff33 none; color : Black; border : 1px solid #BCBCBC; } +td.holiday a.ui-state-default { background: #ffaeae none; color : Black; border : 1px solid #BCBCBC; } +td.repeatableweekly a.ui-state-default { background: #FFFF99 none; color : Black; border : 1px solid #BCBCBC; } +td.repeatableyearly a.ui-state-default { background: #FFCC66 none; color : Black; border : 1px solid #BCBCBC; } +.information { z-index : ; background-color : #DCD2F1; width : 300px; display : none; border : 1px solid #000000; color : #000000; font-size : 8pt; font-weight : bold; background-color : #FFD700; cursor : pointer; padding : 2px; } +.panel { z-index : 1; display : none; border : 3px solid #CCC; padding : 3px; margin-top: .3em; background-color: #FEFEFE; } fieldset.brief { border : 0; margin-top: 0; } +#showHoliday { margin : .5em 0; } h1 select { width: 20em; } div.yui-b fieldset.brief ol { font-size:100%; } div.yui-b fieldset.brief li, div.yui-b fieldset.brief li.radio { padding:0.2em 0; } .help { margin:.3em 0;border:1px solid #EEE;padding:.3em .7em; font-size : 90%; } #holidayweeklyrepeatable, #holidaysyearlyrepeatable, #holidaysunique, #holidayexceptions { font-size : 90%; margin-bottom : 1em;} .calendar td, .calendar th, .calendar .button, .calendar tbody .day { padding : .7em; font-size: 110%; } .calendar { width: auto; border : 0; } +.copyHoliday form li{ display:table-row } .copyHoliday form li b, .copyHoliday form li input{ display:table-cell; margin-bottom: 2px; } +</style> +</head> +<body id="tools_holidays" class="tools"> +[% INCLUDE 'header.inc' %] +[% INCLUDE 'cat-search.inc' %] + +<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> › [% Branches.GetName( branch ) %] calendar</div> + +<div id="doc3" class="yui-t1"> + + <div id="bd"> + <div id="yui-main"> + <div class="yui-b"> + <h2>[% Branches.GetName( branch ) %] calendar</h2> + <div class="yui-g"> + <div class="yui-u first" style="width:60%"> + <label for="branch">Define the holidays for:</label> + <select id="branch" name="branch"> + [% PROCESS options_for_libraries libraries => Branches.all( selected => branch ) %] + </select> + + <h3>Calendar information</h3> + <div id="jcalendar-container" style="float: left"></div> + <!-- ***************************** Panel to deal with new holidays ********************** --> + [% UNLESS datesInfos %] + <div class="alert alert-danger" style="float: left; margin-left:15px"> + <strong>Error!</strong> You have to run generate_discrete_calendar.pl in order to use Discrete Calendar. + + </div> + [% END %] + <div class="panel newHoliday" id="newHoliday" style="float: left; margin-left:15px"> + <form method="post" onsubmit="return validateForm('newHoliday')"> + <fieldset class="brief"> + <h3>Add new holiday</h3> + <ol> + <li> + <strong>Library:</strong> + <span id="newBranchNameOutput"></span> + <input type="hidden" id="BranchName" name="BranchName" /> + </li> + <li> + <strong>From date:</strong> + <span id="newDaynameOutput"></span>, + + [% IF ( dateformat == "us" ) %]<span id="newMonthOutput"></span>/<span id="newDayOutput"></span>/<span id="newYearOutput"></span>[% ELSIF ( dateformat == "metric" ) %]<span id="newDayOutput"></span>/<span id="newMonthOutput"></span>/<span id="newYearOutput"></span>[% ELSIF ( dateformat == "dmydot" ) %]<span id="newDayOutput"></span>.<span id="newMonthOutput"></span>.<span id="newYearOutput"></span>[% ELSE %]<span id="newYearOutput"></span>/<span id="newMonthOutput"></span>/<span id="newDayOutput"></span>[% END %] + + <input type="hidden" id="newDayname" name="showDayname" /> + <input type="hidden" id="Weekday" name="Weekday" /> + <input type="hidden" id="Day" name="Day" /> + <input type="hidden" id="Month" name="Month" /> + <input type="hidden" id="Year" name="Year" /> + </li> + <li class="dateinsert"> + <b>To date: </b> + <input type="text" id="from_copyToDatePicker" name="toDate" size="20" class="datepicker" /> + </li> + <li> + <label for="title">Title: </label><input type="text" name="Title" id="title" size="35" /> + </li> + <li id="holidayType"> + <label for="holidayType">Date type</label> + <select name ='holidayType'> + <option value="empty"></option> + <option value="none">Normal day</option> + <option value="R">Fixed</option> + <option value="E">Exception</option> + <option value="W">Weekly</option> + <option value="F">Floating</option> + <option value="N" disabled>Need validation</option> + </select> + </li> + <li class="radio" id="deleteType" style="display : none;" > + <input type="checkbox" name="deleteType" id="deleteType_checkbox" value="1" ><label for="deleteType_checkbox"> Delete this type</label> + <a href="#" class="helptext">[?]</a> + <div class="hint">Remove all repeated or weekly holidays of the selected date or week day <br> if normal day is selected.</div> + </li> + <li> + <label for="openHour">Open hours: </label><input type="text" name="openHour" id='openHour' style="display :flex" > + </li> + <li> + <label for="closeHour">Close hours: </label><input type="text" name="closeHour" id='closeHour' style="display :flex" > + </li> + <li class="radio"> + <input type="radio" name="action" id="EditRadioButton" value="edit" checked/> + <label for="EditRadioButton">Edit selected dates</label> + </li> + <li class="radio"> + <input type="radio" name="action" id="CopyRadioButton" value="copyDates" /> + <label for="CopyRadioButton">Copy to different dates</label> + <div class="CopyDatePanel" style="display:none; padding-left:15px"> + + <b>From : </b> + <input type="text" id="to_copyFromDatePicker" size="20" class="datepicker"/> + <b>To : </b> + <input type="text" id="to_copyToDatePicker" size="20" class="datepicker"/> + </div> + <input type="hidden" name="daysnumber" id='daysnumber'> + <!-- These yyyy-mm-dd --> + <input type="hidden" name="from_copyFrom" id='from_copyFrom'> + <input type="hidden" name="from_copyTo" id='from_copyTo'> + <input type="hidden" name="to_copyFrom" id='to_copyFrom'> + <input type="hidden" name="to_copyTo" id='to_copyTo'> + </li> + <li class="radio"> + <input type="radio" name="action" id="CopyToBranchRadioButton" value="copyBranch" /> + <label for="CopyToBranchRadioButton">Copy calendar to an other branch</label> + <div class="CopyToBranchPanel" style="display:none; padding-left:15px"> + <label>Copy to : </label> + <select name ='newBranch'> + [% PROCESS options_for_libraries libraries => Branches.all( selected => branch ) %] + </select> + </div> + </li> + </ol> + <fieldset class="action"> + <input type="submit" name="submit" value="Save" /> + <a href="#" class="cancel hidePanel newHoliday">Cancel</a> + </fieldset> + </fieldset> + </form> + </div> + +<!-- ************************************************************************************** --> +<!-- ****** MAIN SCREEN CODE ****** --> +<!-- ************************************************************************************** --> + +</div> +<div class="yui-u" style="width : 40%"> + <div class="help"> + <h4>Hints</h4> + <ul> + <li>Search in the calendar the day you want to set as holiday.</li> + <li>Click the date to add or edit a holiday.</li> + <li>Specify how the holiday should repeat.</li> + <li>Click Save to finish.</li> + <li>PS: + <ul> + <li>You can't edit passed dates</li> + <li>Weekly holidays change open/close hours for all the days affected unless inputs are empty</li> + </ul> + </li> + </ul> + <h4>Key</h4> + <p> + <span class="key normalday">Working day </span> + <span class="key holiday">Unique holiday</span> + <span class="key repeatableweekly">Holiday repeating weekly</span> + <span class="key repeatableyearly">Holiday repeating yearly</span> + <span class="key float">Floating holiday</span> + <span class="key exception">Need validation</span> + </p> + </div> +<div id="holiday-list"> + + [% IF ( NEED_VALIDATION_HOLIDAYS ) %] + <h3>Need validation holidays</h3> + <table id="holidaysunique"> + <thead> + <tr> + <th class="exception">Date</th> + <th class="exception">Title</th> + </tr> + </thead> + <tbody> + [% FOREACH need_validation_holiday IN NEED_VALIDATION_HOLIDAYS %] + <tr> + <td><a href="/cgi-bin/koha/tools/discrete_calendarte_holiday.pl?branch=[% branch %]&calendardate=[% need_validation_holiday.date %]"><span title="[% need_validation_holiday.DATE_SORT %]">[% need_validation_holiday.outputdate %]</span></a></td> + <td>[% need_validation_holiday.note %]</td> + </tr> + [% END %] + </tbody> + </table> + [% END %] + + [% IF ( week_days ) %] + <h3>Weekly - Repeatable holidays</h3> + <table id="holidayweeklyrepeatable"> + <thead> + <tr> + <th class="repeatableweekly">Day of week</th> + <th class="repeatableweekly">Title</th> + </tr> + </thead> + <tbody> + [% FOREACH WEEK_DAYS_LOO IN week_days %] + <tr> + <td>[% WEEK_DAYS_LOO.weekday %]</td> + </td> + <td>[% WEEK_DAYS_LOO.note %]</td> + </tr> + [% END %] + </tbody> +</table> +[% END %] + +[% IF ( repeatable_holidays ) %] +<h3>Yearly - Repeatable holidays</h3> +<table id="holidaysyearlyrepeatable"> + <thead> + <tr> + [% IF ( dateformat == "metric" ) %] + <th class="repeatableyearly">Day/month</th> + [% ELSE %] + <th class="repeatableyearly">Month/day</th> + [% END %] + <th class="repeatableyearly">Title</th> + </tr> + </thead> + <tbody> + [% FOREACH DAY_MONTH_HOLIDAYS_LOO IN repeatable_holidays %] + <tr> + <td><span title="[% DAY_MONTH_HOLIDAYS_LOO.DATE_SORT %]">[%DAY_MONTH_HOLIDAYS_LOO.month%]/[% DAY_MONTH_HOLIDAYS_LOO.day %]</span></td> + <td>[% DAY_MONTH_HOLIDAYS_LOO.note %]</td> + </tr> + [% END %] + </tbody> +</table> +[% END %] + +[% IF ( HOLIDAYS_LOOP ) %] +<h3>Unique holidays</h3> +<table id="holidaysunique"> + <thead> + <tr> + <th class="holiday">Date</th> + <th class="holiday">Title</th> + </tr> + </thead> + <tbody> + [% FOREACH HOLIDAYS_LOO IN HOLIDAYS_LOOP %] + <tr> + <td><a href="/cgi-bin/koha/tools/discrete_calendarte_holiday.pl?branch=[% branch %]&calendardate=[% HOLIDAYS_LOO.date %]"><span title="[% HOLIDAYS_LOO.DATE_SORT %]">[% HOLIDAYS_LOO.outputdate %]</span></a></td> + <td>[% HOLIDAYS_LOO.note %]</td> + </tr> + [% END %] + </tbody> +</table> +[% END %] + +[% IF ( FLOAT_HOLIDAYS ) %] +<h3>Floating upcoming holidays</h3> +<table id="holidaysunique"> + <thead> + <tr> + <th class="float">Date</th> + <th class="float">Title</th> + </tr> + </thead> + <tbody> + [% FOREACH float_holiday IN FLOAT_HOLIDAYS %] + <tr> + <td><a href="/cgi-bin/koha/tools/discrete_calendarte_holiday.pl?branch=[% branch %]&calendardate=[% float_holiday.date %]"><span title="[% float_holiday.DATE_SORT %]">[% float_holiday.outputdate %]</span></a></td> + <td>[% float_holiday.note %]</td> + </tr> + [% END %] + </tbody> +</table> +[% END %] +</div> +</div> +</div> +</div> +</div> + +<div class="yui-b noprint"> +[% INCLUDE 'tools-menu.inc' %] +</div> +</div> +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/misc/cronjobs/add_days_discrete_calendar.pl b/misc/cronjobs/add_days_discrete_calendar.pl new file mode 100755 index 0000000..7c1ee8c --- /dev/null +++ b/misc/cronjobs/add_days_discrete_calendar.pl @@ -0,0 +1,113 @@ +#!/usr/bin/perl + +# +# This script adds one day into discrete_calendar table based on the same day from the week before +# +use strict; +use warnings; +use DateTime; +use DateTime::Format::Strptime; +use Getopt::Long; +use C4::Context; + +# Options +my $help = 0; +my $daysInFuture =1; +GetOptions ( + 'help|?|h' => \$help, + 'n=i' => \$daysInFuture); + +my $usage = << 'ENDUSAGE'; + +This script adds days into discrete_calendar table based on the same day from the week before. + +Examples : + The latest date on discrete_calendar is : 28-07-2017 + The current date : 01-08-2016 + The dates that will be added are : 29-07-2017, 30-07-2017, 31-07-2017, 01-08-2017 +Open close exemples : + Date added is : 29-07-2017 + Opening/closing hours will be base on : 22-07-2017 (- 7 days) + Library open or closed will be based on : 29-07-2017 (- 1 year) +This script has the following parameters: + -h --help: this message + -n : number of days to add in the futre, default : 1 + +PS: This is for testing purposes, the method of knowing whether it's opened or not may be changed. + +ENDUSAGE +my $dbh = C4::Context->dbh; +my $query = "SELECT distinct weekday(date), note FROM discrete_calendar where holidaytype='W'"; +my $stmt = $dbh->prepare($query); +$stmt->execute(); +my @week_days_discrete; +while (my ($weekday,$note) = $stmt->fetchrow_array){ + push @week_days_discrete, {weekday => $weekday, note => $note}; +} + +if ($help) { + print $usage; + exit; +} + +#getting the all the branches +my $selectBranchesSt = 'SELECT branchcode FROM branches'; +my $selectBranchesSth = $dbh->prepare($selectBranchesSt); +$selectBranchesSth->execute(); +my @branches = (); +while ( my $branchCode = $selectBranchesSth->fetchrow_array ) { + + push @branches,$branchCode; +} + +#get the latest date in the table +$query = "SELECT MAX(date) FROM discrete_calendar"; +$stmt = $dbh->prepare($query); +$stmt->execute(); +my $latestedDate = $stmt->fetchrow_array; +my $parser = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %H:%M:%S', + on_error => 'croak', +); +$latestedDate = $parser->parse_datetime($latestedDate); + +my $endDate = DateTime->today->add( years => 1, days => $daysInFuture); +my $newDay = $latestedDate->clone(); + +for ($newDay->add(days => 1);$newDay <= $endDate;$newDay->add(days => 1)){ + my $lastWeekDay = $newDay->clone(); + $lastWeekDay->add(days=> -8); + my $dayOfWeek = $lastWeekDay->day_of_week; + # Representation fix + # DateTime object dow (1-7) where Monday is 1 + # Arrays are 0-based where 0 = Sunday, not 7. + $dayOfWeek -= 1 unless $dayOfWeek == 7; + $dayOfWeek = 0 if $dayOfWeek == 7; + + #checking if it was open on the same day from last year + my $yearAgo = $newDay->clone(); + $yearAgo = $yearAgo->add(years => -1); + my $last_year = 'SELECT isopened, holidaytype, note, openhour, closehour FROM discrete_calendar WHERE date=? AND branchcode=?'; + my $day_last_week = "SELECT openhour, closehour FROM discrete_calendar WHERE DAYOFWEEk(date)=DAYOFWEEK(?) and date < ? order by date desc limit 1"; + my $add_Day = 'INSERT INTO discrete_calendar (date,branchcode,isopened,openhour,closehour) VALUES (?,?,?,?,?)'; + my $note =''; + #insert into discrete_calendar for each branch + foreach my $branchCode(@branches){ + $stmt = $dbh->prepare($last_year); + $stmt->execute($yearAgo,$branchCode); + my ($isOpened, $holidaytype, $note) = $stmt->fetchrow_array; + if ($holidaytype && $holidaytype eq "W"){ + $isOpened = 1; + $note=''; + }elsif ($holidaytype && $holidaytype eq "F" || $holidaytype eq 'E'){ + $holidaytype = 'N'; + } + $holidaytype = '' if $isOpened; + $stmt = $dbh->prepare($day_last_week); + $stmt->execute($newDay, $newDay); + my ($openhour,$closehour ) = $stmt->fetchrow_array; + my $add_Day = 'INSERT INTO discrete_calendar (date,branchcode,isopened,holidaytype, note,openhour,closehour) VALUES (?,?,?,?,?,?,?)'; + $stmt = $dbh->prepare($add_Day); + $stmt->execute($newDay,$branchCode,$isOpened,$holidaytype,$note, $openhour,$closehour); + } +} diff --git a/tools/discrete_calendar.pl b/tools/discrete_calendar.pl new file mode 100755 index 0000000..9b035f5 --- /dev/null +++ b/tools/discrete_calendar.pl @@ -0,0 +1,157 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see <http://www.gnu.org/licenses>. + +#####Sets holiday periods for each branch. Datedues will be extended if branch is closed -TG +use strict; +use warnings; + +use CGI qw ( -utf8 ); + +use C4::Auth; +use C4::Output; + +use Koha::DateUtils; +use Koha::DiscreteCalendar; + +my $input = new CGI; + +# Get the template to use +my ($template, $loggedinuser, $cookie) + = get_template_and_user({template_name => "tools/discrete_calendar.tt", + type => "intranet", + query => $input, + authnotrequired => 0, + flagsrequired => {tools => 'edit_calendar'}, + debug => 1, + }); + +my $branchcode = $input->param('BranchName'); +my $branch = $input->param('branch') || C4::Context->userenv->{'branch'}; +my $calendar = Koha::DiscreteCalendar->new(branchcode => $branch); + +my $weekday = $input->param('Weekday'); + +my $holidaytype = $input->param('holidayType'); +my $allbranches = $input->param('allBranches'); + +my $title = $input->param('Title'); + +my $action = $input->param('action') || ''; + +# calendardate - date passed in url for human readability (syspref) +# if the url has an invalid date default to 'now.' +my $calendarinput_dt = eval { dt_from_string( scalar $input->param('calendardate')); } || dt_from_string; +my $calendardate = output_pref( { dt => $calendarinput_dt, dateonly => 1 } ); + +if($action eq 'copyBranch'){ + copyToBranch(); +} elsif($action eq 'copyDates'){ + copyDates(); +} elsif($action eq 'edit'){ + edit_holiday(); +} + +sub copyToBranch { + $calendar->copyToBranch(scalar $input->param('newBranch')); +} +sub copyDates { + my $from_startDate = $input->param('from_copyFrom') ||''; + my $from_endDate = $input->param('toDate') || ''; + my $to_startDate = $input->param('to_copyFrom') || ''; + my $to_endDate = $input->param('to_copyTo') || ''; + my $daysnumber= $input->param('daysnumber'); + + $from_startDate = dt_from_string(scalar $from_startDate) if$from_startDate ne ''; + $to_startDate = dt_from_string(scalar $to_startDate) if $to_startDate ne ''; + $to_startDate = dt_from_string(scalar $to_startDate) if $to_startDate ne ''; + $to_endDate = dt_from_string(scalar $to_endDate) if $to_endDate ne ''; + + $calendar->copyHoliday($from_startDate, $from_endDate, $to_startDate, $to_endDate, $daysnumber); +} + +sub edit_holiday { + my $openHour = $input->param('openHour'); + my $closeHour = $input->param('closeHour'); + my $toDate = $input->param('toDate'); + my $deleteType = $input->param('deleteType') || 0; + + my $startDate = dt_from_string(scalar $input->param('from_copyFrom')); + + if($toDate ne '' ) { + $toDate = dt_from_string(scalar $toDate); + } else{ + $toDate = $startDate->clone(); + } + #MYSQL DAYOFWEEK Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday). + #JavaScript getDay() Returns the day of the week (from 0 to 6) for the specified date. Sunday is 0, Monday is 1, and so on. + $weekday+=1; + $calendar->edit_holiday($title, $weekday, $holidaytype, $openHour, $closeHour, $startDate, $toDate, $deleteType); +} +# keydate - date passed to calendar.js. calendar.js does not process dashes within a date. + +my $keydate = output_pref( { dt => $calendarinput_dt, dateonly => 1, dateformat => 'iso' } ); +$keydate =~ s/-/\//g; + +# Set all the branches. +my $onlymine = + ( C4::Context->preference('IndependentBranches') + && C4::Context->userenv + && !C4::Context->IsSuperLibrarian() + && C4::Context->userenv->{branch} ? 1 : 0 ); +if ( $onlymine ) { + $branch = C4::Context->userenv->{'branch'}; +} + +# Get all the holidays + +#discrete_calendar weekdays +my @week_days = $calendar->get_week_days_holidays(); + +#discrete_calendar repeatable +my @repeatable_holidays = $calendar->get_day_month_holidays(); + +#single hildays (exceptions) +my @single_holidays =$calendar->get_single_holidays(); +#floating holidays +my @float_holidays =$calendar->get_float_holidays(); +#need validation holidays +my @need_validation_holidays =$calendar->get_need_valdation_holidays(); + +#Calendar maximum date +my $minDate = $calendar->getMinDate($branch); + +#Calendar minimum date +my $maxDate = $calendar->getMaxDate($branch); + +my @datesInfos = $calendar->getDatesInfo($branch); + +$template->param( + HOLIDAYS_LOOP => \@single_holidays, + FLOAT_HOLIDAYS => \@float_holidays, + NEED_VALIDATION_HOLIDAYS => \@need_validation_holidays, + calendardate => $calendardate, + keydate => $keydate, + branch => $branch, + week_days => \@week_days, + repeatable_holidays => \@repeatable_holidays, + minDate => $minDate, + maxDate => $maxDate, + datesInfos => \@datesInfos, +); + +# Shows the template with the real values replaced +output_html_with_http_headers $input, $cookie, $template->output; -- 1.9.1