包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use function MRBS\datetime_format;
|
||||
use function MRBS\day_past_midnight;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\format_iso_date;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\is_hidden_day;
|
||||
use function MRBS\multisite;
|
||||
|
||||
abstract class Calendar
|
||||
{
|
||||
protected $day;
|
||||
protected $month;
|
||||
protected $year;
|
||||
protected $area_id;
|
||||
protected $room_id;
|
||||
protected $view;
|
||||
protected $view_all;
|
||||
protected $start_date;
|
||||
protected $end_date;
|
||||
protected $map;
|
||||
|
||||
|
||||
public function __construct(string $view, int $view_all, int $year, int $month, int $day, int $area_id, int $room_id)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->view_all = $view_all;
|
||||
$this->year = $year;
|
||||
$this->month = $month;
|
||||
$this->day = $day;
|
||||
$this->area_id = $area_id;
|
||||
$this->room_id = $room_id;
|
||||
}
|
||||
|
||||
abstract public function innerHTML() : string;
|
||||
|
||||
|
||||
// Get a series of flex divs for a room in a day index interval for the map.
|
||||
protected function flexDivsHTML(int $room_id, int $start_day_index, int $end_day_index) : string
|
||||
{
|
||||
global $resolution;
|
||||
|
||||
$html = '';
|
||||
|
||||
// Get the time slots
|
||||
$n_time_slots = self::getNTimeSlots();
|
||||
$morning_slot_seconds = self::morningSlotsSeconds();
|
||||
$evening_slot_seconds = $morning_slot_seconds + (($n_time_slots - 1) * $resolution);
|
||||
|
||||
// Loop through the days in the interval
|
||||
for ($i=$start_day_index; $i<=$end_day_index; $i++)
|
||||
{
|
||||
$s = $morning_slot_seconds;
|
||||
|
||||
// Loop through the slots in the day
|
||||
while ($s <= $evening_slot_seconds)
|
||||
{
|
||||
// Get the entry for this slot
|
||||
$this_slot = $this->map->slot($room_id, $i, $s);
|
||||
// Start a FlexDiv if we haven't got one
|
||||
if (!isset($flex_div))
|
||||
{
|
||||
$flex_div = new FlexDiv($this_slot[0]['id'] ?? null);
|
||||
// If it's a booking, set the properties
|
||||
if (!empty($this_slot))
|
||||
{
|
||||
$this_entry = $this_slot[0];
|
||||
$flex_div->setClasses($this->getEntryClasses($this_entry));
|
||||
$flex_div->setLength($this_entry['n_slots']);
|
||||
$flex_div->setName($this_entry['name']);
|
||||
}
|
||||
// Work out how many slots to advance
|
||||
$n = $flex_div->getLength();
|
||||
}
|
||||
// Otherwise, look to see whether this is a continuation of the stored entry,
|
||||
// or else a change, in which case output the stored entry and reset.
|
||||
else
|
||||
{
|
||||
// Another free slot
|
||||
if (empty($this_slot) && !isset($flex_div->id))
|
||||
{
|
||||
$n = 1;
|
||||
$flex_div->addLength($n);
|
||||
}
|
||||
// A continuation of an existing booking
|
||||
elseif (!empty($this_slot) && isset($flex_div->id) && ($flex_div->id == $this_slot[0]['id']))
|
||||
{
|
||||
$n = $this_slot[0]['n_slots'];
|
||||
$flex_div->addLength($n);
|
||||
}
|
||||
// There's been a change. Output the FlexDiv and reset
|
||||
else
|
||||
{
|
||||
$html .= $flex_div->html();
|
||||
unset($flex_div);
|
||||
$n = 0;
|
||||
}
|
||||
}
|
||||
$s = $s + ($n * $resolution); // Advance n slots
|
||||
}
|
||||
}
|
||||
|
||||
// Output the final FlexDiv
|
||||
if (isset($flex_div))
|
||||
{
|
||||
$html .= $flex_div->html();
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
protected function getDate(int $t) : string
|
||||
{
|
||||
global $datetime_formats;
|
||||
|
||||
if (in_array($this->view, ['month', 'year']))
|
||||
{
|
||||
return datetime_format(['pattern' => 'd'], $t);
|
||||
}
|
||||
else
|
||||
{
|
||||
return datetime_format($datetime_formats['view_week_day_month'], $t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function getDay(int $t) : string
|
||||
{
|
||||
// In the month view use a pattern which will tend to give a narrower result, to save space.
|
||||
$pattern = ($this->view == 'month') ? 'cccccc' : 'ccc';
|
||||
|
||||
return datetime_format(['pattern' => $pattern], $t);
|
||||
}
|
||||
|
||||
|
||||
public static function morningSlotsSeconds() : int
|
||||
{
|
||||
global $morningstarts, $morningstarts_minutes;
|
||||
|
||||
return (($morningstarts * 60) + $morningstarts_minutes) * 60;
|
||||
}
|
||||
|
||||
|
||||
// Gets the number of time slots between the beginning and end of the booking
|
||||
// day. (This is the normal number on a non-DST transition day)
|
||||
public static function getNTimeSlots() : int
|
||||
{
|
||||
global $eveningends, $eveningends_minutes;
|
||||
global $resolution;
|
||||
|
||||
$start_first = self::morningSlotsSeconds(); // seconds
|
||||
$end_last = ((($eveningends * 60) + $eveningends_minutes) * 60) + $resolution; // seconds
|
||||
$end_last = $end_last % SECONDS_PER_DAY;
|
||||
if (day_past_midnight())
|
||||
{
|
||||
$end_last += SECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
// Force the result to be an int. It normally will be, but might not be if, say,
|
||||
// $force_resolution is set.
|
||||
return intval(($end_last - $start_first)/$resolution);
|
||||
}
|
||||
|
||||
|
||||
// If we're not using periods, construct an array describing the slots to pass to the JavaScript so that
|
||||
// it can calculate where the timeline should be drawn. (If we are using periods then the timeline is
|
||||
// meaningless because we don't know when periods begin and end.)
|
||||
// $month, $day, $year the start of the interval
|
||||
// $n_days the number of days in the interval
|
||||
// $day_cells if the columns/rows represent a full day (as in the week/month all rooms views)
|
||||
protected function getSlots(int $month, int $day, int $year, int $n_days=1, bool $day_cells=false) : ?array
|
||||
{
|
||||
global $enable_periods, $morningstarts, $morningstarts_minutes, $resolution;
|
||||
|
||||
if ($enable_periods)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$slots = array();
|
||||
|
||||
$n_time_slots = self::getNTimeSlots();
|
||||
$morning_slot_seconds = self::morningSlotsSeconds();
|
||||
$evening_slot_seconds = $morning_slot_seconds + (($n_time_slots - 1) * $resolution);
|
||||
|
||||
for ($j = 0; $j < $n_days; $j++)
|
||||
{
|
||||
$d = $day + $j;
|
||||
|
||||
// If there's more than one day in the interval then don't include the hidden days in the array, because
|
||||
// they don't appear in the DOM. If there's only one day then we've managed to display the hidden day.
|
||||
if (($n_days > 1) &&
|
||||
is_hidden_day(intval(date('w', mktime($morningstarts, $morningstarts_minutes, 0, $month, $d, $year)))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$this_day = array();
|
||||
|
||||
if ($day_cells)
|
||||
{
|
||||
$this_day[] = mktime(0, 0, $morning_slot_seconds, $month, $d, $year);
|
||||
// Need to do mktime() again for the end of the slot as we can't assume that the end slot is $resolution
|
||||
// seconds after the start of the slot because of the possibility of DST transitions
|
||||
$this_day[] = mktime(0, 0, $evening_slot_seconds + $resolution, $month, $d, $year);
|
||||
}
|
||||
else
|
||||
{
|
||||
for ($s = $morning_slot_seconds;
|
||||
$s <= $evening_slot_seconds;
|
||||
$s += $resolution)
|
||||
{
|
||||
$this_slot = array();
|
||||
$this_slot[] = mktime(0, 0, $s, $month, $d, $year);
|
||||
// Need to do mktime() again for the end of the slot as we can't assume that the end slot is $resolution
|
||||
// seconds after the start of the slot because of the possibility of DST transitions
|
||||
$this_slot[] = mktime(0, 0, $s + $resolution, $month, $d, $year);
|
||||
$this_day[] = $this_slot;
|
||||
}
|
||||
}
|
||||
$slots[] = $this_day;
|
||||
}
|
||||
|
||||
if ($day_cells)
|
||||
{
|
||||
$slots = array($slots);
|
||||
}
|
||||
|
||||
return $slots;
|
||||
}
|
||||
|
||||
|
||||
// Get classes for weekends, holidays, etc.
|
||||
protected function getDateClasses(DateTime $date) : array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if ($date->isWeekend())
|
||||
{
|
||||
$result[] = 'weekend';
|
||||
}
|
||||
if ($date->isHoliday())
|
||||
{
|
||||
$result[] = 'holiday';
|
||||
}
|
||||
if ($date->isToday())
|
||||
{
|
||||
$result[] = 'today';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Returns an array of classes to be used for the entry
|
||||
protected function getEntryClasses(array $entry) : array
|
||||
{
|
||||
global $approval_enabled, $confirmation_enabled;
|
||||
|
||||
$classes = array($entry['type']);
|
||||
|
||||
if ($entry['private'])
|
||||
{
|
||||
$classes[] = 'private';
|
||||
}
|
||||
|
||||
if ($approval_enabled && ($entry['awaiting_approval']))
|
||||
{
|
||||
$classes[] = 'awaiting_approval';
|
||||
}
|
||||
|
||||
if ($confirmation_enabled && ($entry['tentative']))
|
||||
{
|
||||
$classes[] = 'tentative';
|
||||
}
|
||||
|
||||
if (isset($entry['repeat_id']))
|
||||
{
|
||||
$classes[] = 'series';
|
||||
}
|
||||
|
||||
if ($entry['allow_registration'])
|
||||
{
|
||||
if ($entry['registrant_limit_enabled'] &&
|
||||
($entry['n_registered'] >= $entry['registrant_limit']))
|
||||
{
|
||||
$classes[] = 'full';
|
||||
}
|
||||
else
|
||||
{
|
||||
$classes[] = 'spaces';
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
|
||||
protected function multidayHeaderRowsHTML(int $day_start_interval, int $n_days, int $start_dow, string $label='') : array
|
||||
{
|
||||
global $row_labels_both_sides;
|
||||
|
||||
$result = array();
|
||||
$n_rows = 2;
|
||||
// Loop through twice: one row for the days of the week, the next for the date.
|
||||
for ($i = 0; $i < $n_rows; $i++)
|
||||
{
|
||||
$result[$i] = "<tr>\n";
|
||||
|
||||
// Could use a rowspan here, but we'd need to make sure the sticky cells work
|
||||
// and change the JavaScript in refresh.js.php
|
||||
$text = ($i == 0) ? '' : $label;
|
||||
$first_last_html = '<th class="first_last">' . escape_html($text) . "</th>\n";
|
||||
$result[$i] .= $first_last_html;
|
||||
|
||||
$vars = [
|
||||
'view' => 'day',
|
||||
'view_all' => $this->view_all,
|
||||
'area' => $this->area_id,
|
||||
'room' => $this->room_id
|
||||
];
|
||||
|
||||
// the standard view, with days along the top and rooms down the side
|
||||
for ($j = 0; $j < $n_days; $j++)
|
||||
{
|
||||
if (is_hidden_day(($j + $start_dow) % DAYS_PER_WEEK))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$vars['page_date'] = format_iso_date($this->year, $this->month, $day_start_interval + $j);
|
||||
$link = "index.php?" . http_build_query($vars, '', '&');
|
||||
$link = multisite($link);
|
||||
$t = mktime(12, 0, 0, $this->month, $day_start_interval + $j, $this->year);
|
||||
$text = ($i === 0) ? $this->getDay($t) : $this->getDate($t);
|
||||
$date = new DateTime();
|
||||
$date->setTimestamp($t);
|
||||
$classes = $this->getDateClasses($date);
|
||||
$result[$i] .= '<th' .
|
||||
// Add the date for JavaScript. Only really necessary for the first row in
|
||||
// the week view when not viewing all the rooms, but just add it always.
|
||||
' data-date="' . escape_html($date->getISODate()) . '"' .
|
||||
((!empty($classes)) ? ' class="' . implode(' ', $classes) . '"' : '') .
|
||||
'><a href="' . escape_html($link) . '">' . escape_html($text) . "</a></th>\n";
|
||||
}
|
||||
|
||||
// next line to display rooms on right side
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$result[$i] .= $first_last_html;
|
||||
}
|
||||
|
||||
$result[$i] .= "</tr>\n";
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Draw a room cell to be used in the header rows/columns of the calendar views
|
||||
// $room contains the room details
|
||||
// $vars an associative array containing the variables to be used to build the link
|
||||
protected function roomCellHTML(array $room, array $vars) : string
|
||||
{
|
||||
$link = 'index.php?' . http_build_query($vars, '', '&');
|
||||
$link = multisite($link);
|
||||
|
||||
switch ($vars['view'])
|
||||
{
|
||||
case 'day':
|
||||
$tag = 'viewday';
|
||||
break;
|
||||
case 'week':
|
||||
$tag = 'viewweek';
|
||||
break;
|
||||
case 'month':
|
||||
$tag = 'viewmonth';
|
||||
break;
|
||||
case 'year':
|
||||
$tag = 'viewyear';
|
||||
break;
|
||||
default:
|
||||
trigger_error("Unknown view '" . $vars['view'] . "'", E_USER_NOTICE);
|
||||
$tag = 'viewweek';
|
||||
break;
|
||||
}
|
||||
|
||||
$title = get_vocab($tag) . "\n\n" . $room['description'];
|
||||
$html = '';
|
||||
$html .= '<th data-room="' . escape_html($room['id']) . '">';
|
||||
$html .= '<a href="' . escape_html($link) . '"' .
|
||||
' title = "' . escape_html($title) . '">';
|
||||
$html .= escape_html($room['room_name']);
|
||||
// Put the capacity in a span to give flexibility in styling
|
||||
$html .= '<span class="capacity';
|
||||
if ($room['capacity'] == 0)
|
||||
{
|
||||
$html .= ' zero';
|
||||
}
|
||||
$html .= '">' . escape_html($room['capacity']);
|
||||
$html .= '</span>';
|
||||
$html .= '</a>';
|
||||
$html .= "</th>\n";
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class CalendarFactory
|
||||
{
|
||||
public static function create(
|
||||
string $view,
|
||||
int $view_all,
|
||||
int $year,
|
||||
int $month,
|
||||
int $day,
|
||||
int $area_id,
|
||||
int $room_id,
|
||||
?int $timetohighlight=null,
|
||||
?string $kiosk=null) : Calendar
|
||||
{
|
||||
switch ($view)
|
||||
{
|
||||
case 'day':
|
||||
return new CalendarMultislotDay($view, $view_all, $year, $month, $day, $area_id, $room_id, $timetohighlight, $kiosk);
|
||||
break;
|
||||
case 'week':
|
||||
if ($view_all)
|
||||
{
|
||||
return new CalendarMultidayMultiroom($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
}
|
||||
return new CalendarMultislotWeek($view, $view_all, $year, $month, $day, $area_id, $room_id, $timetohighlight);
|
||||
break;
|
||||
case 'month':
|
||||
if ($view_all)
|
||||
{
|
||||
return new CalendarMultidayMultiroom($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
}
|
||||
return new CalendarMonthOneRoom($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
break;
|
||||
case 'year':
|
||||
if ($view_all)
|
||||
{
|
||||
return new CalendarMultimonthMultiroom($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
}
|
||||
return new CalendarMultimonthOneRoom($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException("Invalid view: $view");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use MRBS\Language;
|
||||
use function MRBS\datetime_format;
|
||||
use function MRBS\day_name;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\get_end_last_slot;
|
||||
use function MRBS\get_entries_by_room;
|
||||
use function MRBS\get_room_name;
|
||||
use function MRBS\get_start_first_slot;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\hour_min_format;
|
||||
use function MRBS\is_book_admin;
|
||||
use function MRBS\is_hidden_day;
|
||||
use function MRBS\is_visible;
|
||||
use function MRBS\is_weekend;
|
||||
use function MRBS\multisite;
|
||||
use function MRBS\period_time_string;
|
||||
use function MRBS\session;
|
||||
|
||||
class CalendarMonthOneRoom extends Calendar
|
||||
{
|
||||
|
||||
public function innerHTML(): string
|
||||
{
|
||||
global $weekstarts, $view_week_number, $show_plus_link, $monthly_view_entries_details;
|
||||
global $enable_periods, $morningstarts, $morningstarts_minutes;
|
||||
global $prevent_booking_on_holidays, $prevent_booking_on_weekends;
|
||||
|
||||
// Check that we've got a valid, enabled room
|
||||
if (is_null(get_room_name($this->room_id)) || !is_visible($this->room_id))
|
||||
{
|
||||
// No rooms have been created yet, or else they are all disabled
|
||||
// Add an 'empty' data flag so that the JavaScript knows whether this is a real table or not
|
||||
return "<tbody data-empty=1><tr><td><h1>".get_vocab("no_rooms_for_area")."</h1></td></tr></tbody>";
|
||||
}
|
||||
|
||||
$html = '';
|
||||
|
||||
// Month view start time. This ignores morningstarts/eveningends because it
|
||||
// doesn't make sense to not show all entries for the day, and it messes
|
||||
// things up when entries cross midnight.
|
||||
$month_start = mktime(0, 0, 0, $this->month, 1, $this->year);
|
||||
// What column the month starts in: 0 means $weekstarts weekday.
|
||||
$weekday_start = (date("w", $month_start) - $weekstarts + DAYS_PER_WEEK) % DAYS_PER_WEEK;
|
||||
$last_day_of_month = (int) date("t", $month_start);
|
||||
|
||||
$html .= $this->theadHTML();
|
||||
|
||||
// Main body
|
||||
$html .= "<tbody>\n";
|
||||
$html .= "<tr>\n";
|
||||
|
||||
// Skip days in week before the start of the month:
|
||||
for ($weekcol = 0; $weekcol < $weekday_start; $weekcol++)
|
||||
{
|
||||
$html .= $this->tdBlankDayHTML($weekcol);
|
||||
}
|
||||
|
||||
$start_date = (new DateTime())->setTimestamp(get_start_first_slot($this->month, 1, $this->year));
|
||||
$end_date = (new DateTime())->setTimestamp(get_end_last_slot($this->month, $last_day_of_month, $this->year));
|
||||
|
||||
// Get the data. It's much quicker to do a single SQL query getting all the
|
||||
// entries for the interval in one go, rather than doing a query for each day.
|
||||
$entries = get_entries_by_room($this->room_id, $start_date, $end_date);
|
||||
|
||||
// Draw the days of the month:
|
||||
for ($d = 1, $date = clone $start_date; $d <= $last_day_of_month; $d++, $date->modify('+1 day'))
|
||||
{
|
||||
// Get the slot times
|
||||
$start_first_slot = get_start_first_slot($this->month, $d, $this->year);
|
||||
$end_last_slot = get_end_last_slot($this->month, $d, $this->year);
|
||||
|
||||
// if we're at the start of the week (and it's not the first week), start a new row
|
||||
if (($weekcol == 0) && ($d > 1))
|
||||
{
|
||||
$html .= "</tr><tr>\n";
|
||||
}
|
||||
|
||||
// output the day cell
|
||||
if ($date->isHiddenDay())
|
||||
{
|
||||
// These days are to be hidden in the display (as they are hidden, just give the
|
||||
// day of the week in the header row)
|
||||
$html .= "<td class=\"hidden_day\">\n";
|
||||
$html .= "<div class=\"cell_container\">\n";
|
||||
$html .= "<div class=\"cell_header\">\n";
|
||||
// first put in the day of the month
|
||||
$html .= "<span>$d</span>\n";
|
||||
$html .= "</div>\n";
|
||||
$html .= "</div>\n";
|
||||
$html .= "</td>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add classes for weekends and holidays
|
||||
$classes = $this->getDateClasses($date);
|
||||
|
||||
$html .= '<td' . ((empty($classes)) ? '' : ' class="' . implode(' ', $classes) . '"') . ">\n";
|
||||
$html .= "<div class=\"cell_container\">\n";
|
||||
|
||||
$html .= "<div class=\"cell_header\">\n";
|
||||
|
||||
$vars = [
|
||||
'page_date' => $date->getISODate(),
|
||||
'area' => $this->area_id,
|
||||
'room' => $this->room_id
|
||||
];
|
||||
|
||||
// If it's the first day of the week, show the week number
|
||||
if ($view_week_number && $date->isFirstDayOfWeek(Language::getInstance()->getWebLocale()))
|
||||
{
|
||||
$vars['view'] = 'week';
|
||||
$query = http_build_query($vars, '', '&');
|
||||
$html .= '<a class="week_number" href="' . escape_html(multisite("index.php?$query")) . '">';
|
||||
$html .= $date->format('W');
|
||||
$html .= "</a>\n";
|
||||
}
|
||||
// then put in the day of the month
|
||||
$vars['view'] = 'day';
|
||||
$query = http_build_query($vars, '', '&');
|
||||
$html .= '<a class="monthday" href="' . escape_html(multisite("index.php?$query")) . "\">$d</a>\n";
|
||||
|
||||
$html .= "</div>\n";
|
||||
|
||||
// Then the link to make a new booking.
|
||||
// Don't provide a link if the slot doesn't really exist or if the user is logged in, but not a booking admin,
|
||||
// and it's a holiday/weekend and bookings on holidays/weekends are not allowed. (We provide a link if they
|
||||
// are not logged in because they might want to click and login as an admin).
|
||||
if ((null !== session()->getCurrentUser()) && !is_book_admin($this->room_id) &&
|
||||
(($prevent_booking_on_holidays && in_array('holiday', $classes)) ||
|
||||
($prevent_booking_on_weekends && in_array('weekend', $classes))))
|
||||
{
|
||||
$html .= '<span class="not_allowed"></span>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$vars['view'] = $this->view;
|
||||
|
||||
if ($enable_periods)
|
||||
{
|
||||
$vars['period'] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
$vars['hour'] = $morningstarts;
|
||||
$vars['minute'] = $morningstarts_minutes;
|
||||
}
|
||||
|
||||
$query = http_build_query($vars, '', '&');
|
||||
|
||||
$html .= '<a class="new_booking" href="' . escape_html(multisite("edit_entry.php?$query")) . '"' .
|
||||
' aria-label="' . escape_html(get_vocab('create_new_booking')) . "\">\n";
|
||||
if ($show_plus_link)
|
||||
{
|
||||
$html .= "<img src=\"images/new.gif\" alt=\"New\" width=\"10\" height=\"10\">\n";
|
||||
}
|
||||
$html .= "</a>\n";
|
||||
}
|
||||
|
||||
// then any bookings for the day
|
||||
$html .= "<div class=\"booking_list\">\n";
|
||||
// Show the start/stop times, 1 or 2 per line, linked to view_entry.
|
||||
foreach ($entries as $entry)
|
||||
{
|
||||
// We are only interested in this day's entries
|
||||
if (($entry['start_time'] >= $end_last_slot) ||
|
||||
($entry['end_time'] <= $start_first_slot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = Map::prepareEntry($entry);
|
||||
|
||||
$classes = $this->getEntryClasses($entry);
|
||||
$classes[] = $monthly_view_entries_details;
|
||||
|
||||
$html .= '<div class="' . implode(' ', $classes) . '">';
|
||||
|
||||
$vars = [
|
||||
'id' => $entry['id'],
|
||||
'year' => $this->year,
|
||||
'month' => $this->month,
|
||||
'day' => $d
|
||||
];
|
||||
|
||||
$query = http_build_query($vars, '', '&');
|
||||
$booking_link = multisite("view_entry.php?$query");
|
||||
$slot_text = $this->bookingSummaryHTML(
|
||||
$entry['start_time'],
|
||||
$entry['end_time'],
|
||||
$start_first_slot,
|
||||
$end_last_slot
|
||||
);
|
||||
$description_text = mb_substr($entry['name'], 0, 255);
|
||||
$full_text = $slot_text . " " . $description_text;
|
||||
switch ($monthly_view_entries_details)
|
||||
{
|
||||
case "description":
|
||||
{
|
||||
$display_text = $description_text;
|
||||
break;
|
||||
}
|
||||
case "slot":
|
||||
{
|
||||
$display_text = $slot_text;
|
||||
break;
|
||||
}
|
||||
case "both":
|
||||
{
|
||||
$display_text = $full_text;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
$html .= "error: unknown parameter";
|
||||
}
|
||||
}
|
||||
$title_text = $full_text;
|
||||
if (isset($entry['description']) && ($entry['description'] !== ''))
|
||||
{
|
||||
$title_text .= "\n\n" . $entry['description'];
|
||||
}
|
||||
$html .= '<a href="' . escape_html($booking_link) . '"' .
|
||||
' title="' . escape_html($title_text) . '">';
|
||||
$html .= escape_html($display_text) . '</a>';
|
||||
$html .= "</div>\n";
|
||||
}
|
||||
$html .= "</div>\n";
|
||||
|
||||
$html .= "</div>\n";
|
||||
$html .= "</td>\n";
|
||||
}
|
||||
|
||||
// increment the day of the week counter
|
||||
if (++$weekcol == DAYS_PER_WEEK)
|
||||
{
|
||||
$weekcol = 0;
|
||||
}
|
||||
|
||||
} // end of for loop going through valid days of the month
|
||||
|
||||
// Skip from end of month to end of week:
|
||||
if ($weekcol > 0)
|
||||
{
|
||||
for (; $weekcol < DAYS_PER_WEEK; $weekcol++)
|
||||
{
|
||||
$html .= $this->tdBlankDayHTML($weekcol);
|
||||
}
|
||||
}
|
||||
|
||||
$html .= "</tr>\n";
|
||||
$html .= "</tbody>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
// Describe the start and end time, accounting for "all day"
|
||||
// and for entries starting before/ending after today.
|
||||
// There are 9 cases, for start time < = or > midnight this morning,
|
||||
// and end time < = or > midnight tonight.
|
||||
private function bookingSummaryHTML(int $start, int $end, int $day_start, int $day_end) : string
|
||||
{
|
||||
global $enable_periods, $area;
|
||||
|
||||
// Use ~ (not -) to separate the start and stop times, because MSIE
|
||||
// will incorrectly line break after a -.
|
||||
$separator = '~';
|
||||
$after_today = "==>";
|
||||
$before_today = "<==";
|
||||
$midnight = "24:00"; // need to fix this so it works with AM/PM configurations (and for that matter 24h)
|
||||
$all_day = get_vocab('all_day');
|
||||
|
||||
if ($enable_periods)
|
||||
{
|
||||
$start_str = escape_html(period_time_string($start, $area));
|
||||
$end_str = escape_html(period_time_string($end, $area, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
$start_str = escape_html(datetime_format(hour_min_format(), $start));
|
||||
$end_str = escape_html(datetime_format(hour_min_format(), $end));
|
||||
}
|
||||
|
||||
switch (self::cmp3($start, $day_start) . self::cmp3($end, $day_end + 1))
|
||||
{
|
||||
case "> < ": // Starts after midnight, ends before midnight
|
||||
case "= < ": // Starts at midnight, ends before midnight
|
||||
$result = $start_str;
|
||||
// Don't bother showing the end if it's the same as the start period
|
||||
if ($end_str !== $start_str)
|
||||
{
|
||||
$result .= $separator . $end_str;
|
||||
}
|
||||
break;
|
||||
case "> = ": // Starts after midnight, ends at midnight
|
||||
$result = $start_str . $separator . $midnight;
|
||||
break;
|
||||
case "> > ": // Starts after midnight, continues tomorrow
|
||||
$result = $start_str . $separator . $after_today;
|
||||
break;
|
||||
case "= = ": // Starts at midnight, ends at midnight
|
||||
$result = $all_day;
|
||||
break;
|
||||
case "= > ": // Starts at midnight, continues tomorrow
|
||||
$result = $all_day . $after_today;
|
||||
break;
|
||||
case "< < ": // Starts before today, ends before midnight
|
||||
$result = $before_today . $separator . $end_str;
|
||||
break;
|
||||
case "< = ": // Starts before today, ends at midnight
|
||||
$result = $before_today . $all_day;
|
||||
break;
|
||||
case "< > ": // Starts before today, continues tomorrow
|
||||
$result = $before_today . $all_day . $after_today;
|
||||
break;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// 3-value compare: Returns result of compare as "< " "= " or "> ".
|
||||
private static function cmp3(int $a, int $b) : string
|
||||
{
|
||||
if ($a < $b)
|
||||
{
|
||||
return "< ";
|
||||
}
|
||||
if ($a == $b)
|
||||
{
|
||||
return "= ";
|
||||
}
|
||||
return "> ";
|
||||
}
|
||||
|
||||
|
||||
private function tdBlankDayHTML(int $col) : string
|
||||
{
|
||||
global $weekstarts;
|
||||
|
||||
$td_class = (is_hidden_day(($col + $weekstarts) % DAYS_PER_WEEK)) ? 'hidden_day' : 'invalid';
|
||||
return "<td class=\"$td_class\"><div class=\"cell_container\"> </div></td>\n";
|
||||
}
|
||||
|
||||
|
||||
// Gets the table head for the single room month view
|
||||
private function theadHTML() : string
|
||||
{
|
||||
global $weekstarts;
|
||||
|
||||
$html = '';
|
||||
|
||||
// Weekday name header row:
|
||||
$html .= "<thead>\n";
|
||||
$html .= "<tr>\n";
|
||||
for ($i = 0; $i< DAYS_PER_WEEK; $i++)
|
||||
{
|
||||
$classes = [];
|
||||
$dow = ($i + $weekstarts) % DAYS_PER_WEEK;
|
||||
if (is_hidden_day($dow))
|
||||
{
|
||||
$classes[] = 'hidden_day';
|
||||
}
|
||||
if (is_weekend($dow))
|
||||
{
|
||||
$classes[] = 'weekend';
|
||||
}
|
||||
$html .= '<th';
|
||||
if (!empty($classes))
|
||||
{
|
||||
$html .= ' class="' . implode(' ', $classes) . '"';
|
||||
}
|
||||
$html .= '>' . day_name(($i + $weekstarts)%DAYS_PER_WEEK) . '</th>';
|
||||
}
|
||||
$html .= "\n</tr>\n";
|
||||
$html .= "</thead>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use function MRBS\day_of_MRBS_week;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\format_iso_date;
|
||||
use function MRBS\get_end_last_slot;
|
||||
use function MRBS\get_entries_by_area;
|
||||
use function MRBS\get_rooms;
|
||||
use function MRBS\get_start_first_slot;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\multisite;
|
||||
|
||||
|
||||
class CalendarMultidayMultiroom extends Calendar
|
||||
{
|
||||
|
||||
private $day_start_interval;
|
||||
private $n_days;
|
||||
private $start_dow;
|
||||
|
||||
public function __construct(string $view, int $view_all, int $year, int $month, int $day, int $area_id, int $room_id)
|
||||
{
|
||||
global $weekstarts, $resolution;
|
||||
|
||||
parent::__construct($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
|
||||
// Calculate/get:
|
||||
// the first day of the interval
|
||||
// how many days there are in it
|
||||
// the day of the week of the first day in the interval
|
||||
$time = mktime(12, 0, 0, $this->month, $this->day, $this->year);
|
||||
switch ($this->view)
|
||||
{
|
||||
case 'week':
|
||||
$skipback = day_of_MRBS_week($time);
|
||||
$this->day_start_interval = $this->day - $skipback;
|
||||
$this->n_days = DAYS_PER_WEEK;
|
||||
$this->start_dow = $weekstarts;
|
||||
break;
|
||||
case 'month':
|
||||
$this->day_start_interval = 1;
|
||||
$this->n_days = (int) date('t', $time);
|
||||
$this->start_dow = (int) date('N', mktime(12, 0, 0, $this->month, 1, $this->year));
|
||||
break;
|
||||
default:
|
||||
trigger_error("Unsupported view '$this->view'", E_USER_WARNING);
|
||||
break;
|
||||
}
|
||||
|
||||
$this->start_date = (new DateTime())->setTimestamp(get_start_first_slot($this->month, $this->day_start_interval, $this->year));
|
||||
$this->end_date = (new DateTime())->setTimestamp(get_end_last_slot($this->month, $this->day_start_interval + $this->n_days-1, $this->year));
|
||||
|
||||
// Get the data. It's much quicker to do a single SQL query getting all the
|
||||
// entries for the interval in one go, rather than doing a query for each day.
|
||||
$entries = get_entries_by_area($this->area_id, $this->start_date, $this->end_date);
|
||||
|
||||
// We want to build an array containing all the data we want to show and then spit it out.
|
||||
$this->map = new Map($this->start_date, $this->end_date, $resolution);
|
||||
$this->map->addEntries($entries);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Handle the case where there is more than one booking per slot
|
||||
public function innerHTML(): string
|
||||
{
|
||||
global $row_labels_both_sides, $column_labels_both_ends;
|
||||
global $view_all_always_go_to_day_view;
|
||||
|
||||
// It's theoretically possible to display a transposed table with rooms along the top and days
|
||||
// down the side. However, it doesn't seem a very useful display and so hasn't yet been implemented.
|
||||
// The problem is that the results don't look good whether you have the flex direction as 'row' or
|
||||
// 'column'. If you set it to 'row' the bookings are read from left to right within a day, but from
|
||||
// top to bottom within the interval (week/month), so you have to read the display by snaking down
|
||||
// the columns, which is potentially confusing. If you set it to 'column' then the bookings are in
|
||||
// order reading straight down the column, but the text within the bookings is usually clipped unless
|
||||
// the booking lasts the whole day. When the days are along the top and the text is clipped you can
|
||||
// at least see the first few characters which is useful, but when the days are down the side you only
|
||||
// see the top of the line.
|
||||
//
|
||||
// As a result $days_along_top is always true, but is here so that there can be stubs in the code in
|
||||
// case people want a transposed view in future.
|
||||
$days_along_top = true;
|
||||
|
||||
$rooms = get_rooms($this->area_id);
|
||||
$n_rooms = count($rooms);
|
||||
|
||||
// Check to see whether there are any rooms in the area
|
||||
if ($n_rooms == 0)
|
||||
{
|
||||
// Add an 'empty' data flag so that the JavaScript knows whether this is a real table or not
|
||||
return "<tbody data-empty=1><tr><td><h1>" . get_vocab("no_rooms_for_area") . "</h1></td></tr></tbody>";
|
||||
}
|
||||
|
||||
// TABLE HEADER
|
||||
$thead = '<thead';
|
||||
|
||||
$slots = $this->getSlots($this->month, $this->day_start_interval, $this->year, $this->n_days, true);
|
||||
if (isset($slots))
|
||||
{
|
||||
// Add some data to enable the JavaScript to draw the timeline
|
||||
$thead .= ' data-slots="' . escape_html(json_encode($slots)) . '"';
|
||||
$thead .= ' data-timeline-vertical="' . (($days_along_top) ? 'true' : 'false') . '"';
|
||||
$thead .= ' data-timeline-full="true"';
|
||||
}
|
||||
|
||||
$thead .= ">\n";
|
||||
|
||||
if ($days_along_top)
|
||||
{
|
||||
$header_inner_rows = $this->multidayHeaderRowsHTML($this->day_start_interval, $this->n_days, $this->start_dow);
|
||||
}
|
||||
else
|
||||
{
|
||||
// See comment above
|
||||
trigger_error("Not yet implemented", E_USER_WARNING);
|
||||
}
|
||||
|
||||
$thead .= implode('', $header_inner_rows);
|
||||
$thead .= "</thead>\n";
|
||||
|
||||
// Now repeat the header in a footer if required
|
||||
$tfoot = ($column_labels_both_ends) ? "<tfoot>\n" . implode('',array_reverse($header_inner_rows)) . "</tfoot>\n" : '';
|
||||
|
||||
// TABLE BODY LISTING BOOKINGS
|
||||
$tbody = "<tbody>\n";
|
||||
|
||||
$room_link_vars = [
|
||||
'view' => $this->view,
|
||||
'view_all' => 0,
|
||||
'page_date' => format_iso_date($this->year, $this->month, $this->day),
|
||||
'area' => $this->area_id
|
||||
];
|
||||
|
||||
if ($days_along_top)
|
||||
{
|
||||
// the standard view, with days along the top and rooms down the side
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
$room_id = $room['id'];
|
||||
$room_link_vars['room'] = $room_id;
|
||||
$tbody .= "<tr>\n";
|
||||
$row_label = $this->roomCellHTML($room, $room_link_vars);
|
||||
$tbody .= $row_label;
|
||||
|
||||
for ($j = 0, $date = clone $this->start_date; $j < $this->n_days; $j++, $date->modify('+1 day'))
|
||||
{
|
||||
if ($date->isHiddenDay())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add classes for weekends, holidays, etc.
|
||||
$classes = $this->getDateClasses($date);
|
||||
|
||||
$tbody .= '<td';
|
||||
if (!empty($classes))
|
||||
{
|
||||
$tbody .= ' class="' . implode(' ', $classes) . '"';
|
||||
}
|
||||
$tbody .= '>';
|
||||
$vars = [
|
||||
'view_all' => $this->view_all,
|
||||
'page_date' => $date->getISODate(),
|
||||
'area' => $this->area_id,
|
||||
'room' => $room['id']
|
||||
];
|
||||
|
||||
// If there is more than one slot per day, then it can be very difficult to pick
|
||||
// out an individual one, which could be just one pixel wide, so we go to the
|
||||
// day view first where it's easier to see what you are doing. Otherwise, we go
|
||||
// direct to edit_entry.php if the slot is free, or view_entry.php if it is not.
|
||||
// Note: the structure of the cell, with a single link and multiple flex divs,
|
||||
// only allows us to direct to the booking if there's only one slot per day.
|
||||
if ($view_all_always_go_to_day_view || (self::getNTimeSlots() > 1))
|
||||
{
|
||||
$page = 'index.php';
|
||||
$vars['view'] = 'day';
|
||||
}
|
||||
else
|
||||
{
|
||||
$vars['view'] = $this->view;
|
||||
$this_slot = $this->map->slot($room_id, $j, Calendar::morningSlotsSeconds());
|
||||
if (empty($this_slot))
|
||||
{
|
||||
$page = 'edit_entry.php';
|
||||
}
|
||||
else
|
||||
{
|
||||
$page = 'view_entry.php';
|
||||
$vars['id'] = $this_slot[0]['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$link = "$page?" . http_build_query($vars, '', '&');
|
||||
$link = multisite($link);
|
||||
$tbody .= '<a href="' . escape_html($link) . "\">\n";
|
||||
$tbody .= $this->flexDivsHTML($room_id, $j, $j);
|
||||
$tbody .= "</a>\n";
|
||||
$tbody .= "</td>\n";
|
||||
}
|
||||
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$tbody .= $row_label;
|
||||
}
|
||||
$tbody .= "</tr>\n";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// See comment above
|
||||
trigger_error("Not yet implemented", E_USER_WARNING);
|
||||
}
|
||||
|
||||
$tbody .= "</tbody>\n";
|
||||
return $thead . $tfoot . $tbody;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use MRBS\Exception;
|
||||
|
||||
abstract class CalendarMultimonth extends Calendar
|
||||
{
|
||||
protected $n_months;
|
||||
|
||||
public function __construct(string $view, int $view_all, int $year, int $month, int $day, int $area_id, int $room_id)
|
||||
{
|
||||
global $year_start;
|
||||
|
||||
parent::__construct($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
|
||||
if ($view === 'year')
|
||||
{
|
||||
$this->n_months = MONTHS_PER_YEAR;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid view: '$view'");
|
||||
}
|
||||
|
||||
// Get the start and end dates
|
||||
$this->start_date = (new DateTime())->setDate($this->year, $this->month, 1);
|
||||
$this->start_date->setMonthYearStart($year_start);
|
||||
$this->start_date->setStartFirstSlot();
|
||||
|
||||
$this->end_date = clone $this->start_date;
|
||||
$this->end_date->modify('+' . $this->n_months . ' month');
|
||||
$this->end_date->modify('-1 day');
|
||||
$this->end_date->setEndLastSlot();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
|
||||
use MRBS\DateTime;
|
||||
use function MRBS\datetime_format;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\format_iso_date;
|
||||
use function MRBS\get_entries_by_area;
|
||||
use function MRBS\get_rooms;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\multisite;
|
||||
|
||||
class CalendarMultimonthMultiroom extends CalendarMultimonth
|
||||
{
|
||||
public function __construct(string $view, int $view_all, int $year, int $month, int $day, int $area_id, int $room_id)
|
||||
{
|
||||
global $resolution;
|
||||
|
||||
parent::__construct($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
|
||||
// Get the entries. It's much quicker to do a single SQL query getting all the
|
||||
// entries for the interval in one go, rather than doing a query for each day.
|
||||
$entries = get_entries_by_area($this->area_id, $this->start_date, $this->end_date);
|
||||
|
||||
// We want to build an array containing all the data we want to show and then spit it out.
|
||||
$this->map = new Map($this->start_date, $this->end_date, $resolution);
|
||||
$this->map->addEntries($entries);
|
||||
}
|
||||
|
||||
|
||||
public function innerHTML(): string
|
||||
{
|
||||
global $column_labels_both_ends;
|
||||
|
||||
// Check to see whether there are any rooms in the area
|
||||
$rooms = get_rooms($this->area_id);
|
||||
|
||||
if (count($rooms) == 0)
|
||||
{
|
||||
// Add an 'empty' data flag so that the JavaScript knows whether this is a real table or not
|
||||
return "<tbody data-empty=1><tr><td><h1>" . get_vocab("no_rooms_for_area") . "</h1></td></tr></tbody>";
|
||||
}
|
||||
|
||||
// Table header
|
||||
$thead = '<thead';
|
||||
// TODO: get_slots() for JavaScript
|
||||
$thead .= ">\n";
|
||||
$header_row = $this->headerRowHTML();
|
||||
$thead .= $header_row;
|
||||
$thead .= "</thead>\n";
|
||||
|
||||
// Table body
|
||||
$tbody = "<tbody>\n";
|
||||
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
$tbody .= $this->bodyRowHTML($room);
|
||||
}
|
||||
|
||||
$tbody .= "<tbody>\n";
|
||||
|
||||
// Table footer
|
||||
$tfoot = ($column_labels_both_ends) ? "<tfoot>\n$header_row</tfoot>\n" : '';
|
||||
|
||||
return $thead . $tfoot . $tbody;
|
||||
}
|
||||
|
||||
|
||||
private function bodyRowHTML(array $room): string
|
||||
{
|
||||
global $row_labels_both_sides, $year_start;
|
||||
|
||||
$room_link_vars = [
|
||||
'view' => $this->view,
|
||||
'view_all' => 0,
|
||||
'page_date' => format_iso_date($this->year, $this->month, $this->day),
|
||||
'area' => $this->area_id
|
||||
];
|
||||
|
||||
$html = "<tr>\n";
|
||||
$room_link_vars['room'] = $room['id'];
|
||||
$row_label = $this->roomCellHTML($room, $room_link_vars);
|
||||
$html .= $row_label;
|
||||
|
||||
$date = (new DateTime())->setDate($this->year, $this->month, $this->day);
|
||||
$date->setMonthYearStart($year_start);
|
||||
|
||||
// The variables for the link query string
|
||||
$vars = [
|
||||
'view' => 'month',
|
||||
'view_all' => 0,
|
||||
'area' => $this->area_id,
|
||||
'room' => $room['id']
|
||||
];
|
||||
|
||||
$j = 0; // Need to keep track of the day in the Calendar interval (zero indexed)
|
||||
|
||||
// Loop through the months in the interval
|
||||
for ($i=0; $i<$this->n_months; $i++)
|
||||
{
|
||||
$html .= "<td>\n";
|
||||
$vars['page_date'] = $date->getISODate();
|
||||
$link = 'index.php?' . http_build_query($vars, '', '&');
|
||||
$link = multisite($link);
|
||||
$html .= '<a href="' . escape_html($link) . '">';
|
||||
$days_in_month = $date->getDaysInMonth();
|
||||
$html .= $this->flexDivsHTML($room['id'], $j, $j + $days_in_month - 1);
|
||||
$html .= '</a>';
|
||||
$html .= "</td>\n";
|
||||
$j += $days_in_month;
|
||||
$date->modifyMonthsNoOverflow(1, true); // Advance 1 month
|
||||
}
|
||||
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$html .= $row_label;
|
||||
}
|
||||
$html .= "</tr>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
private function headerRowHTML(string $label='') : string
|
||||
{
|
||||
global $datetime_formats, $row_labels_both_sides, $year_start;
|
||||
|
||||
$html = "<tr>\n";
|
||||
|
||||
// The left-hand header column
|
||||
$first_last_html = '<th class="first_last">' . escape_html($label) . "</th>\n";
|
||||
$html .= $first_last_html;
|
||||
|
||||
// The main header cells
|
||||
$date = (new DateTime())->setDate($this->year, $this->month, $this->day);
|
||||
$date->setMonthYearStart($year_start);
|
||||
// The variables for the link query string
|
||||
$vars = [
|
||||
'view' => 'month',
|
||||
'view_all' => $this->view_all,
|
||||
'area' => $this->area_id,
|
||||
'room' => $this->room_id
|
||||
];
|
||||
|
||||
for ($i=0; $i<$this->n_months; $i++)
|
||||
{
|
||||
$vars['page_date'] = $date->getISODate();
|
||||
$link = 'index.php?' . http_build_query($vars, '', '&');
|
||||
$link = multisite($link);
|
||||
$month_name = datetime_format($datetime_formats['month_name_year_view'], $date->getTimestamp());
|
||||
$html .= '<th><a href="' . escape_html($link) . '">' . escape_html($month_name) . "</a></th>\n";
|
||||
$date->modifyMonthsNoOverflow(1, true);
|
||||
}
|
||||
|
||||
// The right-hand header column, if required
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$html .= $first_last_html;
|
||||
}
|
||||
|
||||
$html .= "</tr>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use function MRBS\datetime_format;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\get_entries_by_room;
|
||||
use function MRBS\multisite;
|
||||
|
||||
class CalendarMultimonthOneRoom extends CalendarMultimonth
|
||||
{
|
||||
|
||||
public function __construct($view, $view_all, $year, $month, $day, $area_id, $room_id)
|
||||
{
|
||||
global $resolution;
|
||||
|
||||
parent::__construct($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
|
||||
// Get the entries. It's much quicker to do a single SQL query getting all the
|
||||
// entries for the interval in one go, rather than doing a query for each day.
|
||||
$entries = get_entries_by_room($this->room_id, $this->start_date, $this->end_date);
|
||||
|
||||
// We want to build an array containing all the data we want to show and then spit it out.
|
||||
$this->map = new Map($this->start_date, $this->end_date, $resolution);
|
||||
$this->map->addEntries($entries);
|
||||
}
|
||||
|
||||
public function innerHTML(): string
|
||||
{
|
||||
global $column_labels_both_ends;
|
||||
|
||||
// Table header
|
||||
$thead = '<thead';
|
||||
// TODO: get_slots() for JavaScript
|
||||
$thead .= ">\n";
|
||||
$header_row = $this->headerRowHTML();
|
||||
$thead .= $header_row;
|
||||
$thead .= "</thead>\n";
|
||||
|
||||
// Table body
|
||||
$tbody = $this->bodyHTML();
|
||||
|
||||
// Table footer
|
||||
$tfoot = ($column_labels_both_ends) ? "<tfoot>\n$header_row</tfoot>\n" : '';
|
||||
|
||||
return $thead . $tfoot . $tbody;
|
||||
}
|
||||
|
||||
|
||||
private function bodyHTML(): string
|
||||
{
|
||||
global $year_start, $datetime_formats, $row_labels_both_sides;
|
||||
|
||||
$html = "<tbody>\n";
|
||||
|
||||
// The variables for the link query string
|
||||
$vars = [
|
||||
'view_all' => $this->view_all,
|
||||
'area' => $this->area_id,
|
||||
'room' => $this->room_id
|
||||
];
|
||||
|
||||
$date = (new DateTime())->setDate($this->year, $this->month, 1); // Set to first day of month
|
||||
$date->setMonthYearStart($year_start);
|
||||
|
||||
$d = 0; // Need to keep track of the day in the Calendar interval (zero indexed)
|
||||
|
||||
for ($i=0; $i<$this->n_months; $i++)
|
||||
{
|
||||
$html .= "<tr>\n";
|
||||
$vars['page_date'] = $date->getISODate();
|
||||
$vars['view'] = 'month';
|
||||
$link = 'index.php?' . http_build_query($vars, '', '&');
|
||||
$link = multisite($link);
|
||||
$month_name = datetime_format($datetime_formats['month_name_year_view'], $date->getTimestamp());
|
||||
$first_last_html = '<th><a href="' . escape_html($link) . '">' . escape_html($month_name) . "</a></th>\n";
|
||||
$html .= $first_last_html;
|
||||
|
||||
for ($j=1; $j<=$date->getDaysInMonth(); $j++)
|
||||
{
|
||||
$date->setDay($j);
|
||||
// Although it's possible to add date classes (eg for weekends and holidays) the result
|
||||
// doesn't look very good on the screen because the weekdays aren't all in the same column.
|
||||
$html .= "<td>";
|
||||
$vars['page_date'] = $date->getISODate();
|
||||
$vars['view'] = 'day';
|
||||
$link = 'index.php?' . http_build_query($vars, '', '&');
|
||||
$link = multisite($link);
|
||||
$html .= '<a href="' . escape_html($link) . '">';
|
||||
$html .= $this->flexDivsHTML($this->room_id, $d, $d);
|
||||
$html .= '</a>';
|
||||
$html .= "</td>";
|
||||
$d++;
|
||||
}
|
||||
|
||||
// Fill in the remaining, invalid, days
|
||||
while ($j <= 31)
|
||||
{
|
||||
$html .= '<td class="invalid"></td>';
|
||||
$j++;
|
||||
}
|
||||
|
||||
// The right-hand header column, if required
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$html .= $first_last_html;
|
||||
}
|
||||
$html .= "</tr>\n";
|
||||
$date->modify('+1 day'); // Advance to the next month
|
||||
}
|
||||
|
||||
$html .= "</tbody>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
private function headerRowHTML(string $label='') : string
|
||||
{
|
||||
global $row_labels_both_sides;
|
||||
|
||||
$html = "<tr>\n";
|
||||
|
||||
// The left-hand header column
|
||||
$first_last_html = '<th class="first_last">' . escape_html($label) . "</th>\n";
|
||||
$html .= $first_last_html;
|
||||
|
||||
// Choose a month (January) with 31 days and cycle through all the days.
|
||||
// We can't add a link to the header cells because we don't know which month they refer to.
|
||||
for ($d=1; $d <= 31; $d++)
|
||||
{
|
||||
$t = mktime(12, 0, 0, 1, $d, $this->year);
|
||||
$html .= "<th>" . escape_html($this->getDate($t)) . "</th>\n";
|
||||
}
|
||||
|
||||
// The right-hand header column, if required
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$html .= $first_last_html;
|
||||
}
|
||||
|
||||
$html .= "</tr>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use MRBS\Period;
|
||||
use MRBS\Periods;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\getWritable;
|
||||
use function MRBS\hm_before;
|
||||
use function MRBS\hour_min;
|
||||
use function MRBS\is_book_admin;
|
||||
use function MRBS\multisite;
|
||||
use function MRBS\period_index_nominal;
|
||||
use function MRBS\session;
|
||||
|
||||
abstract class CalendarMultislot extends Calendar
|
||||
{
|
||||
protected $timetohighlight;
|
||||
|
||||
|
||||
// $s is nominal seconds
|
||||
protected function getQueryVars(int $room, int $month, int $day, int $year, int $s) : array
|
||||
{
|
||||
global $morningstarts, $morningstarts_minutes;
|
||||
|
||||
$result = [];
|
||||
|
||||
// check to see if the time is really on the next day
|
||||
$date = getdate(mktime(0, 0, $s, $month, $day, $year));
|
||||
if (hm_before($date, ['hours' => $morningstarts, 'minutes' => $morningstarts_minutes]))
|
||||
{
|
||||
$date['hours'] += 24;
|
||||
}
|
||||
$hour = $date['hours'];
|
||||
$minute = $date['minutes'];
|
||||
$period = period_index_nominal($s);
|
||||
|
||||
$vars = [
|
||||
'view' => $this->view,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $this->area_id
|
||||
];
|
||||
|
||||
$result['booking'] = $vars;
|
||||
$result['new_periods'] = array_merge($vars, ['room' => $room, 'period' => $period]);
|
||||
$result['new_times'] = array_merge($vars, ['room' => $room, 'hour' => $hour, 'minute' => $minute]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
protected function tdHTML(array $cell, array $query_vars, bool $is_invalid = false) : string
|
||||
{
|
||||
// Draws a single cell in the main table of the day and week views
|
||||
//
|
||||
// $cell is an array of entries that occupy that cell. There can be none, one or many
|
||||
// bookings in a cell. If there are no bookings, then a blank cell is drawn with a link
|
||||
// to the edit entry form. If there is one booking, then the booking is shown in that
|
||||
// cell. If there is more than one booking, then all the bookings are shown.
|
||||
|
||||
// $query_vars is an array containing the query vars to be used in the link for the cell.
|
||||
// It is indexed as follows:
|
||||
// ['new_periods'] the vars to be used for an empty cell if using periods
|
||||
// ['new_times'] the vars to be used for an empty cell if using times
|
||||
// ['booking'] the vars to be used for a full cell
|
||||
//
|
||||
// $is_invalid specifies whether the slot actually exists or is one of the non-existent
|
||||
// slots in the transition to DST
|
||||
|
||||
global $enable_periods, $show_plus_link, $prevent_booking_on_holidays, $prevent_booking_on_weekends;
|
||||
|
||||
$html = '';
|
||||
$classes = array();
|
||||
|
||||
// Don't put in a <td> cell if the slot contains a single booking whose n_slots is NULL.
|
||||
// This would mean that it's the second or subsequent slot of a booking and so the
|
||||
// <td> for the first slot would have had a rowspan that extended the cell down for
|
||||
// the number of slots of the booking.
|
||||
|
||||
if (empty($cell) || !is_null($cell[0]['n_slots']))
|
||||
{
|
||||
if (!empty($cell))
|
||||
{
|
||||
$classes[] = 'booked';
|
||||
if (count($cell) > 1)
|
||||
{
|
||||
$classes[] = 'multiply';
|
||||
}
|
||||
}
|
||||
elseif ($is_invalid)
|
||||
{
|
||||
$classes[] = 'invalid';
|
||||
}
|
||||
else
|
||||
{
|
||||
$classes[] = 'new';
|
||||
// Add classes for weekends and holidays
|
||||
$date = new DateTime();
|
||||
$date->setDate(
|
||||
$query_vars['new_times']['year'],
|
||||
$query_vars['new_times']['month'],
|
||||
$query_vars['new_times']['day']
|
||||
);
|
||||
$classes = array_merge($classes, $this->getDateClasses($date));
|
||||
}
|
||||
|
||||
// If there's no booking, or if there are multiple bookings, then make the slot one unit long
|
||||
$slots = (count($cell) == 1) ? $cell[0]['n_slots'] : 1;
|
||||
|
||||
$html .= $this->tdOpeningTagHTML($classes, $slots);
|
||||
|
||||
// If the room isn't booked, then allow it to be booked
|
||||
if (empty($cell))
|
||||
{
|
||||
// Don't provide a link if the slot doesn't really exist or if the user is logged in, but not a booking admin,
|
||||
// and it's a holiday/weekend and bookings on holidays/weekends are not allowed. (We provide a link if they
|
||||
// are not logged in because they might want to click and login as an admin).
|
||||
if ($is_invalid ||
|
||||
((null !== session()->getCurrentUser()) && !is_book_admin($query_vars['new_times']['room']) &&
|
||||
(($prevent_booking_on_holidays && in_array('holiday', $classes)) ||
|
||||
($prevent_booking_on_weekends && in_array('weekend', $classes)))))
|
||||
{
|
||||
$html .= '<span class="not_allowed"></span>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$vars = ($enable_periods) ? $query_vars['new_periods'] : $query_vars['new_times'];
|
||||
$query = http_build_query($vars, '', '&');
|
||||
|
||||
$html .= '<a href="' . escape_html(multisite("edit_entry.php?$query")) . '"' .
|
||||
' aria-label="' . escape_html(get_vocab('create_new_booking')) . "\">";
|
||||
if ($show_plus_link)
|
||||
{
|
||||
$html .= "<img src=\"images/new.gif\" alt=\"New\" width=\"10\" height=\"10\">";
|
||||
}
|
||||
$html .= "</a>";
|
||||
}
|
||||
}
|
||||
else // if it is booked, then show the booking
|
||||
{
|
||||
foreach ($cell as $booking)
|
||||
{
|
||||
$vars = $query_vars['booking'];
|
||||
$vars['id'] = $booking['id'];
|
||||
$query = http_build_query($vars, '', '&');
|
||||
|
||||
// We have to wrap the booking in a <div> because we want the booking itself to be given
|
||||
// an absolute position, and we can't use position relative on a <td> in IE11 and below.
|
||||
// We also need the bookings in a container because jQuery UI resizable has problems
|
||||
// with border-box (see https://stackoverflow.com/questions/18344272). And we need
|
||||
// border-box for the bookings because we are using padding on the bookings and we want
|
||||
// 'width: 100%' and 'height: 100%' to fill the table-cell with the entire booking
|
||||
// including content.
|
||||
|
||||
$classes = $this->getEntryClasses($booking);
|
||||
$classes[] = 'booking';
|
||||
|
||||
if ($booking['is_multiday_start'])
|
||||
{
|
||||
$classes[] = 'multiday_start';
|
||||
}
|
||||
|
||||
if ($booking['is_multiday_end'])
|
||||
{
|
||||
$classes[] = 'multiday_end';
|
||||
}
|
||||
|
||||
// Tell JavaScript to make bookings resizable
|
||||
if ((count($cell) == 1) &&
|
||||
getWritable($booking['create_by'], $booking['room_id']))
|
||||
{
|
||||
$classes[] = 'writable';
|
||||
}
|
||||
|
||||
$html .= '<div class="' . implode(' ', $classes) . '">';
|
||||
$html .= '<a href="' . escape_html(multisite("view_entry.php?$query")) . '"' .
|
||||
' title="' . escape_html($booking['description'] ?? '') . '"' .
|
||||
' class="' . $booking['type'] . '"' .
|
||||
' data-id="' . $booking['id'] . '"' .
|
||||
' data-type="' . $booking['type'] . '">';
|
||||
$html .= escape_html($booking['name']) . '</a>';
|
||||
$html .= "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
$html .= "</td>\n";
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
// Output a start table cell tag <td> with class of $classes.
|
||||
// $classes can be either a string or an array of classes
|
||||
// empty or row_highlight if highlighted.
|
||||
// $slots is the number of time slots high that the cell should be
|
||||
//
|
||||
// $data is an optional third parameter which if set passes an
|
||||
// associative array of name-value pairs to be used in data attributes
|
||||
private function tdOpeningTagHTML(array $classes, int $slots, ?array $data=null) : string
|
||||
{
|
||||
global $times_along_top;
|
||||
|
||||
$html = '<td';
|
||||
|
||||
if (!empty($classes))
|
||||
{
|
||||
$html.= ' class="' . implode(' ', $classes) . '"';
|
||||
}
|
||||
|
||||
if ($slots > 1)
|
||||
// No need to output more HTML than necessary
|
||||
{
|
||||
$html .= (($times_along_top) ? ' colspan' : ' rowspan') . "=\"$slots\"";
|
||||
}
|
||||
|
||||
if (isset($data))
|
||||
{
|
||||
foreach ($data as $name => $value)
|
||||
{
|
||||
$html .= " data-$name=\"$value\"";
|
||||
}
|
||||
}
|
||||
|
||||
$html .= ">";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
// Draw a time cell to be used in the first and last columns of the day and week views
|
||||
// $s the number of seconds since the start of the day (nominal - not adjusted for DST)
|
||||
// $url the url to form the basis of the link in the time cell
|
||||
function tbodyThTimeCellHTML(int $s, string $url) : string
|
||||
{
|
||||
global $enable_periods, $resolution;
|
||||
|
||||
$html = '';
|
||||
|
||||
$html .= "<th data-seconds=\"$s\">";
|
||||
$html .= '<a href="' . escape_html($url) . '" title="' . get_vocab("highlight_line") . "\">";
|
||||
|
||||
if ($enable_periods)
|
||||
{
|
||||
$periods = Periods::getForArea($this->area_id);
|
||||
$html .= escape_html($periods->offsetGetByNominalSeconds($s)->name);
|
||||
}
|
||||
else
|
||||
{
|
||||
$html .= escape_html($this->timeslotText($s, $resolution));
|
||||
}
|
||||
|
||||
$html .= "</a></th>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
protected function theadThTimeCellsHTML(int $start, int $end, int $increment) : string
|
||||
{
|
||||
global $enable_periods;
|
||||
|
||||
$html = '';
|
||||
|
||||
for ($s = $start; $s <= $end; $s += $increment)
|
||||
{
|
||||
// Put the number of seconds since the start of the day (nominal, ignoring DST)
|
||||
// in a data attribute so that JavaScript can pick it up
|
||||
$html .= "<th data-seconds=\"$s\">";
|
||||
// We need the span so that we can apply some padding. We can't apply it
|
||||
// to the <th> because that is used by jQuery.offset() in resizable bookings
|
||||
$html .= "<span>";
|
||||
if ( $enable_periods )
|
||||
{
|
||||
$periods = Periods::getForArea($this->area_id);
|
||||
$html .= escape_html($periods->offsetGetByNominalSeconds($s)->name);
|
||||
}
|
||||
else
|
||||
{
|
||||
$html .= escape_html($this->timeslotText($s, $increment));
|
||||
}
|
||||
$html .= "</span>";
|
||||
$html .= "</th>\n";
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
private function timeslotText(int $s, int $resolution) : string
|
||||
{
|
||||
global $show_slot_endtime;
|
||||
|
||||
$result = hour_min($s);
|
||||
|
||||
if ($show_slot_endtime)
|
||||
{
|
||||
$result .= '-' . hour_min($s + $resolution);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\get_entries_by_area;
|
||||
use function MRBS\get_room_details;
|
||||
use function MRBS\get_rooms;
|
||||
use function MRBS\get_start_first_slot;
|
||||
use function MRBS\get_start_last_slot;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\is_invalid_datetime;
|
||||
use function MRBS\is_possibly_invalid;
|
||||
use function MRBS\multisite;
|
||||
|
||||
class CalendarMultislotDay extends CalendarMultislot
|
||||
{
|
||||
private $kiosk;
|
||||
|
||||
|
||||
public function __construct(string $view, int $view_all, int $year, int $month, int $day, int $area_id, int $room_id, ?int $timetohighlight=null, ?string $kiosk=null)
|
||||
{
|
||||
parent::__construct($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
$this->timetohighlight = $timetohighlight;
|
||||
$this->kiosk = $kiosk;
|
||||
}
|
||||
|
||||
public function innerHTML() : string
|
||||
{
|
||||
global $enable_periods;
|
||||
global $times_along_top, $row_labels_both_sides, $column_labels_both_ends;
|
||||
global $resolution, $morningstarts, $morningstarts_minutes;
|
||||
|
||||
if ($this->kiosk === 'room')
|
||||
{
|
||||
$rooms = array(get_room_details($this->room_id));
|
||||
}
|
||||
else
|
||||
{
|
||||
$rooms = get_rooms($this->area_id);
|
||||
}
|
||||
|
||||
$n_rooms = count($rooms);
|
||||
|
||||
if ($n_rooms == 0)
|
||||
{
|
||||
// Add an 'empty' data flag so that the JavaScript knows whether this is a real table or not
|
||||
return "<tbody data-empty=1><tr><td><h1>" . get_vocab("no_rooms_for_area") . "</h1></td></tr></tbody>";
|
||||
}
|
||||
|
||||
$start_first_slot = get_start_first_slot($this->month, $this->day, $this->year);
|
||||
$start_last_slot = get_start_last_slot($this->month, $this->day, $this->year);
|
||||
// Keep a count of the number of slots at the start of the day that we're
|
||||
// not showing (will only be relevant in kiosk mode).
|
||||
$skipped_slots = 0;
|
||||
|
||||
// If we are in kiosk mode we are not interested in what has already happened.
|
||||
// But if we are in periods mode we don't know when the periods occur, so show them all.
|
||||
if (isset($kiosk) && !$enable_periods)
|
||||
{
|
||||
$now = time();
|
||||
$start_next_slot = $start_first_slot + $resolution;
|
||||
while (($now > $start_next_slot) && ($start_next_slot < $start_last_slot))
|
||||
{
|
||||
$start_first_slot = $start_next_slot;
|
||||
$skipped_slots++;
|
||||
$start_next_slot = $start_first_slot + $resolution;
|
||||
}
|
||||
}
|
||||
|
||||
// Work out whether there's a possibility that a time slot is invalid,
|
||||
// in other words whether the booking day includes a transition into DST.
|
||||
// If we know that there's a transition into DST then some of the slots are
|
||||
// going to be invalid. Knowing whether or not there are possibly invalid slots
|
||||
// saves us bothering to do the detailed calculations of which slots are invalid.
|
||||
$is_possibly_invalid = !$enable_periods && is_possibly_invalid($start_first_slot, $start_last_slot);
|
||||
|
||||
$start_date = (new DateTime())->setTimestamp($start_first_slot);
|
||||
$end_date = (new DateTime())->setTimestamp($start_last_slot + $resolution);
|
||||
|
||||
$entries = get_entries_by_area($this->area_id, $start_date, $end_date);
|
||||
|
||||
// We want to build a map containing all the data we want to show
|
||||
// and then spit it out.
|
||||
$map = new Map($start_date, $end_date, $resolution);
|
||||
$map->addEntries($entries);
|
||||
|
||||
$n_time_slots = self::getNTimeSlots() - $skipped_slots;
|
||||
$morning_slot_seconds = ((($morningstarts * 60) + $morningstarts_minutes) * 60) + ($skipped_slots * $resolution);
|
||||
$evening_slot_seconds = $morning_slot_seconds + (($n_time_slots - 1) * $resolution);
|
||||
|
||||
// TABLE HEADER
|
||||
$thead = '<thead';
|
||||
|
||||
$slots = $this->getSlots($this->month, $this->day, $this->year);
|
||||
if (isset($slots))
|
||||
{
|
||||
// Remove the skipped slots from the start of the first day's array
|
||||
for ($i=0; $i<$skipped_slots; $i++)
|
||||
{
|
||||
array_shift($slots[0]);
|
||||
}
|
||||
// Add some data to enable the JavaScript to draw the timeline
|
||||
$thead .= ' data-slots="' . escape_html(json_encode($slots)) . '"';
|
||||
$thead .= ' data-timeline-vertical="' . (($times_along_top) ? 'true' : 'false') . '"';
|
||||
$thead .= ' data-timeline-full="true"';
|
||||
}
|
||||
|
||||
$thead .= ">\n";
|
||||
|
||||
$header_inner = "<tr>\n";
|
||||
|
||||
if ($times_along_top)
|
||||
{
|
||||
$tag = 'room';
|
||||
}
|
||||
elseif ($enable_periods)
|
||||
{
|
||||
$tag = 'period';
|
||||
}
|
||||
else
|
||||
{
|
||||
$tag = 'time';
|
||||
}
|
||||
|
||||
$first_last_html = '<th class="first_last">' . get_vocab($tag) . "</th>\n";
|
||||
$header_inner .= $first_last_html;
|
||||
|
||||
// We can display the table in two ways
|
||||
if ($times_along_top)
|
||||
{
|
||||
$header_inner .= $this->theadThTimeCellsHTML($morning_slot_seconds, $evening_slot_seconds, $resolution);
|
||||
}
|
||||
else
|
||||
{
|
||||
$vars = [
|
||||
'view' => 'week',
|
||||
'view_all' => 0,
|
||||
'year' => $this->year,
|
||||
'month' => $this->month,
|
||||
'day' => $this->day,
|
||||
'area' => $this->area_id
|
||||
];
|
||||
|
||||
$header_inner .= $this->roomsHeaderCellsHTML($rooms, $vars);
|
||||
} // end standard view (for the header)
|
||||
|
||||
// next: line to display times on right side
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$header_inner .= $first_last_html;
|
||||
}
|
||||
|
||||
$header_inner .= "</tr>\n";
|
||||
$thead .= $header_inner;
|
||||
$thead .= "</thead>\n";
|
||||
|
||||
// Now repeat the header in a footer if required
|
||||
$tfoot = ($column_labels_both_ends) ? "<tfoot>\n$header_inner</tfoot>\n" : '';
|
||||
|
||||
// TABLE BODY LISTING BOOKINGS
|
||||
$tbody = "<tbody>\n";
|
||||
|
||||
// This is the main bit of the display
|
||||
// We loop through time and then the rooms we just got
|
||||
|
||||
// if the today is a day which includes a DST change then use
|
||||
// the day after to generate timesteps through the day as this
|
||||
// will ensure a constant time step
|
||||
|
||||
// We can display the table in two ways
|
||||
if ($times_along_top)
|
||||
{
|
||||
// with times along the top and rooms down the side
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
$tbody .= "<tr>\n";
|
||||
|
||||
$vars = array('view' => 'week',
|
||||
'view_all' => 0,
|
||||
'year' => $this->year,
|
||||
'month' => $this->month,
|
||||
'day' => $this->day,
|
||||
'area' => $this->area_id,
|
||||
'room' => $room['id']);
|
||||
|
||||
$row_label = $this->roomCellHTML($room, $vars);
|
||||
$tbody .= $row_label;
|
||||
$is_invalid = array();
|
||||
for ($s = $morning_slot_seconds;
|
||||
$s <= $evening_slot_seconds;
|
||||
$s += $resolution)
|
||||
{
|
||||
// Work out whether this timeslot is invalid and save the result, so that we
|
||||
// don't have to repeat the calculation for every room
|
||||
if (!isset($is_invalid[$s]))
|
||||
{
|
||||
$is_invalid[$s] = $is_possibly_invalid && is_invalid_datetime(0, 0, $s, $this->month, $this->day, $this->year);
|
||||
}
|
||||
// set up the query vars to be used for the link in the cell
|
||||
$query_vars = $this->getQueryVars($room['id'], $this->month, $this->day, $this->year, $s);
|
||||
|
||||
// and then draw the cell
|
||||
$tbody .= $this->tdHTML($map->slot($room['id'], 0, $s), $query_vars, $is_invalid[$s]);
|
||||
} // end for (looping through the times)
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$tbody .= $row_label;
|
||||
}
|
||||
$tbody .= "</tr>\n";
|
||||
} // end for (looping through the rooms)
|
||||
} // end "times_along_top" view (for the body)
|
||||
|
||||
else
|
||||
{
|
||||
// the standard view, with rooms along the top and times down the side
|
||||
for ($s = $morning_slot_seconds;
|
||||
$s <= $evening_slot_seconds;
|
||||
$s += $resolution)
|
||||
{
|
||||
// Show the time linked to the URL for highlighting that time
|
||||
$classes = array();
|
||||
|
||||
$vars = array(
|
||||
'view' => 'day',
|
||||
'year' => $this->year,
|
||||
'month' => $this->month,
|
||||
'day' => $this->day,
|
||||
'area' => $this->area_id
|
||||
);
|
||||
|
||||
if (isset($this->room_id))
|
||||
{
|
||||
$vars['room'] = $this->room_id;
|
||||
}
|
||||
|
||||
if (isset($this->timetohighlight) && ($s == $this->timetohighlight))
|
||||
{
|
||||
$classes[] = 'row_highlight';
|
||||
}
|
||||
else
|
||||
{
|
||||
$vars['timetohighlight'] = $s;
|
||||
}
|
||||
|
||||
$url = "index.php?" . http_build_query($vars, '', '&');
|
||||
$url = multisite($url);
|
||||
|
||||
$tbody .= '<tr';
|
||||
if (!empty($classes))
|
||||
{
|
||||
$tbody .= ' class="' . implode(' ', $classes) . '"';
|
||||
}
|
||||
$tbody .= ">\n";
|
||||
|
||||
$tbody .= $this->tbodyThTimeCellHTML($s, $url);
|
||||
$is_invalid = $is_possibly_invalid && is_invalid_datetime(0, 0, $s, $this->month, $this->day, $this->year);
|
||||
// Loop through the list of rooms we have for this area
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
// set up the query vars to be used for the link in the cell
|
||||
$query_vars = $this->getQueryVars($room['id'], $this->month, $this->day, $this->year, $s);
|
||||
$tbody .= $this->tdHTML($map->slot($room['id'], 0, $s), $query_vars, $is_invalid);
|
||||
}
|
||||
|
||||
// next lines to display times on right side
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$tbody .= $this->tbodyThTimeCellHTML($s, $url);
|
||||
}
|
||||
|
||||
$tbody .= "</tr>\n";
|
||||
}
|
||||
} // end standard view (for the body)
|
||||
|
||||
$tbody .= "</tbody>\n";
|
||||
|
||||
return $thead . $tfoot . $tbody;
|
||||
}
|
||||
|
||||
|
||||
private function roomsHeaderCellsHTML(array $rooms, array $vars) : string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
foreach($rooms as $room)
|
||||
{
|
||||
$vars['room'] = $room['id'];
|
||||
$html .= $this->roomCellHTML($room, $vars);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use MRBS\DateTime;
|
||||
use function MRBS\datetime_format;
|
||||
use function MRBS\day_of_MRBS_week;
|
||||
use function MRBS\escape_html;
|
||||
use function MRBS\get_entries_by_room;
|
||||
use function MRBS\get_room_name;
|
||||
use function MRBS\get_start_first_slot;
|
||||
use function MRBS\get_start_last_slot;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\is_invalid_datetime;
|
||||
use function MRBS\is_possibly_invalid;
|
||||
use function MRBS\is_visible;
|
||||
use function MRBS\multisite;
|
||||
|
||||
class CalendarMultislotWeek extends CalendarMultislot
|
||||
{
|
||||
|
||||
public function __construct(string $view, int $view_all, int $year, int $month, int $day, int $area_id, int $room_id, ?int $timetohighlight=null)
|
||||
{
|
||||
parent::__construct($view, $view_all, $year, $month, $day, $area_id, $room_id);
|
||||
$this->timetohighlight = $timetohighlight;
|
||||
}
|
||||
|
||||
|
||||
public function innerHTML(): string
|
||||
{
|
||||
global $enable_periods;
|
||||
global $times_along_top, $row_labels_both_sides, $column_labels_both_ends;
|
||||
global $resolution, $morningstarts, $morningstarts_minutes;
|
||||
global $weekstarts, $datetime_formats;
|
||||
|
||||
// Check that we've got a valid, enabled room
|
||||
$room_name = get_room_name($this->room_id);
|
||||
|
||||
if (is_null($room_name) || (!is_visible($this->room_id)))
|
||||
{
|
||||
// No rooms have been created yet, or else they are all disabled
|
||||
// Add an 'empty' data flag so that the JavaScript knows whether this is a real table or not
|
||||
return "<tbody data-empty=1><tr><td><h1>".get_vocab("no_rooms_for_area")."</h1></td></tr></tbody>";
|
||||
}
|
||||
|
||||
// We have a valid room
|
||||
// Calculate how many days to skip back to get to the start of the week
|
||||
$time = mktime(12, 0, 0, $this->month, $this->day, $this->year);
|
||||
$skipback = day_of_MRBS_week($time);
|
||||
$day_start_week = $this->day - $skipback;
|
||||
// We will use $day for links and $day_start_week for anything to do with showing the bookings,
|
||||
// because we want the booking display to start on the first day of the week (eg Sunday if $weekstarts is 0)
|
||||
// but we want to preserve the notion of the current day (or 'sticky day') when switching between pages
|
||||
|
||||
// Define the start and end of each day of the week in a way which is not
|
||||
// affected by daylight saving...
|
||||
for ($j = 0; $j < DAYS_PER_WEEK; $j++)
|
||||
{
|
||||
$start_first_slot[$j] = get_start_first_slot($this->month, $day_start_week+$j, $this->year);
|
||||
$start_last_slot[$j] = get_start_last_slot($this->month, $day_start_week+$j, $this->year);
|
||||
// Work out whether there's a possibility that a time slot is invalid,
|
||||
// in other words whether the booking day includes a transition into DST.
|
||||
// If we know that there's a transition into DST then some of the slots are
|
||||
// going to be invalid. Knowing whether or not there are possibly invalid slots
|
||||
// saves us bothering to do the detailed calculations of which slots are invalid.
|
||||
$is_possibly_invalid[$j] = !$enable_periods && is_possibly_invalid($start_first_slot[$j], $start_last_slot[$j]);
|
||||
}
|
||||
unset($j); // Just so that we pick up any accidental attempt to use it later
|
||||
|
||||
$start_date = (new DateTime())->setTimestamp($start_first_slot[0]);
|
||||
$end_date = (new DateTime())->setTimestamp($start_last_slot[DAYS_PER_WEEK - 1] + $resolution);
|
||||
|
||||
// Get the data. It's much quicker to do a single SQL query getting all the
|
||||
// entries for the interval in one go, rather than doing a query for each day.
|
||||
$entries = get_entries_by_room($this->room_id, $start_date, $end_date);
|
||||
|
||||
$map = new Map($start_date, $end_date, $resolution);
|
||||
$map->addEntries($entries);
|
||||
|
||||
// START DISPLAYING THE MAIN TABLE
|
||||
$n_time_slots = self::getNTimeSlots();
|
||||
$morning_slot_seconds = (($morningstarts * 60) + $morningstarts_minutes) * 60;
|
||||
$evening_slot_seconds = $morning_slot_seconds + (($n_time_slots - 1) * $resolution);
|
||||
|
||||
// TABLE HEADER
|
||||
$thead = '<thead';
|
||||
|
||||
$slots = $this->getSlots($this->month, $day_start_week, $this->year, DAYS_PER_WEEK);
|
||||
if (isset($slots))
|
||||
{
|
||||
// Add some data to enable the JavaScript to draw the timeline
|
||||
$thead .= ' data-slots="' . escape_html(json_encode($slots)) . '"';
|
||||
$thead .= ' data-timeline-vertical="' . (($times_along_top) ? 'true' : 'false') . '"';
|
||||
$thead .= ' data-timeline-full="false"';
|
||||
}
|
||||
$thead .= ">\n";
|
||||
|
||||
if ($times_along_top)
|
||||
{
|
||||
$tag = 'date';
|
||||
}
|
||||
elseif ($enable_periods)
|
||||
{
|
||||
$tag = 'period';
|
||||
}
|
||||
else
|
||||
{
|
||||
$tag = 'time';
|
||||
}
|
||||
$label = get_vocab($tag);
|
||||
|
||||
// We can display the table in two ways
|
||||
if ($times_along_top)
|
||||
{
|
||||
$header_inner = "<tr>\n";
|
||||
$first_last_html = '<th class="first_last">' . $label . "</th>\n";
|
||||
$header_inner .= $first_last_html;
|
||||
$header_inner .= $this->theadThTimeCellsHTML($morning_slot_seconds, $evening_slot_seconds, $resolution);
|
||||
// next line to display times on right side
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$header_inner .= $first_last_html;
|
||||
}
|
||||
$header_inner .= "</tr>\n";
|
||||
$header_inner_rows = [$header_inner];
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
$header_inner_rows = $this->multidayHeaderRowsHTML($day_start_week, DAYS_PER_WEEK, $weekstarts, $label);
|
||||
}
|
||||
|
||||
|
||||
$thead .= implode('', $header_inner_rows);
|
||||
$thead .= "</thead>\n";
|
||||
|
||||
// Now repeat the header in a footer if required
|
||||
$tfoot = ($column_labels_both_ends) ? "<tfoot>\n" . implode('',array_reverse($header_inner_rows)) . "</tfoot>\n" : '';
|
||||
|
||||
// TABLE BODY LISTING BOOKINGS
|
||||
$tbody = "<tbody>\n";
|
||||
|
||||
// We can display the table in two ways
|
||||
if ($times_along_top)
|
||||
{
|
||||
$format = $datetime_formats['view_week_day_date_month'];
|
||||
// with times along the top and days of the week down the side
|
||||
// See note above: weekday==0 is day $weekstarts, not necessarily Sunday.
|
||||
for ($j = 0, $date = clone $start_date; $j < DAYS_PER_WEEK; $j++, $date->modify('+1 day'))
|
||||
{
|
||||
if ($date->isHiddenDay())
|
||||
{
|
||||
// These days are to be hidden in the display: don't display a row
|
||||
continue;
|
||||
}
|
||||
|
||||
$tbody .= "<tr>\n";
|
||||
|
||||
$day_cell_text = datetime_format($format, $date->getTimestamp());
|
||||
|
||||
$vars = array('view' => 'day',
|
||||
'view_all' => $this->view_all,
|
||||
'page_date' => $date->getISODate(),
|
||||
'area' => $this->area_id,
|
||||
'room' => $this->room_id);
|
||||
|
||||
$day_cell_link = 'index.php?' . http_build_query($vars, '', '&');
|
||||
$day_cell_link = multisite($day_cell_link);
|
||||
$row_label = $this->dayCellHTML($day_cell_text, $day_cell_link, $date);
|
||||
$tbody .= $row_label;
|
||||
|
||||
for ($s = $morning_slot_seconds;
|
||||
$s <= $evening_slot_seconds;
|
||||
$s += $resolution)
|
||||
{
|
||||
$is_invalid = $is_possibly_invalid[$j] && is_invalid_datetime(0, 0, $s, $date->getMonth(), $date->getDay(), $date->getYear());
|
||||
// set up the query vars to be used for the link in the cell
|
||||
$query_vars = $this->getQueryVars($this->room_id, $date->getMonth(), $date->getDay(), $date->getYear(), $s);
|
||||
// and then draw the cell
|
||||
$tbody .= $this->tdHTML($map->slot($this->room_id, $j, $s), $query_vars, $is_invalid);
|
||||
} // end looping through the time slots
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$tbody .= $row_label;
|
||||
}
|
||||
$tbody .= "</tr>\n";
|
||||
|
||||
} // end looping through the days of the week
|
||||
|
||||
} // end "times along top" view (for the body)
|
||||
|
||||
else
|
||||
{
|
||||
// the standard view, with days of the week along the top and times down the side
|
||||
for ($s = $morning_slot_seconds;
|
||||
$s <= $evening_slot_seconds;
|
||||
$s += $resolution)
|
||||
{
|
||||
// Show the time linked to the URL for highlighting that time:
|
||||
$classes = array();
|
||||
|
||||
$vars = array('view' => 'week',
|
||||
'view_all' => $this->view_all,
|
||||
'year' => $this->year,
|
||||
'month' => $this->month,
|
||||
'day' => $this->day,
|
||||
'area' => $this->area_id,
|
||||
'room' => $this->room_id);
|
||||
|
||||
if (isset($this->timetohighlight) && ($s == $this->timetohighlight))
|
||||
{
|
||||
$classes[] = 'row_highlight';
|
||||
}
|
||||
else
|
||||
{
|
||||
$vars['timetohighlight'] = $s;
|
||||
}
|
||||
|
||||
$url = 'index.php?' . http_build_query($vars, '', '&');
|
||||
$url = multisite($url);
|
||||
|
||||
$tbody.= '<tr';
|
||||
if (!empty($classes))
|
||||
{
|
||||
$tbody .= ' class="' . implode(' ', $classes) . '"';
|
||||
}
|
||||
$tbody .= ">\n";
|
||||
|
||||
$tbody .= $this->tbodyThTimeCellHTML($s, $url);
|
||||
|
||||
|
||||
// See note above: weekday==0 is day $weekstarts, not necessarily Sunday.
|
||||
for ($j = 0, $date = clone $start_date; $j < DAYS_PER_WEEK; $j++, $date->modify('+1 day'))
|
||||
{
|
||||
if ($date->isHiddenDay())
|
||||
{
|
||||
// These days are to be hidden in the display
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set up the query vars to be used for the link in the cell.
|
||||
$cell_day = $date->getDay();
|
||||
$cell_month = $date->getMonth();
|
||||
$cell_year = $date->getYear();
|
||||
$is_invalid = $is_possibly_invalid[$j] && is_invalid_datetime(0, 0, $s, $cell_month, $cell_day, $cell_year);
|
||||
$query_vars = $this->getQueryVars($this->room_id, $cell_month, $cell_day, $cell_year, $s);
|
||||
|
||||
// and then draw the cell
|
||||
$tbody .= $this->tdHTML($map->slot($this->room_id, $j, $s), $query_vars, $is_invalid);
|
||||
}
|
||||
|
||||
// next lines to display times on right side
|
||||
if ($row_labels_both_sides)
|
||||
{
|
||||
$tbody .= $this->tbodyThTimeCellHTML($s, $url);
|
||||
}
|
||||
|
||||
$tbody .= "</tr>\n";
|
||||
}
|
||||
} // end standard view (for the body)
|
||||
$tbody .= "</tbody>\n";
|
||||
|
||||
return $thead . $tfoot . $tbody;
|
||||
}
|
||||
|
||||
|
||||
// Draw a day cell to be used in the header rows/columns of the week view
|
||||
// $text contains the date, formatted as a string (not escaped - allowed to contain HTML tags)
|
||||
// $link the href to be used for the link
|
||||
// $date the date
|
||||
private function dayCellHTML(string $text, string $link, DateTime $date) : string
|
||||
{
|
||||
$html = '';
|
||||
// Put the date into a data attribute so that it can be picked up by JavaScript
|
||||
$html .= '<th data-date="' . escape_html($date->getISODate()) . '"';
|
||||
|
||||
// Add classes for weekends and holidays
|
||||
$classes = $this->getDateClasses($date);
|
||||
if (!empty($classes))
|
||||
{
|
||||
$html .= ' class="' . implode(' ', $classes) . '"';
|
||||
}
|
||||
|
||||
$html .= '>';
|
||||
$html .= '<a href="' . escape_html($link) . '"' .
|
||||
' title="' . escape_html(get_vocab("viewday")) . '">';
|
||||
$html .= $text; // allowed to contain HTML tags - do not escape
|
||||
$html .= '</a>';
|
||||
$html .= "</th>\n";
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
use function MRBS\escape_html;
|
||||
|
||||
class FlexDiv
|
||||
{
|
||||
public $id;
|
||||
|
||||
private $classes = [];
|
||||
private $length = 1; // slots
|
||||
private $name;
|
||||
|
||||
|
||||
// Create a new FlexDiv, which either represents a booking with id $id,
|
||||
// or free slots
|
||||
public function __construct(?int $id)
|
||||
{
|
||||
if (isset($id))
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->classes = ['free'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function addLength(int $increment) : void
|
||||
{
|
||||
$this->length += $increment;
|
||||
}
|
||||
|
||||
|
||||
public function getLength() : int
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
|
||||
public function setClasses(array $classes) : void
|
||||
{
|
||||
$this->classes = $classes;
|
||||
}
|
||||
|
||||
|
||||
public function setLength(int $length) : void
|
||||
{
|
||||
$this->length = $length;
|
||||
}
|
||||
|
||||
|
||||
public function setName(string $name) : void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
|
||||
public function html(): string
|
||||
{
|
||||
// Fix the size of the div at one pixel per slot. Allow it to grow proportionately,
|
||||
// but not shrink.
|
||||
$html = '<div style="flex: ' . $this->getLength() . ' 0 ' . $this->getLength() . 'px"';
|
||||
|
||||
if (!empty($this->classes))
|
||||
{
|
||||
$html .= ' class="' . escape_html(implode(' ', $this->classes)) . '"';
|
||||
}
|
||||
|
||||
if (isset($this->name) && ($this->name !== ''))
|
||||
{
|
||||
$html .= ' title="' . escape_html($this->name) . '"';
|
||||
}
|
||||
|
||||
$html .= '>';
|
||||
|
||||
if (isset($this->name) && ($this->name !== ''))
|
||||
{
|
||||
$html .= escape_html($this->name);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Calendar;
|
||||
|
||||
// A class for building a map of bookings which can be used for constructing the calendar display
|
||||
use MRBS\DateTime;
|
||||
use MRBS\Exception;
|
||||
use function MRBS\auth;
|
||||
use function MRBS\get_start_first_slot;
|
||||
use function MRBS\get_start_last_slot;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\getWritable;
|
||||
use function MRBS\is_private_event;
|
||||
use function MRBS\nominal_seconds;
|
||||
use function MRBS\round_t_down;
|
||||
use function MRBS\round_t_up;
|
||||
use function MRBS\session;
|
||||
|
||||
class Map
|
||||
{
|
||||
private $start_date;
|
||||
private $end_date;
|
||||
private $resolution;
|
||||
private $data_has_been_coalesced = false;
|
||||
|
||||
// $data is a column of the map of the screen that will be displayed, and is an array indexed
|
||||
// by the room_id, then day, then number of nominal seconds (ie ignoring DST changes) since the
|
||||
// start of the calendar day which has the start of the booking day. Each element of the array
|
||||
// consists of an array of entries that fall in that slot.
|
||||
private $data = [];
|
||||
|
||||
// $entries is an array, indexed by entry id, storing the entries that we are using for this map.
|
||||
// Instead of storing the entry itself in the $data array, which will be many times for entries
|
||||
// spanning multiple slots, we just store the entry id to save memory. Normally this wouldn't
|
||||
// help as PHP uses copy-on-write, but we want to modify the entries to store extra information
|
||||
// relevant to the slot, thus triggering a copy-on-write. Instead, we store the extra information
|
||||
// in an array together with the entry id.
|
||||
private $entries = [];
|
||||
|
||||
// Keys for the entry data stored in $data.
|
||||
private const ENTRY_ID = 0;
|
||||
private const ENTRY_IS_MULTIDAY_START = 1;
|
||||
private const ENTRY_IS_MULTIDAY_END = 2;
|
||||
private const ENTRY_N_SLOTS = 3;
|
||||
|
||||
|
||||
public function __construct(DateTime $start_date, DateTime $end_date, int $resolution)
|
||||
{
|
||||
$this->start_date = $start_date;
|
||||
$this->end_date = $end_date;
|
||||
$this->resolution = $resolution;
|
||||
}
|
||||
|
||||
|
||||
public function addEntries(array $entries) : void
|
||||
{
|
||||
$date = clone $this->start_date;
|
||||
$d = 0;
|
||||
while ($date <= $this->end_date)
|
||||
{
|
||||
$this_day = $date->getDay();
|
||||
$this_month = $date->getMonth();
|
||||
$this_year = $date->getYear();
|
||||
|
||||
$start_first_slot = get_start_first_slot($this_month, $this_day, $this_year);
|
||||
$start_last_slot = get_start_last_slot($this_month, $this_day, $this_year);
|
||||
|
||||
foreach ($entries as $entry)
|
||||
{
|
||||
$this->addEntry($entry, $d, $start_first_slot, $start_last_slot);
|
||||
}
|
||||
|
||||
$d++;
|
||||
$date->modify('+1 day');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add an entry to the map of the bookings being prepared for display.
|
||||
//
|
||||
// $entry a booking from the database
|
||||
// $day_index the index day of the booking, starting at zero
|
||||
// $start_first_slot the start of the first slot of the booking day (Unix timestamp)
|
||||
// $start_last_slot the start of the last slot of the booking day (Unix timestamp)
|
||||
private function addEntry(array $entry, int $day_index, int $start_first_slot, int $start_last_slot) : void
|
||||
{
|
||||
// $entry is expected to have the following keys, when present:
|
||||
// room_id
|
||||
// start_time
|
||||
// end_time
|
||||
// name
|
||||
// repeat_id
|
||||
// id
|
||||
// type
|
||||
// description
|
||||
// create_by
|
||||
// awaiting_approval
|
||||
// private
|
||||
// tentative
|
||||
|
||||
// Normally of course there will only be one entry per slot, but it is possible to have
|
||||
// multiple entries per slot if the resolution is increased, the day shifted since the
|
||||
// original bookings were made, or if the bookings were made using an older version of MRBS
|
||||
// that had faulty conflict detection. For example, if you previously had a resolution of
|
||||
// 1800 seconds, you might have a booking (A) for 1000-1130 and another (B) for 1130-1230.
|
||||
// If you then increase the resolution to 3600 seconds, these two bookings
|
||||
// will both occupy the 1100-1200 time slot.
|
||||
//
|
||||
// We also store the following extra information:
|
||||
// is_multiday_start a boolean indicating if the booking stretches beyond the day start
|
||||
// is_multiday_end a boolean indicating if the booking stretches beyond the day end
|
||||
// n_slots the number of slots the booking lasts (tentatively set to 1)
|
||||
|
||||
// s is the number of nominal seconds (ie ignoring DST changes) since the
|
||||
// start of the calendar day which has the start of the booking day
|
||||
if ($this->data_has_been_coalesced)
|
||||
{
|
||||
throw new Exception("Map: entries cannot be added after output has started");
|
||||
}
|
||||
|
||||
// We're only interested in entries which occur on this day (it's possible
|
||||
// for $entry to contain entries for other days)
|
||||
if (($entry['start_time'] >= $start_last_slot + $this->resolution) ||
|
||||
($entry['end_time'] <= $start_first_slot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill in the map for this meeting. Start at the meeting start time,
|
||||
// or the day start time, whichever is later. End one slot before the
|
||||
// meeting end time (since the next slot is for meetings which start then),
|
||||
// or at the last slot in the day, whichever is earlier.
|
||||
// Time is of the format HHMM without leading zeros.
|
||||
|
||||
// Adjust the starting and ending times so that bookings which don't
|
||||
// start or end at a recognised time still appear.
|
||||
$start_t = max(round_t_down($entry['start_time'], $this->resolution, $start_first_slot), $start_first_slot);
|
||||
$end_t = min(round_t_up($entry['end_time'], $this->resolution, $start_first_slot) - $this->resolution, $start_last_slot);
|
||||
|
||||
// Calculate the times used for indexing - we index by nominal seconds since the start
|
||||
// of the calendar day which has the start of the booking day
|
||||
$start_s = nominal_seconds($start_t);
|
||||
$end_s = nominal_seconds($end_t);
|
||||
|
||||
// Get some additional information about the entry related to the way it displays on the page
|
||||
$is_multiday_start = ($entry['start_time'] < $start_first_slot);
|
||||
$is_multiday_end = ($entry['end_time'] > ($start_last_slot + $this->resolution));
|
||||
|
||||
// Tentatively assume that this booking occupies 1 slot. Call coalesce() later to fix it.
|
||||
$n_slots = 1;
|
||||
|
||||
for ($s = $start_s; $s <= $end_s; $s += $this->resolution)
|
||||
{
|
||||
// Add the entry to the array of entries if it's not already there
|
||||
if (!isset($this->entries[$entry['id']]))
|
||||
{
|
||||
$this->entries[$entry['id']] = self::prepareEntry($entry);
|
||||
}
|
||||
// Store a pointer to this entry, together with the additional data
|
||||
$this->data[$entry['room_id']][$day_index][$s][] = [
|
||||
self::ENTRY_ID => $entry['id'],
|
||||
self::ENTRY_IS_MULTIDAY_START => $is_multiday_start,
|
||||
self::ENTRY_IS_MULTIDAY_END => $is_multiday_end,
|
||||
self::ENTRY_N_SLOTS => $n_slots
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Prepares an entry for display by (a) adding in registration level information
|
||||
// and (b) replacing the text in private fields if necessary.
|
||||
public static function prepareEntry(array $entry) : array
|
||||
{
|
||||
global $is_private_field, $show_registration_level, $auth, $kiosk;
|
||||
|
||||
// Add in the registration level details
|
||||
if ($show_registration_level && $entry['allow_registration'])
|
||||
{
|
||||
// Check whether we should be showing the registrants' names
|
||||
$show_names = ($auth['show_registrant_names_in_calendar'] && ($entry['n_registered'] > 0));
|
||||
if ($show_names && !$auth['show_registrant_names_in_public_calendar'])
|
||||
{
|
||||
// If we're not allowed to show names in the public calendar, check that the user is logged in
|
||||
// and has an access level of at least 1
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
$show_names = isset($mrbs_user) && ($mrbs_user->level > 0);
|
||||
}
|
||||
$names = ($show_names) ? implode(', ', auth()->getRegistrantsDisplayNames($entry)) : '';
|
||||
if ($entry['registrant_limit_enabled'])
|
||||
{
|
||||
$tag = ($show_names) ? 'registration_level_limited_with_names' : 'registration_level_limited';
|
||||
$entry['name'] .= get_vocab($tag, $entry['n_registered'], $entry['registrant_limit'], $names);
|
||||
}
|
||||
else
|
||||
{
|
||||
$tag = ($show_names) ? 'registration_level_unlimited_with_names' : 'registration_level_unlimited';
|
||||
$entry['name'] .= get_vocab($tag, $entry['n_registered'], $names);
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the event is private
|
||||
if (is_private_event($entry['private']) &&
|
||||
($kiosk || !getWritable($entry['create_by'], $entry['room_id'])))
|
||||
{
|
||||
$entry['private'] = true;
|
||||
|
||||
foreach (array('name', 'description') as $key)
|
||||
{
|
||||
if ($is_private_field["entry.$key"])
|
||||
{
|
||||
$entry[$key] = get_vocab('unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($is_private_field['entry.type']))
|
||||
{
|
||||
$entry['type'] = 'private_type';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$entry['private'] = false;
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
|
||||
// Returns the entry or entries that should be displayed at slot $s on day $day for room $room_id.
|
||||
// Returns an empty array if there is no entry.
|
||||
// Should not be called until all the data has been added.
|
||||
public function slot(int $room_id, int $day, int $slot) : array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (!$this->data_has_been_coalesced)
|
||||
{
|
||||
$this->coalesce();
|
||||
}
|
||||
|
||||
foreach ($this->data[$room_id][$day][$slot] ?? [] as $entry_data)
|
||||
{
|
||||
$entry = $this->entries[$entry_data[self::ENTRY_ID]];
|
||||
$entry['is_multiday_start'] = $entry_data[self::ENTRY_IS_MULTIDAY_START];
|
||||
$entry['is_multiday_end'] = $entry_data[self::ENTRY_IS_MULTIDAY_END];
|
||||
$entry['n_slots'] = $entry_data[self::ENTRY_N_SLOTS];
|
||||
$result[] = $entry;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Coalesces map entries that span consecutive time slots.
|
||||
private function coalesce() : void
|
||||
{
|
||||
// The add() method set n_slots=1 for all map entries. For each booking in the
|
||||
// room that spans multiple consecutive time slots, and that does not have
|
||||
// conflicting bookings, the first entry will have its slot count adjusted, and
|
||||
// the continuation entries will have their n_slots attribute set to NULL.
|
||||
foreach ($this->data as &$room_data)
|
||||
{
|
||||
foreach ($room_data as &$day_data)
|
||||
{
|
||||
// Iterate through pairs of consecutive time slots in reverse chronological order
|
||||
for (end($day_data); ($s = key($day_data)) !== null; prev($day_data))
|
||||
{
|
||||
$p = $s - $this->resolution; // The preceding time slot
|
||||
if (isset($day_data[$p]))
|
||||
{
|
||||
if (count($day_data[$s]) == 1)
|
||||
{
|
||||
// Single booking for time slot $s. If this event is a continuation
|
||||
// of a sole event from time slot $p (the previous slot), then
|
||||
// increment the slot count of the same booking in slot $p, and clear
|
||||
// out the redundant attributes in slot $s.
|
||||
if (count($day_data[$p]) == 1 && $day_data[$p][0][self::ENTRY_ID] == $day_data[$s][0][self::ENTRY_ID])
|
||||
{
|
||||
$this_booking = &$day_data[$s][0];
|
||||
$prev_booking = &$day_data[$p][0];
|
||||
$prev_booking[self::ENTRY_N_SLOTS] = 1 + $this_booking[self::ENTRY_N_SLOTS];
|
||||
$this_booking[self::ENTRY_N_SLOTS] = null;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// Multiple bookings for time slot $s. Mark all of them as 1 slot.
|
||||
foreach ($day_data[$s] as &$booking)
|
||||
{
|
||||
$booking[self::ENTRY_N_SLOTS] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->data_has_been_coalesced = true;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user