* @copyright (c) 2004-16 by RalfBecker-At-outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ use EGroupware\Api; use EGroupware\Api\Link; use EGroupware\Api\Framework; use EGroupware\Api\Egw; use EGroupware\Api\Etemplate; /** * Class to generate the calendar views and the necesary widgets * * The listview is in a separate class calendar_uilist! * * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate: * UI only operates in user-time, so there have to be no conversation at all !!! * BO's functions take and return user-time only (!), they convert internaly everything to servertime, because * SO operates only on server-time * * The state of the UI elements is managed in the uical class, which all UI classes extend. * * All permanent debug messages of the calendar-code should done via the debug-message method of the calendar_bo class !!! */ class calendar_uiviews extends calendar_ui { var $public_functions = array( 'index' => True, ); /** * integer level or string function- or widget-name * * @var mixed */ var $debug=false; /** * extra rows above and below the workday * * @var int */ var $extraRows = 2; /** * removes n extra rows below the workday * * @var int */ var $remBotExtraRows = 0; /** * extra rows original (save original value even if it gets changed in the class) * * @var int */ var $extraRowsOriginal; var $timeRow_width = 40; /** * how many rows per day get displayed, gets set by the timeGridWidget * * @var int */ var $rowsToDisplay; /** * height in percent of one row, gets set by the timeGridWidget * * @var int */ var $rowHeight; /** * standard params for calling bocal::search for all views, set by the constructor * * @var array */ var $search_params; /** * should we use a time grid, can be set for week- and month-view to false by the cal_pref no_time_grid * * @var boolean */ var $use_time_grid=true; /** * Pref value of use_time_grid preference * @var string */ var $use_time_grid_pref = ''; /** * Can we display the whole day in a timeGrid of the size of the workday and just scroll to workday start * * @var boolean */ var $scroll_to_wdstart=false; /** * counter for the current whole day event of a single day * * @var int */ var $wholeDayPosCounter=1; /** * Switch to disable private data and possibility to view and edit events * in case of a public view (sitemgr) */ var $allowEdit = true; /** * Display holidays as event, currenlty only used in day-view * * @var array */ var $display_holiday_event_types = array( 'bdays' => false, 'hdays' => false ); /** * Constructor * * @param array $set_states = null to manualy set / change one of the states, default NULL = use $_REQUEST */ function __construct($set_states=null) { parent::__construct(false,$set_states); // call the parent's constructor $this->extraRowsOriginal = $this->extraRows; //save original extraRows value $GLOBALS['egw_info']['flags']['nonavbar'] = False; // Check for GET message (from merge) if($_GET['msg']) { Framework::message($_GET['msg']); unset($_GET['msg']); } // standard params for calling bocal::search for all views $this->owner = str_replace('%2C',',',$this->owner); $this->search_params = array( 'start' => $this->date, 'cat_id' => $this->cat_id ? (is_array($this->cat_id)?$this->cat_id:explode(',',$this->cat_id)) : 0, 'users' => explode(',',$this->owner), 'filter' => $this->filter, 'daywise' => True, 'use_so_events' => $this->test === 'true', ); // $this->holidays = $this->bo->read_holidays($this->year); $this->check_owners_access(); //ATM: Forces use_time_grid preference to use all views by ignoring the preference value //@TODO: the whole use_time_grid preference should be removed (including dependent vars) // after we decided that is not neccessary to have it at all $this->use_time_grid_pref = 'all'; //$this->cal_prefs['use_time_grid']; } /** * Calculate iso8601 week-number, which is defined for Monday as first day of week only * * We addjust the day, if user prefs want a different week-start-day * * @param int|string|DateTime $time * @return string */ public function week_number($time) { if (!is_a($time,'DateTime')) $time = new Api\DateTime($time); // if week does not start Monday and $time is Sunday --> add one day if ($this->cal_prefs['weekdaystarts'] != 'Monday' && !($wday = $time->format('w'))) { $time->modify('+1day'); } // if week does start Saturday and $time is Saturday --> add two days elseif ($this->cal_prefs['weekdaystarts'] == 'Saturday' && $wday == 6) { $time->modify('+2days'); } return $time->format('W'); } /** * Load all views used by calendar, client side switches between them as needed */ function index($content=array()) { if($content['merge']) { // View from sidebox is JSON encoded $this->manage_states(array_merge($content,json_decode($content['view'],true))); if($content['first']) { $this->first = Api\DateTime::to($content['first'],'ts'); } if($content['last']) { $this->last = Api\DateTime::to($content['last'],'ts'); } $_GET['merge'] = $content['merge']; $this->merge(); return; } // handle views in other files if (!isset($this->public_functions[$this->view]) && $this->view !== 'listview') { $this->view = 'week'; } // get manual to load the right page $GLOBALS['egw_info']['flags']['params']['manual'] = array('page' => 'ManualCalendar'.ucfirst($this->view)); // Sidebox & iframe for old views if(in_array($this->view,array('year')) && $_GET['view']) { $GLOBALS['egw_info']['flags']['nonavbar'] = true; $this->manage_states($_GET); $this->{$this->view}(); return; } // Toolbar $tmpl = new Etemplate('calendar.toolbar'); $tmpl->setElementAttribute('toolbar', 'actions', $this->getToolbarActions($content)); // Adjust toolbar for mobile if(Api\Header\UserAgent::mobile()){ $tmpl->setElementAttribute('toolbar','class', 'et2_head_toolbar'); $tmpl->setElementAttribute('toolbar','view_range', '3'); } $tmpl->exec('calendar_uiviews::index',array()); // Load the different views once, we'll switch between them on the client side $todo = new Etemplate('calendar.todo'); $label = ''; $todo->exec('calendar_uiviews::index',array('todos'=>$this->get_todos($label), 'label' => $label)); // Actually, this takes care of most of it... $this->week(); $planner = new Etemplate('calendar.planner'); // Get the actions $planner->setElementAttribute('planner','actions',$this->get_actions()); $planner->exec('calendar_uiviews::index',array()); // List view in a separate file $list_ui = new calendar_uilist(); $list_ui->listview(); } /** * Generate the calendar toolbar actions */ protected function getToolbarActions() { $group = 0; $actions = array( 'add' => array( 'caption' => 'Add', 'icon' => 'add', 'group' => ++$group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Add', 'toolbarDefault' => true, ), 'day_view' => array( 'caption' => 'Day view', 'icon' => 'day', 'group' => ++$group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Day view', 'toolbarDefault' => true, 'data' => array('state' => array('view' => 'day')) ), '4day_view' => array( 'caption' => 'Four days view', 'icon' => 'cal4', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Four days view', 'toolbarDefault' => false, 'data' => array('state' => array('view' => 'day4')) ), 'week_view' => array( 'caption' => 'Week view', 'icon' => 'week', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Week view', 'toolbarDefault' => true, 'data' => array('state' => array('view' => 'week')) ), 'weekN_view' => array( 'caption' => 'Multiple week view', 'icon' => 'multiweek', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Multiple week view', 'toolbarDefault' => true, 'data' => array('state' => array('view' => 'weekN')) ), 'month_view' => array( 'caption' => 'Month view', 'icon' => 'month', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Month view', 'toolbarDefault' => false, 'data' => array('state' => array('view' => 'month')) ), 'planner_category' => array( 'caption' => 'Planner by category', 'icon' => 'planner_category', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Planner by category', 'toolbarDefault' => false, 'data' => array('state' => array('view' => 'planner', 'sortby' => 'category')), ), 'planner_user' => array( 'caption' => 'Planner by user', 'icon' => 'planner', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Planner by user', 'toolbarDefault' => false, 'data' => array('state' => array('view' => 'planner', 'sortby' => 'user')), ), 'planner_month' => array( 'caption' => 'Yearly planner', 'icon' => 'year', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Yearly planner', 'toolbarDefault' => false, 'data' => array('state' => array('view' => 'planner', 'sortby' => 'month')), ), 'list' => array( 'caption' => 'Listview', 'icon' => 'list', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Listview', 'toolbarDefault' => true, 'data' => array('state' => array('view' => 'listview')), ), 'weekend' => array( 'caption' => 'Weekend', 'icon' => 'weekend', 'checkbox' => true, 'checked' => $this->cal_prefs['saved_states']['weekend'], 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Toggle weekend', 'toolbarDefault' => false, 'data' => array('toggle_off' => '5', 'toggle_on' => '7') ), 'previous' => array( 'caption' => 'Previous', 'icon' => 'previous', 'group' => ++$group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Previous', 'toolbarDefault' => true, ), 'today' => array( 'caption' => 'Today', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Today', 'toolbarDefault' => true, ), 'next' => array( 'caption' => 'Next', 'icon' => 'next', 'group' => $group, 'onExecute' => 'javaScript:app.calendar.toolbar_action', 'hint' => 'Next', 'toolbarDefault' => true, ), ); if (Api\Header\UserAgent::mobile()) { foreach (array_keys($actions) as $key) { if (!in_array($key, array('day_view','week_view','next', 'today','previous'))) { $actions[$key]['toolbarDefault'] = false; } else { $actions[$key]['toolbarDefault'] = true; } } $actions['weekend']['data'] = array('toggle_off' => '7', 'toggle_on' => '5'); unset($actions['pgp']); } return $actions; } /** * Displays the planner view * * @param boolean|Etemplate $home = false if etemplate return content suitable for home-page */ function &planner($content = array(), $home=false) { if ($this->sortby == 'month') // yearly planner with month rows { $this->first = $this->bo->date2array($this->date); $this->first['day'] = 1; unset($this->first['raw']); $this->last = $this->first; $this->last['year']++; $this->last = $this->bo->date2ts($this->last)-1; } elseif (!$this->planner_view || $this->planner_view == 'month') // planner monthview { if ($this->day < 15) // show one complete month { $this->_week_align_month($this->first,$this->last); } else // show 2 half month { $this->_week_align_month($this->first,$this->last,15); } } elseif ($this->planner_view == 'week' || $this->planner_view == 'weekN') // weeekview { $start = new Api\DateTime($this->date); $start->setWeekstart(); $this->first = $start->format('ts'); $this->last = $this->bo->date2array($this->first); $this->last['day'] += ($this->planner_view == 'week' ? 7 : 7 * $this->cal_prefs['multiple_weeks'])-1; $this->last['hour'] = 23; $this->last['minute'] = $this->last['sec'] = 59; unset($this->last['raw']); $this->last = $this->bo->date2ts($this->last); } else // dayview { $this->first = $this->bo->date2ts($this->date); $this->last = $this->bo->date2array($this->first); $this->last['day'] += 0; $this->last['hour'] = 23; $this->last['minute'] = $this->last['sec'] = 59; unset($this->last['raw']); $this->last = $this->bo->date2ts($this->last); } $merge = $this->merge(); if($merge) { Egw::redirect_link('/index.php',array( 'menuaction' => 'calendar.calendar_uiviews.index', 'msg' => $merge, )); } $search_params = $this->search_params; $search_params['daywise'] = false; $search_params['start'] = $this->first; $search_params['end'] = $this->last; $search_params['enum_groups'] = $this->sortby == 'user'; $content['planner'] = $this->bo->search($search_params); foreach($content['planner'] as &$event) { $this->to_client($event); } if ($this->debug > 0) $this->bo->debug_message('uiviews::planner() date=%1: first=%2, last=%3',False,$this->date,$this->bo->date2string($this->first),$this->bo->date2string($this->last)); $tmpl = $home ? $home : new Etemplate('calendar.planner'); $tmpl->setElementAttribute('planner','start_date', Api\DateTime::to($this->first, Api\DateTime::ET2)); $tmpl->setElementAttribute('planner','end_date', Api\DateTime::to($this->last, Api\DateTime::ET2)); $tmpl->setElementAttribute('planner','owner', $search_params['users']); $tmpl->setElementAttribute('planner','group_by', $this->sortby); // Get the actions $tmpl->setElementAttribute('planner','actions',$this->get_actions()); $tmpl->exec(__METHOD__, $content); } /** * Displays the monthview or a multiple week-view * * Used for home app * * @param int $weeks = 0 number of weeks to show, if 0 (default) all weeks of one month are shown * @param boolean|etemplate2 $home = false if not false return content suitable for home-page */ function &month($weeks=0,$home=false) { if ($this->debug > 0) $this->bo->debug_message('uiviews::month(weeks=%1) date=%2',True,$weeks,$this->date); if (!$home) { trigger_error(__METHOD__ .' only used by home app', E_USER_DEPRECATED); return; } $this->use_time_grid = !$this->use_time_grid_pref || $this->use_time_grid_pref == 'all'; // all views $granularity = 0; if($weeks) { $granularity = ($this->cal_prefs['interval'] ? (int)$this->cal_prefs['interval'] : 30); $list = $this->cal_prefs['use_time_grid']; if(!is_array($list)) { $list = explode(',',$list); } if(is_array($list)) { $granularity = in_array('weekN',$list) ? 0 : $granularity; } } $content = array('view' => array()); if ($weeks) { $start = new Api\DateTime($this->date); $start->setWeekstart(); $this->first = $start->format('ts'); $this->last = strtotime("+$weeks weeks",$this->first) - 1; $weekNavH = "$weeks weeks"; $navHeader = lang('Week').' '.$this->week_number($this->first).' - '.$this->week_number($this->last).': '. $this->bo->long_date($this->first,$this->last); } else { $this->_week_align_month($this->first,$this->last); $weekNavH = "1 month"; $navHeader = lang(adodb_date('F',$this->bo->date2ts($this->date))).' '.$this->year; } if ($this->debug > 0) $this->bo->debug_message('uiviews::month(%1) date=%2: first=%3, last=%4',False,$weeks,$this->date,$this->bo->date2string($this->first),$this->bo->date2string($this->last)); // Loop through, using Api\DateTime to handle DST $week = 0; $week_start = new EGroupware\Api\DateTime($this->first); $week_start->setTime(0,0,0); $week_end = new Api\DateTime($week_start); $week_end->add(new DateInterval('P6DT23H59M59S')); $last = new EGroupware\Api\DateTime($this->last); for ($week_start; $week_start < $last; $week_start->add('1 week'), $week_end->add('1 week')) { $search_params = $this->search_params; $search_params['start'] = $week_start->format('ts'); $search_params['end'] = $week_end->format('ts'); $content['view'][] = (array)$this->tagWholeDayOnTop($this->bo->search($search_params)) + array( 'id' => $week_start->format('Ymd') ); $home->setElementAttribute("view[$week]",'onchange',false); $home->setElementAttribute("view[$week]",'granularity',$granularity); $home->setElementAttribute("view[$week]",'show_weekend', $this->search_params['weekend']); $week++; } // Get the actions $home->setElementAttribute('view','actions',$this->get_actions()); $home->exec(__METHOD__, $content); } /** * get start and end of a month aligned to full weeks * * @param int &$first timestamp 0h of first day of week containing the first of the current month * @param int &$last timestamp 23:59:59 of last day of week containg the last day of the current month * @param int $day = 1 should the alignment be based on the 1. of the month or an other date, eg. the 15. */ function _week_align_month(&$first,&$last,$day=1) { $start = new Api\DateTime($this->date); $start->setDate($this->year,$this->month,$this->day=$day); $start->setWeekstart(); $first = $start->format('ts'); $start->setDate($this->year,$this->month+1,$day); if ($day == 1) $start->add('-1day'); $start->setWeekstart(); // now we need to calculate the end of the last day of that week // as simple $last += WEEK_s - 1; does NOT work, if daylight saving changes in that week!!! $arr = $start->format('array'); $arr['day'] += 6; $arr['hour'] = 23; $arr['min'] = $arr['sec'] = 59; unset($arr['raw']); // otherwise date2ts does not calc raw new, but uses it $last = $this->bo->date2ts($arr); } /** * Displays the weekview, with 5 or 7 days * * @param int $days = 0 number of days to show, if 0 (default) the value from the URL or the prefs is used * @param boolean|etemplate2 $home = false if not false return content suitable for home-page */ function week($days=0,$home=false) { if (!$days) { $days = isset($_GET['days']) ? $_GET['days'] : $this->cal_prefs['days_in_weekview']; if ($days != 5) $days = 7; if ($days != $this->cal_prefs['days_in_weekview']) // save the preference { $GLOBALS['egw']->preferences->add('calendar','days_in_weekview',$days); $GLOBALS['egw']->preferences->save_repository(); $this->cal_prefs['days_in_weekview'] = $days; } } if ($this->debug > 0) $this->bo->debug_message('uiviews::week(days=%1) date=%2',True,$days,$this->date); if ($days <= 4) // next 4 days view { $wd_start = $this->first = $this->bo->date2ts($this->date); $this->last = strtotime("+$days days",$this->first) - 1; $view = $days == 1 ? 'day' : 'day4'; } else { $start = new Api\DateTime($this->date); $start->setWeekstart(); $wd_start = $this->first = $start->format('ts'); if ($days <= 5) // no weekend-days { switch($this->cal_prefs['weekdaystarts']) { case 'Saturday': $this->first = strtotime("+2 days",$this->first); break; case 'Sunday': $this->first = strtotime("+1 day",$this->first); break; } } $this->last = strtotime("+$days days",$this->first) - 1; $view = 'week'; } $granularity = ($this->cal_prefs['interval'] ? (int)$this->cal_prefs['interval'] : 30); $list = $this->cal_prefs['use_time_grid']; if(!is_array($list)) { $list = explode(',',$list); } if(is_array($list)) { $granularity = in_array($view,$list) ? 0 : $granularity; } $search_params = array( 'start' => $this->first, 'end' => $this->last, ) + $this->search_params; $users = $this->search_params['users']; if (!is_array($users)) $users = array($users); $content = array('view' => array()); if(!$home) { // Fill with the minimum needed 'weeks' $min = max( 6, // Some months need 6 weeks for full display $this->cal_prefs['multiple_weeks'], // WeekN view $this->cal_prefs['week_consolidate'] // We collapse after this many users ); for($i = 0; $i < $min; $i++) { $content['view'][] = array(); } } else { // Always do 7 days for a week so scrolling works properly $this->last = ($days == 4 ? $this->last : $search_params['end'] = strtotime("+$days days",$this->first) - 1); if (count($users) == 1 || count($users) >= $this->cal_prefs['week_consolidate'] ||// for more then X users, show all in one row $days == 1 // Showing just 1 day ) { $content['view'][] = (array)$this->tagWholeDayOnTop($this->bo->search($search_params)) + array('owner' => $users); } else { foreach(array_keys($this->_get_planner_users(false)) as $uid) { $search_params['users'] = $uid; $content['view'][] = $this->tagWholeDayOnTop($this->bo->search($search_params)) + array('owner' => $uid); } } } $tmpl = $home ? $home :new Etemplate('calendar.view'); foreach(array_keys($content['view']) as $index) { $tmpl->setElementAttribute("view[$index]",'granularity',$granularity); $tmpl->setElementAttribute("view[$index]",'show_weekend',$this->search_params['weekend']); } // Get the actions $tmpl->setElementAttribute('view','actions',$this->get_actions()); $tmpl->exec(__METHOD__, $content); } /** * Get todos via AJAX */ public static function ajax_get_todos($_date, $owner) { $date = Api\DateTime::to($_date, 'array'); $ui = new calendar_uiviews(); $ui->year = $date['year']; $ui->month = $date['month']; $ui->day = $date['day']; $ui->owner = (int)$owner; $label = ''; $todos = $ui->get_todos($label); Api\Json\Response::get()->data(array( 'label' => $label, 'todos' => $todos )); } /** * Query the open ToDo's via a hook from InfoLog or any other 'calendar_include_todos' provider * * @param array/string $todo_label label for the todo-box or array with 2 values: the label and a boolean show_all * On return $todo_label contains the label for the todo-box * @return string/boolean Api\Html with a table of open todo's or false if no hook availible */ function get_todos(&$todo_label) { $todos_from_hook = Api\Hooks::process(array( 'location' => 'calendar_include_todos', 'year' => $this->year, 'month' => $this->month, 'day' => $this->day, 'owner' => $this->owner // num. id of the user, not necessary current user )); if(is_array($todo_label)) { list($label,$showall)=$todo_label; } else { $label=$todo_label; $showall=true; } $maxshow = (int)$GLOBALS['egw_info']['user']['preferences']['infolog']['mainscreen_maxshow']; if($maxshow <= 0) { $maxshow=10; } //print_debug("get_todos(): label=$label; showall=$showall; max=$maxshow"); $content = $todo_label = ''; if (is_array($todos_from_hook) && count($todos_from_hook)) { foreach($todos_from_hook as $todos) { $i = 0; if (is_array($todos)) { $todo_label = !empty($label) ? $label : lang("open ToDo's:"); foreach($todos as &$todo) { if(!$showall && ($i++ > $maxshow)) { break; } $icons = ''; foreach($todo['icons'] as $name => $alt) { $icons .= ($icons?' ':'').Api\Html::image('infolog',$name,lang($alt),'border="0" width="15" height="15"'); } $todo['icons'] = $icons; $class = $class == 'row_on' ? 'row_off' : 'row_on'; if($todo['edit']) { $todo['edit_size'] = $todo['edit']['size']; unset($todo['edit']['size']); $edit_icon_href = Api\Html::a_href( $icons, $todo['edit'],'',' data-todo="app|750x590" '); $edit_href = Api\Html::a_href( $todo['title'], $todo['edit'],'',' data-todo="app|750x590" '); $todo['edit'] = Framework::link('/index.php',$todo['edit'],true); } $icon_href = Api\Html::a_href($icons,$todo['view']); $content .= "