<?php /* TASKS $Id$ */

if (!defined('DP_BASE_DIR')) {
	die('You should not access this file directly.');
}

require_once($AppUI->getSystemClass('libmail'));
require_once($AppUI->getSystemClass('dp'));
require_once($AppUI->getModuleClass('projects'));
require_once($AppUI->getSystemClass('event_queue'));
require_once($AppUI->getSystemClass('date'));

// user based access
$task_access = array('0'=>'Public',
					 '4'=>'Privileged',
					 '2'=>'Participant',
					 '1'=>'Protected',
					 '3'=>'Private');

/*
 * TASK DYNAMIC VALUE:
 * 0  = default(OFF), no dep tracking of others, others do track
 * 1  = dynamic, umbrella task, no dep tracking, others do track
 * 11 = OFF, no dep tracking, others do not track
 * 21 = FEATURE, dep tracking, others do not track
 * 31 = ON, dep tracking, others do track
 */

// When calculating a task's start date only consider
// end dates of tasks with these dynamic values.
$tracked_dynamics = array('0' => '0',
						  '1' => '1',
						  '2' => '31');
// Tasks with these dynamics have their dates updated when
// one of their dependencies changes. (They track dependencies)
$tracking_dynamics = array('0' => '21',
						   '1' => '31');

/*
 * CTask Class
 */
class CTask extends CDpObject
{
	/** @var int */
	var $task_id = NULL;
	/** @var string */
	var $task_name = NULL;
	/** @var int */
	var $task_parent = NULL;
	var $task_milestone = NULL;
	var $task_project = NULL;
	var $task_owner = NULL;
	var $task_start_date = NULL;
	var $task_duration = NULL;
	var $task_duration_type = NULL;
	/** @deprecated */
	var $task_hours_worked = NULL;
	var $task_end_date = NULL;
	var $task_status = NULL;
	var $task_priority = NULL;
	var $task_percent_complete = NULL;
	var $task_description = NULL;
	var $task_target_budget = NULL;
	var $task_related_url = NULL;
	var $task_creator = NULL;

	var $task_order = NULL;
	var $task_client_publish = NULL;
	var $task_dynamic = NULL;
	var $task_access = NULL;
	var $task_notify = NULL;
	var $task_departments = NULL;
	var $task_contacts = NULL;
	var $task_custom = NULL;
	var $task_type	 = NULL;

	protected $_reminder_list = array();
	protected $_reminder_sender = null;
	protected $_default_batch_status = null;


	public function __construct() {
		parent::__construct('tasks', 'task_id');
		$this->_module_directory = 'tasks';
	}

	function __toString() {
		return $this -> link . '/' . $this -> type . '/' . $this -> length;
	}

	// overload check
	function check() {
		global $AppUI;

		if ($this->task_id === NULL) {
			return 'task id is NULL';
		}
		// ensure changes to checkboxes are honoured
		$this->task_milestone = intval($this->task_milestone);
		$this->task_dynamic	  = intval($this->task_dynamic);

		$this->task_percent_complete = intval($this->task_percent_complete);

		if ($this->task_milestone) {
			$this->task_duration = '0';
		} else if (!($this->task_duration)) {
			$this->task_duration = '1';
		}
		if (!$this->task_creator) {
			$this->task_creator = $AppUI->user_id;
		}
		if (!$this->task_duration_type) {
			$this->task_duration_type = 1;
		}
		if (!$this->task_related_url) {
			$this->task_related_url = '';
		}
		if (!$this->task_notify) {
			$this->task_notify = 0;
		}

		/*
		 * Check for bad or circular task relationships (dep or child-parent).
		 * These checks are definately not exhaustive it is still quite possible
		 * to get things in a knot.
		 * Note: some of these checks may be problematic and might have to be removed
		 */
		static $addedit;
		if (!isset($addedit)) {
			$addedit = dPgetCleanParam($_POST, 'dosql', '') == 'do_task_aed' ? true : false;
		}
		$this_dependencies = array();

		/*
		 * If we are called from addedit then we want to use the incoming
		 * list of dependencies and attempt to stop bad deps from being created
		 */
		if ($addedit) {
			$hdependencies = dPgetCleanParam($_POST, 'hdependencies', '0');
			if ($hdependencies) {
				$this_dependencies = explode(',', $hdependencies);
			}
		} else {
			$this_dependencies = explode(',', $this->getDependencies());
		}
		// Set to false for recursive updateDynamic calls etc.
		$addedit = false;

		// Has a parent
		if ($this->task_id && $this->task_id != $this->task_parent) {

			$this_children = $this->getChildren();
			$this_child_deps = array();
			foreach ($this_children as $child) {
				$child_dep_str = $this->staticGetDependencies($child);
				$this_child_deps = array_unique(array_merge($this_child_deps,
				                                            explode(',', $child_dep_str)));
			}

			$current_task = $this;
			$parents_children = array();
			//itrative walk through parent tasks
			while ($current_task->task_id != $current_task->task_parent) {
				$current_parent = new CTask();
				$current_parent->load($current_task->task_parent);
				$parents_children = array_unique(array_merge($parents_children,
				                                             $current_parent->getChildren()));

				// Task parent (of any level) cannot be a child of task or its children
				// extra precaution against any UI bugs allowing otherwise
				if (in_array($current_parent->task_id, $parents_children)) {
					return (($this->task_id == $current_parent->task_parent)
					        ? 'BadParent_CircularParent'
					        : array('BadParent_CircularGrandParent',
					                ('(' . $current_parent->task_id . ')')));
				}
				//Task's children cannot have a parent (of any level) as a dependency
				if (in_array($current_parent->task_id, $this_child_deps)) {
					return 'BadParent_ChildDepOnParent';
				}

				//Task cannot have a parent (of any level) as a dependency
				if (in_array($current_parent->task_id, $this_dependencies)) {
					return (($current_task == $this) ? 'BadDep_CannotDependOnParent'
					        : array('BadDep_CircularGrandParent',
					                ('(' . $current_parent->task_id . ')')));
				}

				$parents_dependents = explode(',', $current_parent->dependentTasks());
				$this_intersect = array_intersect($this_dependencies, $parents_dependents);
				//Any tasks dependent on a dynamic parent task cannot be dependencies of task
				if (array_sum($this_intersect)) {
					$ids = '(' . implode(',', $intersect) . ')';
					return array('BadDep_CircularDepOnParentDependent', $ids);
				}

				$current_task = $current_parent;
			}
		} // parent

		// Have deps
		if (array_sum($this_dependencies)) {
			if ($this->task_dynamic == 1) {
				return 'BadDep_DynNoDep';
			}

			$this_dependents = (($this->task_id) ? explode(',', $this->dependentTasks()) : array());
			// Treat parent task, like a dependent
			if ($this->task_id != $this->task_parent) {
				array_push($this_dependents, $this->task_parent);
				$dep_string = $this->dependentTasks($this->task_parent);
				$this_dependents = array_unique(array_merge($this_dependents,
				                                            explode(',', $dep_string)));
			}

			$more_dependents = array();
			// Treat dependents' parents them like dependents too and pull parent dependents
			foreach ($this_dependents as $dependent) {
				$dependent_task = new CTask();
				$dependent_task->load($dependent);
				if ($dependent_task->task_id != $dependent_task->task_parent) {
					$current_task = $dependent_task;
					while ($current_task->task_id != $current_task->task_parent) {
						$current_parent_id = $current_task->task_parent;
						array_push($more_dependents, $current_parent_id);
						$dep_string = $this->dependentTasks($dependent_task->task_parent);
						$more_dependents = array_unique(array_merge($more_dependents,
						                                            explode(',', $dep_string)));

						$current_task = new CTask();
						$current_task->load($current_parent_id);
					}
				}
			}

			$this_dependents = array_unique(array_merge($this_dependents, $more_dependents));

			// Task dependencies can not be dependent on this task
			$intersect = array_intersect($this_dependencies, $this_dependents);
			if (array_sum($intersect)) {
				$ids = '(' . implode(',', $intersect) . ')';
				return array('BadDep_CircularDep', $ids);
			}
		}

		//Is dynamic and no child
		if (dPgetConfig('check_task_empty_dynamic') && $this->task_dynamic == 1) {
			$children_of_dynamic = $this->getChildren();
			if (empty($children_of_dynamic)) {
				return 'BadDyn_NoChild';
			}
		}

		return NULL;
	}


	/*
	 * overload the load function
	 * We need to update dynamic tasks of type '1' on each load process!
	 * @param int $oid optional argument, if not specifed then the value of current key is used
	 * @return any result from the database operation
	 */

	function load($oid=null, $strip=false, $skipUpdate=false) {
		// use parent function to load the given object
		$loaded = parent::load($oid, $strip);

		/*
		 ** Update the values of a dynamic task from
		 ** the children's properties each time the
		 ** dynamic task is loaded.
		 ** Additionally store the values in the db.
		 ** Only treat umbrella tasks of dynamics '1'.
		 */
		if ($this->task_dynamic == 1 && !($skipUpdate)) {
			// update task from children
			$this->htmlDecode();
			$this->updateDynamics(true);

			/*
			 ** Use parent function to store the updated values in the db
			 ** instead of store function of this object in order to
			 ** prevent from infinite loops.
			 */
			parent::store();
			$loaded = parent::load($oid, $strip);
		}

		// return whether the object load process has been successful or not
		return $loaded;
	}

	/*
	 * call the load function but don't update dynamics
	 */
	function peek($oid=null, $strip=false) {
		$loadme = $this->load($oid, $strip, true);
		return $loadme;
	}

	function updateDynamics($fromChildren = false) {
		//Has a parent or children, we will check if it is dynamic so that it's info is updated also
		$q = new DBQuery;
		$modified_task = new CTask();

		if ($fromChildren) {
			if (version_compare(phpversion(), '5.0.0', '>=')) {
				$modified_task = $this;
			} else {
				$modified_task =& $this;
			}
		} else {
			$modified_task->load($this->task_parent);
			$modified_task->htmlDecode();
		}

		if ($modified_task->task_dynamic == '1') {
			//Update allocated hours based on children with duration type of 'hours'
			$q->addTable($this->_tbl);
			$q->addQuery('SUM(task_duration * task_duration_type)');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND task_duration_type = 1 ');
			$q->addGroup('task_parent');
			$sql = $q->prepare();
			$q->clear();
			$children_allocated_hours1 = (float) db_loadResult($sql);

			/*
			 * Update allocated hours based on children with duration type of 'days'
			 * use the daily working hours instead of the full 24 hours to calculate
			 * dynamic task duration!
			 */
			$q->addTable($this->_tbl);
			$q->addQuery(' SUM(task_duration * ' . dPgetConfig('daily_working_hours') . ')');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND task_duration_type <> 1 ');
			$q->addGroup('task_parent');
			$sql = $q->prepare();
			$q->clear();
			$children_allocated_hours2 = (float) db_loadResult($sql);

			// sum up the two distinct duration values for the children with duration type 'hrs'
			// and for those with the duration type 'day'
			$children_allocated_hours = $children_allocated_hours1 + $children_allocated_hours2;

			if ($modified_task->task_duration_type == 1) {
				$modified_task->task_duration = round($children_allocated_hours, 2);
			} else {
				$modified_task->task_duration = round($children_allocated_hours
													  / dPgetConfig('daily_working_hours'), 2);
			}

			//Update worked hours based on children
			$q->addTable('tasks', 't');
			$q->innerJoin('task_log', 'tl', 't.task_id = tl.task_log_task');
			$q->addQuery('SUM(task_log_hours)');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND task_dynamic <> 1 ');
			$sql = $q->prepare();
			$q->clear();
			$children_hours_worked = (float) db_loadResult($sql);


			//Update worked hours based on dynamic children tasks
			$q->addTable('tasks');
			$q->addQuery('SUM(task_hours_worked)');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND task_dynamic = 1 ');
			$sql = $q->prepare();
			$q->clear();
			$children_hours_worked += (float) db_loadResult($sql);

			$modified_task->task_hours_worked = $children_hours_worked;

			//Update percent complete
			//hours
			$q->addTable('tasks');
			$q->addQuery('SUM(task_percent_complete * task_duration * task_duration_type)');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND task_duration_type = 1 ');
			$sql = $q->prepare();
			$q->clear();
			$real_children_hours_worked = (float) db_loadResult($sql);

			//"days"
			$q->addTable('tasks');
			$q->addQuery('SUM(task_percent_complete * task_duration * '
						 . dPgetConfig('daily_working_hours') . ')');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND task_duration_type <> 1 ');
			$sql = $q->prepare();
			$q->clear();
			$real_children_hours_worked += (float) db_loadResult($sql);

			$total_hours_allocated = (float)($modified_task->task_duration
											 * (($modified_task->task_duration_type > 1)
												? dPgetConfig('daily_working_hours') : 1));
			if ($total_hours_allocated > 0) {
				$modified_task->task_percent_complete = ceil($real_children_hours_worked
															 / $total_hours_allocated);
			} else {
				$q->addTable('tasks');
				$q->addQuery('AVG(task_percent_complete)');
				$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
							 . $modified_task->task_id);
				$sql = $q->prepare();
				$q->clear();
				$modified_task->task_percent_complete = db_loadResult($sql);
			}


			//Update start date
			$q->addTable('tasks');
			$q->addQuery('MIN(task_start_date)');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND ! isnull(task_start_date)'
						 . " AND task_start_date <>	'0000-00-00 00:00:00'");
			$sql = $q->prepare();
			$q->clear();
			$d = db_loadResult($sql);
			if ($d) {
				$modified_task->task_start_date = $d;
			} else {
				$modified_task->task_start_date = '0000-00-00 00:00:00';
			}

			//Update end date
			$q->addTable('tasks');
			$q->addQuery('MAX(task_end_date)');
			$q->addWhere('task_parent = ' . $modified_task->task_id . ' AND task_id <> '
						 . $modified_task->task_id . ' AND ! isnull(task_end_date)');
			$sql = $q->prepare();
			$q->clear();
			$modified_task->task_end_date = db_loadResult($sql);

			//If we are updating a dynamic task from its children we don't want to store() it
			//when the method exists the next line in the store calling function will do that
			if ($fromChildren == false) {
				$modified_task->store();
			}
		}
	}

	/*
	 * Copy the current task
	 *
	 * @author handco <handco@users.sourceforge.net>
	 * @param int id of the destination project
	 * @return object The new record object or null if error
	 */
	function copy($destProject_id = 0, $destTask_id = -1) {

		$newObj = $this->duplicate();
		// Copy this task to another project if it's specified
		if ($destProject_id != 0) {
			$newObj->task_project = $destProject_id;
		}

		if ($destTask_id == 0) {
			$newObj->task_parent = $newObj->task_id;
		} else if ($destTask_id > 0) {
			$newObj->task_parent = $destTask_id;
		}

		if ($newObj->task_parent == $this->task_id) {
			$newObj->task_parent = '';
		}
		$newObj->store();

		// Copy assigned users as well
		$q = new DBQuery();
		$q->addQuery('user_id, user_type, perc_assignment, user_task_priority');
		$q->addTable('user_tasks');
		$q->addWhere('task_id = ' . $this->task_id);
		$users = $q->loadList();

		$q->setDelete('user_tasks');
		$q->addWhere('task_id = ' . $newObj->task_id);
		$q->exec();
		$q->clear();

		$fields = array('user_id', 'user_type', 'perc_assignment', 'user_task_priority', 'task_id');
		$values = [];
		foreach ($users as $user) {
			$user['task_id'] = $newObj->task_id;
			$values[] = array_values($user);
		}
		if (count($values)) {
			$q->addTable('user_tasks');
			$q->addInsertMulti($fields, $values);
			$q->exec();
			$q->clear();
		}
		//copy dependancies
		$dep_list = $this->getDependencies();
		$newObj->updateDependencies($dep_list);

		return $newObj;
	}// end of copy()

	function deepCopy($destProject_id = 0, $destTask_id = 0) {
		$children = $this->getChildren();
		$newObj = $this->copy($destProject_id, $destTask_id);
		if (!empty($children)) {
			$tempTask = new CTask();
			$new_child_ids = array();
			foreach ($children as $child) {
				$tempTask->peek($child);
				$tempTask->htmlDecode($child);
				$newChild = $tempTask->deepCopy($destProject_id, $newObj->task_id);
				$newChild->store();

				//old id to new id translation table
				$old_id = $tempTask->task_id;
				$new_child_ids[$old_id] = $newChild->task_id;
			}
			/*
			 * We cannot update beyond the new child id without complicating matters
			 * by mapping "old" id's to new in an array that would be accessible in
			 * *every* level of recursive call and get executed just before returning
			 * from a given call. Also we may not want to do this as there could be
			 * good reasons for keeping some of the old non-child dependancy ids anyway
			 */
			//update dependancies on old child ids to new child id
			$dep_list = $newObj->getDependencies();
			if ($dep_list) {
				$dep_array = explode(',', $dep_list);
				foreach ($dep_array as $key => $dep_id) {
					if ($new_child_ids[$dep_id]) {
						$dep_array[$key] = $new_child_ids[$dep_id];
					}
				}
				$dep_list = implode(',', $dep_array);
				$newObj->updateDependencies($dep_list);
			}
		}
		$newObj->store();

		return $newObj;
	}

	function move($destProject_id = 0, $destTask_id = -1) {
		if ($destProject_id) {
			$this->task_project = $destProject_id;
		}
		if ($destTask_id >= 0) {
			$this->task_parent = (($destTask_id) ? $destTask_id : $this->task_id);
		}
		$this->store();
	}

	function deepMove($destProject_id = 0, $destTask_id = 0) {
		$children = $this->getChildren();
		$this->move($destProject_id, $destTask_id);
		if (!empty($children)) {
			foreach ($children as $child) {
				$tempChild = new CTask();
				$tempChild->peek($child);
				$tempChild->htmlDecode($child);
				$tempChild->deepMove($destProject_id, $this->task_id);
				$tempChild->store();
			}
		}
		$this->store();
	}

	/**
	 * @todo Parent store could be partially used
	 */
	function store($updateNulls = false) {
		GLOBAL $AppUI;
		$q = new DBQuery;

		$this->dPTrimAll();

		$importing_tasks = false;
		$msg = $this->check();
		if ($msg) {
			$return_msg = array(get_class($this) . '::store-check',	 'failed',	'-');
			if (is_array($msg)) {
				return array_merge($return_msg, $msg);
			} else {
				array_push($return_msg, $msg);
				return $return_msg;
			}
		}
		if ($this->task_id) {
			addHistory('tasks', $this->task_id, 'update', $this->task_name, $this->task_project);
			$this->_action = 'updated';

			// Load and globalize the old, not yet updated task object
			// e.g. we need some info later to calculate the shifting time for depending tasks
			// see function update_dep_dates
			GLOBAL $oTsk;
			$oTsk = new CTask();
			$oTsk->peek($this->task_id);

			// if task_status changed, then update subtasks
			if ($this->task_status != $oTsk->task_status) {
				$this->updateSubTasksStatus($this->task_status);
			}

			// Moving this task to another project?
			if ($this->task_project != $oTsk->task_project) {
				$this->updateSubTasksProject($this->task_project);
				// Move any files
				$nq = new DBQuery();
				$nq->addTable('files');
				$nq->addUpdate('file_project', $this->task_project);
				$nq->addWhere("file_task = '" . $this->task_id . "'");
				$nq->exec();
			}

			if ($this->task_dynamic == 1) {
				$this->updateDynamics(true);
			}

			// shiftDependentTasks needs this done first
			$this->check();
			$ret = db_updateObject('tasks', $this, 'task_id', false);

			// Milestone or task end date, or dynamic status has changed,
			// shift the dates of the tasks that depend on this task
			if (($this->task_end_date != $oTsk->task_end_date)
				|| ($this->task_dynamic != $oTsk->task_dynamic)
				|| ($this->task_milestone == '1')) {
				$this->shiftDependentTasks();
			}
		} else {
			$this->_action = 'added';
			if ($this->task_start_date == '')
				$this->task_start_date = '0000-00-00 00:00:00';
			if ($this->task_end_date == '')
				$this->task_end_date = '0000-00-00 00:00:00';

			$ret = db_insertObject('tasks', $this, 'task_id');
			addHistory('tasks', $this->task_id, 'add', $this->task_name, $this->task_project);

			if (empty($this->task_parent)) {  // should avoid throwing a warning (gwyneth 20210425)
				$q->addTable('tasks');
				$q->addUpdate('task_parent', $this->task_id);
				$q->addWhere('task_id = ' . $this->task_id);
				$q->exec();
				$q->clear();
			} else {
				// importing tasks do not update dynamics
				$importing_tasks = true;
			}

			// insert entry in user tasks
			$q->addTable('user_tasks');
			$q->addInsert('user_id', $AppUI->user_id);
			$q->addInsert('task_id', $this->task_id);
			$q->addInsert('user_type', '0');
			$q->exec();
			$q->clear();
		}

		//split out related departments and store them seperatly.
		$q->setDelete('task_departments');
		$q->addWhere('task_id=' . $this->task_id);
		$q->exec();
		$q->clear();
		// print_r($this->task_departments);
		if (!empty($this->task_departments)) {
			$departments = explode(',', $this->task_departments);
			$values = [];
			foreach ($departments as $department) {
			  $values[] = [ $this->task_id, $department ];
			}
			$q->addTable('task_departments');
			$q->addInsertMulti([ 'task_id', 'department_id' ], $values);
			$q->exec();
			$q->clear();
		}

		//split out related contacts and store them seperatly.
		$q->setDelete('task_contacts');
		$q->addWhere('task_id=' . $this->task_id);
		$q->exec();
		$q->clear();
		if (!empty($this->task_contacts)) {
			$contacts = explode(',', $this->task_contacts);
			$values = [];
			foreach ($contacts as $contact) {
				$values[] = [ $this->task_id, $contact ];
			}
			$q->addTable('task_contacts');
			$q->addInsertMulti(['task_id', 'contact_id'], $values);
			$q->exec();
			$q->clear();
		}

		// if is child update parent task
		if ($this->task_parent != $this->task_id) {

			if (!$importing_tasks) {
				$this->updateDynamics(true);
			}

			$pTask = new CTask();
			$pTask->load($this->task_parent);
			$pTask->updateDynamics();

			if (!empty($oTsk) && $oTsk->task_parent != $this->task_parent) {  // $oTsk must have been init'ed (gwyneth 20210425)
				$old_parent = new CTask();
				$old_parent->load($oTsk->task_parent);
				$old_parent->updateDynamics();
			}
		}

		// update dependencies
		if (!empty($this->task_id)) {
			$this->updateDependencies($this->getDependencies());
		} else {
			// print_r($this);
		}

		if (empty($ret)) {
			return get_class($this) . '::store failed <br />' . db_error();
		} else {
			return NULL;
		}
	}

	/**
	 * @todo Parent store could be partially used
	 * @todo Can't delete a task with children
	 */
	function delete($oid = NULL, $history_desc = '', $history_proj = 0) {
		if (empty($this->task_id)) {
			return 'invalid task id';
		}
		$q = new DBQuery;

		if (dPgetConfig('check_task_empty_dynamic') && $this->task_parent != $this->task_id) {
			//Check that we are not deleting the only child of a dynamic parent task
			$task_test = new CTask();
			$task_test->load($this->task_parent);
			$siblings = $task_test->getChildren();
			if ($task_test->task_dynamic == 1 && count($siblings) <= 1) {
				return 'BadDyn_NoChild';
			}
		}

		//load task first because we need info on it to update the parent tasks later
		$task = new CTask();
		$task->load($this->task_id);
		//get child tasks so we can delete them too (no orphans)
		$childrenlist = $task->getDeepChildren();


		//delete task (if we're actually allowed to delete this task)
		$err_msg = parent::delete($task->task_id, $task->task_name, $task->task_project);
		if ($err_msg) {
			return $err_msg;
		}
		$this->_action = 'deleted';

		if ($task->task_parent != $task->task_id) {
			//Has parent, run the update sequence, this child will no longer be in the database
			$this->updateDynamics();
		}
		$q->clear();

		//delete children
		if (!empty($childrenlist)) {
			foreach ($childrenlist as $child_id) {
				$ctask = new CTask();
				$ctask->load($child_id);
				//ignore permissions on child tasks by deleteing task directly from the database
				$q->setDelete('tasks');
				$q->addWhere('task_id=' . $ctask->task_id);
				if (!($q->exec())) {
					return db_error();
				}
				$q->clear();
				addHistory('tasks', $ctask->task_id, 'delete',
				           $ctask->task_name, $ctask->task_project);

				$this->updateDynamics(); //to update after children are deleted (see above)
			}
			$this->_action = 'deleted with children';
		}

		//delete affiliated task_logs (overrides any task_log permissions)
		$q->setDelete('task_log');
		if (!empty($childrenlist)) {
			$q->addWhere('task_log_task IN (' . implode(', ', $childrenlist)
						 . ', ' . $this->task_id . ')');
		} else {
			$q->addWhere('task_log_task=' . $this->task_id);
		}

		if (!($q->exec())) {
			return db_error();
		}
		$q->clear();

		//delete affiliated task_dependencies
		$q->setDelete('task_dependencies');
		if (!empty($childrenlist)) {
			$q->addWhere('dependencies_task_id IN (' . implode(', ', $childrenlist)
						 . ', ' . $task->task_id . ')');
		} else {
			$q->addWhere('dependencies_task_id=' . $task->task_id);
		}

		if (!($q->exec())) {
			return db_error();
		}
		$q->clear();

		// delete linked user tasks
		$q->setDelete('user_tasks');
		if (!empty($childrenlist)) {
			$q->addWhere('task_id IN (' . implode(', ', $childrenlist)
						 . ', ' . $task->task_id . ')');
		} else {
			$q->addWhere('task_id=' . $task->task_id);
		}
		if (!($q->exec())) {
			return db_error();
		}
		$q->clear();

		// delete task departments
		$q->setDelete('task_departments');
		if (!empty($childrenlist)) {
			$q->addWhere('task_id IN (' . implode(', ', $childrenlist)
						 . ', ' . $task->task_id . ')');
		} else {
			$q->addWhere('task_id=' . $task->task_id);
		}
		if (!($q->exec())) {
			return db_error();
		}
		$q->clear();

		// delete task contacts
		$q->setDelete('task_contacts');
		if (!empty($childrenlist)) {
			$q->addWhere('task_id IN (' . implode(', ', $childrenlist)
						 . ', ' . $task->task_id . ')');
		} else {
			$q->addWhere('task_id=' . $task->task_id);
		}
		if (!($q->exec())) {
			return db_error();
		}
		$q->clear();


		return NULL;
	}

	function updateDependencies($cslist) {
		$q = new DBQuery;
		// delete all current entries
		$q->setDelete('task_dependencies');
		$q->addWhere('dependencies_task_id=' . $this->task_id);
		$q->exec();
		$q->clear();

		// process dependencies
		$tarr = explode(',', $cslist);
		$values = [];
		$fields = [ 'dependencies_task_id', 'dependencies_req_task_id' ];
		foreach ($tarr as $task_id) {
			if (intval($task_id) > 0) {
				$values[] = [ $this->task_id, $task_id ];
			}
		}
		if (count($values)) {
			$q->addTable('task_dependencies');
			$q->addInsertMulti($fields, $values);
			$q->exec();
			$q->clear();
		}
	}

	/**
	 *		  Retrieve the tasks dependencies
	 *
	 *		  @author		 handco		   <handco@users.sourceforge.net>
	 *		  @return		 string		   comma delimited list of tasks id's
	 **/
	function getDependencies() {
		// Call the static method for this object
		$result = $this->staticGetDependencies($this->task_id);
		return $result;
	} // end of getDependencies ()


	/**
	 *		  Retrieve the tasks dependencies
	 *
	 *		  @author		 handco		   <handco@users.sourceforge.net>
	 *		  @param		integer		   ID of the task we want dependencies
	 *		  @return		 string		   comma delimited list of tasks id's
	 **/
	function staticGetDependencies($taskId) {
		$q = new DBQuery;
		if (empty($taskId)) {
			return '';
		}
		$q->addTable('task_dependencies', 'td');
		$q->addQuery('dependencies_req_task_id');
		$q->addWhere('td.dependencies_task_id = ' . $taskId);
		$sql = $q->prepare();
		$q->clear();
		$list = db_loadColumn ($sql);
		$result = $list ? implode (',', $list) : '';

		return $result;
	} // end of staticGetDependencies ()


	function notifyOwner() {
		$q = new DBQuery;
		GLOBAL $AppUI, $locale_char_set;

		$q->addTable('projects');
		$q->addQuery('project_name');
		$q->addWhere('project_id=' . $this->task_project);
		$sql = $q->prepare();
		$q->clear();
		$projname = htmlspecialchars_decode(db_loadResult($sql));
		$mail = new Mail;

		$mail->Subject(($projname . '::' . $this->task_name . ' '
		                . $AppUI->_($this->_action, UI_OUTPUT_RAW)), $locale_char_set);

		// c = creator
		// a = assignee
		// o = owner
		$q->addTable('tasks', 't');
		$q->leftJoin('user_tasks', 'u', 'u.task_id = t.task_id');
		$q->leftJoin('users', 'o', 'o.user_id = t.task_owner');
		$q->leftJoin('contacts', 'oc', 'oc.contact_id = o.user_contact');
		$q->leftJoin('users', 'c', 'c.user_id = t.task_creator');
		$q->leftJoin('contacts', 'cc', 'cc.contact_id = c.user_contact');
		$q->leftJoin('users', 'a', 'a.user_id = u.user_id');
		$q->leftJoin('contacts', 'ac', 'ac.contact_id = a.user_contact');
		$q->addQuery('t.task_id, cc.contact_email as creator_email'
					 . ', cc.contact_first_name as creator_first_name'
					 . ', cc.contact_last_name as creator_last_name'
					 . ', oc.contact_email as owner_email'
					 . ', oc.contact_first_name as owner_first_name'
					 . ', oc.contact_last_name as owner_last_name'
					 . ', a.user_id as assignee_id, ac.contact_email as assignee_email'
					 . ', ac.contact_first_name as assignee_first_name'
					 . ', ac.contact_last_name as assignee_last_name');
		$q->addWhere(' t.task_id = ' . $this->task_id);
		$sql = $q->prepare();
		$q->clear();
		$users = db_loadList($sql);

		if (count($users)) {
			$body = ($AppUI->_('Project', UI_OUTPUT_RAW) . ': ' . $projname . "\n<br>"
					 . $AppUI->_('Task', UI_OUTPUT_RAW) . ':	' . $this->task_name . "\n<br>"
					 . $AppUI->_('URL', UI_OUTPUT_RAW) . ': ' . DP_BASE_URL
					 . '/index.php?m=tasks&a=view&task_id=' . $this->task_id . "\n\n<br><br>"
					 . $AppUI->_('Description', UI_OUTPUT_RAW) . ': ' . "\n<br>"
					 . $this->task_description . "\n\n<br><br>"
					 . $AppUI->_('Creator', UI_OUTPUT_RAW) . ': ' . $AppUI->user_first_name . ' '
					 . $AppUI->user_last_name . "\n\n<br><br>"
					 . $AppUI->_('Progress', UI_OUTPUT_RAW) . ': '
					 . $this->task_percent_complete . '%' . "\n\n<br><br>"
					 . dPgetCleanParam($_POST, 'task_log_description'));


			$mail->Body($body, isset($GLOBALS['locale_char_set'])
						? $GLOBALS['locale_char_set']
						: '');
			$mail->From ('"' . $AppUI->user_first_name . ' ' . $AppUI->user_last_name
						  . '" <' . $AppUI->user_email . '>');
		}

		if ($mail->ValidEmail($users[0]['owner_email'])) {
			$mail->To($users[0]['owner_email'], true);
			$mail->Send();
		}

		return '';
	}

	//additional comment will be included in email body
	function notify($comment = '') {
		$q = new DBQuery;
		GLOBAL $AppUI, $locale_char_set;
		$df = $AppUI->getPref('SHDATEFORMAT');
		$df .= ' ' . $AppUI->getPref('TIMEFORMAT');

		$q->addTable("projects");
		$q->addQuery("project_name");
		$q->addWhere("project_id=$this->task_project");
		$projname = htmlspecialchars_decode(db_loadResult($q->prepare(true)));

		$mail = new Mail;

		$mail->Subject(($projname . '::' . $this->task_name . ' '
		                . $AppUI->_($this->_action, UI_OUTPUT_RAW)), $locale_char_set);

		// c = creator
		// a = assignee
		// o = owner
		$q->addTable('tasks', 't');
		$q->leftJoin('user_tasks', 'ut', 'ut.task_id = t.task_id');
		$q->leftJoin('users', 'o', 'o.user_id = t.task_owner');
		$q->leftJoin('contacts', 'oc', 'oc.contact_id = o.user_contact');
		$q->leftJoin('users', 'c', 'c.user_id = t.task_creator');
		$q->leftJoin('contacts', 'cc', 'cc.contact_id = c.user_contact');
		$q->leftJoin('users', 'a', 'a.user_id = ut.user_id');
		$q->leftJoin('contacts', 'ac', 'ac.contact_id = a.user_contact');
		$q->addQuery('t.task_id, c.user_id as creator_id, cc.contact_email as creator_email'
					 . ', cc.contact_first_name as creator_first_name'
					 . ', cc.contact_last_name as creator_last_name'
					 . ', o.user_id as owner_id, oc.contact_email as owner_email'
					 . ', oc.contact_first_name as owner_first_name'
					 . ', oc.contact_last_name as owner_last_name'
					 . ', a.user_id as assignee_id, ac.contact_email as assignee_email'
					 . ', ac.contact_first_name as assignee_first_name'
					 . ', ac.contact_last_name as assignee_last_name');
		$q->addWhere(' t.task_id = ' . $this->task_id);
		$users = $q->loadList();

		if (count($users)) {
			$task_start_date = new CDate($this->task_start_date);
			$task_finish_date = new CDate($this->task_end_date);
			$priority = dPgetSysVal('TaskPriority');

			$body = ($AppUI->_('Project', UI_OUTPUT_RAW) . ': ' . $projname . "\n<br>"
					 . $AppUI->_('Task', UI_OUTPUT_RAW) . ':	 ' . $this->task_name);
			$body .= "\n<br>" . $AppUI->_('Priority'). ': ' . $priority[$this->task_priority];
			$body .= ("\n<br>" . $AppUI->_('Start Date', UI_OUTPUT_RAW) . ': '
					  . $task_start_date->format($df) . "\n<br>"
					  . $AppUI->_('Finish Date', UI_OUTPUT_RAW) . ': '
					  . ($this->task_end_date != '' ? $task_finish_date->format($df) : '') . "\n<br>"
					  . $AppUI->_('URL', UI_OUTPUT_RAW) . ': ' . DP_BASE_URL
					  . '/index.php?m=tasks&a=view&task_id=' . $this->task_id . "\n\n<br><br>"
					  . $AppUI->_('Description', UI_OUTPUT_RAW) . ': ' . "\n<br>"
					  . $this->task_description);
			if ($users[0]['creator_email']) {
				$body .= ("\n\n<br><br>" . $AppUI->_('Creator', UI_OUTPUT_RAW). ':' . "\n<br>"
						  . $users[0]['creator_first_name'] . ' ' . $users[0]['creator_last_name' ]
						  . ', ' . $users[0]['creator_email']);
			}
			$body .= ("\n\n<br><br>" . $AppUI->_('Owner', UI_OUTPUT_RAW).':' . "\n<br>"
					  . $users[0]['owner_first_name'] . ' ' . $users[0]['owner_last_name' ]
					  . ', ' . $users[0]['owner_email']);

			if ($comment != '') {
				$body .= "\n\n<br><br>".$comment;
			}
			$mail->Body($body, (isset($GLOBALS['locale_char_set'])
								? $GLOBALS['locale_char_set'] : ''));
			$mail->From ('"' . $AppUI->user_first_name . ' ' . $AppUI->user_last_name
						 . '" <' . $AppUI->user_email . '>');

			$owner_is_assigned = false;
			foreach ($users as $row) {
				if ($mail->ValidEmail($row['assignee_email'])) {
					$mail->To($row['assignee_email'], true);
					$mail->Send();
				}
				if ($row['assignee_id'] == $row['owner_id']) {
					$owner_is_assigned = true;
				}
			}

			if ($AppUI->getPref('MAILALL') && !($owner_is_assigned)) {
				$last_record = array_pop($users);
				$owner_email = $last_record['owner_email'];
				array_push($users, $last_record);
				if ($mail->ValidEmail($owner_email)) {
					$mail->To($owner_email, true);
					$mail->Send();
				}
			}
		}

		return '';
	}

	/**
	 * Email the task log to assignees, task contacts, project contacts, and others
	 * based upon the information supplied by the user.
	 */
	function email_log(&$log, $assignees, $task_contacts, $project_contacts, $others, $extras) {
		global $AppUI, $locale_char_set, $dPconfig;

		$mail_recipients = array();
		$q = new DBQuery;
		if (isset($assignees) && $assignees == 'on') {
			$q->addTable('user_tasks', 'ut');
			$q->leftJoin('users', 'ua', 'ua.user_id = ut.user_id');
			$q->leftJoin('contacts', 'c', 'c.contact_id = ua.user_contact');
			$q->addQuery('c.contact_email, c.contact_first_name, c.contact_last_name');
			$q->addWhere('ut.task_id = ' . $this->task_id);
			if (! $AppUI->getPref('MAILALL')) {
				$q->addWhere('ua.user_id <>' . $AppUI->user_id);
			}
			$req =& $q->exec(QUERY_STYLE_NUM);
			for ($req; ! $req->EOF; $req->MoveNext()) {
				list($email, $first, $last) = $req->fields;
				if (! isset($mail_recipients[$email])) {
					$mail_recipients[$email] = trim($first) . ' ' . trim($last);
				}
			}
			$q->clear();
		}
		if (isset($task_contacts) && $task_contacts == 'on') {
			$q->addTable('task_contacts', 'tc');
			$q->leftJoin('contacts', 'c', 'c.contact_id = tc.contact_id');
			$q->addQuery('c.contact_email, c.contact_first_name, c.contact_last_name');
			$q->addWhere('tc.task_id = ' . $this->task_id);
			$req =& $q->exec(QUERY_STYLE_NUM);
			for ($req; ! $req->EOF; $req->MoveNext()) {
				list($email, $first, $last) = $req->fields;
				if (! isset($mail_recipients[$email])) {
					$mail_recipients[$email] = $first . ' ' . $last;
				}
			}
			$q->clear();
		}
		if (isset($project_contacts) && $project_contacts == 'on') {
			$q->addTable('project_contacts', 'pc');
			$q->leftJoin('contacts', 'c', 'c.contact_id = pc.contact_id');
			$q->addQuery('c.contact_email, c.contact_first_name, c.contact_last_name');
			$q->addWhere('pc.project_id = ' . $this->task_project);
			$req =& $q->exec(QUERY_STYLE_NUM);
			for ($req; ! $req->EOF; $req->MoveNext()) {
				list($email, $first, $last) = $req->fields;
				if (! isset($mail_recipients[$email])) {
					$mail_recipients[$email] = $first . ' ' . $last;
				}
			}
			$q->clear();
		}
		if (isset($others)) {
			$others = trim($others, " \r\n\t,"); // get rid of empty elements.
			if (mb_strlen($others) > 0) {
				$q->addTable('contacts', 'c');
				$q->addQuery('c.contact_email, c.contact_first_name, c.contact_last_name');
				$q->addWhere('c.contact_id in (' . $others . ')');
				$req =& $q->exec(QUERY_STYLE_NUM);
				for ($req; ! $req->EOF; $req->MoveNext()) {
					list($email, $first, $last) = $req->fields;
					if (! isset($mail_recipients[$email])) {
						$mail_recipients[$email] = $first . ' ' . $last;
					}
				}
				$q->clear();
			}
		}
		if (isset($extras) && $extras) {
			// Search for semi-colons, commas or spaces and allow any to be separators
			$extra_list = preg_split('/[\s,;]+/', $extras);
			foreach ($extra_list as $email) {
				if ($email && ! isset($mail_recipients[$email])) {
					$mail_recipients[$email] = $email;
				}
			}
		}
		$q->clear(); // Reset to the default state.
		if (count($mail_recipients) == 0) {
			return false;
		}

		// Build the email and send it out.
		$char_set = isset($locale_char_set) ? $locale_char_set : '';
		$mail = new Mail;
		// Grab the subject from user preferences
		$prefix = $AppUI->getPref('TASKLOGSUBJ');
		$mail->Subject($prefix .  ' ' . $log->task_log_name, $char_set);

		$q->addTable('projects');
		$q->addQuery('project_name');
		$q->addWhere('project_id=' . $this->task_project);
		$sql = $q->prepare();
		$q->clear();
		$projname = htmlspecialchars_decode(db_loadResult($sql));

		$body = $AppUI->_('Project', UI_OUTPUT_RAW) . ': ' . $projname . "\n<br>";
		if ($this->task_parent != $this->task_id) {
			$q->addTable('tasks');
			$q->addQuery('task_name');
			$q->addWhere('task_id = ' . $this->task_parent);
			$req =& $q->exec(QUERY_STYLE_NUM);
			if ($req) {
				$body .= $AppUI->_('Parent Task', UI_OUTPUT_RAW) . ': '
				  . htmlspecialchars_decode($req->fields[0]) . "\n<br>";
			}
			$q->clear();
		}
		$body .= $AppUI->_('Task', UI_OUTPUT_RAW) . ': ' . $this->task_name . "\n<br>";
		$task_types = dPgetSysVal('TaskType');
		$body .= $AppUI->_('Task Type', UI_OUTPUT_RAW) . ':' . $task_types[$this->task_type] . "\n<br>";
		$body .= $AppUI->_('URL', UI_OUTPUT_RAW)
			. ': ' . DP_BASE_URL . '/index.php?m=tasks&a=view&task_id=' . $this->task_id . "\n\n<br><br>";
		$body .= $AppUI->_('Hours', UI_OUTPUT_RAW) . ': ' . $log->task_log_hours . $AppUI->_('h', UI_OUTPUT_RAW)."\n\n<br><br>";
		$body .= $AppUI->_('Summary', UI_OUTPUT_RAW) . ': ' . $log->task_log_name . "\n\n<br><br>";
		$body .= $log->task_log_description;

		// Append the user signature to the email - if it exists.
		$q->addTable('users');
		$q->addQuery('user_signature');
		$q->addWhere('user_id = ' . $AppUI->user_id);
		if ($res = $q->exec()) {
			if ($res->fields['user_signature']) {
				$body .= "\n--\n" . $res->fields['user_signature'];
			}
		}
		$q->clear();

		$mail->Body($body, $char_set);
		$mail->From($AppUI->user_first_name . ' ' . $AppUI->user_last_name . ' <'
					. $AppUI->user_email . '>');

		$recipient_list = '';
		foreach ($mail_recipients as $email => $name) {
			if ($mail->ValidEmail($email)) {
				$mail->To($email);
				$recipient_list .= $email . ' (' . $name . ")\n";
			} else {
				$recipient_list .= "Invalid email address '" . $email . "' for " . $name . ", not sent\n";
			}
		}
		$mail->Send();
		// Now update the log
		$save_email = @$AppUI->getPref('TASKLOGNOTE');
		if ($save_email) {
			$log->task_log_description .= "\nEmailed " . date('d/m/Y H:i:s')
				. " to:\n{" . $recipient_list . "}";
			return true;
		}

		return false; // No update needed.
	}

	/**
	 * @param Date Start date of the period
	 * @param Date End date of the period
	 * @param integer The target company
	 */
	static function getTasksForPeriod($start_date, $end_date, $company_id=0, $user_id=null,
	                                  $filter_proj_archived=false, $filter_proj_completed=false) {
		GLOBAL $AppUI;
		$q = new DBQuery;
		// convert to default db time stamp
		$db_start = $start_date->format(FMT_DATETIME_MYSQL);
		$db_end = $end_date->format(FMT_DATETIME_MYSQL);

		// Allow for possible passing of user_id 0 to stop user filtering
		if (!isset($user_id)) {
			$user_id = $AppUI->user_id;
		}

		// filter tasks for not allowed projects
		$tasks_filter = '';
		// check permissions on projects
		$proj = new CProject();
		$task_filter_where = $proj->getAllowedSQL($AppUI->user_id, 't.task_project');
		// exclude read denied projects
		$deny = $proj->getDeniedRecords($AppUI->user_id);
		// check permissions on tasks
		$obj = new CTask();
		$allow = $obj->getAllowedSQL($AppUI->user_id, 't.task_id');
		$parent_task_allow = $obj->getAllowedSQL($AppUI->user_id, 't.task_parent');

		$q->addTable('tasks', 't');
		if ($user_id) {
			$q->innerJoin('user_tasks', 'ut', 't.task_id=ut.task_id');
		}
		$q->innerJoin('projects', 'p', 't.task_project = p.project_id');
		$q->addQuery('DISTINCT t.task_id, t.task_name, t.task_start_date, t.task_end_date'
		             . ', t.task_duration, t.task_duration_type'
		             . ', p.project_color_identifier AS color, p.project_name');
		$q->addWhere('task_status > -1'
		             . " AND ((task_start_date <= '" . $db_end . "'"
		             . " AND (task_end_date >= '" . $db_start . "'"
		             . " OR  task_end_date = '0000-00-00 00:00:00' OR task_end_date = NULL)"
		             . " OR task_start_date BETWEEN '" . $db_start . "' AND '" . $db_end .  "'))");
		if ($user_id) {
			$q->addWhere("ut.user_id = '" . $user_id . "'");
		}

		if ($company_id) {
			$q->addWhere('p.project_company = ' . $company_id);
		}
		if (count($task_filter_where) > 0) {
			$q->addWhere('(' . implode(' AND ', $task_filter_where) . ')');
		}
		if (count($deny) > 0) {
			$q->addWhere('(t.task_project NOT IN (' . implode(', ', $deny) . '))');
		}
		if (count($allow) > 0) {
			$q->addWhere('(' . implode(' AND ', $allow) . ')');
		}
		if (count($parent_task_allow) > 0) {
			$q->addWhere('(' . implode(' AND ', $parent_task_allow) . ')');
		}
		if ($filter_proj_archived) {
			$q->addWhere('p.project_status <> 7');
		}
		if ($filter_proj_archived) {
			$q->addWhere('p.project_status <> 5');
		}
		$q->addOrder('t.task_start_date');

		// assemble query
		$sql = $q->prepare();
		$q->clear();
		//echo "<pre>$sql</pre>";
		// execute and return
		return db_loadList($sql);
	}

	function canAccess($user_id) {
		$q = new DBQuery;

		//check whether we are explicitly denied at task level
		$denied_tasks = $this->getDeniedRecords($user_id);
		if (in_array($this->task_id, $denied_tasks)) {
			return false;
		}

		switch ($this->task_access) {
		case 0:
			//public
			$retval = true;
			$proj_obj = new CProject();
			$denied_projects = $proj_obj->getDeniedRecords($user_id);
			if (in_array($this->task_project, $denied_projects)) {
				$retval = false;
			}

			break;
		case 1:
			//protected
			$q->addTable('users', 'u');
			$q->innerJoin('contacts', 'c', 'c.contact_id=u.user_contact');
			$q->addQuery('c.contact_company');
			$q->addWhere('u.user_id=' . $user_id . ' OR u.user_id=' . $this->task_owner);
			$sql = $q->prepare();
			$q->clear();
			$user_owner_companies = db_loadColumn($sql);
			$company_match = true;
			foreach ($user_owner_companies as $current_company) {
				$company_match = $company_match && ((!(isset($last_company)))
													|| $last_company == $current_company);
				$last_company = $current_company;
			}

		case 2:
			//participant
			$company_match = ((isset($company_match)) ? $company_match : true);
			$q->addTable('user_tasks', 'ut');
			$q->addQuery('COUNT(*)');
			$q->addWhere('ut.user_id=' . $user_id . ' AND ut.task_id=' . $this->task_id);
			$sql = $q->prepare();
			$q->clear();
			$count = db_loadResult($sql);
			$retval = (($company_match && $count > 0) || $this->task_owner == $user_id);
			break;
		case 3:
			//private
			$retval = ($this->task_owner == $user_id);
			break;
		case 4:
			//privileged
			$retval = true;
			if ($this->task_project != '') {
				$q->clear();

				$q->addTable('users', 'u');
				$q->innerJoin('contacts', 'c', 'c.contact_id=u.user_contact');
				$q->addQuery('c.contact_company');
				$q->addWhere('u.user_id = ' . $user_id);
				$user_company = $q->loadResult();
				$q->clear();

				$q->addTable('projects', 'p');
				$q->addQuery('p.project_company');
				$q->addWhere('p.project_id = ' . $this->task_project);
				$project_company = $q->loadResult();
				$q->clear();

				$q->addTable('user_tasks', 'ut');
				$q->addQuery('COUNT(ut.user_id) AS user_task_count');
				$q->addWhere('ut.user_id = ' . $user_id . ' AND ut.task_id = ' . $this->task_id);
				$count = $q->loadResult();
				$q->clear();

				$retval = (($user_company == $project_company) || $this->task_owner == $user_id
				           || $count);
			}
			break;
		default:
			$retval = false;
			break;
		}

		return $retval;
	}

	/**
	 *		 retrieve tasks are dependent of another.
	 *		 @param	 integer		 ID of the master task
	 *		 @param	 boolean		 true if is a dep call (recurse call)
	 *		 @param	 boolean		 false for no recursion (needed for calc_end_date)
	 **/
	function dependentTasks ($taskId = false, $isDep = false, $recurse = true) {
		$q = new DBQuery;
		global $aDeps;
		// Initialize the dependencies array
		if ($isDep == false) {
			$aDeps = array();
		}

		if (!$taskId) {
			$taskId = $this->task_id;
		}
		if (empty($taskId)) {
			return '';
		}

		// retrieve dependent tasks
		$q->addTable('task_dependencies', 'td');
		$q->innerJoin('tasks', 't', 'td.dependencies_task_id = t.task_id'); // only "real" task ids
		$q->addQuery('dependencies_task_id');
		$q->addWhere('td.dependencies_req_task_id = ' . $taskId);
		$sql = $q->prepare();
		$q->clear();
		$aBuf = db_loadColumn($sql);
		$aBuf = !empty($aBuf) ? $aBuf : array();
		//$aBuf = array_values(db_loadColumn ($sql));

		if ($recurse) {
			// recurse to find sub dependents
			foreach ($aBuf as $depId) {
				if (!in_array($depId, $aDeps)) { //make sure we haven't done a call with this id yet
					$aDeps[] = $depId;
					$this->dependentTasks($depId, true);
				}
			}

		} else {
			$aDeps = $aBuf;
		}

		// return if we are in a dependency call
		if ($isDep) {
			return;
		}

		return implode (',', $aDeps);

	} // end of dependentTasks()

   /*
	*		 shift dependents tasks dates
	*		 @return void
	*/
	function shiftDependentTasks () {
		// Get tasks that depend on this task
		$csDeps = explode(',', $this->dependentTasks('', '', false));

		if ($csDeps[0] == '') {
			return;
		}

		// Stage 1: Update dependent task dates
		foreach ($csDeps as $task_id) {
			$this->update_dep_dates($task_id);
		}

		// Stage 2: Now shift the dependent tasks' dependents
		foreach ($csDeps as $task_id) {
			$newTask = new CTask();
			$newTask->load($task_id);
			$newTask->shiftDependentTasks();
		}

		return;
	} // end of shiftDependentTasks()

   /*
	*		  Update this task's dates in the DB.
	*		  start date:		  based on latest end date of dependencies
	*		  end date:			  based on start date + appropriate task time span
	*
	*		  @param				integer task_id of task to update
	*/
	function update_dep_dates($task_id) {
		GLOBAL $tracking_dynamics;
		$q = new DBQuery;

		$newTask = new CTask();
		$newTask->load($task_id);

		// Do not update tasks that are not tracking dependencies
		if (!in_array($newTask->task_dynamic, $tracking_dynamics)) {
			return;
		}

		// load original task dates and calculate task time span
		$tsd = new CDate($newTask->task_start_date);
		$ted = new CDate($newTask->task_end_date);
		$duration = $tsd->calcDuration($ted);

		// reset start date
		$nsd = new CDate ($newTask->get_deps_max_end_date($newTask));

		// prefer Wed 8:00 over Tue 16:00 as start date
		$nsd = $nsd->next_working_day();
		$new_start_date = $nsd->format(FMT_DATETIME_MYSQL);

		// Add task time span to End Date again
		$ned = new CDate();
		$ned->copy($nsd);
		$ned->addDuration($duration, '1');

		// make sure one didn't land on a non-working day
		$ned = $ned->next_working_day(true);

		// prefer tue 16:00 over wed 8:00 as an end date
		$ned = $ned->prev_working_day();

		$new_end_date = $ned->format(FMT_DATETIME_MYSQL);

		// update the db
		$q->addTable('tasks');
		$q->addUpdate('task_start_date', $new_start_date);
		$q->addUpdate('task_end_date', $new_end_date);
		$q->addWhere('task_dynamic <> 1 AND task_id = ' . $task_id);
		$q->exec();
		$q->clear();

		if ($newTask->task_parent != $newTask->task_id) {
			$newTask->updateDynamics();
		}

		return;
	}


	/*
	 ** Time related calculations have been moved to /classes/date.class.php
	 ** some have been replaced with more _robust_ functions
	 **
			** Affected functions:
	 ** prev_working_day()
	 ** next_working_day()
	 ** calc_task_end_date()	renamed to addDuration()
	 ** calc_end_date()	renamed to calcDuration()
	 **
	 ** @date	20050525
	 ** @responsible gregorerhardt
	 ** @purpose	reusability, consistence
	 */


	/*

	Get the last end date of all of this task's dependencies

	@param Task object
	returns FMT_DATETIME_MYSQL date

	*/

	function get_deps_max_end_date($taskObj) {
		global $tracked_dynamics;
		$q = new DBQuery;

		$deps = $taskObj->getDependencies();
		$obj = new CTask();

		$last_end_date = false;
		// Don't respect end dates of excluded tasks
		if ($tracked_dynamics && !empty($deps)) {
			$track_these = implode(',', $tracked_dynamics);
			$q->addTable('tasks');
			$q->addQuery('MAX(task_end_date)');
			$q->addWhere('task_id IN (' . $deps . ') AND task_dynamic IN (' . $track_these . ')');
			$sql = $q->prepare();
			$q->clear();
			$last_end_date = db_loadResult($sql);
		}

		if (!$last_end_date) {
			// Set to project start date
			$id = $taskObj->task_project;
			$q->addTable('projects');
			$q->addQuery('project_start_date');
			$q->addWhere('project_id = ' . $id);
			$sql = $q->prepare();
			$q->clear();
			$last_end_date = db_loadResult($sql);
		}

		return $last_end_date;
	}


	/**
	 * Function that returns the amount of hours this
	 * task consumes per user each day
	 */
	function getTaskDurationPerDay($use_percent_assigned = false) {
		$duration = $this->task_duration * ($this->task_duration_type == 24
											? dPgetConfig('daily_working_hours')
											: $this->task_duration_type);
		$task_start_date = new CDate($this->task_start_date);
		$task_finish_date = new CDate($this->task_end_date);
		$assigned_users = $this->getAssignedUsers();
		if ($use_percent_assigned) {
			$number_assigned_users = 0;
			foreach ($assigned_users as $u) {
				$number_assigned_users += ($u['perc_assignment'] / 100);
			}
		} else {
			$number_assigned_users = count($assigned_users);
		}

		$day_diff = $task_finish_date->dateDiff($task_start_date);
		$number_of_days_worked = 0;
		$actual_date = $task_start_date;

		for ($i=0; $i<=$day_diff; $i++) {
			if ($actual_date->isWorkingDay()) {
				$number_of_days_worked++;
			}
			$actual_date->addDays(1);
		}
		// May be it was a Sunday task
		if ($number_of_days_worked == 0) {
			$number_of_days_worked = 1;
		}
		if ($number_assigned_users == 0) {
			$number_assigned_users = 1;
		}
		return ($duration/$number_assigned_users) / $number_of_days_worked;
	}


	/**
	 * Function that returns the amount of hours this
	 * task consumes per user each week
	 */
	function getTaskDurationPerWeek($use_percent_assigned = false) {
		$duration = ($this->task_duration_type == 24 ? dPgetConfig('daily_working_hours')
		             : $this->task_duration_type) * $this->task_duration;
		$task_start_date = new CDate($this->task_start_date);
		$task_finish_date = new CDate($this->task_end_date);
		$assigned_users = $this->getAssignedUsers();
		if ($use_percent_assigned) {
			$number_assigned_users = 0;
			foreach ($assigned_users as $u) {
				$number_assigned_users += ($u['perc_assignment'] / 100);
			}
		} else {
			$number_assigned_users = count($assigned_users);
		}

		$number_of_weeks_worked = $task_finish_date->workingDaysInSpan($task_start_date)
			/ count(explode(',', dPgetConfig('cal_working_days')));
		$number_of_weeks_worked = (($number_of_weeks_worked < 1)
								   ? ceil($number_of_weeks_worked) : $number_of_weeks_worked);

		// zero adjustment
		if ($number_of_weeks_worked == 0) {
			$number_of_weeks_worked = 1;
		}
		if ($number_assigned_users == 0) {
			$number_assigned_users = 1;
		}
		return ($duration/$number_assigned_users) / $number_of_weeks_worked;
	}


	// unassign a user from task
	function removeAssigned($user_id) {
		$q = new DBQuery;
		// delete all current entries
		$q->setDelete('user_tasks');
		$q->addWhere('task_id = ' . $this->task_id . ' AND user_id = ' . $user_id);
		$q->exec();
		$q->clear();
	}

	//using user allocation percentage ($perc_assign)
	// @return returns the Names of the over-assigned users (if any), otherwise false
	function updateAssigned($cslist, $perc_assign, $del=true, $rmUsers=false) {
		$q = new DBQuery;

		// process assignees
		$tarr = explode(',', $cslist);

		// delete all current entries from $cslist
		if ($del == true && $rmUsers == true) {
			foreach ($tarr as $user_id) {
				$user_id = (int)$user_id;
				if (!empty($user_id)) {
					$this->removeAssigned($user_id);
				}
			}

			return false;

		} else if ($del == true) { // delete all users assigned to this task (to properly update)
			$q->setDelete('user_tasks');
			$q->addWhere('task_id = ' . $this->task_id);
			$q->exec();
			$q->clear();
		}

		// get Allocation info in order to check if overAssignment occurs
		// $alloc = $this->getAllocation('user_id');
		$overAssignment = false;

		foreach ($tarr as $user_id) {
			if (intval($user_id) > 0) {
				$perc = $perc_assign[$user_id];
				/*
				if (dPgetConfig('check_overallocation')
					&& $perc > $alloc[$user_id]['freeCapacity']) {
					// add Username of the overAssigned User
					$overAssignment .= ' ' . $alloc[$user_id]['userFC'];
				}
				else {
				*/
					$q->addTable('user_tasks');
					$q->addReplace('user_id', $user_id);
					$q->addReplace('task_id', $this->task_id);
					$q->addReplace('perc_assignment', $perc);
					$q->exec();
					$q->clear();
				// }
			}
		}
		return $overAssignment;
	}

	function getAssignedUsers() {
		$q = new DBQuery;
		$q->addTable('users', 'u');
		$q->innerJoin('user_tasks', 'ut', 'ut.user_id = u.user_id');
		$q->leftJoin('contacts', 'co', ' co.contact_id = u.user_contact');
		$q->addQuery('u.*, ut.perc_assignment, ut.user_task_priority'
		             . ', co.contact_first_name, co.contact_last_name');
		$q->addWhere('ut.task_id = ' . $this->task_id);
		$sql = $q->prepare();
		$q->clear();
		return db_loadHashList($sql, 'user_id');
	}

	/**
	 *	Calculate the extent of utilization of user assignments
	 *	@param string hash	 a hash for the returned hashList
	 *	@param array users	 an array of user_ids calculating their assignment capacity
	 *	@return array		 returns hashList of extent of utilization for assignment of the users
	 */
	function getAllocation($hash = NULL, $users = NULL) {
		global $AppUI;
		static $Allocations = array();

		// if (! dPgetConfig('check_overallocation') && ! dPgetConfig('direct_edit_assignment')) {
		if (!dPgetConfig('direct_edit_assignment')) {
			return array();
		}
		$hash_key = $hash ? $hash : '<NONE>';
		$user_key = $users ? implode(',', $users) : '<NONE>';

		if (! $Allocations[$hash_key] || ! $Allocations[$hash_key][$user_key]) {
			if (! $Allocations[$hash_key]) {
				$Allocations[$hash_key] = array();
			}
			if (! $Allocations[$hash_key][$user_key]) {
				// retrieve the systemwide default preference for the assignment maximum
				$scm = $AppUI->getSystemPref('TASKASSIGNMAX');
				if (! $scm) {
					$scm = 0;
				}
				/*
				 * provide actual assignment charge, individual chargeMax
				 * and freeCapacity of users' assignments to tasks
				*/
				$q = new DBQuery;
				$subqC = new DBQuery;
				$subqUT = new DBQuery;
				$q->addTable('users', 'u');
				$q->addQuery("u.user_id, CONCAT(CONCAT_WS(' [', CONCAT_WS(' '"
							. ', contact_first_name, contact_last_name), IF(IFNULL((IFNULL(up.pref_value'
							. ', ' . $scm . ') - SUM(ut.perc_assignment)), up.pref_value) > 0'
							. ', IFNULL((IFNULL(up.pref_value, ' . $scm . ') - SUM(ut.perc_assignment))'
							. ', up.pref_value), 0)), ' . "'%]')" . ' AS userFC'
							. ', IFNULL(SUM(ut.perc_assignment), 0) AS charge, u.user_username'
							. ', IFNULL(up.pref_value,' . $scm . ') AS chargeMax'
							. ', IF(IFNULL((IFNULL(up.pref_value, ' . $scm . ') '
							. '- SUM(ut.perc_assignment)), up.pref_value) > 0'
							. ', IFNULL((IFNULL(up.pref_value, ' . $scm . ') - SUM(ut.perc_assignment))'
							. ', up.pref_value), 0) AS freeCapacity');
				// Sub-query for contacts table
				$subqC->addTable('contacts');
				$subqC->addGroup('contact_id');
				// Sub-query for user_tasks table
				$subqUT->addTable('user_tasks');
				$subqUT->addQuery("user_id, user_type, task_id, SUM(perc_assignment) AS 'perc_assignment', user_task_priority");
				$subqUT->addGroup('user_id');

				$q->leftJoin($subqC, 'c', 'c.contact_id = u.user_contact');
				$q->leftJoin($subqUT, 'ut', 'ut.user_id = u.user_id');
				$q->leftJoin('user_preferences', 'up', 'up.pref_user = u.user_id');
				$q->addGroup('u.user_id');
				$q->addOrder('contact_last_name, contact_first_name, u.user_id');
				$sql = $q->prepare();
				$q->clear();
				$subqC->clear();
				$subqUT->clear();
				//echo "<pre>$sql</pre>";
				$Allocations[$hash_key][$user_key] = db_loadHashList($sql, $hash);
			}
		}
		return $Allocations[$hash_key][$user_key];
	}

	function getUserSpecificTaskPriority($user_id = 0, $task_id = NULL) {
		$q = new DBQuery;
		// use task_id of given object if the optional parameter task_id is empty
		$task_id = empty($task_id) ? $this->task_id : $task_id;

		$q->addTable('user_tasks');
		$q->addQuery('user_task_priority');
		$q->addWhere('user_id = ' . $user_id . ' AND task_id = ' . $task_id);
		$sql = $q->prepare();
		$q->clear();
		$prio = db_loadHash($sql, $priority);
		return $prio ? $priority['user_task_priority'] : NULL;
	}

	function updateUserSpecificTaskPriority($user_task_priority = 0, $user_id = 0
											, $task_id = NULL) {
		$q = new DBQuery;
		// use task_id of given object if the optional parameter task_id is empty
		$task_id = empty($task_id) ? $this->task_id : $task_id;

		$q->addTable('user_tasks');
		$q->addReplace('user_id', $user_id);
		$q->addReplace('task_id', $task_id);
		$q->addReplace('user_task_priority', $user_task_priority);
		$q->exec();
		$q->clear();
	}

	function getProject() {
		$q = new DBQuery;

		$q->addTable('projects');
		$q->addQuery('project_name, project_short_name, project_color_identifier');
		$q->addWhere("project_id = '" . $this->task_project ."'");
		$sql = $q->prepare();
		$q->clear();
		$proj = db_loadHash($sql, $projects);
		return $projects;
	}

	//Returns task children IDs
	function getChildren() {
		$q = new DBQuery;

		$q->addTable('tasks');
		$q->addQuery('task_id');
		$q->addWhere("task_id <> '" . $this->task_id . "' AND task_parent = '" . $this->task_id ."'");
		$sql = $q->prepare();
		$q->clear();

		return db_loadColumn($sql);
	}

	// Returns task deep children IDs
	function getDeepChildren() {
		$q = new DBQuery;

		$q->addTable('tasks');
		$q->addQuery('task_id');
		$q->addWhere("task_id <> '" . $this->task_id . "' AND task_parent = '" . $this->task_id ."'");
		$sql = $q->prepare();
		$q->clear();
		$children = db_loadColumn($sql);

		if ($children) {
			$deep_children = array();
			$tempTask = new CTask();
			foreach ($children as $child) {
				$tempTask->peek($child);
				$deep_children = array_merge($deep_children, $tempTask->getDeepChildren());
			}

			return array_merge($children, $deep_children);
		}
		return array();
	}

	/**
	 * This function, recursively, updates all tasks status
	 * to the one passed as parameter
	 */
	function updateSubTasksStatus($new_status, $task_id = null) {
		$q = new DBQuery;

		if (is_null($task_id)) {
			$task_id = $this->task_id;
		}

		// get children
		$q->addTable('tasks');
		$q->addQuery('task_id');
		$q->addWhere("task_parent = '" . $task_id . "'");
		$sql = $q->prepare();
		$q->clear();
		$tasks_id = db_loadColumn($sql);
		if (count($tasks_id) == 0) {
			return true;
		}

		// update status of children
		$q->addTable('tasks');
		$q->addUpdate('task_status', $new_status);
		$q->addWhere("task_parent = '" . $task_id . "'");
		$q->exec();
		$q->clear();

		// update status of children's children
		foreach ($tasks_id as $id) {
			if ($id != $task_id) {
				$this->updateSubTasksStatus($new_status, $id);
			}
		}
	}

	/**
	 * This function recursively updates all tasks project
	 * to the one passed as parameter
	 */
	function updateSubTasksProject($new_project , $task_id = null) {
		$q = new DBQuery;

		if (is_null($task_id)) {
			$task_id = $this->task_id;
		}

		$q->addTable('tasks');
		$q->addQuery('task_id');
		$q->addWhere("task_parent = '" . $task_id . "'");
		$sql = $q->prepare();
		$q->clear();
		$tasks_id = db_loadColumn($sql);

		if (count($tasks_id) == 0) {
			return true;
		}

		// update project of children
		$q->addTable('tasks');
		$q->addUpdate('task_project', $new_project);
		$q->addWhere("task_parent = '" . $task_id . "'");
		$q->exec();
		$q->clear();

		// Update any files associated with this task.
		$q->addTable('files', 'f');
		$q->innerJoin('tasks', 't', 't.task_id = f.file_task');
		$q->addUpdate('file_project', $new_project);
		$q->addWhere("t.task_parent = '" . $task_id . "'");
		$q->exec();


		foreach ($tasks_id as $id) {
			if ($id != $task_id) {
				$this->updateSubTasksProject($new_project, $id);
			}
		}
	}

	function canUserEditTimeInformation() {
		global $AppUI;

		$project = new CProject();
		$project->load($this->task_project);

		// Code to see if the current user is
		// enabled to change time information related to task
		$can_edit_time_information = false;
		// Let's see if all users are able to edit task time information
		if (dPgetConfig('restrict_task_time_editing') == true && $this->task_id > 0) {

			// Am I the task owner?
			if ($this->task_owner == $AppUI->user_id) {
				$can_edit_time_information = true;
			}

			// Am I the project owner?
			if ($project->project_owner == $AppUI->user_id) {
				$can_edit_time_information = true;
			}

			// Am I sys admin?
			if (getPermission('admin', 'edit')) {
				$can_edit_time_information = true;
			}

		} else if (dPgetConfig('restrict_task_time_editing') == false || $this->task_id == 0) {
			// If all users are able, then don't check anything
			$can_edit_time_information = true;
		}

		return $can_edit_time_information;
	}

	/**
	 * Injects a reminder event into the event queue.
	 * Repeat interval is one day, repeat count
	 * and days to trigger before event overdue is
	 * set in the system config.
	 */
	function addReminder() {
		$day = 86400;

		if (!dPgetConfig('task_reminder_control')) {
			return;
		}

		if (! $this->task_end_date) { // No end date, can't do anything.
			return $this->clearReminder(true); // Also no point if it is changed to null
		}

		if ( $this->task_status < 0) { // Inactive task, don't remind
			return $this->clearReminder(true);
		}

		if ($this->task_percent_complete >= 100) {
			return $this->clearReminder(true);
		}

		$eq = EventQueue::getInstance();
		$pre_charge = dPgetConfig('task_reminder_days_before', 1);
		$repeat = dPgetConfig('task_reminder_repeat', 100);
		$batched = dPgetConfig('task_reminder_batch', false);

		/*
		 * If we don't need any arguments (and we don't) then we set this to null.
		 * We can't just put null in the call to add as it is passed by reference.
		 */
		$args = null;

		// Find if we have a reminder on this task already
		$old_reminders = $eq->find('tasks', 'remind', $this->task_id);
		if (count($old_reminders)) {
			/*
			 * It shouldn't be possible to have more than one reminder,
			 * but if we do, we may as well clean them up now.
			 */
			foreach ($old_reminders as $old_id => $old_data) {
				$eq->remove($old_id);
			}
		}

		// Find the end date of this task, then subtract the required number of days.
		$date = new CDate($this->task_end_date);
		$today = new CDate(date('Y-m-d'));
		if (CDate::compare($date, $today) < 0) {
			$start_day = time();
		} else {
			$start_day = $date->getDate(DATE_FORMAT_UNIXTIME);
			$start_day -= ($day * $pre_charge);
		}

		// If batched we add a batched request.
		if ($batched) {
			$eq->add($this, $args, 'remind', array(
				'id' => $this->task_id,
				'type' => 'remind',
				'date' => $start_day,
				'repeat_interval' => $day,
				'repeat_count' => $repeat,
				'batch' => $batched));
		}
		// We also add a non-batched version so that we can handle those
		// who don't want digests - we handle that case within the send
		// functions themselves.  This might need to be rethought at some
		// stage.
		$eq->add($this, $args, 'remind', array(
			'id' => $this->task_id,
			'type' => 'remind',
			'date' => $start_day,
			'repeat_interval' => $day,
			'repeat_count' => $repeat));
	}

	/**
	 * Called by the Event Queue processor to process a reminder
	 * on a task.
	 * @access		  public
	 * @param		mixed		$data options controlling execution
	 * @return		  mixed		   true, dequeue event, false, event stays in queue.
	 -1, event is destroyed.
	*/
	function EventQueue_remind_immediate($data) {
		global $locale_char_set, $AppUI;
		$q = new DBQuery;

		if (! isset($this->_default_batch_status)) {
			$q->addTable('user_preferences');
			$q->addQuery('pref_value');
			$q->addWhere('pref_user = 0 and pref_name = \'USEDIGESTS\'');
			$this->_default_batch_status = $q->loadResult();
			$q->clear();
		}
		$id = $data['queue_origin_id'];
		$owner = $data['queue_owner'];
		$df = $AppUI->getPref('SHDATEFORMAT');
		$tf = $AppUI->getPref('TIMEFORMAT');
		// If we don't have preferences set for these, use ISO defaults.
		if (! $df) {
			$df = '%Y-%m-%d';
		}
		if (! $tf) {
			$tf = '%H:%m';
		}
		$df .= ' ' . $tf;

		// At this stage we won't have an object yet
		if (! $this->load($id)) {
			return -1; // No point it trying again later.
		}
		$this->htmlDecode();

		// Only remind on working days.
		$today = new CDate();
		if (! $today->isWorkingDay()) {
			return true;
		}

		// Check if the task is completed
		if ($this->task_percent_complete == 100) {
			return -1;
		}

		if ($this->task_status < 0 ) {
			return -1;
		}

		$q->addTable('projects');
		$q->addQuery('project_name, project_status');
		$q->addWhere('project_id = ' . $this->task_project);
		$project_detail = $q->loadHash();
		if ($project_detail['project_status'] == 4 || $project_detail['project_status'] == 7) {
			return -1;
		}
		$project_name = htmlspecialchars_decode($project_detail['project_name']);

		// Grab the assignee list
		$q->addTable('user_tasks', 'ut');
		$q->leftJoin('users', 'u', 'u.user_id = ut.user_id');
		$q->leftJoin('contacts', 'c', 'c.contact_id = u.user_contact');
		$q->leftJoin('user_preferences', 'pr', 'pr.pref_user = u.user_id and pr.pref_name = \'USEDIGESTS\'');
		$q->addQuery('c.contact_id, contact_first_name, contact_last_name, contact_email');
		$q->addQuery('pr.pref_value as batched, pr.pref_user');
		$q->addWhere('ut.task_id = ' . $id);
		$contacts = $q->loadHashList('contact_id');
		$q->clear();

		// Now we also check the owner of the task, as we will need
		// to notify them as well.
		$owner_is_not_assignee = false;
		$q->addTable('users', 'u');
		$q->leftJoin('contacts', 'c', 'c.contact_id = u.user_contact');
		$q->leftJoin('user_preferences', 'pr', 'pr.pref_user = u.user_id and pr.pref_name = \'USEDIGESTS\'');
		$q->addQuery('c.contact_id, contact_first_name, contact_last_name, contact_email');
		$q->addQuery('pr.pref_value as batched, pr.pref_user');
		$q->addWhere('u.user_id = ' . $this->task_owner);
		if ($q->exec(ADODB_FETCH_NUM)) {
			list($owner_contact, $owner_first_name, $owner_last_name, $owner_email, $owner_batched, $pref_user) = $q->fetchRow();
			if (! isset($contacts[$owner_contact])) {
				$owner_is_not_assignee = true;
				$contacts[$owner_contact] = array(
												  'contact_id' => $owner_contact,
												  'contact_first_name' => $owner_first_name,
												  'contact_last_name' => $owner_last_name,
												  'contact_email' => $owner_email,
												  'batched' => $owner_batched,
												  'pref_user' => $pref_user,
												);
			}
		}
		$q->clear();

		// build the subject line, based on how soon the
		// task will be overdue.
		$starts = new CDate($this->task_start_date);
		$expires = new CDate($this->task_end_date);
		$now = new CDate();
		$diff = $expires->dateDiff($now);

		$prefix = $AppUI->_('Task Due', UI_OUTPUT_RAW);
		if ($diff == 0) {
			$msg = $AppUI->_('TODAY', UI_OUTPUT_RAW);
		} else if ($diff == 1) {
			$msg = $AppUI->_('TOMORROW', UI_OUTPUT_RAW);
		} else if ($diff < 0) {
			$msg = $AppUI->_(array('OVERDUE', abs($diff), 'DAYS'));
			$prefix = $AppUI->_('Task', UI_OUTPUT_RAW);
		} else {
			$msg = $AppUI->_(array($diff, 'DAYS'));
		}

		$subject = $prefix . ' ' .$msg . ' ' . $this->task_name . '::' . $project_name;

		$body = ($AppUI->_('Task Due', UI_OUTPUT_RAW) . ': ' . $msg . "\n"
				 . $AppUI->_('Project', UI_OUTPUT_RAW) . ': ' . $project_name . "\n"
				 . $AppUI->_('Task', UI_OUTPUT_RAW) . ': ' . $this->task_name . "\n"
				 . $AppUI->_('Start Date', UI_OUTPUT_RAW) . ': ' . $starts->format($df) . "\n"
				 . $AppUI->_('Finish Date', UI_OUTPUT_RAW) . ': ' . $expires->format($df) . "\n"
				 . $AppUI->_('URL', UI_OUTPUT_RAW) . ': ' . DP_BASE_URL
				 . '/index.php?m=tasks&a=view&task_id=' . $this->task_id . '&reminded=1' . "\n\n"
				 . $AppUI->_('Resources', UI_OUTPUT_RAW) . ":\n");
		foreach ($contacts as $contact) {
			if ($owner_is_not_assignee || $contact['contact_id'] != $owner_contact) {
				$body .= ($contact['contact_first_name'] . ' ' . $contact['contact_last_name']
						  . ' <' . $contact['contact_email'] . ">\n");
			}
		}
		$body .= ("\n" . $AppUI->_('Description', UI_OUTPUT_RAW) . ":\n"
				  . $this->task_description . "\n");

		$mail = new Mail;
		// Send out those that require an individual email for each event
		$send_count = 0;
		$batched = dPgetConfig('task_reminder_batch', false);
		foreach ($contacts as $contact) {
			if ($mail->ValidEmail($contact['contact_email'])) {
				$contact_batched = $contact['pref_user'] ? $contact_batched : $this->_default_batch_status;
				if (!$batched || !$contact_batched) {
					$mail->To($contact['contact_email']);
					$send_count++;
				}
			}
		}
		if ($send_count) {
			$mail->From ('"' . $owner_first_name . ' ' . $owner_last_name . '" <' . $owner_email . '>');
			$mail->Subject($subject, $locale_char_set);
			$mail->Body($body, $locale_char_set);
			return $mail->Send();
		} else {
			return true; // Have to assume we sent it out OK.
		}
	}

	/**
	 * Called by the Event Queue processor to process a reminder marked as batched
	 * This basically means we construct a digest for each user that requests it.
	 *
	 * @access		  public
	 * @param		mixed		$data options controlling execution
	 * @return		  mixed		   true, dequeue event, false, event stays in queue.
	 -1, event is destroyed.
	*/
	function EventQueue_remind_batched($data) {
		global $locale_char_set, $AppUI;
		$q = new DBQuery;

		if (! isset($this->_default_batch_status)) {
			$q->addTable('user_preferences');
			$q->addQuery('pref_value');
			$q->addWhere('pref_user = 0 and pref_name = \'USEDIGESTS\'');
			$this->_default_batch_status = $q->loadResult();
			$q->clear();
		}
		$id = $data['queue_origin_id'];
		$owner = $data['queue_owner'];
		$df = $AppUI->getPref('SHDATEFORMAT');
		$tf = $AppUI->getPref('TIMEFORMAT');
		// If we don't have preferences set for these, use ISO defaults.
		if (! $df) {
			$df = '%Y-%m-%d';
		}
		if (! $tf) {
			$tf = '%H:%m';
		}
		$df .= ' ' . $tf;

		// At this stage we won't have an object yet
		if (! $this->load($id)) {
			return -1; // No point it trying again later.
		}
		$this->htmlDecode();

		// Only remind on working days.
		$today = new CDate();
		if (! $today->isWorkingDay()) {
			return true;
		}

		// Check if the task is completed
		if ($this->task_percent_complete == 100) {
			return -1;
		}

		// Task is inactive
		if ($this->task_status < 0) {
			return -1;
		}

		$q->addTable('projects');
		$q->addQuery('project_name, project_status');
		$q->addWhere('project_id = ' . $this->task_project);
		$project_detail = $q->loadHash();
		if ($project_detail['project_status'] == 4 || $project_detail['project_status'] == 7) {
			return -1;
		}
		$project_name = htmlspecialchars_decode($project_detail['project_name']);

		// Grab the assignee list
		$q->addTable('user_tasks', 'ut');
		$q->leftJoin('users', 'u', 'u.user_id = ut.user_id');
		$q->leftJoin('contacts', 'c', 'c.contact_id = u.user_contact');
		$q->leftJoin('user_preferences', 'pr', "pr.pref_user = u.user_id and pr.pref_name = 'USEDIGESTS'");
		$q->addQuery('c.contact_id, contact_first_name, contact_last_name, contact_email');
		$q->addQuery('pr.pref_value as batched, pr.pref_user');
		$q->addWhere('ut.task_id = ' . $id);
		$contacts = $q->loadHashList('contact_id');
		$q->clear();

		// Now we also check the owner of the task, as we will need
		// to notify them as well.
		$owner_is_not_assignee = false;
		$q->addTable('users', 'u');
		$q->leftJoin('contacts', 'c', 'c.contact_id = u.user_contact');
		$q->leftJoin('user_preferences', 'pr', "pr.pref_user = u.user_id and pr.pref_name = 'USEDIGESTS'");
		$q->addQuery('c.contact_id, contact_first_name, contact_last_name, contact_email');
		$q->addQuery('pr.pref_value as batched, pr.pref_user');
		$q->addWhere('u.user_id = ' . $this->task_owner);
		if ($q->exec(ADODB_FETCH_NUM)) {
			list($owner_contact, $owner_first_name, $owner_last_name, $owner_email, $owner_batched, $pref_user) = $q->fetchRow();
			if (! isset($contacts[$owner_contact])) {
				$owner_is_not_assignee = true;
				$contacts[$owner_contact] = array(
												  'contact_id' => $owner_contact,
												  'contact_first_name' => $owner_first_name,
												  'contact_last_name' => $owner_last_name,
												  'contact_email' => $owner_email,
												  'batched' => $owner_batched,
												  'pref_user' => $pref_user,
												);
			}
		}
		$q->clear();

		// build the subject line, based on how soon the
		// task will be overdue.
		$starts = new CDate($this->task_start_date);
		$expires = new CDate($this->task_end_date);
		$now = new CDate();
		$diff = $expires->dateDiff($now);

		$prefix = $AppUI->_('Task Due', UI_OUTPUT_RAW);
		if ($diff == 0) {
			$msg = $AppUI->_('TODAY', UI_OUTPUT_RAW);
		} else if ($diff == 1) {
			$msg = $AppUI->_('TOMORROW', UI_OUTPUT_RAW);
		} else if ($diff < 0) {
			$msg = $AppUI->_(array('OVERDUE', abs($diff), 'DAYS'));
			$prefix = $AppUI->_('Task', UI_OUTPUT_RAW);
		} else {
			$msg = $AppUI->_(array($diff, 'DAYS'));
		}

		$subject = $prefix . ' ' .$msg . ' ' . $this->task_name . '::' . $project_name;

		$body = ($AppUI->_('Task Due', UI_OUTPUT_RAW) . ': ' . $msg . "\n"
				 . $AppUI->_('Project', UI_OUTPUT_RAW) . ': ' . $project_name . "\n"
				 . $AppUI->_('Task', UI_OUTPUT_RAW) . ': ' . $this->task_name . "\n"
				 . $AppUI->_('Start Date', UI_OUTPUT_RAW) . ': ' . $starts->format($df) . "\n"
				 . $AppUI->_('Finish Date', UI_OUTPUT_RAW) . ': ' . $expires->format($df) . "\n"
				 . $AppUI->_('URL', UI_OUTPUT_RAW) . ': ' . DP_BASE_URL
				 . '/index.php?m=tasks&a=view&task_id=' . $this->task_id . '&reminded=1' . "\n\n"
				 . $AppUI->_('Resources', UI_OUTPUT_RAW) . ":\n");
		foreach ($contacts as $contact) {
			if ($owner_is_not_assignee || $contact['contact_id'] != $owner_contact) {
				$body .= ($contact['contact_first_name'] . ' ' . $contact['contact_last_name']
						  . ' <' . $contact['contact_email'] . ">\n");
			}
		}
		$body .= ("\n" . $AppUI->_('Description', UI_OUTPUT_RAW) . ":\n"
				  . $this->task_description . "\n");

		foreach ($contacts as $contact) {
			$contact_batched = $contact['pref_user'] ? $contact['batched'] : $this->_default_batch_status;
			if ($contact_batched) {
				if (! isset($this->_reminder_list[$contact['contact_email']])) {
					$this->_reminder_list[$contact['contact_email']] = array(
						'contact' => $contact,
						'email' => array(),
					);
				}
				$this->_reminder_list[$contact['contact_email']]['email'][] = array(
					'subject' => $subject,
					'from' => $owner_first_name . ' ' . $owner_last_name,
					'body' => $body,);
			}
		}
		return true;
	}

	/**
	 * Handler function to terminate batched requests.  This allows us to
	 * send out any digest style reminders that users have asked for.
	 */
	function EventQueue_remind_terminateBatch() {
		global $locale_char_set, $AppUI;

		$site = dPgetConfig('company_name', 'dotProject');
		$domain = dPgetConfig('site_domain', 'example.com');
		$from_name = dPgetConfig('admin_username', 'admin');
		$from_mail = $site . ' Site Admin <'.$from_name.'@'.$domain.'>';
		foreach ($this->_reminder_list as $reminder) {
			$mail = new Mail();
			$mail->From($from_mail);
			$mail->Subject('Task Reminder Digest', $locale_char_set);
			if (! $mail->ValidEmail($reminder['contact']['contact_email'])) {
				continue;
			}
			$mail->To($reminder['contact']['contact_email']);
			$body = '';
			foreach ($reminder['email'] as $email) {
				$body .= "================================================\n\n";
				$body .= $email['subject'] . "\n\n";
				$body .= $email['body'] . "\n\n";
			}
			$mail->Body($body, $locale_char_set);
			$mail->Send();
		}
		$this->_reminder_list = array();
	}

	function EventQueue_remind_initiateBatch() {
		$this->_reminder_list = array();
	}

	/**
	 *
	 */
	function clearReminder($dont_check = false) {
		$ev = EventQueue::getInstance();

		$event_list = $ev->find('tasks', 'remind', $this->task_id);
		if (count($event_list)) {
			foreach ($event_list as $id => $data) {
				if ($dont_check || $this->task_percent_complete >= 100 || $this->task_status < 0) {
					$ev->remove($id);
				}
			}
		}
	}

}


/**
 * CTaskLog Class
 */
class CTaskLog extends CDpObject
{
	var $task_log_id = NULL;
	var $task_log_task = NULL;
	var $task_log_name = NULL;
	var $task_log_description = NULL;
	var $task_log_creator = NULL;
	var $task_log_hours = NULL;
	var $task_log_date = NULL;
	var $task_log_costcode = NULL;
	var $task_log_problem = NULL;
	var $task_log_reference = NULL;
	var $task_log_related_url = NULL;


	function __construct() {
		parent::__construct('task_log', 'task_log_id');

		// ensure changes to checkboxes are honoured
		$this->task_log_problem = intval($this->task_log_problem);
	}

	function dPTrimAll() {
		$spacedDescription = $this->task_log_description;
		parent::dPTrimAll();
		$this->task_log_description = $spacedDescription;
	}

	// overload check method
	function check() {
		$this->task_log_hours = sprintf('%.3F', (float) $this->task_log_hours);
		return NULL;
	}
}

function openClosedTask($task) {
	global $tasks_opened;
	$tasks_opened = (($tasks_opened) ? $tasks_opened : array());
	$to_open_task = new CTask();

	if (is_array($task) || $task > 0) {
		if (is_array($task)) {
			$task_dynamic = $task['task_dynamic'];
			$task_id = $task['task_id'];

		} else {
			$to_open_task->peek($task);
			$task_dynamic = $to_open_task->task_dynamic;
			$task_id = $task;
		}
		// don't "open" non-dynamic tasks or tasks without children
		if ($task_dynamic == 1 || count($to_open_task->getChildren())) {
			$tasks_opened[$task_id] = $task_id;
		}
	}
}

function openClosedTaskRecursive($task_id) {
	global $tasks_opened;
	$tasks_opened = (($tasks_opened) ? $tasks_opened : array());
	$open_task = new CTask();

	if ($task_id > 0) {
		openClosedTask($task_id);

		$open_task->peek($task_id) ;
		$children_to_open = $open_task->getChildren();
		foreach ($children_to_open as $to_open) {
			openClosedTaskRecursive($to);
		}
	}
}

/**
 * Find the first closed item within the supplied array
 * and open it, walking down the hierarchy.
 */
function openFirstClosed(&$opened, $parent) {
	global $tasks_opened;
	if (empty($opened[$parent])) {
		$tasks_opened[$parent] = $parent;
	} else {
		foreach ($opened[$parent] as $child) {
			openFirstClosed($opened, $child);
		}
	}
}

/**
 * Find the last opened and close it.
 */
function closeLastOpened(&$opened, $parent) {
	global $tasks_opened;
	// Check if any of the children are opened
	$found = false;
	// If not opened, return
	if (empty($opened[$parent])) {
		return;
	}
	foreach ($opened[$parent] as $child) {
		if ($opened[$child]) {
			closeLastOpened($opened, $child);
			$found = true;
		}
	}
	if ($found == false) {
		$tasks_opened[$parent] = false;
	}
}

/*
 * This is far more complicated than it needs be.  The entire
 * task array building and manipulating is overly complex and
 * needs to be reviewed.
 */
function OpenCloseOneLevel(&$projects, $open = true) {
	global $tasks_opened;
	$children_of = array();
	$opened = array();
	$children = array();
	$top_level = array();
	// We are called before the children_of array is built,
	// so we build it here.
	foreach ($projects as $project) {
		if (empty($project['tasks'])) {
			continue;
		}
		foreach ($project['tasks'] as $task) {
			if ($task['task_parent'] != $task['task_id']) {
				if (! isset($children_of[$task['task_parent']])) {
					$children_of[$task['task_parent']] = array();
				}
				$children_of[$task['task_parent']][] = $task['task_id'];
			}
		}
	}
	foreach ($children_of as $parent => $wean) {
		foreach ($wean as $child) {
			$children[$child] = $parent;
		}
	}
	foreach ($children_of as $parent => $wean) {
		if (!empty($tasks_opened[$parent])) {
			$opened[$parent] = $wean;
		}
		if (empty($children[$parent])) {
			$top_level[$parent] = $wean;
		}
	}
	if ($open) {
		foreach ($top_level as $parent => $wean) {
			openFirstClosed($opened, $parent);
		}
	} else {
		foreach ($top_level as $parent => $wean) {
			closeLastOpened($opened, $parent);
		}
	}
}

function closeOpenedTask($task) {
	global $tasks_opened;
	$tasks_opened = (($tasks_opened) ? $tasks_opened : array());
	$to_close_task = new CTask();

	if (is_array($task) || $task > 0) {
		if (is_array($task)) {
			$task_id = $task['task_id'];
			$task_dynamic = $task['task_dynamic'];
		} else {
			$to_close_task->peek($task);
			$task_id = $task;
			$task_dynamic = $to_close_task->task_dynamic;
		}
		// don't "close" non-dynamic tasks or tasks without children - why?
		if ($task_dynamic == 1 || count($to_close_task->getChildren())) {
			$tasks_opened[$task_id] = false;
		}
	}
}

function closeOpenedTaskRecursive($task_id) {
	global $tasks_opened;
	$tasks_opened = (($tasks_opened) ? $tasks_opened : array());
	$close_task = new CTask();

	if ($task_id > 0) {
		closeOpenedTask($task_id);

		$close_task->peek($task_id) ;
		$children_to_close = $close_task->getChildren();
		foreach ($children_to_close as $to_close) {
		  closeOpenedTaskRecursive($to_close);
		}
	}
}

//This kludgy function echos children tasks as threads

function showtask(&$a, $level=0, $is_opened = true, $today_view = false, $hideOpenCloseLink=false
				  , $allowRepeat = false) {
	global $AppUI, $done, $query_string, $durnTypes, $userAlloc, $showEditCheckbox;
	global $tasks_opened, $user_id, $dPconfig;

	$tasks_opened = (($tasks_opened) ? $tasks_opened : array());  // does this write _globally_? Is that a good idea? (gwyneth 20210425)
	$done = (($done) ? $done : array());

	$now = new CDate();
	$df = $AppUI->getPref('SHDATEFORMAT');
	$df .= ' ' . $AppUI->getPref('TIMEFORMAT');
	$show_all_assignees = dPgetConfig('show_all_task_assignees', false);

	if (!isset($done[$a['task_id']]))	{
		$done[$a['task_id']] = 1;
	} else if (!($allowRepeat)) {
		//by default, we shouldn't allow repeat displays of the same task
		return;
	}

	$task_obj = new CTask();
	$task_obj->peek($a['task_id']);
	if (!($task_obj->canAccess((($user_id) ? $user_id : $AppUI->user_id)))) {
		//don't show tasks that we can't access
		return;
	}

	if ($is_opened) {
		openClosedTask($a);
	} else {
		closeOpenedTask($a);
	}

	$start_date = intval($a['task_start_date']) ? new CDate($a['task_start_date'], FMT_DATETIME_HTML5) : null;
	$end_date = intval($a['task_end_date']) ? new CDate($a['task_end_date'], FMT_DATETIME_HTML5) : null;
	$last_update = ((isset($a['last_update']) && intval($a['last_update']))
					? new CDate($a['last_update']) : null);

	// prepare coloured highlight of task time information
	$style = '';
	if ($start_date) {

		if ($now->after($start_date) && $a['task_percent_complete'] == 0) {
			$style = 'background-color:#ffeebb';
		} else if ($now->after($start_date) && $a['task_percent_complete'] < 100) {
			$style = 'background-color:#e6eedd';
		}

		if (!empty($end_date) && $now->after($end_date)) {
			$style = 'background-color:#cc6666;color:#ffffff';
		}

		if (empty($end_date)) {
			/*
			 ** end date calc has been moved to calcEndByStartAndDuration()-function
			 ** called from array_csort and tasks.php
			 ** perhaps this fallback if-clause could be deleted in the future,
			 ** didn't want to remove it shortly before the 2.0.2
			 */
			$end_date = new CDate('0000-00-00 00:00:00');
		}

		if ($a['task_percent_complete'] == 100) {
			$style = 'background-color:#aaddaa; color:#00000';
		}

		$days = $end_date->dateDiff($now);
	}

	$s = "\n<tr>";
	// edit icon
	$s .= "\n\t<td>";
	$canEdit = getPermission('tasks', 'edit', $a['task_id']);
	$canViewLog = getPermission('task_log', 'view', $a['task_id']);
	if ($canEdit) {
		$s .= ("\n\t\t".'<a href="?m=tasks&amp;a=addedit&amp;task_id=' . $a['task_id'] . '">'
			   . "\n\t\t\t".'<img src="./images/icons/pencil.gif" alt="' . $AppUI->_('Edit Task')
			   . '" border="0" width="12" height="12" />' . "\n\t\t</a>");
	}
	$s .= "\n\t</td>";
	// pinned
	$pin_prefix = $a['task_pinned'] ? '' : 'un';
	$s .= ("\n\t<td>\n\t\t" . '<a href="?m=tasks&amp;pin=' . ($a['task_pinned']?0:1)
		   . '&task_id=' . $a['task_id'] . '">'
		   . "\n\t\t\t".'<img src="./images/icons/' . $pin_prefix . 'pin.gif" alt="'
		   . $AppUI->_($pin_prefix . 'pin Task') . '" border="0" width="12" height="12" />'
		   . "\n\t\t</a>\n\t</td>");
	// New Log
	$s .= ("\n\t" . '<td align="center">');
	if ($canViewLog && $a['task_dynamic'] != 1) {
		$s .= ('<a href="?m=tasks&amp;a=view&amp;task_id=' . $a['task_id'] . '&tab=1">'
			   . $AppUI->_('Log') . '</a>');
	} else {
		$s .= $AppUI->_('-');
	}
	$s .= ('</td>');
	// percent complete and priority
	$s .= ("\n\t" . '<td align="right">' . intval($a['task_percent_complete']) . '%</td>'
		   . "\n\t" . '<td align="center" nowrap="nowrap">');
	if (@$a['task_log_problem']>0) {
		$s .= ('<a href="?m=tasks&amp;a=view&amp;task_id='
			   . $a['task_id'] . '&amp;tab=0&amp;problem=1">'
			   . dPshowImage('./images/icons/dialog-warning5.png', 16, 16, 'Problem', 'Problem!')
			   . '</a>');
	} else if ($a['task_priority'] != 0) {
		$s .= "\n\t\t" . dPshowImage(('./images/icons/priority' . (($a['task_priority'] > 0)
																   ? '_up_' : '_down_')
									  . abs($a['task_priority']) . '.gif'), 13, 16, '', '');
	}
	$s .= ((@$a['file_count'] > 0) ? '<img src="./images/clip.png" alt="F" />' : '') . '</td>';
	// dots
	$s .= '<td width="' . (($today_view) ? '50%' : '90%') . '">';
	//level
	if ($level == -1) {
		$s .= '...';
	}
	for ($y=0; $y < $level; $y++) {
		$s .= ('<img src="' . (($y+1 == $level) ? './images/corner-dots.gif' : './images/shim.gif')
			   . '" width="16" height="12" border="0" alt="" />');
	}
	// name link
	/*
	$alt = ((mb_strlen($a['task_description']) > 80)
			? (mb_substr($a['task_description'], 0, 80) . '...') : $a['task_description']);
	// instead of the statement below
	$alt = str_replace('"', '&quot;', $alt);
	$alt = htmlspecialchars($alt);
	$alt = str_replace("\r", ' ', $alt);
	$alt = str_replace("\n", ' ', $alt);
	*/
	$alt = '';
	if (!empty($a['task_description'])) {
		$alt = ('onmouseover="javascript:return overlib(' . "'"
			. str_replace(array("\n", "\r"), '',
				$AppUI->___('<div><p>'
				. str_replace(array('"', "'"), array("&quot;", "\\'"), $a['task_description'])
				. '</p></div>')) . "', CAPTION, '"
			. $AppUI->_('Description')
			. "', CENTER" .  dPgetConfig('overlib_extra_parameters', ', DONOTHING') . ');" onmouseout="nd();"');
      dprint(__FILE__, __LINE__, 2, "[DEBUG] " . __FUNCTION__ . ": Extra parameters for overLib: '" . dPgetConfig('overlib_extra_parameters', '[empty]') . "'");
	}

	if (!empty($a['task_milestone'])) { // changed as above; it was: $a['task_milestone'] > 0 (gwyneth 20210425)
		$s .= ('&nbsp;<a href="./index.php?m=tasks&amp;a=view&amp;task_id=' . $a['task_id'] . '" '
			   . $alt . '>' . '<b>' . $a['task_name'] . '</b></a>'
			   . '<img src="./images/icons/milestone.gif" border="0" alt="Milestone" /></td>');
	} else if ($a['task_dynamic'] == 1 || count($task_obj->getChildren()) ) {
		if (! ($today_view || $hideOpenCloseLink)) {
			$s .= ('<a href="index.php' . $query_string
				   . (($is_opened)
					  ? ('&close_task_id='.$a['task_id']
						 . '"><img src="images/icons/collapse.gif" align="center"')
					  : ('&open_task_id='.$a['task_id'] . '"><img src="images/icons/expand.gif"'))
				   . ' border="0" alt="" /></a>');
		}
		$s .= ('&nbsp;<a href="./index.php?m=tasks&amp;a=view&amp;task_id=' . $a['task_id'] . '" '
			   . $alt . '>' . (($a['task_dynamic'] == 1) ? '<b><i>' : '') . $a['task_name'] . (($a['task_dynamic'] == 1) ? '</i></b>' : '') . '</a></td>');
	} else {
	  $s .= ('&nbsp;<a href="./index.php?m=tasks&amp;a=view&amp;task_id=' . $a['task_id'] . '" '
			 . $alt . '>' . $AppUI->___($a['task_name']) . '</a></td>');
	}

	if (!empty($today_view)) { // Show the project name
		$s .= ('<td width="50%"><a href="?m=projects&amp;a=view&amp;project_id='
			   . $a['task_project'] . '">' . '<span style="padding:2px;background-color:'
			   . $a['project_color_identifier'] . ';color:'
			   . bestColor($a['project_color_identifier']) . '">' . $a['project_name'] . '</span>'
			   . '</a></td>');
	}
	// task owner
	if (empty($today_view)) {
		$s .= ('<td nowrap="nowrap" align="center">' . '<a href="?m=admin&amp;a=viewuser&amp;user_id='
			   . $a['user_id'] . '">' . $a['user_username'] . '</a>' . '</td>');
	}
	// $s .= '<td nowrap="nowrap" align="center">' . $a['user_username'] . '</td>';
	if (!empty($a['task_assigned_users']) && ($assigned_users = $a['task_assigned_users'])) {
		$a_u_tmp_array = array();
		if ($show_all_assignees) {
			$s .= '<td align="center">';
			foreach ($assigned_users as $val) {
				/*
				$a_u_tmp_array[] = ('<a href="mailto:' . $val['user_email'] . '">'
									. $val['user_username'] . '</a>');
				*/
				$a_u_tmp_array[] = ('<a href="?m=admin&amp;a=viewuser&amp;user_id=' . $val['user_id'] . '"'
						  . 'title="' . $AppUI->_('Extent of Assignment') . ':'
						  . ($userAlloc[$val['user_id']]['charge'] ?? '--'). '%; '
						  . $AppUI->_('Free Capacity') . ':'
						  . ($userAlloc[$val['user_id']]['freeCapacity'] ?? '--') . '%' . '">'
						  . $AppUI->___($val['user_username']) . ' (' . ($val['perc_assignment'] ?? '--') . '%)</a>');
			}
			$s .= join (', ', $a_u_tmp_array) . '</td>';
		} else {
			$s .= ('<td align="center" nowrap="nowrap">'
				   .'<a href="?m=admin&amp;a=viewuser&amp;user_id=' . $assigned_users[0]['user_id']
				   . '" title="' . $AppUI->_('Extent of Assignment') . ':'
				   . ($userAlloc[$assigned_users[0]['user_id']]['charge'] ?? '--') . '%; '
				   . $AppUI->_('Free Capacity') . ':'
				   . ($userAlloc[$assigned_users[0]['user_id']]['freeCapacity'] ?? '--') . '%">'
				   . $AppUI->___($assigned_users[0]['user_username'] ?? '[unknown username]')
				   .' (' . ($assigned_users[0]['perc_assignment'] ?? '--') .'%)</a>');
			if (!empty($a['assignee_count']) && $a['assignee_count'] > 1) {
				$s .= (' <a href="javascript: void(0);" onclick="javascript:toggle_users('
					   . "'users_" . $a['task_id'] . "'" . ');" title="'
					   . join (', ', $a_u_tmp_array) .'">(+' . ($a['assignee_count'] - 1) . ')</a>'
					   . '<span style="display: none" id="users_' . $a['task_id'] . '">');
				$a_u_tmp_array[] = $AppUI->___($assigned_users[0]['user_username']);
				for ($i = 1, $xi = count($assigned_users) ?? 0; $i < $xi; $i++) {
					$a_u_tmp_array[] = $AppUI->___($assigned_users[$i]['user_username']);
					$s .= ('<br /><a href="?m=admin&amp;a=viewuser&amp;user_id='
						   . $assigned_users[$i]['user_id'] . '" title="'
						   . $AppUI->_('Extent of Assignment') . ':'
						   . ($userAlloc[$assigned_users[$i]['user_id']]['charge']) ?? '--') . '%; '
						   . $AppUI->_('Free Capacity') . ':'
						   . ($userAlloc[$assigned_users[$i]['user_id']]['freeCapacity'] ?? '--') . '%">'
						   . $AppUI->___(($assigned_users[$i]['user_username'] ?? '[unknown username]')  . ' ('
						   . ($assigned_users[$i]['perc_assignment'] ?? '--') . '%)</a>');
				}
				$s .= '</span>';
			}
			$s .= '</td>';
		}
	} else if (empty($today_view)) {
		// No users asigned to task
		$s .= '<td align="center">-</td>';
	}
	// duration or milestone
	$s .= ('<td nowrap="nowrap" align="center" style="' . $style . '">'
		   . (!empty($start_date) ? $start_date->format($df) : '-') . '</td>'
		   . '<td align="center" nowrap="nowrap" style="' . $style . '">' . $a['task_duration']
		   . ' ' . $AppUI->_($durnTypes[$a['task_duration_type']]) . '</td>'
		   . '<td nowrap="nowrap" align="center" style="' . $style . '">'
		   . (!empty($end_date) ? $end_date->format($df) : '-') . '</td>');
	if (!empty($today_view)) {
		$s .= ('<td nowrap="nowrap" align="center" style="' . $style . '">'
			   . $a['task_due_in'] . '</td>');
	} else if (!empty($AppUI->isActiveModule('history')) && getPermission('history', 'view')) {
		$s .= ('<td nowrap="nowrap" align="center" style="' . $style.'">'
			   . (!empty($last_update) ? $last_update->format($df) : '-') . '</td>');
	}

	// Assignment checkbox
	if (!empty($showEditCheckbox)) {
		$s .= ("\n\t" . '<td align="center">' . '<input type="checkbox" name="selected_task['
			   . $a['task_id'] . ']" value="' . $a['task_id'] . '"/></td>');
	}
	$s .= '</tr>';
	echo $s;
}

function findchild(&$tarr, $parent, $level=0) {
	global $tasks_opened, $tasks_filtered, $children_of;
	$tasks_opened = (($tasks_opened) ? $tasks_opened : array());

	$level = $level+1;

	foreach ($tarr as $x => $task) {
		if ($task['task_parent'] == $parent && $task['task_parent'] != $task['task_id']) {
			$is_opened = !empty($tasks_opened[$task['task_id']]);

			//check for child
			$no_children = empty($children_of[$task['task_id']]);

			showtask($task, $level, $is_opened, false, $no_children);
			if ($is_opened && !($no_children)) {
				/*
				 * Yes, this is stupid, but there was previously a bug where if you had
				 * two dynamic tasks at the same level and the child of a dynamic task,
				 * they would only both display if the first one was closed.  The moment
				 * you opened the first one, the second would disappear.
				 *
				 * There is something screwy happening in this function in the pass by
				 * reference.  I suspect it's a PHP4 vs PHP5 oddity.
				 */
				$tmp = $tarr;
				findchild($tmp, $task['task_id'], $level);
			}
		}
	}
}

/*
array_csort($data_array [,$col, $order, $type [, $col, $order, $type [...]]]);

$data_array - multi-dimensional array of query results to sort
$col - data table "column" to sort by
$order - SORT_ASC or SORT_DESC flag values
$type - SORT_REGULAR, SORT_NUMERIC, or SORT_STRING flag values

...any number of column, order, and type values can be passed
but they must be specified in that order and all sets except
the last must be defined fully

Examples -
valid:
array_csort($data_array,$col, $order, $type, $col2, $order2, $type2);
array_csort($data_array,$col, $order, $type, $col2, $order2,);
array_csort($data_array,$col, $order, $type, $col2, $type2);
array_csort($data_array);

invalid:
array_csort($data_array,$col, $type, $col2, $order2, $type2);

*/
function array_csort() {

	$args = func_get_args();
	$marray = array_shift($args);

	if (empty($marray)) {
		return array();
	}

	$i = 0;
	$msortline = 'return(array_multisort(';
	$sortarr = array();
	foreach ($args as $arg) {
		if ($i % 3) {
			$msortline .= $arg . ', ';
		} else {
			foreach ($marray as $j => $item) {

				/* we have to calculate the end_date via start_date+duration for
				 ** end='0000-00-00 00:00:00' before sorting, see mantis #1509:

				 ** Task definition writes the following to the DB:
				 ** A without start date: start = end = NULL
				 ** B with start date and empty end date: start = startdate,
				                                          end = '0000-00-00 00:00:00'
				 ** C start + end date: start= startdate, end = end date

				 ** A the end_date for the middle task (B) is ('dynamically') calculated on display
				 ** via start_date+duration, it may be that the order gets wrong due to the fact
				 ** that sorting has taken place _before_.
				 */
				if ($item['task_end_date'] == '0000-00-00 00:00:00') {
					$item['task_end_date'] = calcEndByStartAndDuration($marray[$j]);
				}
				$sortarr[$i][$j] = $marray[$j][$arg];
			}
			$msortline .= '$sortarr[' . $i . '], ';
		}
		$i++;
	}
	$msortline .= '$marray));';
	eval($msortline);

	return $marray;
}

/*
 ** Calc End Date via Startdate + Duration
 ** @param array task	A DB row from the earlier fetched tasklist
 ** @return string	Return calculated end date in MySQL-TIMESTAMP format
 */

function calcEndByStartAndDuration($task) {
	$end_date = new CDate($task['task_start_date']);
	$end_date->addSeconds(@$task['task_duration'] * $task['task_duration_type'] * SEC_HOUR);
	return $end_date->format(FMT_DATETIME_MYSQL);
}

function sort_by_item_title($title, $item_name, $item_type) {
	global $AppUI, $project_id, $task_id, $min_view;
	global $task_sort_item1, $task_sort_type1, $task_sort_order1;
	global $task_sort_item2, $task_sort_type2, $task_sort_order2;


	if ($task_sort_item2 == $item_name) {
		$item_order = $task_sort_order2;
	}
	if ($task_sort_item1 == $item_name) {
		$item_order = $task_sort_order1;
	}
	//Hack for Problem Log/Priority Sorting
	if ($item_name == 'task_log_problem_priority' && $task_sort_item2 == 'task_priority') {
		$item_order = $task_sort_order2;
	}

	if (isset($item_order)) {
		echo ('<img src="./images/arrow-' . (($item_order == SORT_ASC) ? 'up' : 'down')
			  . '.gif" width="11" height="11" alt="" />');
	} else {
		$item_order = SORT_DESC;
	}

	/* flip the sort order for the link */
	$item_order = ($item_order == SORT_ASC) ? SORT_DESC : SORT_ASC;

	$link = '<a href="./index.php?';
	$not_first = 0;
	foreach ($_GET as $var => $val) {
		if (!(in_array($var, array('task_sort_item1', 'task_sort_type1', 'task_sort_order1',
		                           'task_sort_item2', 'task_sort_type2', 'task_sort_order2')))) {
			$link .= ((($not_first) ? '&' : '') . $var . '=' . dPgetCleanParam($_GET,$var));
			$not_first = 1;
		}
	}

	if ($item_name == 'task_log_problem_priority') {
		$link .= '&task_sort_item1=task_log_problem';
		$link .= '&task_sort_type1=' . $item_type;
		$link .= '&task_sort_order1=' . SORT_DESC;
		$link .= '&task_sort_item2=task_priority';
		$link .= '&task_sort_type2=' . $item_type;
		$link .= '&task_sort_order2=' . $item_order;
	} else {
		$link .= '&task_sort_item1=' . $item_name;
		$link .= '&task_sort_type1=' . $item_type;
		$link .= '&task_sort_order1=' . $item_order;

		if ((($task_sort_item1 && $task_sort_item1 != $item_name)
			 || $task_sort_item2) && $task_sort_item2 != 'task_priority')  {
			$item_num = (($task_sort_item1 == $item_name) ? '2' : '1');
			$link .= '&task_sort_item2=' . ${'task_sort_item' . $item_num};
			$link .= '&task_sort_type2=' . ${'task_sort_type' . $item_num};
			$link .= '&task_sort_order2=' . ${'task_sort_order' . $item_num};
		}
	}
	$link .= '" class="hdr">';

	$link .= $AppUI->_($title);

	$link .= '</a>';
	echo dPsanitiseHTML($link);
}

