* @copyright (c) 2004-10 by RalfBecker-At-outdoor-training.de
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/
/**
* 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(
'day' => True,
'day4' => True,
'week' => True,
'weekN' => True,
'month' => True,
'planner' => True,
'index' => True,
);
/**
* integer level or string function- or widget-name
*
* @var mixed
*/
var $debug=false;
/**
* minimum width for an event
*
* @var int
*/
var $eventCol_min_width = 80;
/**
* extra rows above and below the workday
*
* @var int
*/
var $extraRows = 2;
/**
* 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 be the timeGridWidget
*
* @var int
*/
var $rowsToDisplay;
/**
* height in percent of one row, gets set be 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;
/**
* Dragdrop Object
*
* @var dragdrop;
*/
var $dragdrop;
/**
* 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;
/**
* 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;
$app_header = array(
'day' => lang('Dayview'),
'4day' => lang('Four days view'),
'week' => lang('Weekview'),
'month' => lang('Monthview'),
'planner' => lang('Group planner'),
);
$GLOBALS['egw_info']['flags']['app_header'] = $GLOBALS['egw_info']['apps']['calendar']['title'].
(isset($app_header[$this->view]) ? ' - '.$app_header[$this->view] : '').
// for a single owner we add it's name to the app-header
(count(explode(',',$this->owner)) == 1 ? ': '.$this->bo->participant_name($this->owner) : '');
// standard params for calling bocal::search for all views
$this->search_params = array(
'start' => $this->date,
'cat_id' => $this->cat_id,
'users' => explode(',',$this->owner),
'filter' => $this->filter,
'daywise' => True,
);
$this->holidays = $this->bo->read_holidays($this->year);
$this->check_owners_access();
if($GLOBALS['egw_info']['user']['preferences']['common']['enable_dragdrop'])
{
$this->dragdrop = new dragdrop();
// if the object would auto-disable itself unset object
// to avoid unneccesary dragdrop calls later
if(!$this->dragdrop->validateBrowser()) $this->dragdrop = false;
}
}
/**
* Show the last view or the default one, if no last
*/
function index()
{
if (!$this->view) $this->view = 'week';
// handle views in other files
if (!isset($this->public_functions[$this->view]))
{
$GLOBALS['egw']->redirect_link('/index.php',array('menuaction'=>$this->view_menuaction));
}
// get manual to load the right page
$GLOBALS['egw_info']['flags']['params']['manual'] = array('page' => 'ManualCalendar'.ucfirst($this->view));
$this->{$this->view}();
}
/**
* Show the calendar on the home page
*
* @return string with content
*/
function &home()
{
// set some stuff for the home-page
$this->__construct(array(
'date' => $this->bo->date2string($this->bo->now_su),
'cat_id' => 0,
'filter' => 'all',
'owner' => substr($this->cal_prefs['defaultcalendar'],0,7) == 'planner' && $this->cal_prefs['planner_start_with_group'] ?
$this->cal_prefs['planner_start_with_group'] : $this->user,
'multiple' => 0,
'view' => $this->bo->cal_prefs['defaultcalendar'],
));
if (($error = $this->check_owners_access()))
{
return $error;
}
if ($this->group_warning)
{
$group_warning = '
weekdaystarts='".$this->cal_prefs['weekdaystarts']."', get_weekday_start($this->year,$this->month,$this->day)=".date('l Y-m-d',$wd_start).", first=".date('l Y-m-d',$this->first)."
\n";
$search_params = array(
'start' => $this->first,
'end' => $this->last,
) + $this->search_params;
$users = $this->search_params['users'];
if (!is_array($users)) $users = array($users);
if (count($users) == 1 || count($users) > 5) // for more then 3 users, show all in one row
{
$content =& $this->timeGridWidget($this->tagWholeDayOnTop($this->bo->search($search_params)),$this->cal_prefs['interval']);
}
else
{
$content = '';
foreach($this->_get_planner_users(false) as $uid => $label)
{
$search_params['users'] = $uid;
$content .= ''."\n";
$html .= $indent."\t".'\n";
if ($this->use_time_grid)
{
$off = false; // Off-row means a different bgcolor
$add_links = count($daysEvents) == 1;
// the hour rows
for($t = $this->scroll_to_wdstart ? 0 : $this->wd_start,$i = 1+$this->extraRows;
$t <= $this->wd_end || $this->scroll_to_wdstart && $t < 24*60;
$t += $this->granularity_m,++$i)
{
$set_id = '';
if ($t == $this->wd_start)
{
list($id) = @each($daysEvents);
$id = 'wd_start_'.$id;
$set_id = ' id="'.$id.'"';
}
$html .= $indent."\t".'
'."\n";
// show time for full hours, allways for 45min interval and at least on every 3 row
$time = '';
static $show = array(
5 => array(0,15,30,45),
10 => array(0,30),
15 => array(0,30),
45 => array(0,15,30,45),
);
if (!isset($show[$this->granularity_m]) ? $t % 60 == 0 : in_array($t % 60,$show[$this->granularity_m]))
{
$time = $GLOBALS['egw']->common->formattime(sprintf('%02d',$t/60),sprintf('%02d',$t%60));
}
if ($add_links) $time = $this->add_link($time,$this->date,(int) ($t/60),$t%60);
$html .= $indent."\t\t".'
'.$time."
\n";
$html .= $indent."\t
\n"; // calTimeRow
$off = !$off;
}
}
if (is_array($daysEvents) && count($daysEvents))
{
$numberOfDays = count($daysEvents);
$dayColWidth = 100/$numberOfDays;
$dayCols_width = $width - $this->timeRow_width - 1;
$html .= $indent."\t".'
\n"; // calTimeGrid
if ($this->scroll_to_wdstart)
{
$html .= "\n";
}
return $html;
}
/**
* Creates (if necessary multiple) columns for the events of a day
*
* Uses the eventColWidget to display each column.
*
* @param string/int $day_ymd date as Ymd
* @param array $events of events to show
* @param int $left start of the widget
* @param int $width width of the widget
* @param string $indent string for correct indention
* @param boolean/string $short_title=True should we add a label (weekday, day) with link to the day-view above each day or string with title
* @param boolean $on_off=false start with row_on or row_off, default false=row_off
* @param int $owner=0 if != 0 owner to add to the add-event link
*/
function dayColWidget($day_ymd,$events,$left,$width,$indent,$short_title=True,$on_off=False,$owner=0)
{
if ($this->debug > 1 || $this->debug==='dayColWidget') $this->bo->debug_message('uiviews::dayColWidget(%1,%2,left=%3,width=%4,)',False,$day_ymd,$events,$left,$width);
$day_start = $this->bo->date2ts((string)$day_ymd);
// if daylight saving is switched on or off, correct $day_start
// gives correct times after 2am, times between 0am and 2am are wrong
if(($daylight_diff = $day_start + 12*HOUR_s - ($this->bo->date2ts($day_ymd."T120000"))))
{
$day_start -= $daylight_diff;
}
// sorting the event into columns with none-overlapping events, the events are already sorted by start-time
$eventCols = $col_ends = array();
foreach($events as $event)
{
$event['multiday'] = False;
$event['start_m'] = ($event['start'] - $day_start) / 60;
if ($event['start_m'] < 0)
{
$event['start_m'] = 0;
$event['multiday'] = True;
}
$event['end_m'] = ($event['end'] - $day_start) / 60;
if ($event['end_m'] >= 24*60)
{
$event['end_m'] = 24*60-1;
$event['multiday'] = True;
}
if ($this->use_time_grid && !$event['whole_day_on_top'])
{
for($c = 0; $event['start_m'] < $col_ends[$c]; ++$c);
$col_ends[$c] = $event['end_m'];
}
else
{
$c = 0; // without grid we only use one column
}
$eventCols[$c][] = $event;
}
$html = $indent.'
'."\n";
// Creation of the header-column with date, evtl. holiday-names and a matching background-color
$ts = $this->bo->date2ts((string)$day_ymd);
$title = !is_bool($short_title) ? $short_title :
($short_title ? lang(adodb_date('l',$ts)).' '.adodb_date('d.',$ts) : $this->bo->long_date($ts,0,false,true));
$day_view = array(
'menuaction' => 'calendar.calendar_uiviews.day',
'date' => $day_ymd,
);
$this->_day_class_holiday($day_ymd,$class,$holidays);
// the weekday and date
if (!$short_title && $holidays) $title .= ': '.$holidays;
if ($short_title === true)
{
if ($this->allowEdit)
{
$title = html::a_href($title,$day_view,'',
!isset($this->holidays[$day_ymd])?' title="'.lang('Dayview').'"':'');
}
}
elseif ($short_title === false)
{
// add arrows to go to the previous and next day (dayview only)
$day_view['date'] = $this->bo->date2string($ts -= 12*HOUR_s);
if ($this->allowEdit)
{
$title = html::a_href(html::image('phpgwapi','left',$this->bo->long_date($ts)),$day_view).' '.$title;
}
else
{
$title = $day_view.' '.$title;
}
$day_view['date'] = $this->bo->date2string($ts += 48*HOUR_s);
if ($this->allowEdit)
{
$title .= ' '.html::a_href(html::image('phpgwapi','right',$this->bo->long_date($ts)),$day_view);
}
else
{
$title .= ' '.$day_view;
}
}
$html .= $indent."\t".'\n";
if ($this->use_time_grid)
{
// drag and drop: check if the current user has EDIT permissions on the grid
if(is_object($this->dragdrop))
{
if($owner)
{
$dropPermission = $this->bo->check_perms(EGW_ACL_EDIT,0,$owner);
}
else
{
$dropPermission = true;
}
}
// adding divs to click on for each row / time-span
for($t = $this->scroll_to_wdstart ? 0 : $this->wd_start,$i = 1+$this->extraRows;
$t <= $this->wd_end || $this->scroll_to_wdstart && $t < 24*60;
$t += $this->granularity_m,++$i)
{
$linkData = array(
'menuaction' =>'calendar.calendar_uiforms.edit',
'date' => $day_ymd,
'hour' => sprintf("%02d",floor($t / 60)),
'minute' => sprintf("%02d",floor($t % 60)),
);
if ($owner) $linkData['owner'] = $owner;
$droppableDateTime = $linkData['date'] . "T" . $linkData['hour'] . $linkData['minute'];
$droppableID='drop_'.$droppableDateTime.'_O'.$owner;
$html .= $indent."\t".'
allowEdit)
{
$html .= ' onclick="'.$this->popup($GLOBALS['egw']->link('/index.php',$linkData)).';return false;"';
}
$html .= '>
'."\n";
if(is_object($this->dragdrop) && $dropPermission)
{
$this->dragdrop->addDroppable(
$droppableID,
array(
'datetime'=>$droppableDateTime,
'owner'=>$owner ? $owner : $this->user,
)
);
}
}
}
// displaying all event columns of the day
foreach($eventCols as $n => $eventCol)
{
$html .= $this->eventColWidget($eventCol,!$n ? 0 : 60-10*(count($eventCols)-$n),
count($eventCols) == 1 ? 100 : (!$n ? 80 : 50),$indent."\t",
$owner ? $owner : $this->user);
}
$html .= $indent."
\n"; // calDayCol
return $html;
}
/**
* get the CSS class and holidays for a given day
*
* @param string $day_ymd date
* @param string &$class class to use
* @param string &$holidays commaseparted holidays or empty if none
* @param boolean $only_weekend=false show only the weekend in header-color, otherwise every second days is shown too
*/
function _day_class_holiday($day_ymd,&$class,&$holidays,$only_weekend=false)
{
$class = $holidays = '';
$bday = false;
if (isset($this->holidays[$day_ymd]))
{
foreach($this->holidays[$day_ymd] as $holiday)
{
if (isset($holiday['birthyear']))
{
$bday = true;
}
else
{
$class = 'calHoliday';
}
$holidays[] = $holiday['name'];
}
$holidays = implode(', ',$holidays);
}
if (!$class)
{
if ($day_ymd == $this->bo->date2string($this->bo->now_su))
{
$class = 'calToday';
}
else
{
$day = (int) date('w',$this->bo->date2ts((string) $day_ymd));
if ($only_weekend)
{
$class = $day == 0 || $day == 6 ? 'th' : 'row_off';
}
else
{
$class = $day & 1 ? 'row_on' : 'th';
}
}
}
if ($bday) $class .= ' calBirthday';
}
/**
* Creates colunm for non-overlaping (!) events
*
* Uses the eventWidget to display each event.
*
* @param array $events of events to show
* @param int $left start of the widget
* @param int $width width of the widget
* @param string $indent string for correct indention
* @param int $owner owner of the eventCol
*/
function eventColWidget($events,$left,$width,$indent,$owner)
{
if ($this->debug > 1 || $this->debug==='eventColWidget') $this->bo->debug_message('uiviews::eventColWidget(%1,left=%2,width=%3,)',False,$events,$left,$width);
$html = $indent.'
'."\n";
foreach($events as $event)
{
$html .= $this->eventWidget($event,$width,$indent."\t",$owner);
}
$html .= $indent."
\n";
return $html;
}
/**
* Shows one event
*
* The display of the event and it's tooltip is done via the event_widget.tpl template
*
* @param $event array with the data of event to show
* @param $width int width of the widget
* @param string $indent string for correct indention
* @param int $owner owner of the calendar the event is in
* @param boolean $return_array=false should an array with keys(tooltip,popup,html) be returned or the complete widget as string
* @param string $block='event_widget' template used the render the widget
* @return string/array
*/
function eventWidget($event,$width,$indent,$owner,$return_array=false,$block='event_widget')
{
if ($this->debug > 1 || $this->debug==='eventWidget') $this->bo->debug_message('uiviews::eventWidget(%1,width=%2)',False,$event,$width);
if($this->use_time_grid && $event['whole_day_on_top']) $block = 'event_widget_wholeday_on_top';
static $tpl = False;
if (!$tpl)
{
$tpl = $GLOBALS['egw']->template;
$tpl->set_root($GLOBALS['egw']->common->get_tpl_dir('calendar'));
$tpl->set_file('event_widget_t','event_widget.tpl');
$tpl->set_block('event_widget_t','event_widget');
$tpl->set_block('event_widget_t','event_widget_wholeday_on_top');
$tpl->set_block('event_widget_t','event_tooltip');
$tpl->set_block('event_widget_t','planner_event');
}
if (($return_array || $event['start_m'] == 0) && $event['end_m'] >= 24*60-1)
{
if ($return_array && $event['end_m'] > 24*60)
{
$timespan = $this->bo->format_date($event['start'],false).' - '.$this->bo->format_date($event['end']);
}
else
{
$timespan = lang('all day');
}
}
else
{
$timespan = $this->bo->timespan($event['start_m'],$event['end_m']);
}
$is_private = !$this->bo->check_perms(EGW_ACL_READ,$event);
$icons = !$is_private ? $this->event_icons($event) : array(html::image('calendar','private',lang('private')));
$cats = $this->bo->categories($this->categories->check_list(EGW_ACL_READ, $event['category']),$color);
// these values control varius aspects of the geometry of the eventWidget
$small_trigger_width = 120 + 20*count($icons);
$corner_radius=$width > $small_trigger_width ? 10 : 5;
$header_height=$width > $small_trigger_width ? 19 : 12; // multi_3 icon has a height of 19=16+2*1padding+1border !
if (!$return_array) $height = $this->times2height($event['start_m'],$event['end_m'],$header_height);
//$body_height = max(0,$height - $header_height - $corner_radius);
$border=1;
$headerbgcolor = $color ? $color : '#808080';
$headercolor = self::brightness($headerbgcolor) > 128 ? 'black' : 'white';
// the body-colors (gradient) are calculated from the headercolor, which depends on the cat of an event
$bodybgcolor1 = $this->brighter($headerbgcolor,$headerbgcolor == '#808080' ? 100 : 170);
$bodybgcolor2 = $this->brighter($headerbgcolor,220);
// mark event as invitation, by NOT using category based background color, but plain white
if ($event['participants'][$this->user][0] == 'U')
{
$bodybgcolor1 = $bodybgcolor2 = 'white';
}
// get status class of event: calEventAllAccepted, calEventAllAnswered or calEventSomeUnknown
$status_class = 'calEventAllAccepted';
foreach($event['participants'] as $id => $status)
{
calendar_so::split_status($status,$quantity,$role);
switch ($status)
{
case 'A':
break;
case 'U':
$status_class = 'calEventSomeUnknown';
break 2; // break foreach
default:
$status_class = 'calEventAllAnswered';
break;
}
}
// seperate each participant types
$part_array = array();
if ($this->allowEdit)
{
foreach($this->bo->participants($event) as $part_key => $participant)
{
if(is_numeric($part_key))
{
$part_array[lang('Participants')][$part_key] = $participant;
}
elseif(isset($this->bo->resources[$part_key[0]]))
{
$part_array[((isset($this->bo->resources[$part_key[0]]['participants_header'])) ? $this->bo->resources[$part_key[0]]['participants_header'] : lang($this->bo->resources[$part_key[0]]['app']))][$part_key] = $participant;
}
}
foreach($part_array as $part_group => $participant)
{
$participants .= $this->add_nonempty($participant,$part_group,True,False);
}
}
// as we only deal with percentual widht, we consider only the full dayview (1 colum) as NOT small
$small = $this->view != 'day' || $width < 50;
// $small = $width <= $small_trigger_width
$small_height = $this->use_time_grid && ( $event['end_m']-$event['start_m'] < 2*$this->granularity_m ||
$event['end_m'] <= $this->wd_start || $event['start_m'] >= $this->wd_end);
$tpl->set_var(array(
// event-content, some of it displays only if it really has content or is needed
'owner' => $GLOBALS['egw']->common->grab_owner_name($event['owner']),
'header_icons' => $small ? '' : implode("",$icons),
'body_icons' => $small ? implode("\n",$icons) : '',
'icons' => implode('',$icons),
'timespan' => $timespan,
'title' => ($title = !$is_private ? html::htmlspecialchars($event['title']) : lang('private')),
'header' => $small_height ? $title : $timespan,
'description' => !$is_private ? nl2br(html::htmlspecialchars($event['description'])) : '',
'location' => !$is_private ? $this->add_nonempty($event['location'],lang('Location')) : '',
'participants' => $participants,
'times' => !$event['multiday'] ? $this->add_nonempty($this->bo->timespan($event['start_m'],$event['end_m'],true),lang('Time')) :
$this->add_nonempty($this->bo->format_date($event['start']),lang('Start')).
$this->add_nonempty($this->bo->format_date($event['end']),lang('End')),
'multidaytimes' => !$event['multiday'] ? '' :
$this->add_nonempty($this->bo->format_date($event['start']),lang('Start')).
$this->add_nonempty($this->bo->format_date($event['end']),lang('End')),
'category' => !$is_private ? $this->add_nonempty($cats,lang('Category')) : '',
// the tooltip is based on the content of the actual widget, this way it takes no extra bandwidth/volum
// 'tooltip' => html::tooltip(False,False,array('BorderWidth'=>0,'Padding'=>0)),
// various aspects of the geometry or style
'corner_radius' => $corner_radius.'px',
'header_height' => $header_height.'px',
//'body_height' => $body_height.'px',
'height' => $height,
'width' => ($width-20).'px',
'border' => $border,
'bordercolor' => $headerbgcolor,
'headerbgcolor' => $headerbgcolor,
'headercolor' => $headercolor,
'bodybackground' => ($background = 'url('.$GLOBALS['egw_info']['server']['webserver_url'].
'/calendar/inc/gradient.php?color1='.urlencode($bodybgcolor1).'&color2='.urlencode($bodybgcolor2).
'&width='.$width.') repeat-y '.$bodybgcolor2),
'Small' => $small ? 'Small' : '', // to use in css class-names
'indent' => $indent."\t",
'status_class' => $status_class,
));
/* not used at the moment
foreach(array(
'upper_left'=>array('width'=>-$corner_radius,'height'=>$header_height,'border'=>0,'bgcolor'=>$headerbgcolor),
'upper_right'=>array('width'=>$corner_radius,'height'=>$header_height,'border'=>0,'bgcolor'=>$headerbgcolor),
'lower_left'=>array('width'=>-$corner_radius,'height'=>-$corner_radius,'border'=>$border,'color'=>$headerbgcolor,'bgcolor'=>$bodybgcolor1),
'lower_right'=>array('width'=>$corner_radius,'height'=>-$corner_radius,'border'=>$border,'color'=>$headerbgcolor,'bgcolor'=>$bodybgcolor2),
) as $name => $data)
{
$tpl->set_var($name.'_corner',$GLOBALS['egw_info']['server']['webserver_url'].
'/calendar/inc/round_corners.php?width='.$data['width'].'&height='.$data['height'].
'&bgcolor='.urlencode($data['bgcolor']).
(isset($data['color']) ? '&color='.urlencode($data['color']) : '').
(isset($data['border']) ? '&border='.urlencode($data['border']) : ''));
}
*/
$tooltip = $tpl->fp('tooltip','event_tooltip');
$tpl->set_var('tooltip',html::tooltip($tooltip,False,array('BorderWidth'=>0,'Padding'=>0)));
$html = $tpl->fp('out',$block);
$view_link = $GLOBALS['egw']->link('/index.php',array('menuaction'=>'calendar.calendar_uiforms.edit','cal_id'=>$event['id'],'date'=>$this->bo->date2string($event['start'])));
if ($event['recur_type'] != MCAL_RECUR_NONE)
{
$view_link_confirm_abort = $GLOBALS['egw']->link('/index.php',array('menuaction'=>'calendar.calendar_uiforms.edit','cal_id'=>$event['id'],'date'=>$this->bo->date2string($event['start']),'exception'=>1));
$view_link_confirm_text=lang('do you want to edit serialevent als exception? - Ok = Edit Exception, Abort = Edit Serial');
$popup = ($is_private || ! $this->allowEdit) ? '' : ' onclick="'.$this->popup($view_link_confirm_abort,null,750,410,$view_link,$view_link_confirm_text).'; return false;"';
}
else
{
$popup = ($is_private || ! $this->allowEdit) ? '' : ' onclick="'.$this->popup($view_link).'; return false;"';
}
//_debug_array($event);
if ($return_array)
{
return array(
'tooltip' => $tooltip,
'popup' => $popup,
'html' => $html,
'private' => $is_private,
'color' => $color,
);
}
$ie_fix = '';
if (html::$user_agent == 'msie') // add a transparent image to make the event "opaque" to mouse events
{
$ie_fix = $indent."\t".html::image('calendar','transparent.gif','',
html::tooltip($tooltip,False,array('BorderWidth'=>0,'Padding'=>0)).
' style="top:0px; left:0px; position:absolute; height:100%; width:100%; z-index:1"') . "\n";
}
if ($this->use_time_grid)
{
if($event['whole_day_on_top'])
{
$style = 'top: '.($this->rowHeight*$this->wholeDayPosCounter).'%; height: '.$this->rowHeight.'%;';
$this->wholeDayPosCounter++;
}
else
{
$style = 'top: '.$this->time2pos($event['start_m']).'%; height: '.$height.'%;';
}
}
else
{
$style = 'position: relative; margin-top: 3px;';
}
$draggableID = 'drag_'.$event['id'].'_O'.$event['owner'].'_C'.$owner;
$html = $indent.'
0,'Padding'=>0)).
'>'."\n".$ie_fix.$html."\n".
$indent."
"."\n";
// ATM we do not support whole day events or recurring events for dragdrop
if (is_object($this->dragdrop) &&
$this->use_time_grid &&
$this->bo->check_perms(EGW_ACL_EDIT,$event) &&
!$event['whole_day_on_top'] &&
!$event['whole_day'] &&
!$event['recur_type']
)
{
// register event as draggable
$this->dragdrop->addDraggable(
$draggableID,
array(
'eventId'=>$event['id'],
'eventOwner'=>$event['owner'],
'calendarOwner'=>$owner,
'errorImage'=>addslashes(html::image('phpgwapi','dialog_error',false,'style="width: 16px;"')),
'loaderImage'=>addslashes(html::image('phpgwapi','ajax-loader')),
),
'calendar.dragDropFunctions.dragEvent',
'calendar.dragDropFunctions.dropEvent',
'top center 2'
);
}
return $html;
}
function add_nonempty($content,$label,$one_per_line=False,$space = True)
{
if (is_array($content))
{
if($space)
{
$content = implode($one_per_line ? ",\n" : ', ',$content);
}
else
{
$content = implode($one_per_line ? "\n" : ', ',$content);
}
}
if (!empty($content))
{
return '
'.$label.':'.
($one_per_line ? '
' : ' ').
nl2br(html::htmlspecialchars($content)).'
';
}
return '';
}
/**
* Calculates a brighter color for a given color
*
* @param $rgb string color as #rrggbb value
* @param $decr int value to add to each component, default 64
* @return string the brighter color
*/
static function brighter($rgb,$decr=64)
{
if (!preg_match('/^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/',$rgb,$components))
{
return '#ffffff';
}
$brighter = '#';
for ($i = 1; $i <=3; ++$i)
{
$val = hexdec($components[$i]) + $decr;
if ($val > 255) $val = 255;
$brighter .= sprintf('%02x',$val);
}
//echo "brighter($rgb=".print_r($components,True).")=$brighter\n";
return $brighter;
}
/**
* Calculates the brightness of a hexadecimal rgb color (median of the r, g and b components)
*
* @param string $rgb eg. #808080
* @return int between 0 and 255
*/
static function brightness($rgb)
{
if ($rgb[0] != '#' || strlen($rgb) != 7)
{
return 128; // no rgb color, return some default
}
$dec = hexdec(substr($rgb,1));
for($i = 0; $i < 24; $i += 8)
{
$sum += ($dec >> $i) & 255;
}
return (int)round($sum / 3.0, 0);
}
/**
* Number of month to display in yearly planner
*/
const YEARLY_PLANNER_NUM_MONTH = 12;
/**
* Creates a planner view: grid with columns for the time and rows for categories or users
*
* Uses the plannerRowWidget to display rows
*
* @param array $events events to show
* @param mixed $start start-time of the grid
* @param mixed $end end-time of the grid
* @param string|int $by_cat rows by sub-categories of $by_cat (cat_id or 0 for upmost level) or by 'user' or 'month'
* @param string $indent='' string for correct indention
* @return string with widget
*/
function &plannerWidget(&$events,$start,$end,$by_cat=0,$indent='')
{
$content = $indent.'
'."\n";
// display the header, containing a headerTitle and multiple headerRows with the scales
$content .= $indent."\t".'\n"; // end of the plannerHeader
// sort the events after user or category
$rows = array();
if (!is_array($events)) $events = array();
foreach($events as $key => $event)
{
if ($by_cat === 'user') // planner by user
{
foreach($event['participants'] as $sort => $status)
{
// only show if participant has not rejected or user wants to see rejections
if (isset($sort2label[$sort]) && ($status != 'R' || $this->bo->cal_prefs['show_rejected']))
{
$rows[$sort][] =& $events[$key];
}
}
}
elseif ($by_cat === 'month') // planner by month / yearly planner
{
$sort = date('Y-m',$event['start']);
$rows[$sort][] =& $events[$key];
// end in a different month?
if ($sort != ($end_sort = date('Y-m',$event['end'])))
{
while($sort != $end_sort)
{
list($y,$m) = explode('-',$sort);
if (++$m > 12)
{
++$y;
$m = 1;
}
$sort = sprintf('%04d-%02d',$y,$m);
$rows[$sort][] =& $events[$key];
}
}
}
else // planner by cat
{
foreach($this->_get_planner_cats($event['category'],$sort2label,$sort2color) as $sort)
{
if (!is_array($rows[$sort])) $rows[$sort] = array();
$rows[$sort][] =& $events[$key];
}
}
}
// display a plannerRowWidget for each row (user or category)
foreach($sort2label as $sort => $label)
{
if (!isset($rows[$sort]) && (!$this->cal_prefs['planner_show_empty_rows'] ||
$by_cat === 'user' && $this->cal_prefs['planner_show_empty_rows'] == 'cat' ||
is_int($by_cat) && $this->cal_prefs['planner_show_empty_rows'] == 'user'))
{
continue; // dont show empty categories or user rows
}
$class = $class == 'row_on' ? 'row_off' : 'row_on';
if ($by_cat === 'month')
{
$time = new egw_time($sort.'-01');
$start = $time->format('ts');
$time->modify('+1month -1second');
$end = $time->format('ts');
}
$content .= $this->plannerRowWidget(isset($rows[$sort]) ? $rows[$sort] : array(),$start,$end,$label,$class,$indent."\t");
}
$content .= $indent."
\n"; // end of the plannerWidget
return $content;
}
/**
* get all users to display in the planner_by_user
*
* @param boolean $enum_groups=true should groups be returned as there members (eg. planner) or not (day & week)
* @return array with uid => label pairs, first all users alphabetically sorted, then all resources
*/
function _get_planner_users($enum_groups=true)
{
$users = $resources = array();
foreach(explode(',',$this->owner) as $user)
{
if (!is_numeric($user)) // resources
{
$resources[$user] = $this->bo->participant_name($user);
}
elseif ($enum_groups && $GLOBALS['egw']->accounts->get_type($user) == 'g') // groups
{
foreach((array) $GLOBALS['egw']->accounts->member($user) as $data)
{
$user = $data['account_id'];
if ($this->bo->check_perms(EGW_ACL_READ,0,$user))
{
$users[$user] = $this->bo->participant_name($user);
}
}
}
else // users
{
$users[$user] = $this->bo->participant_name($user);
}
}
asort($users);
asort($resources);
return $users+$resources;
}
/**
* get all categories used as sort criteria for the planner by category
*
* the returned cat is as direct sub-category of $this->cat_id or a main (level 1) category if !$this->cat_id
*
* @param string $cats comma-delimited cat_id's or empty for no cat
* @param array &$sort2label labels for the returned cats
* @return array with cat_id's
*/
function _get_planner_cats($cats,&$sort2label)
{
static $cat2sort;
if (!is_array($cat2sort))
{
$cat2sort = array();
foreach((array)$this->categories->return_array('all',0,false,'','','',true) as $data)
{
if ($data['parent'] == $this->cat_id || $data['id'] == $this->cat_id) // cat is a direct sub of $this->cat_id
{
$cat2sort[$data['id']] = $data['id'];
$sort2label[$data['id']] = stripslashes($data['name']);
}
elseif(isset($cat2sort[$data['parent']])) // parent is already in the array => add us with same target
{
$cat2sort[$data['id']] = $cat2sort[$data['parent']];
}
}
}
$ret = array();
foreach(!is_array($cats) ? explode(',',$cats) : $cats as $cat)
{
if (isset($cat2sort[$cat]) && !in_array($cat2sort[$cat],$ret))
{
$ret[] = $cat2sort[$cat];
}
}
if (!count($ret))
{
$sort2label[0] = lang('none');
$ret[] = 0;
}
//echo "
uiviews::_get_planner_cats($cats=".$this->categories->id2name($cats).") (this->cat_id=$this->cat_id) = ".print_r($ret,true).'='.$this->categories->id2name($ret[0])."
\n";
return $ret;
}
/**
* Creates month scale for the planner
*
* @param int $start start-time (12h) of the scale
* @param int $days number of days to display
* @param string $indent='' string for correct indention
* @return string with scale
*/
function plannerMonthScale($start,$days,$indent)
{
$day_width = round(100 / $days,2);
$content .= $indent.'
'."\n";
for($t = $start,$left = 0,$i = 0; $i < $days; $t += $days_in_month*DAY_s,$left += $days_in_month*$day_width,$i += $days_in_month)
{
$t_arr = $this->bo->date2array($t);
unset($t_arr['raw']); // force recalculation
unset($t_arr['full']);
$days_in_month = $this->datetime->days_in_month($t_arr['month'],$t_arr['year']) - ($t_arr['day']-1);
if ($i + $days_in_month > $days)
{
$days_in_month = $days - $i;
}
if ($days_in_month > 10)
{
$title = lang(date('F',$t)).' '.$t_arr['year'];
// previous links
$prev = $t_arr;
$prev['day'] = 1;
if ($prev['month']-- <= 1)
{
$prev['month'] = 12;
$prev['year']--;
}
if ($this->bo->date2ts($prev) < $start-20*DAY_s)
{
$prev['day'] = $this->day;
$full = $this->bo->date2string($prev);
if ($this->day >= 15) $prev = $t_arr; // we stay in the same month
$prev['day'] = $this->day < 15 ? 15 : 1;
$half = $this->bo->date2string($prev);
$title = html::a_href(html::image('phpgwapi','first',lang('back one month'),$options=' alt="<<"'),array(
'menuaction' => $this->view_menuaction,
'date' => $full,
)) . ' '.
html::a_href(html::image('phpgwapi','left',lang('back half a month'),$options=' alt="<"'),array(
'menuaction' => $this->view_menuaction,
'date' => $half,
)) . ' '.$title;
}
// next links
$next = $t_arr;
if ($next['month']++ >= 12)
{
$next['month'] = 1;
$next['year']++;
}
// dont show next scales, if there are more then 10 days in the next month or there is no next month
$days_in_next_month = (int) date('d',$end = $start+$days*DAY_s);
if ($days_in_next_month <= 10 || date('m',$end) == date('m',$t))
{
if ($this->day >= 15) $next = $t_arr; // we stay in the same month
$next['day'] = $this->day;
$full = $this->bo->date2string($next);
if ($this->day < 15) $next = $t_arr; // we stay in the same month
$next['day'] = $this->day < 15 ? 15 : 1;
$half = $this->bo->date2string($next);
$title .= ' '.html::a_href(html::image('phpgwapi','right',lang('forward half a month'),$options=' alt=">>"'),array(
'menuaction' => $this->view_menuaction,
'date' => $half,
)). ' '.
html::a_href(html::image('phpgwapi','last',lang('forward one month'),$options=' alt=">>"'),array(
'menuaction' => $this->view_menuaction,
'date' => $full,
));
}
}
else
{
$title = ' ';
}
$class = $class == 'row_on' ? 'th' : 'row_on';
$content .= $indent."\t".'
'.
$title."
\n";
}
$content .= $indent."
\n"; // end of plannerScale
return $content;
}
/**
* Creates a week scale for the planner
*
* @param int $start start-time (12h) of the scale
* @param int $days number of days to display
* @param string $indent='' string for correct indention
* @return string with scale
*/
function plannerWeekScale($start,$days,$indent)
{
$week_width = round(100 / $days * ($days <= 7 ? $days : 7),2);
$content .= $indent.'
'."\n";
for($t = $start,$left = 0,$i = 0; $i < $days; $t += 7*DAY_s,$left += $week_width,$i += 7)
{
$title = lang('Week').' '.date('W',$t);
if ($days > 7)
{
$title = html::a_href($title,array(
'menuaction' => 'calendar.calendar_uiviews.planner',
'planner_days' => 7,
'date' => date('Ymd',$t),
),false,' title="'.html::htmlspecialchars(lang('Weekview')).'"');
}
else
{
// prev. week
$title = html::a_href(html::image('phpgwapi','first',lang('previous'),$options=' alt="<<"'),array(
'menuaction' => $this->view_menuaction,
'date' => date('Ymd',$t-7*DAY_s),
)) . '
'.$title;
// next week
$title .= ' '.html::a_href(html::image('phpgwapi','last',lang('next'),$options=' alt=">>"'),array(
'menuaction' => $this->view_menuaction,
'date' => date('Ymd',$t+7*DAY_s),
));
}
$class = $class == 'row_on' ? 'th' : 'row_on';
$content .= $indent."\t".'
'.$title."
\n";
}
$content .= $indent."
\n"; // end of plannerScale
return $content;
}
/**
* Creates day scale for the planner
*
* @param int $start start-time (12h) of the scale
* @param int $days number of days to display
* @param string $indent='' string for correct indention
* @return string with scale
*/
function plannerDayScale($start,$days,$indent)
{
$day_width = round(100 / $days,2);
$content .= $indent.'
'."\n";
for($t = $start,$left = 0,$i = 0; $i < $days; $t += DAY_s,$left += $day_width,++$i)
{
$this->_day_class_holiday($this->bo->date2string($t),$class,$holidays,$days > 7);
if ($days <= 3)
{
$title = '
'.lang(date('l',$t)).', '.date('j',$t).'. '.lang(date('F',$t)).'';
}
elseif ($days <= 7)
{
$title = lang(date('l',$t)).'
'.date('j',$t).'. '.lang(date('F',$t));
}
else
{
$title = substr(lang(date('D',$t)),0,2).'
'.date('j',$t);
}
if ($days > 1)
{
$title = html::a_href($title,array(
'menuaction' => 'calendar.calendar_uiviews.planner',
'planner_days' => 1,
'date' => date('Ymd',$t),
),false,strpos($class,'calHoliday') !== false || strpos($class,'calBirthday') !== false ? '' : ' title="'.html::htmlspecialchars(lang('Dayview')).'"');
}
if ($days < 5)
{
if (!$i) // prev. day only for the first day
{
$title = html::a_href(html::image('phpgwapi','first',lang('previous'),$options=' alt="<<"'),array(
'menuaction' => $this->view_menuaction,
'date' => date('Ymd',$start-DAY_s),
)) . ' '.$title;
}
if ($i == $days-1) // next day only for the last day
{
$title .= ' '.html::a_href(html::image('phpgwapi','last',lang('next'),$options=' alt=">>"'),array(
'menuaction' => $this->view_menuaction,
'date' => date('Ymd',$start+DAY_s),
));
}
}
$content .= $indent."\t".'
'.$title."
\n";
}
$content .= $indent."
\n"; // end of plannerScale
return $content;
}
/**
* Creates DayOfMonth scale for planner by month
*
* @param string $indent
* @return string
*/
function plannerDayOfMonthScale($indent)
{
$day_width = round(100 / 31,2);
// month scale with navigation
$content .= $indent.'
'."\n";
$title = lang(egw_time::to($this->first,'F')).' '.egw_time::to($this->first,'Y').' - '.
lang(egw_time::to($this->last,'F')).' '.egw_time::to($this->last,'Y');
// calculate date for navigation links
$time = new egw_time($this->first);
$time->modify('-1year');
$last_year = $time->format('Ymd');
$time->modify('+11month');
$last_month = $time->format('Ymd');
$time->modify('+2month');
$next_month = $time->format('Ymd');
$time->modify('+11month');
$next_year = $time->format('Ymd');
$title = html::a_href(html::image('phpgwapi','first',lang('back one year'),$options=' alt="<<"'),array(
'menuaction' => $this->view_menuaction,
'date' => $last_year,
)) . ' '.
html::a_href(html::image('phpgwapi','left',lang('back one month'),$options=' alt="<"'),array(
'menuaction' => $this->view_menuaction,
'date' => $last_month,
)) . ' '.$title;
$title .= ' '.html::a_href(html::image('phpgwapi','right',lang('forward one month'),$options=' alt=">>"'),array(
'menuaction' => $this->view_menuaction,
'date' => $next_month,
)). ' '.
html::a_href(html::image('phpgwapi','last',lang('forward one year'),$options=' alt=">>"'),array(
'menuaction' => $this->view_menuaction,
'date' => $next_year,
));
$content .= $indent."\t".'
'.
$title."
\n";
$content .= $indent."
\n"; // end of plannerScale
// day of month scale
$content .= $indent.'
'."\n";
$today = egw_time::to('now','d');
for($left = 0,$i = 0; $i < 31; $left += $day_width,++$i)
{
$class = $i & 1 ? 'row_on' : 'row_off';
$content .= $indent."\t".'
'.
(1+$i)."
\n";
}
$content .= $indent."
\n"; // end of plannerScale
return $content;
}
/**
* Creates hour scale for the planner
*
* @param int $start start-time (12h) of the scale
* @param int $days number of days to display
* @param string $indent='' string for correct indention
* @return string with scale
*/
function plannerHourScale($start,$days,$indent)
{
foreach(array(1,2,3,4,6,8,12) as $d) // numbers dividing 24 without rest
{
if ($d > $days) break;
$decr = $d;
}
$hours = $days * 24;
if ($days == 1) // for a single day we calculate the hours of a days, to take into account daylight saving changes (23 or 25 hours)
{
$t_arr = $this->bo->date2array($start);
unset($t_arr['raw']);
$t_arr['hour'] = $t_arr['minute'] = $t_arr['second'] = 0;
$s = $this->bo->date2ts($t_arr);
$t_arr['hour'] = 23; $t_arr['minute'] = $t_arr['second'] = 59;
$hours = ($this->bo->date2ts($t_arr) - $s) / HOUR_s;
}
$cell_width = round(100 / $hours * $decr,2);
$content .= $indent.'
'."\n";
for($t = $start,$left = 0,$i = 0; $i < $hours; $t += $decr*HOUR_s,$left += $cell_width,$i += $decr)
{
$title = date($this->cal_prefs['timeformat'] == 12 ? 'ha' : 'H',$t);
$class = $class == 'row_on' ? 'th' : 'row_on';
$content .= $indent."\t".'
'.$title."
\n";
}
$content .= $indent."
\n"; // end of plannerScale
return $content;
}
/**
* Creates a row for one user or category, with a header (user or category name) and (multiple) rows with non-overlapping events
*
* Uses the eventRowWidget to display a row of non-overlapping events
*
* @param array $events to show
* @param int $start start-time of the row
* @param int $end end-time of the row
* @param string $header user or category name for the row-header
* @param string $class additional css class for the row
* @param string $indent='' string for correct indention
* @return string with widget
*/
function plannerRowWidget($events,$start,$end,$header,$class,$indent='')
{
$content = $indent.'
\n"; // end of the plannerRowWidget
return $content;
}
/**
* Mark weekends and other special days in yearly planner
*
* @param int $start timestamp of start of row
* @param int $days number of days in month of row
* @param string $indent=''
* @return string
*/
function yearlyPlannerMarkDays($start,$days,$indent='')
{
$day_width = round(100/$days,2);
for($t = $start,$left = 0,$i = 0; $i < $days; $t += DAY_s,$left += $day_width,++$i)
{
$this->_day_class_holiday($this->bo->date2string($t),$class,$holidays,true);
if ($class != 'row_on' && $class != 'row_off') // no regular weekday
{
$content .= $indent.'
'."\n";
}
}
return $content;
}
/**
* Creates a row with non-overlapping events
*
* Uses the plannerEventWidget to display the events
*
* @param array $events non-overlapping events to show
* @param int $start start-time of the row
* @param int $end end-time of the row
* @param string $indent='' string for correct indention
* @return string with widget
*/
function eventRowWidget($events,$start,$end,$indent='')
{
$content = $indent.'
'."\n";
foreach($events as $event)
{
$content .= $this->plannerEventWidget($event,$start,$end,$indent."\t");
}
$content .= $indent."
\n";
return $content;
}
/**
* Calculate a time-dependent position in the planner
*
* We use a non-linear scale in the planner monthview, which shows the workday start or end
* as start or end of the whole day. This improves the resolution a bit.
*
* @param int $time
* @param int $start start-time of the planner
* @param int $end end-time of the planner
* @return float percentage position between 0-100
*/
function _planner_pos($time,$start,$end)
{
if ($time <= $start) return 0; // we are left of our scale
if ($time >= $end) return 100; // we are right of our scale
if ($this->planner_days || $this->sortby == 'month')
{
$percent = ($time - $start) / ($end - $start);
}
else // monthview
{
$t_arr = $this->bo->date2array($time);
$day_start = $this->bo->date2ts((string)$t_arr['full']);
$percent = ($day_start - $start) / ($end - $start);
$time_of_day = 60 * $t_arr['hour'] + $t_arr['minute'];
if ($time_of_day >= $this->wd_start)
{
if ($time_of_day > $this->wd_end)
{
$day_percentage = 1;
}
else
{
$wd_lenght = $this->wd_end - $this->wd_start;
if ($wd_lenght <= 0) $wd_lenght = 24*60;
$day_percentage = ($time_of_day-$this->wd_start) / $wd_lenght; // between 0 and 1
}
$days = ($end - $start) / DAY_s;
$percent += $day_percentage / $days;
}
}
$percent = round(100 * $percent,2);
//echo "
_planner_pos(".date('Y-m-d H:i',$time).', '.date('Y-m-d H:i',$start).', '.date('Y-m-d H:i',$end).") = $percent
\n";
return $percent;
}
/**
* Displays one event for the planner, using the eventWidget of the other views
*
* @param array $event
* @param int $start start-time of the planner
* @param int $end end-time of the planner
* @return string with widget
*/
function plannerEventWidget($event,$start,$end,$indent='')
{
// some fields set by the dayColWidget for the other views
$day_start = $this->bo->date2ts((string)$this->bo->date2string($event['start']));
$event['start_m'] = ($event['start'] - $day_start) / 60;
$event['end_m'] = round(($event['end'] - $day_start) / 60);
$event['multiday'] = true;
unset($event['whole_day_on_top']);
$data = $this->eventWidget($event,200,$indent,$this->owner,true,'planner_event');
$left = $this->_planner_pos($event['start'],$start,$end);
$width = $this->_planner_pos($event['end'],$start,$end) - $left;
$color = $data['color'] ? $data['color'] : 'gray';
return $indent.'
0,'Padding'=>0)).'>'."\n".$data['html'].$indent."
\n";
}
/**
* Marks whole day events for later usage and increments extraRows
*
* @param array $dayEvents
* @return array $dayEvents
*/
function tagWholeDayOnTop($dayEvents)
{
$this->extraRows = $this->extraRowsOriginal;
foreach ($dayEvents as $day=>$oneDayEvents)
{
$extraRowsToAdd = 0;
foreach ($oneDayEvents as $num=>$event)
{
$start = $this->bo->date2array($event['start']);
$end = $this->bo->date2array($event['end']);
if(!$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59)
{
if($event['non_blocking'])
{
$dayEvents[$day][$num]['whole_day_on_top']=true;
$this->whole_day_positions[$num]=($this->rowHeight*($num+2));
$extraRowsToAdd++;
}
else
{
$dayEvents[$day][$num]['whole_day']=true;
}
}
}
// check after every day if we have to increase $this->extraRows
if(($this->extraRowsOriginal+$extraRowsToAdd) > $this->extraRows) { $this->extraRows = ($this->extraRowsOriginal+$extraRowsToAdd); }
}
return $dayEvents;
}
}