2516 lines
88 KiB
PHP
2516 lines
88 KiB
PHP
<?php
|
|
/**
|
|
* ---------------------------------------------------------------------
|
|
* GLPI - Gestionnaire Libre de Parc Informatique
|
|
* Copyright (C) 2015-2020 Teclib' and contributors.
|
|
*
|
|
* http://glpi-project.org
|
|
*
|
|
* based on GLPI - Gestionnaire Libre de Parc Informatique
|
|
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
|
|
*
|
|
* ---------------------------------------------------------------------
|
|
*
|
|
* LICENSE
|
|
*
|
|
* This file is part of GLPI.
|
|
*
|
|
* GLPI is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* GLPI is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
|
|
* ---------------------------------------------------------------------
|
|
*/
|
|
|
|
if (!defined('GLPI_ROOT')) {
|
|
die("Sorry. You can't access this file directly");
|
|
}
|
|
|
|
use RRule\RRule;
|
|
use Sabre\VObject\Component\VCalendar;
|
|
use Sabre\VObject\Property\FlatText;
|
|
use Sabre\VObject\Reader;
|
|
use Sabre\VObject\ParseException;
|
|
use Sabre\VObject\Component\VEvent;
|
|
use Sabre\VObject\Component\VTodo;
|
|
use Sabre\VObject\Property\ICalendar\Recur;
|
|
|
|
/**
|
|
* Planning Class
|
|
**/
|
|
class Planning extends CommonGLPI {
|
|
|
|
static $rightname = 'planning';
|
|
|
|
static $palette_bg = ['#FFEEC4', '#D4EDFB', '#E1D0E1', '#CDD7A9', '#F8C8D2',
|
|
'#D6CACA', '#D3D6ED', '#C8E5E3', '#FBD5BF', '#E9EBA2',
|
|
'#E8E5E5', '#DBECDF', '#FCE7F2', '#E9D3D3', '#D2DBDC'];
|
|
|
|
static $palette_fg = ['#57544D', '#59707E', '#5B3B5B', '#3A431A', '#58242F',
|
|
'#3B2727', '#272D59', '#2E4645', '#6F4831', '#46481B',
|
|
'#4E4E4E', '#274C30', '#6A535F', '#473232', '#454545',];
|
|
|
|
static $palette_ev = ['#E94A31', '#5174F2', '#51C9F2', '#FFCC29', '#20C646',
|
|
'#364959', '#8C5344', '#FF8100', '#F600C4', '#0017FF',
|
|
'#000000', '#FFFFFF', '#005800', '#925EFF'];
|
|
|
|
static $directgroup_itemtype = ['ProjectTask', 'TicketTask', 'ProblemTask', 'ChangeTask'];
|
|
|
|
const READMY = 1;
|
|
const READGROUP = 1024;
|
|
const READALL = 2048;
|
|
|
|
const INFO = 0;
|
|
const TODO = 1;
|
|
const DONE = 2;
|
|
|
|
/**
|
|
* @since 0.85
|
|
*
|
|
* @param $nb
|
|
**/
|
|
static function getTypeName($nb = 0) {
|
|
return __('Planning');
|
|
}
|
|
|
|
|
|
static function getMenuContent() {
|
|
$menu = [];
|
|
|
|
if (Planning::canView()) {
|
|
$menu = [
|
|
'title' => static::getMenuName(),
|
|
'shortcut' => static::getMenuShorcut(),
|
|
'page' => static::getSearchURL(false),
|
|
'icon' => static::getIcon(),
|
|
];
|
|
|
|
if ($data = static::getAdditionalMenuLinks()) {
|
|
$menu['links'] = $data;
|
|
}
|
|
|
|
if ($options = static::getAdditionalMenuOptions()) {
|
|
$menu['options'] = $options;
|
|
}
|
|
}
|
|
|
|
return $menu;
|
|
}
|
|
|
|
|
|
static function getAdditionalMenuLinks() {
|
|
global $CFG_GLPI;
|
|
|
|
$links = [];
|
|
|
|
if (Planning::canView()) {
|
|
$title = Planning::getTypeName(Session::getPluralNumber());
|
|
$planning = "<i class='fa far fa-calendar-alt pointer' title='$title'>
|
|
<span class='sr-only'>$title</span>
|
|
</i>";
|
|
|
|
$links[$planning] = Planning::getSearchURL(false);
|
|
}
|
|
|
|
if (PlanningExternalEvent::canView()) {
|
|
$ext_title = PlanningExternalEvent::getTypeName(Session::getPluralNumber());
|
|
$external = "<i class='fa fas fa-calendar-week pointer' title='$ext_title'>
|
|
<span class='sr-only'>$ext_title</span>
|
|
</i>";
|
|
|
|
$links[$external] = PlanningExternalEvent::getSearchURL(false);
|
|
}
|
|
|
|
if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) {
|
|
$caldav_title = __('CalDAV browser interface');
|
|
$caldav = "<i class='fa fas fa-sync pointer' title='$caldav_title'>
|
|
<span class='sr-only'>$caldav_title</span>
|
|
</i>";
|
|
|
|
$links[$caldav] = '/caldav.php';
|
|
}
|
|
|
|
return $links;
|
|
}
|
|
|
|
|
|
static function getAdditionalMenuOptions() {
|
|
if (PlanningExternalEvent::canView()) {
|
|
return [
|
|
'external' => [
|
|
'title' => PlanningExternalEvent::getTypeName(Session::getPluralNumber()),
|
|
'page' => PlanningExternalEvent::getSearchURL(false),
|
|
'links' => [
|
|
'add' => '/front/planningexternalevent.form.php',
|
|
'search' => '/front/planningexternalevent.php',
|
|
] + static::getAdditionalMenuLinks()
|
|
]
|
|
];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @see CommonGLPI::getMenuShorcut()
|
|
*
|
|
* @since 0.85
|
|
**/
|
|
static function getMenuShorcut() {
|
|
return 'p';
|
|
}
|
|
|
|
|
|
/**
|
|
* @since 0.85
|
|
**/
|
|
static function canView() {
|
|
|
|
return Session::haveRightsOr(self::$rightname, [self::READMY, self::READGROUP,
|
|
self::READALL]);
|
|
}
|
|
|
|
|
|
function defineTabs($options = []) {
|
|
|
|
$ong = [];
|
|
$ong['no_all_tab'] = true;
|
|
|
|
$this->addStandardTab(__CLASS__, $ong, $options);
|
|
|
|
return $ong;
|
|
}
|
|
|
|
|
|
function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
|
|
|
|
if ($item->getType() == __CLASS__) {
|
|
$tabs[1] = self::getTypeName();
|
|
|
|
return $tabs;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
|
|
static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) {
|
|
|
|
if ($item->getType() == __CLASS__) {
|
|
switch ($tabnum) {
|
|
case 1 : // all
|
|
Planning::showPlanning($_SESSION['glpiID']);
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get planning state name
|
|
*
|
|
* @param $value status ID
|
|
**/
|
|
static function getState($value) {
|
|
|
|
switch ($value) {
|
|
case static::INFO :
|
|
return _n('Information', 'Information', 1);
|
|
|
|
case static::TODO :
|
|
return __('To do');
|
|
|
|
case static::DONE :
|
|
return __('Done');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Dropdown of planning state
|
|
*
|
|
* @param $name select name
|
|
* @param $value default value (default '')
|
|
* @param $display display of send string ? (true by default)
|
|
* @param $options options
|
|
**/
|
|
static function dropdownState($name, $value = '', $display = true, $options = []) {
|
|
|
|
$values = [static::INFO => _n('Information', 'Information', 1),
|
|
static::TODO => __('To do'),
|
|
static::DONE => __('Done')];
|
|
|
|
return Dropdown::showFromArray($name, $values, array_merge(['value' => $value,
|
|
'display' => $display], $options));
|
|
}
|
|
|
|
|
|
/**
|
|
* Check already planned user for a period
|
|
*
|
|
* @param integer $users_id user id
|
|
* @param string $begin begin date
|
|
* @param string $end end date
|
|
* @param array $except items which not be into account ['Reminder' => [1, 2, id_of_items]]
|
|
**/
|
|
static function checkAlreadyPlanned($users_id, $begin, $end, $except = []) {
|
|
global $CFG_GLPI;
|
|
|
|
$planned = false;
|
|
$message = '';
|
|
|
|
foreach ($CFG_GLPI['planning_types'] as $itemtype) {
|
|
$item = new $itemtype;
|
|
$data = $item->populatePlanning([
|
|
'who' => $users_id,
|
|
'whogroup' => 0,
|
|
'begin' => $begin,
|
|
'end' => $end,
|
|
'check_planned' => true
|
|
]);
|
|
if (isPluginItemType($itemtype)) {
|
|
if (isset($data['items'])) {
|
|
$data = $data['items'];
|
|
} else {
|
|
$data = [];
|
|
}
|
|
}
|
|
|
|
if (count($data)
|
|
&& method_exists($itemtype, 'getAlreadyPlannedInformation')) {
|
|
foreach ($data as $val) {
|
|
if (!isset($except[$itemtype])
|
|
|| (is_array($except[$itemtype]) && !in_array($val['id'], $except[$itemtype]))) {
|
|
|
|
$planned = true;
|
|
$message .= '- ' . $item->getAlreadyPlannedInformation($val);
|
|
$message .= '<br/>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($planned) {
|
|
$user = new User();
|
|
$user->getFromDB($users_id);
|
|
Session::addMessageAfterRedirect(
|
|
sprintf(
|
|
__('The user %1$s is busy at the selected timeframe.'),
|
|
'<a href="' . $user->getFormURLWithID($users_id) . '">' . $user->getName() . '</a>'
|
|
).'<br/>'.$message,
|
|
false,
|
|
WARNING
|
|
);
|
|
}
|
|
return $planned;
|
|
}
|
|
|
|
|
|
/**
|
|
* Show the availability of a user
|
|
*
|
|
* @since 0.83
|
|
*
|
|
* @param $params array of params
|
|
* must contain :
|
|
* - begin: begin date to check (default '')
|
|
* - end: end date to check (default '')
|
|
* - itemtype : User or Object type (Ticket...)
|
|
* - foreign key field of the itemtype to define which item to used
|
|
* optional :
|
|
* - limitto : limit display to a specific user
|
|
*
|
|
* @return void
|
|
**/
|
|
static function checkAvailability($params = []) {
|
|
global $CFG_GLPI;
|
|
|
|
if (!isset($params['itemtype'])) {
|
|
return false;
|
|
}
|
|
if (!($item = getItemForItemtype($params['itemtype']))) {
|
|
return false;
|
|
}
|
|
if (!isset($params[$item->getForeignKeyField()])
|
|
|| !$item->getFromDB($params[$item->getForeignKeyField()])) {
|
|
return false;
|
|
}
|
|
// No limit by default
|
|
if (!isset($params['limitto'])) {
|
|
$params['limitto'] = 0;
|
|
}
|
|
if (isset($params['begin']) && !empty($params['begin'])) {
|
|
$begin = $params['begin'];
|
|
} else {
|
|
$begin = date("Y-m-d");
|
|
}
|
|
if (isset($params['end']) && !empty($params['end'])) {
|
|
$end = $params['end'];
|
|
} else {
|
|
$end = date("Y-m-d");
|
|
}
|
|
|
|
if ($end < $begin) {
|
|
$end = $begin;
|
|
}
|
|
$realbegin = $begin." ".$CFG_GLPI["planning_begin"];
|
|
$realend = $end." ".$CFG_GLPI["planning_end"];
|
|
if ($CFG_GLPI["planning_end"] == "24:00") {
|
|
$realend = $end." 23:59:59";
|
|
}
|
|
|
|
$users = [];
|
|
|
|
switch ($item->getType()) {
|
|
case 'User' :
|
|
$users[$item->getID()] = $item->getName();
|
|
break;
|
|
|
|
default :
|
|
if (is_a($item, 'CommonITILObject', true)) {
|
|
foreach ($item->getUsers(CommonITILActor::ASSIGN) as $data) {
|
|
$users[$data['users_id']] = getUserName($data['users_id']);
|
|
}
|
|
foreach ($item->getGroups(CommonITILActor::ASSIGN) as $data) {
|
|
foreach (Group_User::getGroupUsers($data['groups_id']) as $data2) {
|
|
$users[$data2['id']] = formatUserName($data2["id"], $data2["name"],
|
|
$data2["realname"], $data2["firstname"]);
|
|
}
|
|
}
|
|
}
|
|
if ($itemtype = 'Ticket') {
|
|
$task = new TicketTask();
|
|
} else if ($itemtype = 'Problem') {
|
|
$task = new ProblemTask();
|
|
}
|
|
if ($task->getFromDBByCrit(['tickets_id' => $item->fields['id']])) {
|
|
$users['users_id'] = getUserName($task->fields['users_id_tech']);
|
|
$group_id = $task->fields['groups_id_tech'];
|
|
if ($group_id) {
|
|
foreach (Group_User::getGroupUsers($group_id) as $data2) {
|
|
$users[$data2['id']] = formatUserName($data2["id"], $data2["name"],
|
|
$data2["realname"], $data2["firstname"]);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
asort($users);
|
|
// Use get method to check availability
|
|
echo "<div class='center'><form method='GET' name='form' action='planning.php'>\n";
|
|
echo "<table class='tab_cadre_fixe'>";
|
|
$colspan = 5;
|
|
if (count($users) > 1) {
|
|
$colspan++;
|
|
}
|
|
echo "<tr class='tab_bg_1'><th colspan='$colspan'>".__('Availability')."</th>";
|
|
echo "</tr>";
|
|
|
|
echo "<tr class='tab_bg_1'>";
|
|
echo "<td>".__('Start')."</td>\n";
|
|
echo "<td>";
|
|
Html::showDateField("begin", ['value' => $begin,
|
|
'maybeempty' => false]);
|
|
echo "</td>\n";
|
|
echo "<td>".__('End')."</td>\n";
|
|
echo "<td>";
|
|
Html::showDateField("end", ['value' => $end,
|
|
'maybeempty' => false]);
|
|
echo "</td>\n";
|
|
if (count($users) > 1) {
|
|
echo "<td width='40%'>";
|
|
$data = [0 => __('All')];
|
|
$data += $users;
|
|
Dropdown::showFromArray('limitto', $data, ['width' => '100%',
|
|
'value' => $params['limitto']]);
|
|
echo "</td>";
|
|
}
|
|
|
|
echo "<td class='center'>";
|
|
echo "<input type='hidden' name='".$item->getForeignKeyField()."' value=\"".$item->getID()."\">";
|
|
echo "<input type='hidden' name='itemtype' value=\"".$item->getType()."\">";
|
|
echo "<input type='submit' class='submit' name='checkavailability' value=\"".
|
|
_sx('button', 'Search') ."\">";
|
|
echo "</td>\n";
|
|
|
|
echo "</tr>";
|
|
echo "</table>";
|
|
Html::closeForm();
|
|
echo "</div>\n";
|
|
|
|
if (($params['limitto'] > 0) && isset($users[$params['limitto']])) {
|
|
$displayuser[$params['limitto']] = $users[$params['limitto']];
|
|
} else {
|
|
$displayuser = $users;
|
|
}
|
|
|
|
if (count($displayuser)) {
|
|
foreach ($displayuser as $who => $whoname) {
|
|
$params = [
|
|
'who' => $who,
|
|
'whogroup' => 0,
|
|
'begin' => $realbegin,
|
|
'end' => $realend
|
|
];
|
|
|
|
$interv = [];
|
|
foreach ($CFG_GLPI['planning_types'] as $itemtype) {
|
|
$interv = array_merge($interv, $itemtype::populatePlanning($params));
|
|
if (method_exists($itemtype, 'populateNotPlanned')) {
|
|
$interv = array_merge($interv, $itemtype::populateNotPlanned($params));
|
|
}
|
|
}
|
|
|
|
// Print Headers
|
|
echo "<br><div class='center'><table class='tab_cadre_fixe'>";
|
|
$colnumber = 1;
|
|
$plan_begin = explode(":", $CFG_GLPI["planning_begin"]);
|
|
$plan_end = explode(":", $CFG_GLPI["planning_end"]);
|
|
$begin_hour = intval($plan_begin[0]);
|
|
$end_hour = intval($plan_end[0]);
|
|
if ($plan_end[1] != 0) {
|
|
$end_hour++;
|
|
}
|
|
$colsize = floor((100-15)/($end_hour-$begin_hour));
|
|
$timeheader = '';
|
|
for ($i=$begin_hour; $i<$end_hour; $i++) {
|
|
$from = ($i<10?'0':'').$i;
|
|
$timeheader.= "<th width='$colsize%' colspan='4'>".$from.":00</th>";
|
|
$colnumber += 4;
|
|
}
|
|
|
|
// Print Headers
|
|
echo "<tr class='tab_bg_1'><th colspan='$colnumber'>";
|
|
echo $whoname;
|
|
echo "</th></tr>";
|
|
echo "<tr class='tab_bg_1'><th width='15%'> </th>";
|
|
echo $timeheader;
|
|
echo "</tr>";
|
|
|
|
$day_begin = strtotime($realbegin);
|
|
$day_end = strtotime($realend);
|
|
|
|
for ($time=$day_begin; $time<$day_end; $time+=DAY_TIMESTAMP) {
|
|
$current_day = date('Y-m-d', $time);
|
|
echo "<tr><th>".Html::convDate($current_day)."</th>";
|
|
$begin_quarter = $begin_hour*4;
|
|
$end_quarter = $end_hour*4;
|
|
for ($i=$begin_quarter; $i<$end_quarter; $i++) {
|
|
$begin_time = date("Y-m-d H:i:s", strtotime($current_day)+($i)*HOUR_TIMESTAMP/4);
|
|
$end_time = date("Y-m-d H:i:s", strtotime($current_day)+($i+1)*HOUR_TIMESTAMP/4);
|
|
// Init activity interval
|
|
$begin_act = $end_time;
|
|
$end_act = $begin_time;
|
|
|
|
reset($interv);
|
|
while ($data = current($interv)) {
|
|
if (($data["begin"] >= $begin_time)
|
|
&& ($data["end"] <= $end_time)) {
|
|
// In
|
|
if ($begin_act > $data["begin"]) {
|
|
$begin_act = $data["begin"];
|
|
}
|
|
if ($end_act < $data["end"]) {
|
|
$end_act = $data["end"];
|
|
}
|
|
unset($interv[key($interv)]);
|
|
|
|
} else if (($data["begin"] < $begin_time)
|
|
&& ($data["end"] > $end_time)) {
|
|
// Through
|
|
$begin_act = $begin_time;
|
|
$end_act = $end_time;
|
|
next($interv);
|
|
|
|
} else if (($data["begin"] >= $begin_time)
|
|
&& ($data["begin"] < $end_time)) {
|
|
// Begin
|
|
if ($begin_act > $data["begin"]) {
|
|
$begin_act = $data["begin"];
|
|
}
|
|
$end_act = $end_time;
|
|
next($interv);
|
|
|
|
} else if (($data["end"] > $begin_time)
|
|
&& ($data["end"] <= $end_time)) {
|
|
//End
|
|
$begin_act = $begin_time;
|
|
if ($end_act < $data["end"]) {
|
|
$end_act = $data["end"];
|
|
}
|
|
unset($interv[key($interv)]);
|
|
|
|
} else { // Defautl case
|
|
next($interv);
|
|
}
|
|
}
|
|
if ($begin_act < $end_act) {
|
|
if (($begin_act <= $begin_time)
|
|
&& ($end_act >= $end_time)) {
|
|
// Activity in quarter
|
|
echo "<td class='notavailable'> </td>";
|
|
} else {
|
|
// Not all the quarter
|
|
if ($begin_act <= $begin_time) {
|
|
echo "<td class='partialavailableend'> </td>";
|
|
} else {
|
|
echo "<td class='partialavailablebegin'> </td>";
|
|
}
|
|
}
|
|
} else {
|
|
// No activity
|
|
echo "<td class='available'> </td>";
|
|
}
|
|
}
|
|
echo "</tr>";
|
|
}
|
|
echo "<tr class='tab_bg_1'><td colspan='$colnumber'> </td></tr>";
|
|
echo "</table></div>";
|
|
}
|
|
}
|
|
echo "<div><table class='tab_cadre'>";
|
|
echo "<tr class='tab_bg_1'>";
|
|
echo "<th>".__('Caption')."</th>";
|
|
echo "<td class='available' colspan=8>".__('Available')."</td>";
|
|
echo "<td class='notavailable' colspan=8>".__('Unavailable')."</td>";
|
|
echo "</tr>";
|
|
echo "</table></div>";
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Show the planning
|
|
*
|
|
* Function name change since version 0.84 show() => showPlanning
|
|
* Function prototype changes in 9.1 (no more parameters)
|
|
*
|
|
* @return void
|
|
**/
|
|
static function showPlanning($fullview = true) {
|
|
if (!static::canView()) {
|
|
return false;
|
|
}
|
|
|
|
self::initSessionForCurrentUser();
|
|
|
|
// scheduler feature key
|
|
// schedular part of fullcalendar is distributed with opensource licence (GLPv3)
|
|
// but this licence is incompatible with GLPI (GPLv2)
|
|
// see https://fullcalendar.io/license
|
|
$scheduler_key = Plugin::doHookFunction('planning_scheduler_key');
|
|
|
|
echo "<div" . ($fullview ? " id='planning_container'" : "") . ">";
|
|
|
|
// define options for current page
|
|
$rand = '';
|
|
if ($fullview) {
|
|
// full planning view (Assistance > Planning)
|
|
Planning::showPlanningFilter();
|
|
$options = [
|
|
'full_view' => true,
|
|
'default_view' => $_SESSION['glpi_plannings']['lastview'] ?? 'timeGridWeek',
|
|
'license_key' => $scheduler_key,
|
|
'resources' => self::getTimelineResources(),
|
|
'now' => date("Y-m-d H:i:s"),
|
|
];
|
|
} else {
|
|
// short view (on Central page)
|
|
$rand = rand();
|
|
$options = [
|
|
'full_view' => false,
|
|
'default_view' => 'listFull',
|
|
'header' => false,
|
|
'height' => 'auto',
|
|
'rand' => $rand,
|
|
'now' => date("Y-m-d H:i:s"),
|
|
];
|
|
}
|
|
|
|
// display planning (and call js from js/planning.js)
|
|
echo "<div id='planning$rand'></div>";
|
|
echo "</div>";
|
|
|
|
echo Html::scriptBlock("$(function() {
|
|
GLPIPlanning.display(".json_encode($options).");
|
|
GLPIPlanning.planningFilters();
|
|
});");
|
|
|
|
return;
|
|
}
|
|
|
|
static function getTimelineResources() {
|
|
$resources = [];
|
|
foreach ($_SESSION['glpi_plannings']['plannings'] as $planning_id => $planning) {
|
|
if ($planning['type'] == 'external') {
|
|
$resources[] = [
|
|
'id' => $planning_id,
|
|
'title' => $planning['name'],
|
|
'group_id' => false,
|
|
'is_visible' => $planning['display'],
|
|
'itemtype' => null,
|
|
'items_id' => null
|
|
];
|
|
continue; // Ignore external calendars
|
|
}
|
|
|
|
$exploded = explode('_', $planning_id);
|
|
if ($planning['type'] == 'group_users') {
|
|
$group_exploded = explode('_', $planning_id);
|
|
$group_id = (int) $group_exploded[1];
|
|
$group = new Group;
|
|
$group->getFromDB($group_id);
|
|
$resources[] = [
|
|
'id' => $planning_id,
|
|
'title' => $group->getName(),
|
|
'eventAllow' => false,
|
|
'is_visible' => $planning['display'],
|
|
'itemtype' => 'Group_User',
|
|
'items_id' => $group_id
|
|
];
|
|
foreach (array_keys($planning['users']) as $planning_id_user) {
|
|
$child_exploded = explode('_', $planning_id_user);
|
|
$user = new User;
|
|
$users_id = (int) $child_exploded[1];
|
|
$user->getFromDB($users_id);
|
|
$planning_id_user = "gu_".$planning_id_user;
|
|
$resources[] = [
|
|
'id' => $planning_id_user,
|
|
'title' => $user->getName(),
|
|
'is_visible' => $planning['display'],
|
|
'itemtype' => 'User',
|
|
'items_id' => $users_id,
|
|
'parentId' => $planning_id,
|
|
];
|
|
}
|
|
} else {
|
|
$itemtype = $exploded[0];
|
|
$object = new $itemtype;
|
|
$users_id = (int) $exploded[1];
|
|
$object->getFromDB($users_id);
|
|
|
|
$resources[] = [
|
|
'id' => $planning_id,
|
|
'title' => $object->getName(),
|
|
'group_id' => false,
|
|
'is_visible' => $planning['display'],
|
|
'itemtype' => $itemtype,
|
|
'items_id' => $users_id
|
|
];
|
|
}
|
|
}
|
|
|
|
return $resources;
|
|
}
|
|
|
|
/**
|
|
* Return a palette array (for example self::$palette_bg)
|
|
* @param string $palette_name the short name for palette (bg, fg, ev)
|
|
* @return mixed the palette array or false
|
|
*
|
|
* @since 9.1.1
|
|
*/
|
|
static function getPalette($palette_name = 'bg') {
|
|
if (in_array($palette_name, ['bg', 'fg', 'ev'])) {
|
|
return self::${"palette_$palette_name"};
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return an hexa color from a palette
|
|
* @param string $palette_name the short name for palette (bg, fg, ev)
|
|
* @param integer $color_index The color index in this palette
|
|
* @return mixed the color in hexa (ex: #FFFFFF) or false
|
|
*
|
|
* @since 9.1.1
|
|
*/
|
|
static function getPaletteColor($palette_name = 'bg', $color_index = 0) {
|
|
if ($palette = self::getPalette($palette_name)) {
|
|
if ($color_index > count($palette)) {
|
|
$color_index = $color_index % count($palette);
|
|
}
|
|
|
|
return $palette[$color_index];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static function getPlanningTypes() {
|
|
global $CFG_GLPI;
|
|
|
|
return array_merge(
|
|
$CFG_GLPI['planning_types'],
|
|
['NotPlanned', 'OnlyBgEvents']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Init $_SESSION['glpi_plannings'] var with thses keys :
|
|
* - 'filters' : type of planning available (ChangeTask, Reminder, etc)
|
|
* - 'plannings' : all plannings definided for current user.
|
|
*
|
|
* If currently logged user, has no plannings or filter, this function wiil init them
|
|
*
|
|
* Also manage color index in $_SESSION['glpi_plannings_color_index']
|
|
*
|
|
* @return void
|
|
*/
|
|
static function initSessionForCurrentUser() {
|
|
// new user in planning, init session
|
|
if (!isset($_SESSION['glpi_plannings']['filters'])) {
|
|
$_SESSION['glpi_plannings']['filters'] = [];
|
|
$_SESSION['glpi_plannings']['plannings'] = ['user_'.$_SESSION['glpiID'] => [
|
|
'color' => self::getPaletteColor('bg', 0),
|
|
'display' => true,
|
|
'type' => 'user']];
|
|
}
|
|
|
|
// complete missing filters
|
|
$filters = &$_SESSION['glpi_plannings']['filters'];
|
|
$index_color = 0;
|
|
foreach (self::getPlanningTypes() as $planning_type) {
|
|
if (in_array($planning_type, ['NotPlanned', 'OnlyBgEvents']) || $planning_type::canView()) {
|
|
if (!isset($filters[$planning_type])) {
|
|
$filters[$planning_type] = [
|
|
'color' => self::getPaletteColor('ev', $index_color),
|
|
'display' => !in_array($planning_type, ['NotPlanned', 'OnlyBgEvents']),
|
|
'type' => 'event_filter'
|
|
];
|
|
}
|
|
$index_color++;
|
|
}
|
|
}
|
|
|
|
// compute color index for plannings
|
|
$_SESSION['glpi_plannings_color_index'] = 0;
|
|
foreach ($_SESSION['glpi_plannings']['plannings'] as $planning) {
|
|
if ($planning['type'] == 'group_users') {
|
|
$_SESSION['glpi_plannings_color_index']+= count($planning['users']);
|
|
} else {
|
|
$_SESSION['glpi_plannings_color_index']++;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Display left part of planning who contains filters and planning with delete/toggle buttons
|
|
* and color choosing.
|
|
* Call self::showSingleLinePlanningFilter for each filters and plannings
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showPlanningFilter() {
|
|
global $CFG_GLPI;
|
|
|
|
$headings = ['filters' => __("Events type"),
|
|
'plannings' => __('Plannings')];
|
|
|
|
echo "<div id='planning_filter'>";
|
|
|
|
echo "<div id='planning_filter_toggle'>";
|
|
echo "<a class='toggle pointer' title='".__s("Toggle filters")."'></a>";
|
|
echo "</div>";
|
|
|
|
echo "<div id='planning_filter_content'>";
|
|
foreach ($_SESSION['glpi_plannings'] as $filter_heading => $filters) {
|
|
if (!in_array($filter_heading, array_keys($headings))) {
|
|
continue;
|
|
}
|
|
|
|
echo "<div>";
|
|
echo "<h3>";
|
|
echo $headings[$filter_heading];
|
|
if ($filter_heading == "plannings") {
|
|
echo "<a class='planning_link planning_add_filter' href='".$CFG_GLPI['root_doc'].
|
|
'/ajax/planning.php?action=add_planning_form'."'>";
|
|
echo "<img class='pointer' src='".$CFG_GLPI['root_doc']."/pics/add_dark.png'>";
|
|
echo "</a>";
|
|
}
|
|
echo "</h3>";
|
|
echo "<ul class='filters'>";
|
|
foreach ($filters as $filter_key => $filter_data) {
|
|
self::showSingleLinePlanningFilter($filter_key,
|
|
$filter_data,
|
|
['filter_color_index' => 0]);
|
|
}
|
|
echo "</ul>";
|
|
echo "</div>";
|
|
}
|
|
echo "</div>";
|
|
echo "</div>";
|
|
}
|
|
|
|
|
|
/**
|
|
* Display a single line of planning filter.
|
|
* See self::showPlanningFilter function
|
|
*
|
|
* @param $filter_key : identify curent line of filter
|
|
* @param $filter_data : array of filter date, must contains :
|
|
* * 'show_delete' (boolean): show delete button
|
|
* * 'filter_color_index' (integer): index of the color to use in self::$palette_bg
|
|
* @param $options
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showSingleLinePlanningFilter($filter_key, $filter_data, $options = []) {
|
|
global $CFG_GLPI;
|
|
|
|
$params['show_delete'] = true;
|
|
$params['filter_color_index'] = 0;
|
|
if (is_array($options) && count($options)) {
|
|
foreach ($options as $key => $val) {
|
|
$params[$key] = $val;
|
|
}
|
|
}
|
|
|
|
$actor = explode('_', $filter_key);
|
|
$uID = 0;
|
|
$gID = 0;
|
|
if ($filter_data['type'] == 'user') {
|
|
$uID = $actor[1];
|
|
$user = new User();
|
|
$user->getFromDB($actor[1]);
|
|
$title = $user->getName();
|
|
} else if ($filter_data['type'] == 'group_users') {
|
|
$group = new Group();
|
|
$group->getFromDB($actor[1]);
|
|
$title = $group->getName();
|
|
} else if ($filter_data['type'] == 'group') {
|
|
$gID = $actor[1];
|
|
$group = new Group();
|
|
$group->getFromDB($actor[1]);
|
|
$title = $group->getName();
|
|
} else if ($filter_data['type'] == 'external') {
|
|
$title = $filter_data['name'];
|
|
} else if ($filter_data['type'] == 'event_filter') {
|
|
if ($filter_key == 'NotPlanned') {
|
|
$title = __('Not planned tasks');
|
|
} else if ($filter_key == 'OnlyBgEvents') {
|
|
$title = __('Only background events');
|
|
} else {
|
|
if (!getItemForItemtype($filter_key)) {
|
|
return false;
|
|
} else if (!$filter_key::canView()) {
|
|
return false;
|
|
}
|
|
$title = $filter_key::getTypeName();
|
|
}
|
|
}
|
|
|
|
echo "<li event_type='".$filter_data['type']."'
|
|
event_name='$filter_key'
|
|
class='".$filter_data['type']."'>";
|
|
Html::showCheckbox(['name' => 'filters[]',
|
|
'value' => $filter_key,
|
|
'title' => $title,
|
|
'checked' => $filter_data['display']]);
|
|
|
|
if ($filter_data['type'] != 'event_filter') {
|
|
$exploded = explode('_', $filter_data['type']);
|
|
$icon = "user";
|
|
if ($exploded[0] === 'group') {
|
|
$icon = "users";
|
|
}
|
|
echo "<i class='actor_icon fa fa-fw fa-$icon'></i>";
|
|
}
|
|
|
|
echo "<label for='$filter_key'>$title</label>";
|
|
|
|
$color = self::$palette_bg[$params['filter_color_index']];
|
|
if (isset($filter_data['color']) && !empty($filter_data['color'])) {
|
|
$color = $filter_data['color'];
|
|
} else {
|
|
$params['filter_color_index']++;
|
|
$color = self::getPaletteColor('bg', $params['filter_color_index']);
|
|
}
|
|
|
|
if ($filter_data['type'] != 'event_filter') {
|
|
echo "<span class='filter_option'>";
|
|
echo "<img class='pointer' src='".$CFG_GLPI['root_doc']."/pics/down.png' />";
|
|
echo "<ul>";
|
|
if ($params['show_delete']) {
|
|
echo "<li class='delete_planning' value='$filter_key'>".__("Delete")."</li>";
|
|
}
|
|
if ($filter_data['type'] != 'group_users' && $filter_data['type'] != 'external') {
|
|
$url = parse_url($CFG_GLPI["url_base"]);
|
|
$port = 80;
|
|
if (isset($url['port'])) {
|
|
$port = $url['port'];
|
|
} else if (isset($url['scheme']) && ($url["scheme"] == 'https')) {
|
|
$port = 443;
|
|
}
|
|
|
|
$loginUser = new User();
|
|
$loginUser->getFromDB(Session::getLoginUserID(true));
|
|
$cal_url = "/front/planning.php?genical=1&uID=".$uID."&gID=".$gID.
|
|
//"&limititemtype=$limititemtype".
|
|
"&entities_id=".$_SESSION["glpiactive_entity"].
|
|
"&is_recursive=".$_SESSION["glpiactive_entity_recursive"].
|
|
"&token=".$loginUser->getAuthToken();
|
|
|
|
echo "<li><a target='_blank' href='".$CFG_GLPI["root_doc"]."$cal_url'>".
|
|
_sx("button", "Export")." - ".__("Ical")."</a></li>";
|
|
|
|
echo "<li><a target='_blank' href='webcal://".$url['host'].":$port".
|
|
(isset($url['path'])?$url['path']:'')."$cal_url'>".
|
|
_sx("button", "Export")." - ".__("Webcal")."</a></li>";
|
|
|
|
echo "<li><a target='_blank' href='".$CFG_GLPI['root_doc'].
|
|
"/front/planningcsv.php?uID=".$uID."&gID=".$gID."'>".
|
|
_sx("button", "Export")." - ".__("CSV")."</a></li>";
|
|
|
|
$caldav_url = $CFG_GLPI['url_base']
|
|
. '/caldav.php/'
|
|
. self::getCaldavBaseCalendarUrl($filter_data['type'] == 'user' ? $user : $group);
|
|
$copy_js = 'copyTextToClipboard("' . $caldav_url . '");'
|
|
. ' alert("' . __s('CalDAV URL has been copied to clipboard') . '");'
|
|
. ' return false;';
|
|
echo "<li><a target='_blank' href='#'
|
|
onclick='$copy_js'>".
|
|
__s("Copy CalDAV URL to clipboard")."</a></li>";
|
|
}
|
|
echo "</ul>";
|
|
echo "</span>";
|
|
}
|
|
|
|
// colors not for groups
|
|
if ($filter_data['type'] != 'group_users' && $filter_key != 'OnlyBgEvents') {
|
|
echo "<span class='color_input'>";
|
|
Html::showColorField($filter_key."_color",
|
|
['value' => $color]);
|
|
echo "</span>";
|
|
}
|
|
if ($filter_data['type'] == 'group_users') {
|
|
echo "<span class='toggle pointer'></span>";
|
|
}
|
|
|
|
if ($filter_data['type'] == 'group_users') {
|
|
echo "<ul class='group_listofusers filters'>";
|
|
foreach ($filter_data['users'] as $user_key => $userdata) {
|
|
self::showSingleLinePlanningFilter($user_key,
|
|
$userdata,
|
|
['show_delete' => false,
|
|
'filter_color_index' => $params['filter_color_index']]);
|
|
}
|
|
echo "</ul>";
|
|
}
|
|
|
|
echo "</li>";
|
|
}
|
|
|
|
|
|
/**
|
|
* Display ajax form to add actor on planning
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showAddPlanningForm() {
|
|
global $CFG_GLPI;
|
|
|
|
$rand = mt_rand();
|
|
echo "<form action='".self::getFormURL()."'>";
|
|
echo __("Actor").": <br>";
|
|
|
|
$planning_types = ['user' => User::getTypeName(1)];
|
|
|
|
if (Session::haveRightsOr('planning', [self::READGROUP, self::READALL])) {
|
|
$planning_types['group_users'] = __('All users of a group');
|
|
$planning_types['group'] = Group::getTypeName(1);
|
|
}
|
|
|
|
$planning_types['external'] = __('External calendar');
|
|
|
|
Dropdown::showFromArray('planning_type',
|
|
$planning_types,
|
|
['display_emptychoice' => true,
|
|
'rand' => $rand]);
|
|
echo Html::scriptBlock("
|
|
$(function() {
|
|
$('#dropdown_planning_type$rand').on( 'change', function( e ) {
|
|
var planning_type = $(this).val();
|
|
$('#add_planning_subform$rand').load('".$CFG_GLPI['root_doc']."/ajax/planning.php',
|
|
{action: 'add_'+planning_type+'_form'});
|
|
});
|
|
});");
|
|
echo "<br><br>";
|
|
echo "<div id='add_planning_subform$rand'></div>";
|
|
Html::closeForm();
|
|
}
|
|
|
|
|
|
/**
|
|
* Display 'User' part of self::showAddPlanningForm spcified by planning type dropdown.
|
|
* Actually called by ajax/planning.php
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showAddUserForm() {
|
|
$used = [];
|
|
foreach (array_keys($_SESSION['glpi_plannings']) as $actor) {
|
|
$actor = explode("_", $actor);
|
|
if ($actor[0] == "user") {
|
|
$used[] = $actor[1];
|
|
}
|
|
}
|
|
echo User::getTypeName(1)." :<br>";
|
|
|
|
// show only users with right to add planning events
|
|
$rights = ['change', 'problem', 'reminder', 'task', 'projecttask'];
|
|
// Can we see only personnal planning ?
|
|
if (!Session::haveRightsOr('planning', [self::READALL, self::READGROUP])) {
|
|
$rights = 'id';
|
|
}
|
|
// Can we see user of my groups ?
|
|
if (Session::haveRight('planning', self::READGROUP)
|
|
&& !Session::haveRight('planning', self::READALL)) {
|
|
$rights = 'groups';
|
|
}
|
|
|
|
User::dropdown(['entity' => $_SESSION['glpiactive_entity'],
|
|
'entity_sons' => $_SESSION['glpiactive_entity_recursive'],
|
|
'right' => $rights,
|
|
'used' => $used]);
|
|
echo "<br /><br />";
|
|
echo Html::hidden('action', ['value' => 'send_add_user_form']);
|
|
echo Html::submit(_sx('button', 'Add'));
|
|
}
|
|
|
|
|
|
/**
|
|
* Recieve 'User' data from self::showAddPlanningForm and save them to session and DB
|
|
*
|
|
* @param $params (array) : must contais form data (typically $_REQUEST)
|
|
*/
|
|
static function sendAddUserForm($params = []) {
|
|
$_SESSION['glpi_plannings']['plannings']["user_".$params['users_id']]
|
|
= ['color' => self::getPaletteColor('bg', $_SESSION['glpi_plannings_color_index']),
|
|
'display' => true,
|
|
'type' => 'user'];
|
|
self::savePlanningsInDB();
|
|
$_SESSION['glpi_plannings_color_index']++;
|
|
}
|
|
|
|
|
|
/**
|
|
* Display 'All users of a group' part of self::showAddPlanningForm spcified by planning type dropdown.
|
|
* Actually called by ajax/planning.php
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showAddGroupUsersForm() {
|
|
echo Group::getTypeName(1)." : <br>";
|
|
|
|
$condition = ['is_task' => 1];
|
|
// filter groups
|
|
if (!Session::haveRight('planning', self::READALL)) {
|
|
$condition['id'] = $_SESSION['glpigroups'];
|
|
}
|
|
|
|
Group::dropdown([
|
|
'entity' => $_SESSION['glpiactive_entity'],
|
|
'entity_sons' => $_SESSION['glpiactive_entity_recursive'],
|
|
'condition' => $condition
|
|
]);
|
|
echo "<br /><br />";
|
|
echo Html::hidden('action', ['value' => 'send_add_group_users_form']);
|
|
echo Html::submit(_sx('button', 'Add'));
|
|
}
|
|
|
|
|
|
/**
|
|
* Recieve 'All users of a group' data from self::showAddGroupUsersForm and save them to session and DB
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param $params (array) : must contais form data (typically $_REQUEST)
|
|
*/
|
|
static function sendAddGroupUsersForm($params = []) {
|
|
$current_group = &$_SESSION['glpi_plannings']['plannings']["group_".$params['groups_id']."_users"];
|
|
$current_group = ['display' => true,
|
|
'type' => 'group_users',
|
|
'users' => []];
|
|
$users = Group_User::getGroupUsers($params['groups_id'], [
|
|
'glpi_users.is_active' => 1,
|
|
'glpi_users.is_deleted' => 0,
|
|
[
|
|
'OR' => [
|
|
['glpi_users.begin_date' => null],
|
|
['glpi_users.begin_date' => ['<', new QueryExpression('NOW()')]],
|
|
],
|
|
],
|
|
[
|
|
'OR' => [
|
|
['glpi_users.end_date' => null],
|
|
['glpi_users.end_date' => ['>', new QueryExpression('NOW()')]],
|
|
]
|
|
]
|
|
]);
|
|
|
|
foreach ($users as $user_data) {
|
|
$current_group['users']['user_'.$user_data['id']] = [
|
|
'color' => self::getPaletteColor('bg', $_SESSION['glpi_plannings_color_index']),
|
|
'display' => true,
|
|
'type' => 'user'
|
|
];
|
|
$_SESSION['glpi_plannings_color_index']++;
|
|
}
|
|
self::savePlanningsInDB();
|
|
}
|
|
|
|
|
|
static function editEventForm($params = []) {
|
|
if (!$params['itemtype'] instanceof CommonDBTM) {
|
|
echo "<div class='center'>";
|
|
echo "<a href='".$params['url']."'>".__("View this item in his context")."</a>";
|
|
echo "</div>";
|
|
echo "<hr>";
|
|
$rand = mt_rand();
|
|
$options = [
|
|
'from_planning_edit_ajax' => true,
|
|
'formoptions' => "id='edit_event_form$rand'",
|
|
'start' => date("Y-m-d", strtotime($params['start']))
|
|
];
|
|
if (isset($params['parentitemtype'])) {
|
|
$options['parent'] = getItemForItemtype($params['parentitemtype']);
|
|
$options['parent']->getFromDB($params['parentid']);
|
|
}
|
|
$item = getItemForItemtype($params['itemtype']);
|
|
$item->showForm(intval($params['id']), $options);
|
|
$callback = "$('.ui-dialog-content').dialog('close');
|
|
GLPIPlanning.refresh();
|
|
displayAjaxMessageAfterRedirect();";
|
|
Html::ajaxForm("#edit_event_form$rand", $callback);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Display 'Group' part of self::showAddPlanningForm spcified by planning type dropdown.
|
|
* Actually called by ajax/planning.php
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showAddGroupForm() {
|
|
|
|
$condition = ['is_task' => 1];
|
|
// filter groups
|
|
if (!Session::haveRight('planning', self::READALL)) {
|
|
$condition['id'] = $_SESSION['glpigroups'];
|
|
}
|
|
|
|
echo Group::getTypeName(1)." : <br>";
|
|
Group::dropdown([
|
|
'entity' => $_SESSION['glpiactive_entity'],
|
|
'entity_sons' => $_SESSION['glpiactive_entity_recursive'],
|
|
'condition' => $condition
|
|
]);
|
|
echo "<br /><br />";
|
|
echo Html::hidden('action', ['value' => 'send_add_group_form']);
|
|
echo Html::submit(_sx('button', 'Add'));
|
|
}
|
|
|
|
|
|
/**
|
|
* Recieve 'Group' data from self::showAddGroupForm and save them to session and DB
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param $params (array) : must contais form data (typically $_REQUEST)
|
|
*/
|
|
static function sendAddGroupForm($params = []) {
|
|
$_SESSION['glpi_plannings']['plannings']["group_".$params['groups_id']]
|
|
= ['color' => self::getPaletteColor('bg',
|
|
$_SESSION['glpi_plannings_color_index']),
|
|
'display' => true,
|
|
'type' => 'group'];
|
|
self::savePlanningsInDB();
|
|
$_SESSION['glpi_plannings_color_index']++;
|
|
}
|
|
|
|
|
|
/**
|
|
* Display 'External' part of self::showAddPlanningForm specified by planning type dropdown.
|
|
* Actually called by ajax/planning.php
|
|
*
|
|
* @since 9.5
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showAddExternalForm() {
|
|
|
|
$rand = mt_rand();
|
|
|
|
echo '<label for ="name' . $rand . '">' . __("Calendar name") . ' : </label> ';
|
|
echo '<br />';
|
|
echo Html::input(
|
|
'name',
|
|
[
|
|
'value' => '',
|
|
'id' => 'name' . $rand,
|
|
]
|
|
);
|
|
echo '<br />';
|
|
echo '<br />';
|
|
|
|
echo '<label for ="url' . $rand . '">' . __("Calendar URL") . ' : </label> ';
|
|
echo '<br />';
|
|
echo '<input type="url" name="url" id="url' . $rand . '" required>';
|
|
echo '<br /><br />';
|
|
|
|
echo Html::hidden('action', ['value' => 'send_add_external_form']);
|
|
echo Html::submit(_sx('button', 'Add'));
|
|
}
|
|
|
|
|
|
/**
|
|
* Receive 'External' data from self::showAddExternalForm and save them to session and DB
|
|
*
|
|
* @since 9.5
|
|
*
|
|
* @param array $params Form data
|
|
*
|
|
* @return void
|
|
*/
|
|
static function sendAddExternalForm($params = []) {
|
|
$_SESSION['glpi_plannings']['plannings']['external_' . md5($params['url'])] = [
|
|
'color' => self::getPaletteColor('bg', $_SESSION['glpi_plannings_color_index']),
|
|
'display' => true,
|
|
'type' => 'external',
|
|
'name' => $params['name'],
|
|
'url' => $params['url'],
|
|
];
|
|
self::savePlanningsInDB();
|
|
$_SESSION['glpi_plannings_color_index']++;
|
|
}
|
|
|
|
|
|
static function showAddEventForm($params = []) {
|
|
global $CFG_GLPI;
|
|
|
|
if (count ($CFG_GLPI['planning_add_types']) == 1) {
|
|
$params['itemtype'] = $CFG_GLPI['planning_add_types'][0];
|
|
self::showAddEventSubForm($params);
|
|
} else {
|
|
$rand = mt_rand();
|
|
$select_options = [];
|
|
foreach ($CFG_GLPI['planning_add_types'] as $add_types) {
|
|
$select_options[$add_types] = $add_types::getTypeName(1);
|
|
}
|
|
echo __("Event type")." : <br>";
|
|
Dropdown::showFromArray('itemtype',
|
|
$select_options,
|
|
['display_emptychoice' => true,
|
|
'rand' => $rand]);
|
|
|
|
echo Html::scriptBlock("
|
|
$(function() {
|
|
$('#dropdown_itemtype$rand').on('change', function() {
|
|
var current_itemtype = $(this).val();
|
|
$('#add_planning_subform$rand').load('".$CFG_GLPI['root_doc']."/ajax/planning.php',
|
|
{action: 'add_event_sub_form',
|
|
itemtype: current_itemtype,
|
|
begin: '".$params['begin']."',
|
|
end: '".$params['end']."'});
|
|
});
|
|
});");
|
|
echo "<br><br>";
|
|
echo "<div id='add_planning_subform$rand'></div>";
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Display form after selecting date range in planning
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param $params (array): must contains this keys :
|
|
* - begin : start of selection range.
|
|
* (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
|
|
* - end : end of selection range.
|
|
* (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
|
|
*
|
|
* @return void
|
|
*/
|
|
static function showAddEventSubForm($params = []) {
|
|
|
|
$rand = mt_rand();
|
|
$params = self::cleanDates($params);
|
|
|
|
$params['res_itemtype'] = $params['res_itemtype'] ?? '';
|
|
$params['res_items_id'] = $params['res_items_id'] ?? 0;
|
|
if ($item = getItemForItemtype($params['itemtype'])) {
|
|
$item->showForm('', [
|
|
'from_planning_ajax' => true,
|
|
'begin' => $params['begin'],
|
|
'end' => $params['end'],
|
|
'res_itemtype' => $params['res_itemtype'],
|
|
'res_items_id' => $params['res_items_id'],
|
|
'formoptions' => "id='ajax_reminder$rand'"
|
|
]);
|
|
$callback = "$('.ui-dialog-content').dialog('close');
|
|
GLPIPlanning.refresh();
|
|
displayAjaxMessageAfterRedirect();";
|
|
Html::ajaxForm("#ajax_reminder$rand", $callback);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Former front/planning.php before 9.1.
|
|
* Display a classic form to plan an event (with begin fiel and duration)
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param $params (array): array of parameters whou should contain :
|
|
* - id (integer): id of item who receive the planification
|
|
* - itemtype (string): itemtype of item who receive the planification
|
|
* - begin (string) : start date of event
|
|
* - _display_dates (bool) : display dates fields (default true)
|
|
* - end (optionnal) (string) : end date of event. Ifg missing, it will computerd from begin+1hour
|
|
* - rand_user (integer) : users_id to check planning avaibility
|
|
* - rand : specific rand if needed (default is generated one)
|
|
*/
|
|
static function showAddEventClassicForm($params = []) {
|
|
global $CFG_GLPI;
|
|
|
|
if (isset($params["id"]) && ($params["id"] > 0)) {
|
|
echo "<input type='hidden' name='plan[id]' value='".$params["id"]."'>";
|
|
}
|
|
|
|
$rand = mt_rand();
|
|
if (isset($params['rand'])) {
|
|
$rand = $params['rand'];
|
|
}
|
|
|
|
$display_dates = $params['_display_dates'] ?? true;
|
|
|
|
$mintime = $CFG_GLPI["planning_begin"];
|
|
if (isset($params["begin"]) && !empty($params["begin"])) {
|
|
$begin = $params["begin"];
|
|
$begintime = date( "H:i:s", strtotime($begin));
|
|
if ($begintime < $mintime) {
|
|
$mintime = $begintime;
|
|
}
|
|
|
|
} else {
|
|
$ts = $CFG_GLPI['time_step'] * 60; // passage en minutes
|
|
$time = time() + $ts - 60;
|
|
$time = floor($time / $ts) * $ts;
|
|
$begin = date("Y-m-d H:i", $time);
|
|
}
|
|
|
|
if (isset($params["end"]) && !empty($params["end"])) {
|
|
$end = $params["end"];
|
|
|
|
} else {
|
|
$end = date("Y-m-d H:i:s", strtotime($begin)+HOUR_TIMESTAMP);
|
|
}
|
|
|
|
echo "<table class='card'>";
|
|
|
|
if ($display_dates) {
|
|
echo "<tr class='tab_bg_2'><td>".__('Start date')."</td><td>";
|
|
Html::showDateTimeField("plan[begin]", [
|
|
'value' => $begin,
|
|
'maybeempty' => false,
|
|
'canedit' => true,
|
|
'mindate' => '',
|
|
'maxdate' => '',
|
|
'mintime' => $mintime,
|
|
'maxtime' => $CFG_GLPI["planning_end"],
|
|
'rand' => $rand,
|
|
]);
|
|
echo "</td></tr>";
|
|
}
|
|
|
|
echo "<tr class='tab_bg_2'><td>".__('Period')." ";
|
|
|
|
if (isset($params["rand_user"])) {
|
|
echo "<span id='user_available".$params["rand_user"]."'>";
|
|
include_once(GLPI_ROOT.'/ajax/planningcheck.php');
|
|
echo "</span>";
|
|
}
|
|
|
|
echo "</td><td>";
|
|
|
|
$empty_label = Dropdown::EMPTY_VALUE;
|
|
$default_delay = $params['duration'] ?? 0;
|
|
if ($display_dates) {
|
|
$empty_label = __('Specify an end date');
|
|
$default_delay = floor((strtotime($end)-strtotime($begin))/$CFG_GLPI['time_step']/MINUTE_TIMESTAMP)*$CFG_GLPI['time_step']*MINUTE_TIMESTAMP;
|
|
}
|
|
|
|
Dropdown::showTimeStamp("plan[_duration]", [
|
|
'min' => 0,
|
|
'max' => 50*HOUR_TIMESTAMP,
|
|
'value' => $default_delay,
|
|
'emptylabel' => $empty_label,
|
|
'rand' => $rand,
|
|
]);
|
|
echo "<br><div id='date_end$rand'></div>";
|
|
|
|
$event_options = [
|
|
'duration' => '__VALUE__',
|
|
'end' => $end,
|
|
'name' => "plan[end]",
|
|
'global_begin' => $CFG_GLPI["planning_begin"],
|
|
'global_end' => $CFG_GLPI["planning_end"]
|
|
];
|
|
|
|
if ($display_dates) {
|
|
Ajax::updateItemOnSelectEvent("dropdown_plan[_duration]$rand", "date_end$rand",
|
|
$CFG_GLPI["root_doc"]."/ajax/planningend.php", $event_options);
|
|
|
|
if ($default_delay == 0) {
|
|
$params['duration'] = 0;
|
|
Ajax::updateItem("date_end$rand", $CFG_GLPI["root_doc"]."/ajax/planningend.php", $params);
|
|
}
|
|
}
|
|
|
|
echo "</td></tr>\n";
|
|
|
|
if ((!isset($params["id"]) || ($params["id"] == 0))
|
|
&& isset($params['itemtype'])
|
|
&& PlanningRecall::isAvailable()) {
|
|
echo "<tr class='tab_bg_2'><td>"._x('Planning', 'Reminder')."</td><td>";
|
|
PlanningRecall::dropdown([
|
|
'itemtype' => $params['itemtype'],
|
|
'items_id' => $params['items_id'],
|
|
'rand' => $rand,
|
|
]);
|
|
echo "</td></tr>";
|
|
}
|
|
echo "</table>\n";
|
|
}
|
|
|
|
|
|
/**
|
|
* Clone an event
|
|
*
|
|
* @since 9.5
|
|
*
|
|
* @param array $event the event to clone
|
|
*
|
|
* @return mixed the id (integer) or false if it failed
|
|
*/
|
|
static function cloneEvent(array $event = []) {
|
|
$item = new $event['old_itemtype'];
|
|
$item->getFromDB((int) $event['old_items_id']);
|
|
|
|
$input = array_merge($item->fields, [
|
|
'plan' => [
|
|
'begin' => date("Y-m-d H:i:s", strtotime($event['start'])),
|
|
'end' => date("Y-m-d H:i:s", strtotime($event['end'])),
|
|
],
|
|
]);
|
|
unset($input['id'], $input['uuid']);
|
|
|
|
if (isset($item->fields['name'])) {
|
|
$input['name'] = sprintf(__('Copy of %s'), $item->fields['name']);
|
|
}
|
|
|
|
// manage change of assigment for CommonITILTask
|
|
if ($item instanceof CommonITILTask
|
|
&& isset($event['actor']['itemtype'])
|
|
&& isset($event['actor']['items_id'])) {
|
|
switch ($event['actor']['itemtype']) {
|
|
case "group":
|
|
$key = "groups_id_tech";
|
|
break;
|
|
case "user":
|
|
$key = isset($item->fields['users_id_tech']) ? "users_id_tech" : "users_id";
|
|
break;
|
|
}
|
|
|
|
unset(
|
|
$input['users_id_tech'],
|
|
$input['users_id'],
|
|
$input['groups_id_tech'],
|
|
$input['groups_id']
|
|
);
|
|
|
|
$input[$key] = $event['actor']['items_id'];
|
|
}
|
|
|
|
$new_items_id = $item->add(Toolbox::addslashes_deep($input));
|
|
|
|
// manage all assigments for ProjectTask
|
|
if ($item instanceof ProjectTask
|
|
&& isset($event['actor']['itemtype'])
|
|
&& isset($event['actor']['items_id'])) {
|
|
$team = new ProjectTaskTeam;
|
|
$team->add([
|
|
'projecttasks_id' => $new_items_id,
|
|
'itemtype' => ucfirst($event['actor']['itemtype']),
|
|
'items_id' => $event['actor']['items_id']
|
|
]);
|
|
}
|
|
|
|
return $new_items_id;
|
|
}
|
|
|
|
/**
|
|
* Delete an event
|
|
*
|
|
* @since 9.5
|
|
*
|
|
* @param array $event the event to clone (with itemtype and items_id keys)
|
|
*
|
|
* @return bool
|
|
*/
|
|
static function deleteEvent(array $event = []):bool {
|
|
$item = new $event['itemtype'];
|
|
|
|
if (isset($event['day'])
|
|
&& isset($event['instance'])
|
|
&& $event['instance']
|
|
&& method_exists($item, "deleteInstance")) {
|
|
return $item->deleteInstance((int) $event['items_id'], $event['day']);
|
|
} else {
|
|
return $item->delete([
|
|
'id' => (int) $event['items_id']
|
|
]);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* toggle display for selected line of $_SESSION['glpi_plannings']
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param array $options: should contains :
|
|
* - type : event type, can be event_filter, user, group or group_users
|
|
* - parent : in case of type=users_group, must contains the id of the group
|
|
* - name : contains a string with type and id concatened with a '_' char (ex user_41).
|
|
* - display : boolean value to set to his line
|
|
* @return void
|
|
*/
|
|
static function toggleFilter($options = []) {
|
|
|
|
$key = 'filters';
|
|
if (in_array($options['type'], ['user', 'group', 'group_users', 'external'])) {
|
|
$key = 'plannings';
|
|
}
|
|
if (!isset($options['parent'])
|
|
|| empty($options['parent'])) {
|
|
$_SESSION['glpi_plannings'][$key][$options['name']]['display']
|
|
= ($options['display'] === 'true');
|
|
} else {
|
|
$_SESSION['glpi_plannings']['plannings'][$options['parent']]['users']
|
|
[$options['name']]['display']
|
|
= ($options['display'] === 'true');
|
|
}
|
|
self::savePlanningsInDB();
|
|
}
|
|
|
|
|
|
/**
|
|
* change color for selected line of $_SESSION['glpi_plannings']
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param array $options: should contains :
|
|
* - type : event type, can be event_filter, user, group or group_users
|
|
* - parent : in case of type=users_group, must contains the id of the group
|
|
* - name : contains a string with type and id concatened with a '_' char (ex user_41).
|
|
* - color : rgb color (preceded by '#'' char)
|
|
* @return void
|
|
*/
|
|
static function colorFilter($options = []) {
|
|
$key = 'filters';
|
|
if (in_array($options['type'], ['user', 'group', 'group_users', 'external'])) {
|
|
$key = 'plannings';
|
|
}
|
|
if (!isset($options['parent'])
|
|
|| empty($options['parent'])) {
|
|
$_SESSION['glpi_plannings'][$key][$options['name']]['color'] = $options['color'];
|
|
} else {
|
|
$_SESSION['glpi_plannings']['plannings'][$options['parent']]['users']
|
|
[$options['name']]['color'] = $options['color'];
|
|
}
|
|
self::savePlanningsInDB();
|
|
}
|
|
|
|
|
|
/**
|
|
* delete selected line in $_SESSION['glpi_plannings']
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param array $options: should contains :
|
|
* - type : event type, can be event_filter, user, group or group_users
|
|
* - filter : contains a string with type and id concatened with a '_' char (ex user_41).
|
|
* @return void
|
|
*/
|
|
static function deleteFilter($options = []) {
|
|
|
|
$current = $_SESSION['glpi_plannings']['plannings'][$options['filter']];
|
|
if ($current['type'] == 'group_users') {
|
|
$_SESSION['glpi_plannings_color_index']-= count($current['users']);
|
|
} else {
|
|
$_SESSION['glpi_plannings_color_index']--;
|
|
}
|
|
|
|
unset($_SESSION['glpi_plannings']['plannings'][$options['filter']]);
|
|
self::savePlanningsInDB();
|
|
}
|
|
|
|
|
|
static function savePlanningsInDB() {
|
|
|
|
$user = new User;
|
|
$user->update(['id' => $_SESSION['glpiID'],
|
|
'plannings' => exportArrayToDB($_SESSION['glpi_plannings'])]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Prepare a set of events for jquery fullcalendar.
|
|
* Call populatePlanning functions for all $CFG_GLPI['planning_types'] types
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param array $options with this keys:
|
|
* - begin: mandatory, planning start.
|
|
* (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
|
|
* - end: mandatory, planning end.
|
|
* (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
|
|
* - display_done_events: default true, show also events tagged as done
|
|
* - force_all_events: even if the range is big, don't reduce the returned set
|
|
* @return array $events : array with events in fullcalendar.io format
|
|
*/
|
|
static function constructEventsArray($options = []) {
|
|
global $CFG_GLPI;
|
|
|
|
$param['start'] = '';
|
|
$param['end'] = '';
|
|
$param['view_name'] = '';
|
|
$param['display_done_events'] = true;
|
|
$param['force_all_events'] = false;
|
|
|
|
if (is_array($options) && count($options)) {
|
|
foreach ($options as $key => $val) {
|
|
$param[$key] = $val;
|
|
}
|
|
}
|
|
|
|
$time_begin = strtotime($param['start']);
|
|
$time_end = strtotime($param['end']);
|
|
|
|
// if the dates range is greater than a certain amount, and we're not on a list view
|
|
// we certainly are on this view (as our biggest view apart list is month one).
|
|
// we must avoid at all cost to calculate rrules events on a big range
|
|
if (!$param['force_all_events']
|
|
&& $param['view_name'] != "listFull"
|
|
&& ($time_end - $time_begin) > (2 * MONTH_TIMESTAMP)) {
|
|
$param['view_name'] = "listFull";
|
|
return [];
|
|
}
|
|
|
|
$param['begin'] = date("Y-m-d H:i:s", $time_begin);
|
|
$param['end'] = date("Y-m-d H:i:s", $time_end);
|
|
|
|
$raw_events = [];
|
|
$not_planned = [];
|
|
foreach ($CFG_GLPI['planning_types'] as $planning_type) {
|
|
if (!$planning_type::canView()) {
|
|
continue;
|
|
}
|
|
if ($_SESSION['glpi_plannings']['filters'][$planning_type]['display']) {
|
|
$event_type_color = $_SESSION['glpi_plannings']['filters'][$planning_type]['color'];
|
|
foreach ($_SESSION['glpi_plannings']['plannings'] as $actor => $actor_params) {
|
|
if ($actor_params['type'] == 'external') {
|
|
continue; // Ignore external calendars
|
|
}
|
|
$actor_params['event_type_color'] = $event_type_color;
|
|
$actor_params['planning_type'] = $planning_type;
|
|
self::constructEventsArraySingleLine($actor,
|
|
array_merge($param, $actor_params),
|
|
$raw_events, $not_planned);
|
|
}
|
|
}
|
|
}
|
|
|
|
//handle not planned events
|
|
$raw_events = array_merge($raw_events, $not_planned);
|
|
|
|
// get external calendars events (ical)
|
|
// and on list view, only get future events
|
|
$begin_ical = $param['begin'];
|
|
if ($param['view_name'] == "listFull") {
|
|
$begin_ical = date('Y-m-d 00:00:00');
|
|
}
|
|
$raw_events = array_merge(
|
|
$raw_events,
|
|
self::getExternalCalendarRawEvents($begin_ical, $param['end'])
|
|
);
|
|
|
|
// construct events (in fullcalendar format)
|
|
$events = [];
|
|
foreach ($raw_events as $event) {
|
|
if ($_SESSION['glpi_plannings']['filters']['OnlyBgEvents']['display']
|
|
&& (!isset($event['background']) || !$event['background'])) {
|
|
continue;
|
|
}
|
|
|
|
$users_id = (isset($event['users_id_tech']) && !empty($event['users_id_tech'])?
|
|
$event['users_id_tech']:
|
|
$event['users_id']);
|
|
$content = Planning::displayPlanningItem($event, $users_id, 'in', false) ?: ($event['content'] ?? "");
|
|
$tooltip = Planning::displayPlanningItem($event, $users_id, 'in', true) ?: ($event['tooltip'] ?? "");
|
|
|
|
// dates should be set with the user timezone
|
|
$begin = $event['begin'];
|
|
$end = $event['end'];
|
|
|
|
// retreive all day events
|
|
if (strpos($event['begin'], "00:00:00") != false
|
|
&& (strtotime($event['end']) - strtotime($event['begin'])) % DAY_TIMESTAMP == 0) {
|
|
$begin = date('Y-m-d', strtotime($event['begin']));
|
|
$end = date('Y-m-d', strtotime($event['end']));
|
|
}
|
|
|
|
// get duration in milliseconds
|
|
$ms_duration = (strtotime($end) - strtotime($begin)) * 1000;
|
|
|
|
$index_color = array_search("user_$users_id", array_keys($_SESSION['glpi_plannings']));
|
|
$new_event = [
|
|
'title' => $event['name'],
|
|
'content' => $content,
|
|
'tooltip' => $tooltip,
|
|
'start' => $begin,
|
|
'end' => $end,
|
|
'duration' => $ms_duration,
|
|
'_duration' => $ms_duration, // sometimes duration is removed from event object in fullcalendar
|
|
'_editable' => $event['editable'], // same, avoid loss of editable key in fullcalendar
|
|
'rendering' => isset($event['background'])
|
|
&& $event['background']
|
|
&& !$_SESSION['glpi_plannings']['filters']['OnlyBgEvents']['display']
|
|
? 'background'
|
|
: '',
|
|
'color' => (empty($event['color'])?
|
|
Planning::$palette_bg[$index_color]:
|
|
$event['color']),
|
|
'borderColor' => (empty($event['event_type_color'])?
|
|
self::getPaletteColor('ev', $event['itemtype']):
|
|
$event['event_type_color']),
|
|
'textColor' => Planning::$palette_fg[$index_color],
|
|
'typeColor' => (empty($event['event_type_color'])?
|
|
self::getPaletteColor('ev', $event['itemtype']):
|
|
$event['event_type_color']),
|
|
'url' => $event['url'] ?? "",
|
|
'ajaxurl' => $event['ajaxurl'] ?? "",
|
|
'itemtype' => $event['itemtype'] ?? "",
|
|
'parentitemtype' => $event['parentitemtype'] ?? "",
|
|
'items_id' => $event['id'] ?? "",
|
|
'resourceId' => $event['resourceId'] ?? "",
|
|
'priority' => $event['priority'] ?? "",
|
|
'state' => $event['state'] ?? "",
|
|
];
|
|
|
|
// if we can't update the event, pass the editable key
|
|
if (!$event['editable']) {
|
|
$new_event['editable'] = false;
|
|
}
|
|
|
|
// override color if view is ressource and category color exists
|
|
// maybe we need a better way for displaying categories color
|
|
if ($param['view_name'] == "resourceWeek"
|
|
&& !empty($event['event_cat_color'])) {
|
|
$new_event['color'] = $event['event_cat_color'];
|
|
}
|
|
|
|
// manage reccurent events
|
|
if (isset($event['rrule']) && count($event['rrule'])) {
|
|
$rrule = $event['rrule'];
|
|
|
|
// the fullcalencard plugin waits for integer types for number (not strings)
|
|
if (isset($rrule['interval'])) {
|
|
$rrule['interval'] = (int) $rrule['interval'];
|
|
}
|
|
if (isset($rrule['count'])) {
|
|
$rrule['count'] = (int) $rrule['count'];
|
|
}
|
|
|
|
// clean empty values in rrule
|
|
foreach ($rrule as $key => $value) {
|
|
if (is_null($value) || $value == '') {
|
|
unset($rrule[$key]);
|
|
}
|
|
}
|
|
|
|
$rset = PlanningExternalEvent::getRsetFromRRuleField($rrule, $new_event['start']);
|
|
|
|
// append icon to distinguish reccurent event in views
|
|
// use UTC datetime to avoid some issues with rlan/phprrule
|
|
$dtstart_datetime = new \DateTime($new_event['start']);
|
|
unset($rrule['exceptions']); // remove exceptions key (as libraries throw exception for unknow keys)
|
|
$hr_rrule_o = new RRule(
|
|
array_merge(
|
|
$rrule,
|
|
[
|
|
'dtstart' => $dtstart_datetime->format('Ymd\THis\Z')
|
|
]
|
|
)
|
|
);
|
|
$new_event = array_merge($new_event, [
|
|
'icon' => 'fas fa-history',
|
|
'icon_alt' => $hr_rrule_o->humanReadable(),
|
|
]);
|
|
|
|
// for fullcalendar, we need to pass start in the rrule key
|
|
unset($new_event['start'], $new_event['end']);
|
|
|
|
// For list view, only display only the next occurence
|
|
// to avoid issues performances (range in list view can be 10 years long)
|
|
if ($param['view_name'] == "listFull") {
|
|
$next_date = $rset->getNthOccurrenceAfter(new DateTime(), 1);
|
|
if ($next_date) {
|
|
$new_event = array_merge($new_event, [
|
|
'start' => $next_date->format('c'),
|
|
'end' => $next_date->add(new DateInterval("PT".($ms_duration / 1000)."S"))
|
|
->format('c'),
|
|
]);
|
|
}
|
|
} else {
|
|
$rrule_string = "";
|
|
foreach ($rset->getRRules() as $occurence) {
|
|
$rrule_string.= $occurence->rfcString(false)."\n";
|
|
}
|
|
$ex_dates = [];
|
|
foreach ($rset->getExDates() as $occurence) {
|
|
// we forge the ex date with only the date part of the exception
|
|
// and the hour of the dtstart.
|
|
// This to presents only date selection to the user
|
|
$ex_dates[] = "EXDATE:".$occurence->format('Ymd\THis');
|
|
}
|
|
|
|
if (count($ex_dates)) {
|
|
$rrule_string.= implode("\n", $ex_dates)."\n";
|
|
}
|
|
|
|
$new_event = array_merge($new_event, [
|
|
'is_recurrent' => true,
|
|
'rrule' => $rrule_string,
|
|
'duration' => $ms_duration
|
|
]);
|
|
|
|
}
|
|
}
|
|
|
|
$events[] = $new_event;
|
|
}
|
|
|
|
return $events;
|
|
}
|
|
|
|
|
|
/**
|
|
* construct a single line for self::constructEventsArray()
|
|
* Recursively called to construct $raw_events param.
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param string $actor: a type and id concaneted separated by '_' char, ex 'user_41'
|
|
* @param array $params: must contains this keys :
|
|
* - display: boolean for pass or not the consstruction of this line (a group of users can be displayed but its users not).
|
|
* - type: event type, can be event_filter, user, group or group_users
|
|
* - who: integer for identify user
|
|
* - whogroup: integer for identify group
|
|
* - color: string with #rgb color for event's foreground color.
|
|
* - event_type_color : string with #rgb color for event's foreground color.
|
|
* @param array $raw_events: (passed by reference) the events array in construction
|
|
* @param array $not_planned (passed by references) not planned events array in construction
|
|
* @return void
|
|
*/
|
|
static function constructEventsArraySingleLine($actor, $params = [], &$raw_events = [], &$not_planned = []) {
|
|
|
|
if ($params['display']) {
|
|
$actor_array = explode("_", $actor);
|
|
if ($params['type'] == "group_users") {
|
|
$subparams = $params;
|
|
unset($subparams['users']);
|
|
$subparams['from_group_users'] = true;
|
|
foreach ($params['users'] as $user => $userdata) {
|
|
$subparams = array_merge($subparams, $userdata);
|
|
self::constructEventsArraySingleLine($user, $subparams, $raw_events, $not_planned);
|
|
}
|
|
} else {
|
|
$params['who'] = $actor_array[1];
|
|
$params['whogroup'] = 0;
|
|
if ($params['type'] == "group"
|
|
&& in_array($params['planning_type'], self::$directgroup_itemtype)) {
|
|
$params['who'] = 0;
|
|
$params['whogroup'] = $actor_array[1];
|
|
}
|
|
|
|
$current_events = $params['planning_type']::populatePlanning($params);
|
|
if (count($current_events) > 0) {
|
|
$raw_events = array_merge($raw_events, $current_events);
|
|
}
|
|
if ($_SESSION['glpi_plannings']['filters']['NotPlanned']['display']
|
|
&& method_exists($params['planning_type'], 'populateNotPlanned')
|
|
) {
|
|
$not_planned = array_merge($not_planned, $params['planning_type']::populateNotPlanned($params));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($params['from_group_users']) && $params['from_group_users']) {
|
|
$actor = "gu_".$actor;
|
|
}
|
|
|
|
// fill type of planning
|
|
$raw_events = array_map(function($arr) use($actor) {
|
|
return $arr + ['resourceId' => $actor];
|
|
}, $raw_events);
|
|
|
|
if ($_SESSION['glpi_plannings']['filters']['NotPlanned']['display']) {
|
|
$not_planned = array_map(function($arr) use($actor) {
|
|
return $arr + [
|
|
'not_planned' => true,
|
|
'resourceId' => $actor,
|
|
'event_type_color' => $_SESSION['glpi_plannings']['filters']['NotPlanned']['color']
|
|
];
|
|
}, $not_planned);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return events fetched from user external calendars.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function getExternalCalendarRawEvents(string $limit_begin, string $limit_end): array {
|
|
$raw_events = [];
|
|
|
|
foreach ($_SESSION['glpi_plannings']['plannings'] as $planning_id => $planning_params) {
|
|
if ('external' !== $planning_params['type'] || !$planning_params['display']) {
|
|
continue; // Ignore non external and inactive calendars
|
|
}
|
|
$calendar_data = Toolbox::getURLContent($planning_params['url']);
|
|
if (empty($calendar_data)) {
|
|
continue;
|
|
}
|
|
try {
|
|
$vcalendar = Reader::read($calendar_data);
|
|
} catch (ParseException $exception) {
|
|
Toolbox::logError(sprintf('Unable to parse calendar data from URL "%s"', $planning_params['url']));
|
|
continue;
|
|
}
|
|
if (!$vcalendar instanceof VCalendar) {
|
|
Toolbox::logError(sprintf('No VCalendar object found at URL "%s"', $planning_params['url']));
|
|
continue;
|
|
}
|
|
foreach ($vcalendar->getComponents() as $vcomp) {
|
|
if (!($vcomp instanceof VEvent || $vcomp instanceof VTodo)) {
|
|
continue;
|
|
}
|
|
|
|
$end_date_prop = $vcomp instanceof VTodo ? 'DUE' : 'DTEND';
|
|
if (!$vcomp->DTSTART instanceof \Sabre\VObject\Property\ICalendar\DateTime
|
|
|| !$vcomp->$end_date_prop instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
|
|
continue;
|
|
}
|
|
$user_tz = new \DateTimeZone(date_default_timezone_get());
|
|
$begin_dt = $vcomp->DTSTART->getDateTime();
|
|
$begin_dt = $begin_dt->setTimeZone($user_tz);
|
|
$end_dt = $vcomp->$end_date_prop->getDateTime();
|
|
$end_dt = $end_dt->setTimeZone($user_tz);
|
|
|
|
if (!($vcomp->RRULE instanceof Recur)
|
|
&& ($limit_end < $begin_dt->format('Y-m-d H:i:s') || $limit_begin > $end_dt->format('Y-m-d H:i:s'))) {
|
|
continue; // Ignore events not inside dates range
|
|
}
|
|
|
|
$title = $vcomp->SUMMARY instanceof FlatText ? $vcomp->SUMMARY->getValue() : '';
|
|
$description = $vcomp->DESCRIPTION instanceof FlatText ? $vcomp->DESCRIPTION->getValue() : '';
|
|
|
|
$raw_events[] = [
|
|
'users_id' => Session::getLoginUserID(),
|
|
'name' => $title,
|
|
'tooltip' => trim($title."\n".$description),
|
|
'content' => $description,
|
|
'begin' => $begin_dt->format('Y-m-d H:i:s'),
|
|
'end' => $end_dt->format('Y-m-d H:i:s'),
|
|
'event_type_color' => $planning_params['color'],
|
|
'color' => $planning_params['color'],
|
|
'rrule' => $vcomp->RRULE instanceof Recur
|
|
? current($vcomp->RRULE->getJsonValue())
|
|
: null,
|
|
'editable' => false,
|
|
'resourceId' => $planning_id,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $raw_events;
|
|
}
|
|
|
|
|
|
/**
|
|
* Change dates of a selected event.
|
|
* Called from a drag&drop in planning
|
|
*
|
|
* @since 9.1
|
|
*
|
|
* @param array $options: must contains this keys :
|
|
* - items_id : integer to identify items
|
|
* - itemtype : string to identify items
|
|
* - begin : planning start .
|
|
* (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
|
|
* - end : planning end .
|
|
* (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime)
|
|
* @return bool
|
|
*/
|
|
static function updateEventTimes($params = []) {
|
|
if ($item = getItemForItemtype($params['itemtype'])) {
|
|
$params = self::cleanDates($params);
|
|
|
|
if ($item->getFromDB($params['items_id'])
|
|
&& empty($item->fields['is_deleted'])
|
|
) {
|
|
// item exists and is not in bin
|
|
|
|
$abort = false;
|
|
|
|
// we should not edit events from closed parent
|
|
if (!empty($item->fields['tickets_id'])) {
|
|
// todo: to same checks for changes, problems, projects and maybe reminders and others depending on incoming itemtypes
|
|
$ticket = new Ticket();
|
|
|
|
if (!$ticket->getFromDB($item->fields['tickets_id'])
|
|
|| $ticket->fields['is_deleted']
|
|
|| $ticket->fields['status'] == CommonITILObject::CLOSED
|
|
) {
|
|
$abort = true;
|
|
}
|
|
}
|
|
|
|
// if event has rrule property, check if we need to create a clone instance
|
|
if (isset($item->fields['rrule'])
|
|
&& strlen($item->fields['rrule'])) {
|
|
if (isset($params['move_instance'])
|
|
&& filter_var($params['move_instance'], FILTER_VALIDATE_BOOLEAN)) {
|
|
$item = $item->createInstanceClone(
|
|
$item->fields['id'],
|
|
$params['old_start']
|
|
);
|
|
$params['items_id'] = $item->fields['id'];
|
|
}
|
|
}
|
|
|
|
if (!$abort) {
|
|
$update = [
|
|
'id' => $params['items_id'],
|
|
'plan' => [
|
|
'begin' => $params['start'],
|
|
'end' => $params['end']
|
|
]
|
|
];
|
|
|
|
if (isset($item->fields['users_id_tech'])) {
|
|
$update['users_id_tech'] = $item->fields['users_id_tech'];
|
|
}
|
|
|
|
// manage moving event between resource (actors)
|
|
if (isset($params['new_actor_itemtype'])
|
|
&& isset($params['new_actor_items_id'])
|
|
&& !empty($params['new_actor_itemtype'])
|
|
&& !empty($params['new_actor_items_id'])) {
|
|
|
|
$new_actor_itemtype = strtolower($params['new_actor_itemtype']);
|
|
|
|
// reminders don't have group assignement for planning
|
|
if (!($new_actor_itemtype === 'group'
|
|
&& $item instanceof Reminder)) {
|
|
switch ($new_actor_itemtype) {
|
|
case "group":
|
|
$update['groups_id_tech'] = $params['new_actor_items_id'];
|
|
if (strtolower($params['old_actor_itemtype']) === "user") {
|
|
$update['users_id_tech'] = 0;
|
|
}
|
|
break;
|
|
|
|
case "user":
|
|
if (isset($item->fields['users_id_tech'])) {
|
|
$update['users_id_tech'] = $params['new_actor_items_id'];
|
|
if (strtolower($params['old_actor_itemtype']) === "group") {
|
|
$update['groups_id_tech'] = 0;
|
|
}
|
|
} else {
|
|
$update['users_id'] = $params['new_actor_items_id'];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// special case for project tasks
|
|
// which have a link tables for their relation with groups/users
|
|
if ($item instanceof ProjectTask) {
|
|
// get actor for finding relation with item
|
|
$actor = new $params['old_actor_itemtype'];
|
|
$actor->getFromDB((int) $params['old_actor_items_id']);
|
|
|
|
// get current relation
|
|
$team_old = new ProjectTaskTeam;
|
|
$team_old->getFromDBForItems($item, $actor);
|
|
|
|
// if new relation already exists, delete old relation
|
|
$actor_new = new $params['new_actor_itemtype'];
|
|
$actor_new->getFromDB((int) $params['new_actor_items_id']);
|
|
$team_new = new ProjectTaskTeam;
|
|
if ($team_new->getFromDBForItems($item, $actor_new)) {
|
|
$team_old->delete([
|
|
'id' => $team_old->fields['id']
|
|
]);
|
|
|
|
} else {
|
|
// else update relation
|
|
$team_old->update([
|
|
'id' => $team_old->fields['id'],
|
|
'itemtype' => $params['new_actor_itemtype'],
|
|
'items_id' => $params['new_actor_items_id'],
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_subclass_of($item, "CommonITILTask")) {
|
|
$parentitemtype = $item->getItilObjectItemType();
|
|
if (!$update["_job"] = getItemForItemtype($parentitemtype)) {
|
|
return;
|
|
}
|
|
|
|
$fkfield = $update["_job"]->getForeignKeyField();
|
|
$update[$fkfield] = $item->fields[$fkfield];
|
|
}
|
|
|
|
return $item->update($update);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean timezone informations from dates fields,
|
|
* as fullcalendar doesn't support easily timezones, let's consider it sends raw dates
|
|
* (remove timezone suffix), we will manage timezone directy on database
|
|
* see https://fullcalendar.io/docs/timeZone
|
|
*
|
|
* @since 9.5
|
|
*
|
|
* @param array $params parameters send by fullcalendar
|
|
*
|
|
* @return array cleaned $params
|
|
*/
|
|
static function cleanDates(array $params = []): array {
|
|
$dates_fields = [
|
|
'start', 'begin', 'end'
|
|
];
|
|
|
|
foreach ($params as $key => &$value) {
|
|
if (in_array($key, $dates_fields)) {
|
|
$value = date("Y-m-d H:i:s", strtotime(trim($value, 'Z')));
|
|
}
|
|
}
|
|
|
|
return $params;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Display a Planning Item
|
|
*
|
|
* @param $val Array of the item to display
|
|
* @param $who ID of the user (0 if all)
|
|
* @param $type position of the item in the time block (in, through, begin or end)
|
|
* (default '')
|
|
* @param $complete complete display (more details) (default 0)
|
|
*
|
|
* @return string
|
|
**/
|
|
static function displayPlanningItem(array $val, $who, $type = "", $complete = 0) {
|
|
$html = "";
|
|
|
|
// bg event shouldn't have content displayed
|
|
if (!$complete && $_SESSION['glpi_plannings']['filters']['OnlyBgEvents']['display']) {
|
|
return "";
|
|
}
|
|
|
|
// Plugins case
|
|
if (isset($val['itemtype'])
|
|
&& !empty($val['itemtype'])
|
|
&& $val['itemtype'] != 'NotPlanned'
|
|
&& method_exists($val['itemtype'], "displayPlanningItem")) {
|
|
$html.= $val['itemtype']::displayPlanningItem($val, $who, $type, $complete);
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Show the planning for the central page of a user
|
|
*
|
|
* @param $who ID of the user
|
|
*
|
|
* @return void
|
|
**/
|
|
static function showCentral($who) {
|
|
global $CFG_GLPI;
|
|
|
|
if (!Session::haveRight(self::$rightname, self::READMY)
|
|
|| ($who <= 0)) {
|
|
return false;
|
|
}
|
|
|
|
echo "<table class='tab_cadrehov'>";
|
|
echo "<tr class='noHover'><th>";
|
|
echo "<a href='".$CFG_GLPI["root_doc"]."/front/planning.php'>".__('Your planning')."</a>";
|
|
echo "</th></tr>";
|
|
|
|
echo "<tr class='noHover'>";
|
|
echo "<td class='planning_on_central'>";
|
|
self::showPlanning(false);
|
|
echo "</td></tr>";
|
|
echo "</table>";
|
|
}
|
|
|
|
|
|
|
|
//*******************************************************************************************************************************
|
|
// *********************************** Implementation ICAL ***************************************************************
|
|
//*******************************************************************************************************************************
|
|
|
|
/**
|
|
* Generate ical file content
|
|
*
|
|
* @param $who user ID
|
|
* @param $whogroup group ID
|
|
* @param $limititemtype itemtype only display this itemtype (default '')
|
|
*
|
|
* @return icalendar string
|
|
**/
|
|
static function generateIcal($who, $whogroup, $limititemtype = '') {
|
|
global $CFG_GLPI;
|
|
|
|
if (($who === 0)
|
|
&& ($whogroup === 0)) {
|
|
return false;
|
|
}
|
|
|
|
if (!empty( $CFG_GLPI["version"])) {
|
|
$unique_id = "GLPI-Planning-".trim($CFG_GLPI["version"]);
|
|
} else {
|
|
$unique_id = "GLPI-Planning-UnknownVersion";
|
|
}
|
|
|
|
// create vcalendar
|
|
$vcalendar = new VCalendar();
|
|
|
|
// $xprops = array( "X-LIC-LOCATION" => $tz );
|
|
// iCalUtilityFunctions::createTimezone( $v, $tz, $xprops );
|
|
|
|
$interv = [];
|
|
$begin = time()-MONTH_TIMESTAMP*12;
|
|
$end = time()+MONTH_TIMESTAMP*12;
|
|
$begin = date("Y-m-d H:i:s", $begin);
|
|
$end = date("Y-m-d H:i:s", $end);
|
|
$params = [
|
|
'genical' => true,
|
|
'who' => $who,
|
|
'whogroup' => $whogroup,
|
|
'begin' => $begin,
|
|
'end' => $end
|
|
];
|
|
|
|
if (empty($limititemtype)) {
|
|
foreach ($CFG_GLPI['planning_types'] as $itemtype) {
|
|
$interv = array_merge($interv, $itemtype::populatePlanning($params));
|
|
}
|
|
} else {
|
|
$interv = $limititemtype::populatePlanning($params);
|
|
}
|
|
|
|
if (count($interv) > 0) {
|
|
foreach ($interv as $key => $val) {
|
|
if (isset($val['itemtype'])) {
|
|
if (isset($val[getForeignKeyFieldForItemType($val['itemtype'])])) {
|
|
$uid = $val['itemtype']."#".$val[getForeignKeyFieldForItemType($val['itemtype'])];
|
|
} else {
|
|
$uid = "Other#".$key;
|
|
}
|
|
} else {
|
|
$uid = "Other#".$key;
|
|
}
|
|
|
|
$vevent['UID'] = $uid;
|
|
|
|
$dateBegin = new DateTime($val["begin"]);
|
|
$dateBegin->setTimeZone(new DateTimeZone('UTC'));
|
|
|
|
$dateEnd = new DateTime($val["end"]);
|
|
$dateEnd->setTimeZone(new DateTimeZone('UTC'));
|
|
|
|
$vevent['DTSTART'] = $dateBegin;
|
|
$vevent['DTEND'] = $dateEnd;
|
|
|
|
if (isset($val["tickets_id"])) {
|
|
$summary = sprintf(__('Ticket #%1$s %2$s'), $val["tickets_id"], $val["name"]);
|
|
} else if (isset($val["name"])) {
|
|
$summary = $val["name"];
|
|
}
|
|
$vevent['SUMMARY'] = $summary;
|
|
|
|
if (isset($val["content"])) {
|
|
$description = $val["content"];
|
|
// be sure to replace nl by \r\n
|
|
$description = preg_replace("/<br( [^>]*)?".">/i", "\r\n", $description);
|
|
$description = Html::clean($description);
|
|
} else if (isset($val["text"])) {
|
|
$description = $val["text"];
|
|
// be sure to replace nl by \r\n
|
|
$description = preg_replace("/<br( [^>]*)?".">/i", "\r\n", $description);
|
|
$description = Html::clean($description);
|
|
} else if (isset($val["name"])) {
|
|
$description = $val["name"];
|
|
// be sure to replace nl by \r\n
|
|
$description = preg_replace("/<br( [^>]*)?".">/i", "\r\n", $description);
|
|
$description = Html::clean($description);
|
|
}
|
|
$vevent['DESCRIPTION'] = $description;
|
|
|
|
if (isset($val["url"])) {
|
|
$vevent['URL'] = $val["url"];
|
|
}
|
|
$vcalendar->add('VEVENT', $vevent);
|
|
}
|
|
}
|
|
|
|
$output = $vcalendar->serialize();
|
|
$filename = date( 'YmdHis' ).'.ics';
|
|
|
|
@header("Content-Disposition: attachment; filename=\"$filename\"");
|
|
//@header("Content-Length: ".Toolbox::strlen($output));
|
|
@header("Connection: close");
|
|
@header("content-type: text/calendar; charset=utf-8");
|
|
|
|
echo $output;
|
|
}
|
|
|
|
/**
|
|
* @since 0.85
|
|
**/
|
|
function getRights($interface = 'central') {
|
|
|
|
$values[self::READMY] = __('See personnal planning');
|
|
$values[self::READGROUP] = __('See schedule of people in my groups');
|
|
$values[self::READALL] = __('See all plannings');
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Save the last view used in fullcalendar
|
|
*
|
|
* @since 9.5
|
|
*
|
|
* @param string $view_name
|
|
* @return void
|
|
*/
|
|
static function viewChanged($view_name = "ListView") {
|
|
$_SESSION['glpi_plannings']['lastview'] = $view_name;
|
|
}
|
|
|
|
/**
|
|
* Returns actor type from 'planning' key (key comes from user 'plannings' field).
|
|
*
|
|
* @param string $key
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public static function getActorTypeFromPlanningKey($key) {
|
|
if (preg_match('/group_\d+_users/', $key)) {
|
|
return Group_User::getType();
|
|
}
|
|
$itemtype = ucfirst(preg_replace('/^([a-z]+)_\d+$/', '$1', $key));
|
|
return class_exists($itemtype) ? $itemtype : null;
|
|
}
|
|
|
|
/**
|
|
* Returns actor id from 'planning' key (key comes from user 'plannings' field).
|
|
*
|
|
* @param string $key
|
|
*
|
|
* @return integer|null
|
|
*/
|
|
public static function getActorIdFromPlanningKey($key) {
|
|
$items_id = preg_replace('/^[a-z]+_(\d+)(?:_[a-z]+)?$/', '$1', $key);
|
|
return is_numeric($items_id) ? (int)$items_id : null;
|
|
}
|
|
|
|
/**
|
|
* Returns planning key for given actor (key is used in user 'plannings' field).
|
|
*
|
|
* @param string $itemtype
|
|
* @param integer $items_id
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function getPlanningKeyForActor($itemtype, $items_id) {
|
|
if ('Group_User' === $itemtype) {
|
|
return 'group_' . $items_id . '_users';
|
|
}
|
|
|
|
return strtolower($itemtype) . '_' . $items_id;
|
|
}
|
|
|
|
/**
|
|
* Get CalDAV base calendar URL for given actor.
|
|
*
|
|
* @param CommonDBTM $item
|
|
*
|
|
* @return string|null
|
|
*/
|
|
private static function getCaldavBaseCalendarUrl(\CommonDBTM $item) {
|
|
|
|
$calendar_uri = null;
|
|
|
|
switch (get_class($item)) {
|
|
case \Group::class:
|
|
$calendar_uri = \Glpi\CalDAV\Backend\Calendar::PREFIX_GROUPS
|
|
. '/' . $item->fields['id']
|
|
. '/' . \Glpi\CalDAV\Backend\Calendar::BASE_CALENDAR_URI;
|
|
break;
|
|
case \User::class:
|
|
$calendar_uri = \Glpi\CalDAV\Backend\Calendar::PREFIX_USERS
|
|
. '/' . $item->fields['name']
|
|
. '/' . \Glpi\CalDAV\Backend\Calendar::BASE_CALENDAR_URI;
|
|
break;
|
|
}
|
|
|
|
return $calendar_uri;
|
|
}
|
|
|
|
static function getIcon() {
|
|
return "far fa-calendar-alt";
|
|
}
|
|
}
|