| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace Grocy\Services; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | #use \Grocy\Services\StockService;
 | 
					
						
							| 
									
										
										
										
											2019-09-18 10:02:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | class ChoresService extends BaseService | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 	const CHORE_PERIOD_TYPE_MANUALLY = 'manually'; | 
					
						
							|  |  |  | 	const CHORE_PERIOD_TYPE_DYNAMIC_REGULAR = 'dynamic-regular'; | 
					
						
							|  |  |  | 	const CHORE_PERIOD_TYPE_DAILY = 'daily'; | 
					
						
							|  |  |  | 	const CHORE_PERIOD_TYPE_WEEKLY = 'weekly'; | 
					
						
							|  |  |  | 	const CHORE_PERIOD_TYPE_MONTHLY = 'monthly'; | 
					
						
							| 
									
										
										
										
											2019-10-04 11:24:51 +02:00
										 |  |  | 	const CHORE_PERIOD_TYPE_YEARLY = 'yearly'; | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	const CHORE_ASSIGNMENT_TYPE_NO_ASSIGNMENT = 'no-assignment'; | 
					
						
							|  |  |  | 	const CHORE_ASSIGNMENT_TYPE_WHO_LEAST_DID_FIRST = 'who-least-did-first'; | 
					
						
							|  |  |  | 	const CHORE_ASSIGNMENT_TYPE_RANDOM = 'random'; | 
					
						
							|  |  |  | 	const CHORE_ASSIGNMENT_TYPE_IN_ALPHABETICAL_ORDER = 'in-alphabetical-order'; | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-18 10:02:52 +02:00
										 |  |  | 	public function __construct() | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		parent::__construct(); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 	public function GetCurrent() | 
					
						
							|  |  |  | 	{ | 
					
						
							| 
									
										
										
										
											2020-03-06 20:46:27 +01:00
										 |  |  | 		$sql = 'SELECT chores_current.*, chores.name AS chore_name from chores_current join chores on chores_current.chore_id = chores.id'; | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	public function GetChoreDetails(int $choreId) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		if (!$this->ChoreExists($choreId)) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			throw new \Exception('Chore does not exist'); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		 | 
					
						
							|  |  |  | 			$users = $this->getUsersService()->GetUsersAsDto(); | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$chore = $this->getDatabase()->chores($choreId); | 
					
						
							|  |  |  | 		$choreTrackedCount = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0', $choreId)->count(); | 
					
						
							|  |  |  | 		$choreLastTrackedTime = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0', $choreId)->max('tracked_time'); | 
					
						
							|  |  |  | 		$nextExecutionTime = $this->getDatabase()->chores_current()->where('chore_id', $choreId)->min('next_estimated_execution_time'); | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$lastChoreLogRow =  $this->getDatabase()->chores_log()->where('chore_id = :1 AND tracked_time = :2 AND undone = 0', $choreId, $choreLastTrackedTime)->fetch(); | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 		$lastDoneByUser = null; | 
					
						
							|  |  |  | 		if ($lastChoreLogRow !== null && !empty($lastChoreLogRow)) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			$lastDoneByUser = FindObjectInArrayByPropertyValue($users, 'id', $lastChoreLogRow->done_by_user_id); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 		$nextExecutionAssignedUser = null; | 
					
						
							|  |  |  | 		if (!empty($chore->next_execution_assigned_to_user_id)) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			$nextExecutionAssignedUser = FindObjectInArrayByPropertyValue($users, 'id', $chore->next_execution_assigned_to_user_id); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 		return array( | 
					
						
							|  |  |  | 			'chore' => $chore, | 
					
						
							|  |  |  | 			'last_tracked' => $choreLastTrackedTime, | 
					
						
							|  |  |  | 			'tracked_count' => $choreTrackedCount, | 
					
						
							|  |  |  | 			'last_done_by' => $lastDoneByUser, | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 			'next_estimated_execution_time' => $nextExecutionTime, | 
					
						
							|  |  |  | 			'next_execution_assigned_user' => $nextExecutionAssignedUser | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 		); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	public function TrackChore(int $choreId, string $trackedTime, $doneBy = GROCY_USER_ID) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		if (!$this->ChoreExists($choreId)) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			throw new \Exception('Chore does not exist'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$userRow = $this->getDatabase()->users()->where('id = :1', $doneBy)->fetch(); | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 		if ($userRow === null) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			throw new \Exception('User does not exist'); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2019-05-04 16:13:05 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$chore = $this->getDatabase()->chores($choreId); | 
					
						
							| 
									
										
										
										
											2019-05-04 16:13:05 +02:00
										 |  |  | 		if ($chore->track_date_only == 1) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			$trackedTime = substr($trackedTime, 0, 10) . ' 00:00:00'; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		$logRow = $this->getDatabase()->chores_log()->createRow(array( | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 			'chore_id' => $choreId, | 
					
						
							|  |  |  | 			'tracked_time' => $trackedTime, | 
					
						
							|  |  |  | 			'done_by_user_id' => $doneBy | 
					
						
							|  |  |  | 		)); | 
					
						
							|  |  |  | 		$logRow->save(); | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$lastInsertId = $this->getDatabase()->lastInsertId(); | 
					
						
							| 
									
										
										
										
											2019-09-18 10:02:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 		$this->CalculateNextExecutionAssignment($choreId); | 
					
						
							| 
									
										
										
										
											2019-09-18 10:02:52 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		if ($chore->consume_product_on_execution == 1 && !empty($chore->product_id)) | 
					
						
							|  |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 			$this->getStockService()->ConsumeProduct($chore->product_id, $chore->product_amount, false, StockService::TRANSACTION_TYPE_CONSUME); | 
					
						
							| 
									
										
										
										
											2019-09-18 10:02:52 +02:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 		return $lastInsertId; | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	private function ChoreExists($choreId) | 
					
						
							|  |  |  | 	{ | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$choreRow = $this->getDatabase()->chores()->where('id = :1', $choreId)->fetch(); | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | 		return $choreRow !== null; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2018-10-27 10:55:30 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	public function UndoChoreExecution($executionId) | 
					
						
							|  |  |  | 	{ | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$logRow = $this->getDatabase()->chores_log()->where('id = :1 AND undone = 0', $executionId)->fetch(); | 
					
						
							| 
									
										
										
										
											2018-10-27 10:55:30 +02:00
										 |  |  | 		if ($logRow == null) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			throw new \Exception('Execution does not exist or was already undone'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Update log entry
 | 
					
						
							|  |  |  | 		$logRow->update(array( | 
					
						
							|  |  |  | 			'undone' => 1, | 
					
						
							|  |  |  | 			'undone_timestamp' => date('Y-m-d H:i:s') | 
					
						
							|  |  |  | 		)); | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	public function CalculateNextExecutionAssignment($choreId) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		if (!$this->ChoreExists($choreId)) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			throw new \Exception('Chore does not exist'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 		$chore = $this->getDatabase()->chores($choreId); | 
					
						
							|  |  |  | 		$choreLastTrackedTime = $this->getDatabase()->chores_log()->where('chore_id = :1 AND undone = 0', $choreId)->max('tracked_time'); | 
					
						
							|  |  |  | 		$lastChoreLogRow =  $this->getDatabase()->chores_log()->where('chore_id = :1 AND tracked_time = :2 AND undone = 0', $choreId, $choreLastTrackedTime)->fetch(); | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 		$lastDoneByUserId = $lastChoreLogRow->done_by_user_id; | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		$users = $this->getUsersService()->GetUsersAsDto(); | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 		$assignedUsers = array(); | 
					
						
							|  |  |  | 		foreach ($users as $user) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			if (in_array($user->id, explode(',', $chore->assignment_config))) | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				$assignedUsers[] = $user; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		$nextExecutionUserId = null; | 
					
						
							|  |  |  | 		if ($chore->assignment_type == self::CHORE_ASSIGNMENT_TYPE_RANDOM) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			// Random assignment and only 1 user in the group? Well, ok - will be hard to guess the next one...
 | 
					
						
							|  |  |  | 			if (count($assignedUsers) == 1) | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				$nextExecutionUserId = array_shift($assignedUsers)->id; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			else | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				// Randomness in small groups will likely often result in the same user, so try it as long as this is the case
 | 
					
						
							|  |  |  | 				while ($nextExecutionUserId == null || $nextExecutionUserId == $lastDoneByUserId) | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					$nextExecutionUserId = $assignedUsers[array_rand($assignedUsers)]->id; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		else if ($chore->assignment_type == self::CHORE_ASSIGNMENT_TYPE_IN_ALPHABETICAL_ORDER) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			usort($assignedUsers, function($a, $b) | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				return strcmp($a->display_name, $b->display_name); | 
					
						
							|  |  |  | 			}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			$nextRoundMatches = false; | 
					
						
							|  |  |  | 			foreach ($assignedUsers as $user) | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				if ($nextRoundMatches) | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					$nextExecutionUserId = $user->id; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				if ($user->id == $lastDoneByUserId) | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					$nextRoundMatches = true; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// If nothing has matched, probably it was the last user in the sorted list -> the first one is the next one
 | 
					
						
							|  |  |  | 			if ($nextExecutionUserId == null) | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				$nextExecutionUserId = array_shift($assignedUsers)->id; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		else if ($chore->assignment_type == self::CHORE_ASSIGNMENT_TYPE_WHO_LEAST_DID_FIRST) | 
					
						
							|  |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2020-03-01 23:47:47 +07:00
										 |  |  | 			$row = $this->getDatabase()->chores_execution_users_statistics()->where('chore_id = :1', $choreId)->orderBy('execution_count')->limit(1)->fetch(); | 
					
						
							| 
									
										
										
										
											2019-09-17 13:13:26 +02:00
										 |  |  | 			if ($row != null) | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				$nextExecutionUserId = $row->user_id; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		$chore->update(array( | 
					
						
							|  |  |  | 			'next_execution_assigned_to_user_id' => $nextExecutionUserId | 
					
						
							|  |  |  | 		)); | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2018-09-22 13:26:58 +02:00
										 |  |  | } |