summaryrefslogtreecommitdiff
path: root/includes/view
diff options
context:
space:
mode:
authormsquare <msquare@notrademark.de>2016-11-09 17:43:56 +0100
committerGitHub <noreply@github.com>2016-11-09 17:43:56 +0100
commitd43eb41d25d0d5c0509417247030dd6c21118cf6 (patch)
tree2c3f78bf8fbd4215e70af7dbc2ce30cfef674816 /includes/view
parentd5d2acc7d80920eef5f0ed779a3738a12d5db348 (diff)
parent22520532c78b3a032aec6ececb7623ba094da8de (diff)
Merge pull request #274 from engelsystem/task-164-shift-view
Task 164 shift view
Diffstat (limited to 'includes/view')
-rw-r--r--includes/view/AngelTypes_view.php2
-rw-r--r--includes/view/Rooms_view.php11
-rw-r--r--includes/view/ShiftCalendarLane.php63
-rw-r--r--includes/view/ShiftCalendarRenderer.php208
-rw-r--r--includes/view/ShiftCalendarShiftRenderer.php188
-rw-r--r--includes/view/ShiftsFilterRenderer.php69
-rw-r--r--includes/view/Shifts_view.php3
7 files changed, 539 insertions, 5 deletions
diff --git a/includes/view/AngelTypes_view.php b/includes/view/AngelTypes_view.php
index cdaa9f12..10c02745 100644
--- a/includes/view/AngelTypes_view.php
+++ b/includes/view/AngelTypes_view.php
@@ -96,7 +96,7 @@ function AngelType_view($angeltype, $members, $user_angeltype, $admin_user_angel
$page = [
msg(),
- buttons($buttons)
+ buttons($buttons)
];
$page[] = '<h3>' . _("Description") . '</h3>';
diff --git a/includes/view/Rooms_view.php b/includes/view/Rooms_view.php
index c820e983..7afdc67b 100644
--- a/includes/view/Rooms_view.php
+++ b/includes/view/Rooms_view.php
@@ -1,8 +1,17 @@
<?php
+use Engelsystem\ShiftsFilterRenderer;
+use Engelsystem\ShiftCalendarRenderer;
+
+function Room_view($room, ShiftsFilterRenderer $shiftsFilterRenderer, ShiftCalendarRenderer $shiftCalendarRenderer) {
+ return page_with_title(glyph('map-marker') . $room['Name'], [
+ $shiftsFilterRenderer->render(room_link($room)) ,
+ $shiftCalendarRenderer->render()
+ ]);
+}
function Room_name_render($room) {
global $privileges;
- if (in_array('admin_rooms', $privileges)) {
+ if (in_array('view_rooms', $privileges)) {
return '<a href="' . room_link($room) . '">' . glyph('map-marker') . $room['Name'] . '</a>';
}
return glyph('map-marker') . $room['Name'];
diff --git a/includes/view/ShiftCalendarLane.php b/includes/view/ShiftCalendarLane.php
new file mode 100644
index 00000000..33fccec3
--- /dev/null
+++ b/includes/view/ShiftCalendarLane.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Engelsystem;
+
+/**
+ * Represents a single lane in a shifts calendar.
+ */
+class ShiftCalendarLane {
+
+ private $firstBlockStartTime;
+
+ private $blockCount;
+
+ private $header;
+
+ private $shifts = [];
+
+ public function __construct($header, $firstBlockStartTime, $blockCount) {
+ $this->header = $header;
+ $this->firstBlockStartTime = $firstBlockStartTime;
+ $this->blockCount = $blockCount;
+ }
+
+ /**
+ * Adds a shift to the lane, but only if it fits.
+ * Returns true on success.
+ *
+ * @param Shift $shift
+ * The shift to add
+ * @return boolean true on success
+ */
+ public function addShift($shift) {
+ if ($this->shiftFits($shift)) {
+ $this->shifts[] = $shift;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if given shift fits into this lane.
+ *
+ * @param Shift $shift
+ * The shift to fit into this lane
+ */
+ public function shiftFits($newShift) {
+ foreach ($this->shifts as $laneShift) {
+ if (! ($newShift['start'] >= $laneShift['end'] || $newShift['end'] <= $laneShift['start'])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function getHeader() {
+ return $this->header;
+ }
+
+ public function getShifts() {
+ return $this->shifts;
+ }
+}
+?> \ No newline at end of file
diff --git a/includes/view/ShiftCalendarRenderer.php b/includes/view/ShiftCalendarRenderer.php
new file mode 100644
index 00000000..70948ec5
--- /dev/null
+++ b/includes/view/ShiftCalendarRenderer.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace Engelsystem;
+
+class ShiftCalendarRenderer {
+
+ /**
+ * 15m * 60s/m = 900s
+ */
+ const SECONDS_PER_ROW = 900;
+
+ /**
+ * Height of a block in pixel.
+ * Do not change - corresponds with theme/css
+ */
+ const BLOCK_HEIGHT = 30;
+
+ /**
+ * Distance between two shifts in pixels
+ */
+ const MARGIN = 5;
+
+ private $lanes;
+
+ private $shiftsFilter;
+
+ private $firstBlockStartTime = null;
+
+ private $blocksPerSlot = null;
+
+ public function __construct($shifts, ShiftsFilter $shiftsFilter) {
+ $this->shiftsFilter = $shiftsFilter;
+ $this->firstBlockStartTime = $this->calcFirstBlockStartTime($shifts);
+ $this->lanes = $this->assignShiftsToLanes($shifts);
+ }
+
+ /**
+ * Assigns the shifts to different lanes per room if they collide
+ *
+ * @param Shift[] $shifts
+ * The shifts to assign
+ *
+ * @return Returns an array that assigns a room_id to an array of ShiftCalendarLane containing the shifts
+ */
+ private function assignShiftsToLanes($shifts) {
+ // array that assigns a room id to a list of lanes (per room)
+ $lanes = [];
+
+ foreach ($shifts as $shift) {
+ $room_id = $shift['RID'];
+ if (! isset($lanes[$room_id])) {
+ // initialize room with one lane
+ $header = Room_name_render([
+ 'RID' => $room_id,
+ 'Name' => $shift['room_name']
+ ]);
+ $lanes[$room_id] = [
+ new ShiftCalendarLane($header, $this->getFirstBlockStartTime(), $this->getBlocksPerSlot())
+ ];
+ }
+ // Try to add the shift to the existing lanes for this room
+ $shift_added = false;
+ foreach ($lanes[$room_id] as $lane) {
+ $shift_added = $lane->addShift($shift);
+ if ($shift_added == true) {
+ break;
+ }
+ }
+ // If all lanes for this room are busy, create a new lane and add shift to it
+ if ($shift_added == false) {
+ $newLane = new ShiftCalendarLane("", $this->getFirstBlockStartTime(), $this->getBlocksPerSlot());
+ if (! $newLane->addShift($shift)) {
+ engelsystem_error("Unable to add shift to new lane.");
+ }
+ $lanes[$room_id][] = $newLane;
+ }
+ }
+
+ return $lanes;
+ }
+
+ public function getFirstBlockStartTime() {
+ return $this->firstBlockStartTime;
+ }
+
+ public function getBlocksPerSlot() {
+ if ($this->blocksPerSlot == null) {
+ $this->blocksPerSlot = $this->calcBlocksPerSlot();
+ }
+ return $this->blocksPerSlot;
+ }
+
+ /**
+ * Renders the whole calendar
+ *
+ * @return the generated html
+ */
+ public function render() {
+ return div('shift-calendar', [
+ $this->renderTimeLane(),
+ $this->renderShiftLanes()
+ ]);
+ }
+
+ /**
+ * Renders the lanes containing the shifts
+ */
+ private function renderShiftLanes() {
+ $html = "";
+ foreach ($this->lanes as $room_lanes) {
+ foreach ($room_lanes as $lane) {
+ $html .= $this->renderLane($lane);
+ }
+ }
+ return $html;
+ }
+
+ /**
+ * Renders a single lane
+ *
+ * @param ShiftCalendarLane $lane
+ * The lane to render
+ */
+ private function renderLane(ShiftCalendarLane $lane) {
+ $shift_renderer = new ShiftCalendarShiftRenderer();
+ $html = "";
+ $rendered_until = $this->getFirstBlockStartTime();
+ foreach ($lane->getShifts() as $shift) {
+ while ($rendered_until + ShiftCalendarRenderer::SECONDS_PER_ROW <= $shift['start']) {
+ $html .= $this->renderTick($rendered_until);
+ $rendered_until += ShiftCalendarRenderer::SECONDS_PER_ROW;
+ }
+
+ list($shift_height, $shift_html) = $shift_renderer->render($shift);
+ $html .= $shift_html;
+ $rendered_until += $shift_height * ShiftCalendarRenderer::SECONDS_PER_ROW;
+ }
+ while ($rendered_until <= $this->shiftsFilter->getEndTime()) {
+ $html .= $this->renderTick($rendered_until);
+ $rendered_until += ShiftCalendarRenderer::SECONDS_PER_ROW;
+ }
+
+ return div('lane', [
+ div('header', $lane->getHeader()),
+ $html
+ ]);
+ }
+
+ /**
+ * Renders a tick/block for given time
+ *
+ * @param int $time
+ * unix timestamp
+ * @param boolean $label
+ * Should time labels be generated?
+ * @return rendered tick html
+ */
+ private function renderTick($time, $label = false) {
+ if ($time % (24 * 60 * 60) == 23 * 60 * 60) {
+ if (! $label) {
+ return div('tick day');
+ }
+ return div('tick day', [
+ date('Y-m-d<b\r />H:i', $time)
+ ]);
+ } elseif ($time % (60 * 60) == 0) {
+ if (! $label) {
+ return div('tick hour');
+ }
+ return div('tick hour', [
+ date('H:i', $time)
+ ]);
+ }
+ return div('tick');
+ }
+
+ /**
+ * Renders the left time lane including hour/day ticks
+ */
+ private function renderTimeLane() {
+ $time_slot = [
+ div('header', [
+ _("Time")
+ ])
+ ];
+ for ($block = 0; $block < $this->getBlocksPerSlot(); $block ++) {
+ $thistime = $this->getFirstBlockStartTime() + ($block * ShiftCalendarRenderer::SECONDS_PER_ROW);
+ $time_slot[] = $this->renderTick($thistime, true);
+ }
+ return div('lane time', $time_slot);
+ }
+
+ private function calcFirstBlockStartTime($shifts) {
+ $start_time = $this->shiftsFilter->getEndTime();
+ foreach ($shifts as $shift) {
+ if ($shift['start'] < $start_time) {
+ $start_time = $shift['start'];
+ }
+ }
+ return ShiftCalendarRenderer::SECONDS_PER_ROW * floor(($start_time - 60 * 60) / ShiftCalendarRenderer::SECONDS_PER_ROW);
+ }
+
+ private function calcBlocksPerSlot() {
+ return ceil(($this->shiftsFilter->getEndTime() - $this->getFirstBlockStartTime()) / ShiftCalendarRenderer::SECONDS_PER_ROW);
+ }
+}
+
+?> \ No newline at end of file
diff --git a/includes/view/ShiftCalendarShiftRenderer.php b/includes/view/ShiftCalendarShiftRenderer.php
new file mode 100644
index 00000000..e86f7baa
--- /dev/null
+++ b/includes/view/ShiftCalendarShiftRenderer.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace Engelsystem;
+
+/**
+ * Renders a single shift for the shift calendar
+ */
+class ShiftCalendarShiftRenderer {
+
+ /**
+ * Renders a shift
+ *
+ * @param Shift $shift
+ * The shift to render
+ */
+ public function render($shift) {
+ global $privileges;
+
+ $collides = $this->collides();
+ $info_text = "";
+ if ($shift['title'] != '') {
+ $info_text = glyph('info-sign') . $shift['title'] . '<br>';
+ }
+ list($is_free, $shifts_row) = $this->renderShiftNeededAngeltypes($shift, $collides);
+
+ if (isset($shift['own']) && $shift['own'] && ! in_array('user_shifts_admin', $privileges)) {
+ $class = 'primary';
+ } elseif ($collides && ! in_array('user_shifts_admin', $privileges)) {
+ $class = 'default';
+ } elseif ($is_free) {
+ $class = 'danger';
+ } else {
+ $class = 'success';
+ }
+
+ $blocks = ceil(($shift["end"] - $shift["start"]) / ShiftCalendarRenderer::SECONDS_PER_ROW);
+ $blocks = max(1, $blocks);
+ return [
+ $blocks,
+ '<td class="shift" rowspan="' . $blocks . '">' . div('shift panel panel-' . $class . '" style="height: ' . ($blocks * ShiftCalendarRenderer::BLOCK_HEIGHT - ShiftCalendarRenderer::MARGIN) . 'px"', [
+ $this->renderShiftHead($shift),
+ div('panel-body', [
+ $info_text,
+ Room_name_render([
+ 'RID' => $shift['RID'],
+ 'Name' => $shift['room_name']
+ ])
+ ]),
+ $shifts_row,
+ div('shift-spacer')
+ ]) . '</td>'
+ ];
+ }
+
+ private function renderShiftNeededAngeltypes($shift, $collides) {
+ global $privileges;
+
+ $html = "";
+ $is_free = false;
+ $angeltypes = NeededAngelTypes_by_shift($shift['SID']);
+ foreach ($angeltypes as $angeltype) {
+ list($angeltype_free, $angeltype_html) = $this->renderShiftNeededAngeltype($shift, $angeltype, $collides);
+ $is_free |= $angeltype_free;
+ $html .= $angeltype_html;
+ }
+ if (in_array('user_shifts_admin', $privileges)) {
+ $html .= '<li class="list-group-item">' . button(page_link_to('user_shifts') . '&amp;shift_id=' . $shift['SID'] . '&amp;type_id=' . $angeltype['id'], _("Add more angels"), 'btn-xs') . '</li>';
+ }
+ if ($html != '') {
+ return [
+ $is_free,
+ '<ul class="list-group">' . $html . '</ul>'
+ ];
+ }
+ return [
+ $is_free,
+ ""
+ ];
+ }
+
+ /**
+ * Renders a list entry containing the needed angels for an angeltype
+ *
+ * @param Shift $shift
+ * The shift which is rendered
+ * @param Angeltype $angeltype
+ * The angeltype, containing informations about needed angeltypes and already signed up angels
+ * @param boolean $collides
+ * true if the shift collides with the users shifts
+ */
+ private function renderShiftNeededAngeltype($shift, $angeltype, $collides) {
+ global $privileges;
+
+ $is_free = false;
+ $entry_list = [];
+ $freeloader = 0;
+ foreach ($angeltype['shift_entries'] as $entry) {
+ $style = '';
+ if ($entry['freeloaded']) {
+ $freeloader ++;
+ $style = " text-decoration: line-through;";
+ }
+ $entry_list[] = "<span style=\"$style\">" . User_Nick_render(User($entry['UID'])) . "</span>";
+ }
+ if ($angeltype['count'] - count($angeltype['shift_entries']) - $freeloader > 0) {
+ $inner_text = sprintf(ngettext("%d helper needed", "%d helpers needed", $angeltype['count'] - count($angeltype['shift_entries'])), $angeltype['count'] - count($angeltype['shift_entries']));
+ // is the shift still running or alternatively is the user shift admin?
+ $user_may_join_shift = true;
+
+ // you cannot join if user alread joined a parallel or this shift
+ $user_may_join_shift &= ! $collides;
+
+ // you cannot join if user is not of this angel type
+ $user_may_join_shift &= isset($angeltype['user_id']);
+
+ // you cannot join if you are not confirmed
+ if ($angeltype['restricted'] == 1 && isset($angeltype['user_id'])) {
+ $user_may_join_shift &= isset($angeltype['confirm_user_id']);
+ }
+
+ // you can only join if the shift is in future or running
+ $user_may_join_shift &= time() < $shift['start'];
+
+ // User shift admins may join anybody in every shift
+ $user_may_join_shift |= in_array('user_shifts_admin', $privileges);
+ if ($user_may_join_shift) {
+ $entry_list[] = '<a href="' . page_link_to('user_shifts') . '&amp;shift_id=' . $shift['SID'] . '&amp;type_id=' . $angeltype['id'] . '">' . $inner_text . '</a> ' . button(page_link_to('user_shifts') . '&amp;shift_id=' . $shift['SID'] . '&amp;type_id=' . $angeltype['id'], _('Sign up'), 'btn-xs btn-primary');
+ } else {
+ if (time() > $shift['start']) {
+ $entry_list[] = $inner_text . ' (' . _('ended') . ')';
+ } elseif ($angeltype['restricted'] == 1 && isset($angeltype['user_id']) && ! isset($angeltype['confirm_user_id'])) {
+ $entry_list[] = $inner_text . glyph('lock');
+ } elseif ($angeltype['restricted'] == 1) {
+ $entry_list[] = $inner_text;
+ } elseif ($collides) {
+ $entry_list[] = $inner_text;
+ } else {
+ $entry_list[] = $inner_text . '<br />' . button(page_link_to('user_angeltypes') . '&action=add&angeltype_id=' . $angeltype['id'], sprintf(_('Become %s'), $angeltype['name']), 'btn-xs');
+ }
+ }
+
+ unset($inner_text);
+ $is_free = true;
+ }
+
+ $shifts_row = '<li class="list-group-item">';
+ $shifts_row .= '<strong>' . AngelType_name_render($angeltype) . ':</strong> ';
+ $shifts_row .= join(", ", $entry_list);
+ $shifts_row .= '</li>';
+ return [
+ $is_free,
+ $shifts_row
+ ];
+ }
+
+ /**
+ * Renders the shift header
+ *
+ * @param Shift $shift
+ * The shift
+ */
+ private function renderShiftHead($shift) {
+ global $privileges;
+
+ $header_buttons = "";
+ if (in_array('admin_shifts', $privileges)) {
+ $header_buttons = '<div class="pull-right">' . table_buttons([
+ button(page_link_to('user_shifts') . '&edit_shift=' . $shift['SID'], glyph('edit'), 'btn-xs'),
+ button(page_link_to('user_shifts') . '&delete_shift=' . $shift['SID'], glyph('trash'), 'btn-xs')
+ ]) . '</div>';
+ }
+ $shift_heading = date('H:i', $shift['start']) . ' &dash; ' . date('H:i', $shift['end']) . ' &mdash; ' . ShiftType($shift['shifttype_id'])['name'];
+ return div('panel-heading', [
+ '<a href="' . shift_link($shift) . '">' . $shift_heading . '</a>',
+ $header_buttons
+ ]);
+ }
+
+ /**
+ * Does the shift collide with the user's shifts
+ */
+ private function collides() {
+ // TODO
+ return false;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/includes/view/ShiftsFilterRenderer.php b/includes/view/ShiftsFilterRenderer.php
new file mode 100644
index 00000000..301f31a2
--- /dev/null
+++ b/includes/view/ShiftsFilterRenderer.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Engelsystem;
+
+class ShiftsFilterRenderer {
+
+ /**
+ * The shiftFilter to render.
+ *
+ * @var ShiftsFilter
+ */
+ private $shiftsFilter;
+
+ /**
+ * Should the filter display a day selection.
+ *
+ * @var boolean
+ */
+ private $daySelectionEnabled = false;
+
+ /**
+ * Days that can be selected.
+ * Format Y-m-d
+ *
+ * @var string[]
+ */
+ private $days = [];
+
+ public function __construct(ShiftsFilter $shiftsFilter) {
+ $this->shiftsFilter = $shiftsFilter;
+ }
+
+ /**
+ * Renders the filter.
+ *
+ * @return Generated HTML
+ */
+ public function render($link_base) {
+ $toolbar = [];
+ if ($this->daySelectionEnabled && ! empty($this->days)) {
+ $selected_day = date("Y-m-d", $this->shiftsFilter->getStartTime());
+ $day_dropdown_items = [];
+ foreach ($this->days as $day) {
+ $day_dropdown_items[] = toolbar_item_link($link_base . '&shifts_filter_day=' . $day, '', $day);
+ }
+ $toolbar[] = toolbar_dropdown('', $selected_day, $day_dropdown_items, 'active');
+ }
+ return div('form-group', [
+ toolbar_pills($toolbar)
+ ]);
+ }
+
+ /**
+ * Should the filter display a day selection.
+ */
+ public function enableDaySelection($days) {
+ $this->daySelectionEnabled = true;
+ $this->days = $days;
+ }
+
+ /**
+ * Should the filter display a day selection.
+ */
+ public function isDaySelectionEnabled() {
+ return $this->daySelectionEnabled;
+ }
+}
+
+?> \ No newline at end of file
diff --git a/includes/view/Shifts_view.php b/includes/view/Shifts_view.php
index 95282eb5..1b910f5c 100644
--- a/includes/view/Shifts_view.php
+++ b/includes/view/Shifts_view.php
@@ -16,9 +16,6 @@ function Shift_signup_button_render($shift, $angeltype, $user_angeltype = null,
if ($user_angeltype == null) {
$user_angeltype = UserAngelType_by_User_and_AngelType($user, $angeltype);
- if ($user_angeltype === false) {
- engelsystem_error('Unable to load user angeltype.');
- }
}
if (Shift_signup_allowed($shift, $angeltype, $user_angeltype, $user_shifts)) {