MRBS 1.12.2 等保2.0二级整改完整提交
Docker image / push (push) Canceled after 0s

包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
人事系统开发
2026-09-08 21:19:47 +08:00
commit 48092cab42
2221 changed files with 659586 additions and 0 deletions
+411
View File
@@ -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;
}
}
+51
View File
@@ -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\">&nbsp;</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;
}
}
+307
View File
@@ -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;
}
}
+89
View File
@@ -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;
}
}
+302
View File
@@ -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;
}
}