diff options
Diffstat (limited to 'app')
512 files changed, 36031 insertions, 10143 deletions
diff --git a/app/Action/Base.php b/app/Action/Base.php index 80930a4c..f29e9323 100644 --- a/app/Action/Base.php +++ b/app/Action/Base.php @@ -2,23 +2,26 @@ namespace Action; -use Core\Listener; -use Core\Registry; -use Core\Tool; +use Event\GenericEvent; +use Pimple\Container; /** * Base class for automatic actions * * @package action * @author Frederic Guillot - * - * @property \Model\Acl $acl - * @property \Model\Task $task - * @property \Model\TaskFinder $taskFinder */ -abstract class Base implements Listener +abstract class Base extends \Core\Base { /** + * Flag for called listener + * + * @access private + * @var boolean + */ + private $called = false; + + /** * Project id * * @access private @@ -43,12 +46,12 @@ abstract class Base implements Listener protected $event_name = ''; /** - * Registry instance + * Container instance * * @access protected - * @var \Core\Registry + * @var \Pimple\Container */ - protected $registry; + protected $container; /** * Execute the action @@ -100,15 +103,16 @@ abstract class Base implements Listener * Constructor * * @access public - * @param \Core\Registry $registry Regsitry instance - * @param integer $project_id Project id - * @param string $event_name Attached event name + * @param \Pimple\Container $container Container + * @param integer $project_id Project id + * @param string $event_name Attached event name */ - public function __construct(Registry $registry, $project_id, $event_name) + public function __construct(Container $container, $project_id, $event_name) { - $this->registry = $registry; + $this->container = $container; $this->project_id = $project_id; $this->event_name = $event_name; + $this->called = false; } /** @@ -123,18 +127,6 @@ abstract class Base implements Listener } /** - * Load automatically models - * - * @access public - * @param string $name Model name - * @return mixed - */ - public function __get($name) - { - return Tool::loadModel($this->registry, $name); - } - - /** * Set an user defined parameter * * @access public @@ -178,7 +170,6 @@ abstract class Base implements Listener * Check if the event is compatible with the action * * @access public - * @param array $data Event data dictionary * @return bool */ public function hasCompatibleEvent() @@ -220,12 +211,20 @@ abstract class Base implements Listener * Execute the action * * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed + * @param \Event\GenericEvent $event Event data dictionary + * @return bool True if the action was executed or false when not executed */ - public function execute(array $data) + public function execute(GenericEvent $event) { + // Avoid infinite loop, a listener instance can be called only one time + if ($this->called) { + return false; + } + + $data = $event->getAll(); + if ($this->isExecutable($data)) { + $this->called = true; return $this->doAction($data); } diff --git a/app/Action/CommentCreation.php b/app/Action/CommentCreation.php new file mode 100644 index 00000000..54d7be7d --- /dev/null +++ b/app/Action/CommentCreation.php @@ -0,0 +1,83 @@ +<?php + +namespace Action; + +use Integration\GithubWebhook; + +/** + * Create automatically a comment from a webhook + * + * @package action + * @author Frederic Guillot + */ +class CommentCreation extends Base +{ + /** + * Get the list of compatible events + * + * @access public + * @return string[] + */ + public function getCompatibleEvents() + { + return array( + GithubWebhook::EVENT_ISSUE_COMMENT, + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return string[] + */ + public function getActionRequiredParameters() + { + return array(); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return array + */ + public function getEventRequiredParameters() + { + return array( + 'reference', + 'comment', + 'user_id', + 'task_id', + ); + } + + /** + * Execute the action (create a new comment) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + return (bool) $this->comment->create(array( + 'reference' => $data['reference'], + 'comment' => $data['comment'], + 'task_id' => $data['task_id'], + 'user_id' => $data['user_id'], + )); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return true; + } +} diff --git a/app/Action/TaskAssignCategoryColor.php b/app/Action/TaskAssignCategoryColor.php index be15f659..ba319a1f 100644 --- a/app/Action/TaskAssignCategoryColor.php +++ b/app/Action/TaskAssignCategoryColor.php @@ -67,7 +67,7 @@ class TaskAssignCategoryColor extends Base 'category_id' => $this->getParam('category_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/app/Action/TaskAssignCategoryLabel.php b/app/Action/TaskAssignCategoryLabel.php index 5e1b025e..1383d491 100644 --- a/app/Action/TaskAssignCategoryLabel.php +++ b/app/Action/TaskAssignCategoryLabel.php @@ -2,7 +2,7 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; /** * Set a category automatically according to a label @@ -67,7 +67,7 @@ class TaskAssignCategoryLabel extends Base 'category_id' => isset($data['category_id']) ? $data['category_id'] : $this->getParam('category_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/app/Action/TaskAssignColorCategory.php b/app/Action/TaskAssignColorCategory.php index f5a9ac5a..a362c68f 100644 --- a/app/Action/TaskAssignColorCategory.php +++ b/app/Action/TaskAssignColorCategory.php @@ -67,7 +67,7 @@ class TaskAssignColorCategory extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/app/Action/TaskAssignColorColumn.php b/app/Action/TaskAssignColorColumn.php new file mode 100644 index 00000000..deb3e2b0 --- /dev/null +++ b/app/Action/TaskAssignColorColumn.php @@ -0,0 +1,85 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Assign a color to a task + * + * @package action + * @author Frederic Guillot + */ +class TaskAssignColorColumn extends Base +{ + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array( + Task::EVENT_CREATE, + Task::EVENT_MOVE_COLUMN, + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + 'color_id' => t('Color'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action (set the task color) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + $values = array( + 'id' => $data['task_id'], + 'color_id' => $this->getParam('color_id'), + ); + + return $this->taskModification->update($values); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/app/Action/TaskAssignColorUser.php b/app/Action/TaskAssignColorUser.php index 00680186..6161514d 100644 --- a/app/Action/TaskAssignColorUser.php +++ b/app/Action/TaskAssignColorUser.php @@ -68,7 +68,7 @@ class TaskAssignColorUser extends Base 'color_id' => $this->getParam('color_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/app/Action/TaskAssignCurrentUser.php b/app/Action/TaskAssignCurrentUser.php index 7a9cf70b..ff3aaee1 100644 --- a/app/Action/TaskAssignCurrentUser.php +++ b/app/Action/TaskAssignCurrentUser.php @@ -62,12 +62,16 @@ class TaskAssignCurrentUser extends Base */ public function doAction(array $data) { + if (! $this->userSession->isLogged()) { + return false; + } + $values = array( 'id' => $data['task_id'], - 'owner_id' => $this->acl->getUserId(), + 'owner_id' => $this->userSession->getId(), ); - return $this->task->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/app/Action/TaskAssignSpecificUser.php b/app/Action/TaskAssignSpecificUser.php index f70459be..4c96f7f0 100644 --- a/app/Action/TaskAssignSpecificUser.php +++ b/app/Action/TaskAssignSpecificUser.php @@ -68,7 +68,7 @@ class TaskAssignSpecificUser extends Base 'owner_id' => $this->getParam('user_id'), ); - return $this->task->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/app/Action/TaskAssignUser.php b/app/Action/TaskAssignUser.php index 29ea91e6..cf2a9a4b 100644 --- a/app/Action/TaskAssignUser.php +++ b/app/Action/TaskAssignUser.php @@ -2,7 +2,7 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; /** * Assign a task to someone @@ -64,7 +64,7 @@ class TaskAssignUser extends Base 'owner_id' => $data['owner_id'], ); - return $this->task->update($values, false); + return $this->taskModification->update($values); } /** diff --git a/app/Action/TaskClose.php b/app/Action/TaskClose.php index f71d4b0e..b7cd4dbf 100644 --- a/app/Action/TaskClose.php +++ b/app/Action/TaskClose.php @@ -2,7 +2,9 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GitlabWebhook; +use Integration\GithubWebhook; +use Integration\BitbucketWebhook; use Model\Task; /** @@ -25,6 +27,9 @@ class TaskClose extends Base Task::EVENT_MOVE_COLUMN, GithubWebhook::EVENT_COMMIT, GithubWebhook::EVENT_ISSUE_CLOSED, + GitlabWebhook::EVENT_COMMIT, + GitlabWebhook::EVENT_ISSUE_CLOSED, + BitbucketWebhook::EVENT_COMMIT, ); } @@ -39,6 +44,9 @@ class TaskClose extends Base switch ($this->event_name) { case GithubWebhook::EVENT_COMMIT: case GithubWebhook::EVENT_ISSUE_CLOSED: + case GitlabWebhook::EVENT_COMMIT: + case GitlabWebhook::EVENT_ISSUE_CLOSED: + case BitbucketWebhook::EVENT_COMMIT: return array(); default: return array('column_id' => t('Column')); @@ -56,6 +64,9 @@ class TaskClose extends Base switch ($this->event_name) { case GithubWebhook::EVENT_COMMIT: case GithubWebhook::EVENT_ISSUE_CLOSED: + case GitlabWebhook::EVENT_COMMIT: + case GitlabWebhook::EVENT_ISSUE_CLOSED: + case BitbucketWebhook::EVENT_COMMIT: return array('task_id'); default: return array('task_id', 'column_id'); @@ -71,7 +82,7 @@ class TaskClose extends Base */ public function doAction(array $data) { - return $this->task->close($data['task_id']); + return $this->taskStatus->close($data['task_id']); } /** @@ -86,6 +97,9 @@ class TaskClose extends Base switch ($this->event_name) { case GithubWebhook::EVENT_COMMIT: case GithubWebhook::EVENT_ISSUE_CLOSED: + case GitlabWebhook::EVENT_COMMIT: + case GitlabWebhook::EVENT_ISSUE_CLOSED: + case BitbucketWebhook::EVENT_COMMIT: return true; default: return $data['column_id'] == $this->getParam('column_id'); diff --git a/app/Action/TaskCreation.php b/app/Action/TaskCreation.php index 41d0200c..1c093eee 100644 --- a/app/Action/TaskCreation.php +++ b/app/Action/TaskCreation.php @@ -2,7 +2,8 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; +use Integration\GitlabWebhook; /** * Create automatically a task from a webhook @@ -22,6 +23,7 @@ class TaskCreation extends Base { return array( GithubWebhook::EVENT_ISSUE_OPENED, + GitlabWebhook::EVENT_ISSUE_OPENED, ); } @@ -59,11 +61,11 @@ class TaskCreation extends Base */ public function doAction(array $data) { - return $this->task->create(array( + return (bool) $this->taskCreation->create(array( 'project_id' => $data['project_id'], 'title' => $data['title'], 'reference' => $data['reference'], - 'description' => $data['description'], + 'description' => isset($data['description']) ? $data['description'] : '', )); } diff --git a/app/Action/TaskDuplicateAnotherProject.php b/app/Action/TaskDuplicateAnotherProject.php index 4ab88534..55ebc76e 100644 --- a/app/Action/TaskDuplicateAnotherProject.php +++ b/app/Action/TaskDuplicateAnotherProject.php @@ -64,9 +64,7 @@ class TaskDuplicateAnotherProject extends Base */ public function doAction(array $data) { - $task = $this->taskFinder->getById($data['task_id']); - $this->task->duplicateToAnotherProject($this->getParam('project_id'), $task); - return true; + return (bool) $this->taskDuplication->duplicateToProject($data['task_id'], $this->getParam('project_id')); } /** diff --git a/app/Action/TaskLogMoveAnotherColumn.php b/app/Action/TaskLogMoveAnotherColumn.php new file mode 100644 index 00000000..621e8e6c --- /dev/null +++ b/app/Action/TaskLogMoveAnotherColumn.php @@ -0,0 +1,84 @@ +<?php + +namespace Action; + +use Model\GithubWebhook; +use Model\Task; + +/** + * Add a log of the triggering event to the task description. + * + * @package action + * @author Oren Ben-Kiki + */ +class TaskLogMoveAnotherColumn extends Base +{ + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array( + Task::EVENT_MOVE_COLUMN, + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array('column_id' => t('Column')); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array('task_id', 'column_id'); + } + + /** + * Execute the action (append to the task description). + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + if (! $this->userSession->isLogged()) { + return false; + } + + $column = $this->board->getColumn($data['column_id']); + + return (bool) $this->comment->create(array( + 'comment' => t('Moved to column %s', $column['title']), + 'task_id' => $data['task_id'], + 'user_id' => $this->userSession->getId(), + )); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/app/Action/TaskMoveAnotherProject.php b/app/Action/TaskMoveAnotherProject.php index d852f56d..ee212998 100644 --- a/app/Action/TaskMoveAnotherProject.php +++ b/app/Action/TaskMoveAnotherProject.php @@ -64,9 +64,7 @@ class TaskMoveAnotherProject extends Base */ public function doAction(array $data) { - $task = $this->taskFinder->getById($data['task_id']); - $this->task->moveToAnotherProject($this->getParam('project_id'), $task); - return true; + return $this->taskDuplication->moveToProject($data['task_id'], $this->getParam('project_id')); } /** diff --git a/app/Action/TaskMoveColumnAssigned.php b/app/Action/TaskMoveColumnAssigned.php new file mode 100644 index 00000000..decf4b01 --- /dev/null +++ b/app/Action/TaskMoveColumnAssigned.php @@ -0,0 +1,90 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Move a task to another column when an assignee is set + * + * @package action + * @author Francois Ferrand + */ +class TaskMoveColumnAssigned extends Base +{ + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array( + Task::EVENT_ASSIGNEE_CHANGE, + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'src_column_id' => t('Source column'), + 'dest_column_id' => t('Destination column') + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + 'project_id', + 'owner_id' + ); + } + + /** + * Execute the action (move the task to another column) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + $original_task = $this->taskFinder->getById($data['task_id']); + + return $this->taskPosition->movePosition( + $data['project_id'], + $data['task_id'], + $this->getParam('dest_column_id'), + $original_task['position'], + $original_task['swimlane_id'], + false + ); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('src_column_id') && $data['owner_id']; + } +} diff --git a/app/Action/TaskMoveColumnUnAssigned.php b/app/Action/TaskMoveColumnUnAssigned.php new file mode 100644 index 00000000..b773252d --- /dev/null +++ b/app/Action/TaskMoveColumnUnAssigned.php @@ -0,0 +1,90 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Move a task to another column when an assignee is cleared + * + * @package action + * @author Francois Ferrand + */ +class TaskMoveColumnUnAssigned extends Base +{ + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array( + Task::EVENT_ASSIGNEE_CHANGE + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'src_column_id' => t('Source column'), + 'dest_column_id' => t('Destination column') + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + 'project_id', + 'owner_id' + ); + } + + /** + * Execute the action (move the task to another column) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + $original_task = $this->taskFinder->getById($data['task_id']); + + return $this->taskPosition->movePosition( + $data['project_id'], + $data['task_id'], + $this->getParam('dest_column_id'), + $original_task['position'], + $original_task['swimlane_id'], + false + ); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('src_column_id') && ! $data['owner_id']; + } +} diff --git a/app/Action/TaskOpen.php b/app/Action/TaskOpen.php index 6847856c..73f1fad3 100644 --- a/app/Action/TaskOpen.php +++ b/app/Action/TaskOpen.php @@ -2,7 +2,7 @@ namespace Action; -use Model\GithubWebhook; +use Integration\GithubWebhook; /** * Open automatically a task @@ -56,7 +56,7 @@ class TaskOpen extends Base */ public function doAction(array $data) { - return $this->task->open($data['task_id']); + return $this->taskStatus->open($data['task_id']); } /** diff --git a/app/Action/TaskUpdateStartDate.php b/app/Action/TaskUpdateStartDate.php new file mode 100644 index 00000000..4cd50c9a --- /dev/null +++ b/app/Action/TaskUpdateStartDate.php @@ -0,0 +1,83 @@ +<?php + +namespace Action; + +use Model\Task; + +/** + * Set the start date of task + * + * @package action + * @author Frederic Guillot + */ +class TaskUpdateStartDate extends Base +{ + /** + * Get the list of compatible events + * + * @access public + * @return array + */ + public function getCompatibleEvents() + { + return array( + Task::EVENT_MOVE_COLUMN, + ); + } + + /** + * Get the required parameter for the action (defined by the user) + * + * @access public + * @return array + */ + public function getActionRequiredParameters() + { + return array( + 'column_id' => t('Column'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + ); + } + + /** + * Execute the action (set the task color) + * + * @access public + * @param array $data Event data dictionary + * @return bool True if the action was executed or false when not executed + */ + public function doAction(array $data) + { + $values = array( + 'id' => $data['task_id'], + 'date_started' => time(), + ); + + return $this->taskModification->update($values); + } + + /** + * Check if the event data meet the action condition + * + * @access public + * @param array $data Event data dictionary + * @return bool + */ + public function hasRequiredCondition(array $data) + { + return $data['column_id'] == $this->getParam('column_id'); + } +} diff --git a/app/Api/Action.php b/app/Api/Action.php new file mode 100644 index 00000000..6187b776 --- /dev/null +++ b/app/Api/Action.php @@ -0,0 +1,98 @@ +<?php + +namespace Api; + +/** + * Action API controller + * + * @package api + * @author Frederic Guillot + */ +class Action extends Base +{ + public function getAvailableActions() + { + return $this->action->getAvailableActions(); + } + + public function getAvailableActionEvents() + { + return $this->action->getAvailableEvents(); + } + + public function getCompatibleActionEvents($action_name) + { + return $this->action->getCompatibleEvents($action_name); + } + + public function removeAction($action_id) + { + return $this->action->remove($action_id); + } + + public function getActions($project_id) + { + $actions = $this->action->getAllByProject($project_id); + + foreach ($actions as $index => $action) { + + $params = array(); + + foreach($action['params'] as $param) { + $params[$param['name']] = $param['value']; + } + + $actions[$index]['params'] = $params; + } + + return $actions; + } + + public function createAction($project_id, $event_name, $action_name, $params) + { + $values = array( + 'project_id' => $project_id, + 'event_name' => $event_name, + 'action_name' => $action_name, + 'params' => $params, + ); + + list($valid,) = $this->action->validateCreation($values); + + if (! $valid) { + return false; + } + + // Check if the action exists + $actions = $this->action->getAvailableActions(); + + if (! isset($actions[$action_name])) { + return false; + } + + // Check the event + $action = $this->action->load($action_name, $project_id, $event_name); + + if (! in_array($event_name, $action->getCompatibleEvents())) { + return false; + } + + $required_params = $action->getActionRequiredParameters(); + + // Check missing parameters + foreach($required_params as $param => $value) { + if (! isset($params[$param])) { + return false; + } + } + + // Check extra parameters + foreach($params as $param => $value) { + if (! isset($required_params[$param])) { + return false; + } + } + + return $this->action->create($values); + } +} diff --git a/app/Api/App.php b/app/Api/App.php new file mode 100644 index 00000000..2fc32e91 --- /dev/null +++ b/app/Api/App.php @@ -0,0 +1,22 @@ +<?php + +namespace Api; + +/** + * App API controller + * + * @package api + * @author Frederic Guillot + */ +class App extends Base +{ + public function getTimezone() + { + return $this->config->get('application_timezone'); + } + + public function getVersion() + { + return APP_VERSION; + } +} diff --git a/app/Api/Base.php b/app/Api/Base.php new file mode 100644 index 00000000..445b35be --- /dev/null +++ b/app/Api/Base.php @@ -0,0 +1,33 @@ +<?php + +namespace Api; + +use JsonRPC\AuthenticationFailure; +use Symfony\Component\EventDispatcher\Event; + +/** + * Base class + * + * @package api + * @author Frederic Guillot + */ +abstract class Base extends \Core\Base +{ + /** + * Check api credentials + * + * @access public + * @param string $username + * @param string $password + * @param string $class + * @param string $method + */ + public function authentication($username, $password, $class, $method) + { + $this->container['dispatcher']->dispatch('api.bootstrap', new Event); + + if (! ($username === 'jsonrpc' && $password === $this->config->get('api_token'))) { + throw new AuthenticationFailure('Wrong credentials'); + } + } +} diff --git a/app/Api/Board.php b/app/Api/Board.php new file mode 100644 index 00000000..163131b6 --- /dev/null +++ b/app/Api/Board.php @@ -0,0 +1,52 @@ +<?php + +namespace Api; + +/** + * Board API controller + * + * @package api + * @author Frederic Guillot + */ +class Board extends Base +{ + public function getBoard($project_id) + { + return $this->board->getBoard($project_id); + } + + public function getColumns($project_id) + { + return $this->board->getColumns($project_id); + } + + public function getColumn($column_id) + { + return $this->board->getColumn($column_id); + } + + public function moveColumnUp($project_id, $column_id) + { + return $this->board->moveUp($project_id, $column_id); + } + + public function moveColumnDown($project_id, $column_id) + { + return $this->board->moveDown($project_id, $column_id); + } + + public function updateColumn($column_id, $title, $task_limit = 0, $description = '') + { + return $this->board->updateColumn($column_id, $title, $task_limit, $description); + } + + public function addColumn($project_id, $title, $task_limit = 0, $description = '') + { + return $this->board->addColumn($project_id, $title, $task_limit, $description); + } + + public function removeColumn($column_id) + { + return $this->board->removeColumn($column_id); + } +} diff --git a/app/Api/Category.php b/app/Api/Category.php new file mode 100644 index 00000000..f457ddf1 --- /dev/null +++ b/app/Api/Category.php @@ -0,0 +1,49 @@ +<?php + +namespace Api; + +/** + * Category API controller + * + * @package api + * @author Frederic Guillot + */ +class Category extends Base +{ + public function getCategory($category_id) + { + return $this->category->getById($category_id); + } + + public function getAllCategories($project_id) + { + return $this->category->getAll($project_id); + } + + public function removeCategory($category_id) + { + return $this->category->remove($category_id); + } + + public function createCategory($project_id, $name) + { + $values = array( + 'project_id' => $project_id, + 'name' => $name, + ); + + list($valid,) = $this->category->validateCreation($values); + return $valid ? $this->category->create($values) : false; + } + + public function updateCategory($id, $name) + { + $values = array( + 'id' => $id, + 'name' => $name, + ); + + list($valid,) = $this->category->validateModification($values); + return $valid && $this->category->update($values); + } +} diff --git a/app/Api/Comment.php b/app/Api/Comment.php new file mode 100644 index 00000000..19b84383 --- /dev/null +++ b/app/Api/Comment.php @@ -0,0 +1,51 @@ +<?php + +namespace Api; + +/** + * Comment API controller + * + * @package api + * @author Frederic Guillot + */ +class Comment extends Base +{ + public function getComment($comment_id) + { + return $this->comment->getById($comment_id); + } + + public function getAllComments($task_id) + { + return $this->comment->getAll($task_id); + } + + public function removeComment($comment_id) + { + return $this->comment->remove($comment_id); + } + + public function createComment($task_id, $user_id, $content) + { + $values = array( + 'task_id' => $task_id, + 'user_id' => $user_id, + 'comment' => $content, + ); + + list($valid,) = $this->comment->validateCreation($values); + + return $valid ? $this->comment->create($values) : false; + } + + public function updateComment($id, $content) + { + $values = array( + 'id' => $id, + 'comment' => $content, + ); + + list($valid,) = $this->comment->validateModification($values); + return $valid && $this->comment->update($values); + } +} diff --git a/app/Api/File.php b/app/Api/File.php new file mode 100644 index 00000000..65dff729 --- /dev/null +++ b/app/Api/File.php @@ -0,0 +1,48 @@ +<?php + +namespace Api; + +/** + * File API controller + * + * @package api + * @author Frederic Guillot + */ +class File extends Base +{ + public function getFile($file_id) + { + return $this->file->getById($file_id); + } + + public function getAllFiles($task_id) + { + return $this->file->getAll($task_id); + } + + public function downloadFile($file_id) + { + $file = $this->file->getById($file_id); + + if (! empty($file)) { + + $filename = FILES_DIR.$file['path']; + + if (file_exists($filename)) { + return base64_encode(file_get_contents($filename)); + } + } + + return ''; + } + + public function createFile($project_id, $task_id, $filename, $is_image, $blob) + { + return $this->file->uploadContent($project_id, $task_id, $filename, $is_image, $blob); + } + + public function removeFile($file_id) + { + return $this->file->remove($file_id); + } +} diff --git a/app/Api/Link.php b/app/Api/Link.php new file mode 100644 index 00000000..b9084784 --- /dev/null +++ b/app/Api/Link.php @@ -0,0 +1,111 @@ +<?php + +namespace Api; + +/** + * Link API controller + * + * @package api + * @author Frederic Guillot + */ +class Link extends Base +{ + /** + * Get a link by id + * + * @access public + * @param integer $link_id Link id + * @return array + */ + public function getLinkById($link_id) + { + return $this->link->getById($link_id); + } + + /** + * Get a link by name + * + * @access public + * @param string $label + * @return array + */ + public function getLinkByLabel($label) + { + return $this->link->getByLabel($label); + } + + /** + * Get the opposite link id + * + * @access public + * @param integer $link_id Link id + * @return integer + */ + public function getOppositeLinkId($link_id) + { + return $this->link->getOppositeLinkId($link_id); + } + + /** + * Get all links + * + * @access public + * @return array + */ + public function getAllLinks() + { + return $this->link->getAll(); + } + + /** + * Create a new link label + * + * @access public + * @param string $label + * @param string $opposite_label + * @return boolean|integer + */ + public function createLink($label, $opposite_label = '') + { + $values = array( + 'label' => $label, + 'opposite_label' => $opposite_label, + ); + + list($valid,) = $this->link->validateCreation($values); + return $valid ? $this->link->create($label, $opposite_label) : false; + } + + /** + * Update a link + * + * @access public + * @param integer $link_id + * @param integer $opposite_link_id + * @param string $label + * @return boolean + */ + public function updateLink($link_id, $opposite_link_id, $label) + { + $values = array( + 'id' => $link_id, + 'opposite_id' => $opposite_link_id, + 'label' => $label, + ); + + list($valid,) = $this->link->validateModification($values); + return $valid && $this->link->update($values); + } + + /** + * Remove a link a the relation to its opposite + * + * @access public + * @param integer $link_id + * @return boolean + */ + public function removeLink($link_id) + { + return $this->link->remove($link_id); + } +} diff --git a/app/Api/Project.php b/app/Api/Project.php new file mode 100644 index 00000000..2451cd9c --- /dev/null +++ b/app/Api/Project.php @@ -0,0 +1,85 @@ +<?php + +namespace Api; + +/** + * Project API controller + * + * @package api + * @author Frederic Guillot + */ +class Project extends Base +{ + public function getProjectById($project_id) + { + return $this->project->getById($project_id); + } + + public function getProjectByName($name) + { + return $this->project->getByName($name); + } + + public function getAllProjects() + { + return $this->project->getAll(); + } + + public function removeProject($project_id) + { + return $this->project->remove($project_id); + } + + public function enableProject($project_id) + { + return $this->project->enable($project_id); + } + + public function disableProject($project_id) + { + return $this->project->disable($project_id); + } + + public function enableProjectPublicAccess($project_id) + { + return $this->project->enablePublicAccess($project_id); + } + + public function disableProjectPublicAccess($project_id) + { + return $this->project->disablePublicAccess($project_id); + } + + public function getProjectActivities(array $project_ids) + { + return $this->projectActivity->getProjects($project_ids); + } + + public function getProjectActivity($project_id) + { + return $this->projectActivity->getProject($project_id); + } + + public function createProject($name, $description = null) + { + $values = array( + 'name' => $name, + 'description' => $description + ); + + list($valid,) = $this->project->validateCreation($values); + return $valid ? $this->project->create($values) : false; + } + + public function updateProject($id, $name, $description = null) + { + $values = array( + 'id' => $id, + 'name' => $name, + 'description' => $description + ); + + list($valid,) = $this->project->validateModification($values); + return $valid && $this->project->update($values); + } +} diff --git a/app/Api/ProjectPermission.php b/app/Api/ProjectPermission.php new file mode 100644 index 00000000..a31faf3d --- /dev/null +++ b/app/Api/ProjectPermission.php @@ -0,0 +1,27 @@ +<?php + +namespace Api; + +/** + * ProjectPermission API controller + * + * @package api + * @author Frederic Guillot + */ +class ProjectPermission extends Base +{ + public function getMembers($project_id) + { + return $this->projectPermission->getMembers($project_id); + } + + public function revokeUser($project_id, $user_id) + { + return $this->projectPermission->revokeMember($project_id, $user_id); + } + + public function allowUser($project_id, $user_id) + { + return $this->projectPermission->addMember($project_id, $user_id); + } +} diff --git a/app/Api/Subtask.php b/app/Api/Subtask.php new file mode 100644 index 00000000..2e6c30f2 --- /dev/null +++ b/app/Api/Subtask.php @@ -0,0 +1,64 @@ +<?php + +namespace Api; + +/** + * Subtask API controller + * + * @package api + * @author Frederic Guillot + */ +class Subtask extends Base +{ + public function getSubtask($subtask_id) + { + return $this->subtask->getById($subtask_id); + } + + public function getAllSubtasks($task_id) + { + return $this->subtask->getAll($task_id); + } + + public function removeSubtask($subtask_id) + { + return $this->subtask->remove($subtask_id); + } + + public function createSubtask($task_id, $title, $user_id = 0, $time_estimated = 0, $time_spent = 0, $status = 0) + { + $values = array( + 'title' => $title, + 'task_id' => $task_id, + 'user_id' => $user_id, + 'time_estimated' => $time_estimated, + 'time_spent' => $time_spent, + 'status' => $status, + ); + + list($valid,) = $this->subtask->validateCreation($values); + return $valid ? $this->subtask->create($values) : false; + } + + public function updateSubtask($id, $task_id, $title = null, $user_id = null, $time_estimated = null, $time_spent = null, $status = null) + { + $values = array( + 'id' => $id, + 'task_id' => $task_id, + 'title' => $title, + 'user_id' => $user_id, + 'time_estimated' => $time_estimated, + 'time_spent' => $time_spent, + 'status' => $status, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid,) = $this->subtask->validateApiModification($values); + return $valid && $this->subtask->update($values); + } +} diff --git a/app/Api/Swimlane.php b/app/Api/Swimlane.php new file mode 100644 index 00000000..322b0805 --- /dev/null +++ b/app/Api/Swimlane.php @@ -0,0 +1,77 @@ +<?php + +namespace Api; + +/** + * Swimlane API controller + * + * @package api + * @author Frederic Guillot + */ +class Swimlane extends Base +{ + public function getActiveSwimlanes($project_id) + { + return $this->swimlane->getSwimlanes($project_id); + } + + public function getAllSwimlanes($project_id) + { + return $this->swimlane->getAll($project_id); + } + + public function getSwimlaneById($swimlane_id) + { + return $this->swimlane->getById($swimlane_id); + } + + public function getSwimlaneByName($project_id, $name) + { + return $this->swimlane->getByName($project_id, $name); + } + + public function getSwimlane($swimlane_id) + { + return $this->swimlane->getById($swimlane_id); + } + + public function getDefaultSwimlane($project_id) + { + return $this->swimlane->getDefault($project_id); + } + + public function addSwimlane($project_id, $name) + { + return $this->swimlane->create($project_id, $name); + } + + public function updateSwimlane($swimlane_id, $name) + { + return $this->swimlane->rename($swimlane_id, $name); + } + + public function removeSwimlane($project_id, $swimlane_id) + { + return $this->swimlane->remove($project_id, $swimlane_id); + } + + public function disableSwimlane($project_id, $swimlane_id) + { + return $this->swimlane->disable($project_id, $swimlane_id); + } + + public function enableSwimlane($project_id, $swimlane_id) + { + return $this->swimlane->enable($project_id, $swimlane_id); + } + + public function moveSwimlaneUp($project_id, $swimlane_id) + { + return $this->swimlane->moveUp($project_id, $swimlane_id); + } + + public function moveSwimlaneDown($project_id, $swimlane_id) + { + return $this->swimlane->moveDown($project_id, $swimlane_id); + } +} diff --git a/app/Api/Task.php b/app/Api/Task.php new file mode 100644 index 00000000..c98b24a6 --- /dev/null +++ b/app/Api/Task.php @@ -0,0 +1,113 @@ +<?php + +namespace Api; + +use Model\Task as TaskModel; + +/** + * Task API controller + * + * @package api + * @author Frederic Guillot + */ +class Task extends Base +{ + public function getTask($task_id) + { + return $this->taskFinder->getById($task_id); + } + + public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN) + { + return $this->taskFinder->getAll($project_id, $status_id); + } + + public function getOverdueTasks() + { + return $this->taskFinder->getOverdueTasks(); + } + + public function openTask($task_id) + { + return $this->taskStatus->open($task_id); + } + + public function closeTask($task_id) + { + return $this->taskStatus->close($task_id); + } + + public function removeTask($task_id) + { + return $this->task->remove($task_id); + } + + public function moveTaskPosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0) + { + return $this->taskPosition->movePosition($project_id, $task_id, $column_id, $position, $swimlane_id); + } + + public function createTask($title, $project_id, $color_id = '', $column_id = 0, $owner_id = 0, $creator_id = 0, + $date_due = '', $description = '', $category_id = 0, $score = 0, $swimlane_id = 0, + $recurrence_status = 0, $recurrence_trigger = 0, $recurrence_factor = 0, $recurrence_timeframe = 0, + $recurrence_basedate = 0) + { + $values = array( + 'title' => $title, + 'project_id' => $project_id, + 'color_id' => $color_id, + 'column_id' => $column_id, + 'owner_id' => $owner_id, + 'creator_id' => $creator_id, + 'date_due' => $date_due, + 'description' => $description, + 'category_id' => $category_id, + 'score' => $score, + 'swimlane_id' => $swimlane_id, + 'recurrence_status' => $recurrence_status, + 'recurrence_trigger' => $recurrence_trigger, + 'recurrence_factor' => $recurrence_factor, + 'recurrence_timeframe' => $recurrence_timeframe, + 'recurrence_basedate' => $recurrence_basedate, + ); + + list($valid,) = $this->taskValidator->validateCreation($values); + + return $valid ? $this->taskCreation->create($values) : false; + } + + public function updateTask($id, $title = null, $project_id = null, $color_id = null, $column_id = null, $owner_id = null, + $creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null, + $swimlane_id = null, $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, + $recurrence_timeframe = null, $recurrence_basedate = null) + { + $values = array( + 'id' => $id, + 'title' => $title, + 'project_id' => $project_id, + 'color_id' => $color_id, + 'column_id' => $column_id, + 'owner_id' => $owner_id, + 'creator_id' => $creator_id, + 'date_due' => $date_due, + 'description' => $description, + 'category_id' => $category_id, + 'score' => $score, + 'swimlane_id' => $swimlane_id, + 'recurrence_status' => $recurrence_status, + 'recurrence_trigger' => $recurrence_trigger, + 'recurrence_factor' => $recurrence_factor, + 'recurrence_timeframe' => $recurrence_timeframe, + 'recurrence_basedate' => $recurrence_basedate, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid) = $this->taskValidator->validateApiModification($values); + return $valid && $this->taskModification->update($values); + } +} diff --git a/app/Api/TaskLink.php b/app/Api/TaskLink.php new file mode 100644 index 00000000..c3e1a83c --- /dev/null +++ b/app/Api/TaskLink.php @@ -0,0 +1,77 @@ +<?php + +namespace Api; + +/** + * TaskLink API controller + * + * @package api + * @author Frederic Guillot + */ +class TaskLink extends Base +{ + /** + * Get a task link + * + * @access public + * @param integer $task_link_id Task link id + * @return array + */ + public function getTaskLinkById($task_link_id) + { + return $this->taskLink->getById($task_link_id); + } + + /** + * Get all links attached to a task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAllTaskLinks($task_id) + { + return $this->taskLink->getAll($task_id); + } + + /** + * Create a new link + * + * @access public + * @param integer $task_id Task id + * @param integer $opposite_task_id Opposite task id + * @param integer $link_id Link id + * @return integer Task link id + */ + public function createTaskLink($task_id, $opposite_task_id, $link_id) + { + return $this->taskLink->create($task_id, $opposite_task_id, $link_id); + } + + /** + * Update a task link + * + * @access public + * @param integer $task_link_id Task link id + * @param integer $task_id Task id + * @param integer $opposite_task_id Opposite task id + * @param integer $link_id Link id + * @return boolean + */ + public function updateTaskLink($task_link_id, $task_id, $opposite_task_id, $link_id) + { + return $this->taskLink->update($task_link_id, $task_id, $opposite_task_id, $link_id); + } + + /** + * Remove a link between two tasks + * + * @access public + * @param integer $task_link_id + * @return boolean + */ + public function removeTaskLink($task_link_id) + { + return $this->taskLink->remove($task_link_id); + } +} diff --git a/app/Api/User.php b/app/Api/User.php new file mode 100644 index 00000000..166ef2c1 --- /dev/null +++ b/app/Api/User.php @@ -0,0 +1,88 @@ +<?php + +namespace Api; + +use Auth\Ldap; + +/** + * User API controller + * + * @package api + * @author Frederic Guillot + */ +class User extends Base +{ + public function getUser($user_id) + { + return $this->user->getById($user_id); + } + + public function getAllUsers() + { + return $this->user->getAll(); + } + + public function removeUser($user_id) + { + return $this->user->remove($user_id); + } + + public function createUser($username, $password, $name = '', $email = '', $is_admin = 0, $default_project_id = 0) + { + $values = array( + 'username' => $username, + 'password' => $password, + 'confirmation' => $password, + 'name' => $name, + 'email' => $email, + 'is_admin' => $is_admin, + 'default_project_id' => $default_project_id, + ); + + list($valid,) = $this->user->validateCreation($values); + + return $valid ? $this->user->create($values) : false; + } + + public function createLdapUser($username = '', $email = '', $is_admin = 0, $default_project_id = 0) + { + $ldap = new Ldap($this->container); + $user = $ldap->lookup($username, $email); + + if (! $user) { + return false; + } + + $values = array( + 'username' => $user['username'], + 'name' => $user['name'], + 'email' => $user['email'], + 'is_ldap_user' => 1, + 'is_admin' => $is_admin, + 'default_project_id' => $default_project_id, + ); + + return $this->user->create($values); + } + + public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $default_project_id = null) + { + $values = array( + 'id' => $id, + 'username' => $username, + 'name' => $name, + 'email' => $email, + 'is_admin' => $is_admin, + 'default_project_id' => $default_project_id, + ); + + foreach ($values as $key => $value) { + if (is_null($value)) { + unset($values[$key]); + } + } + + list($valid,) = $this->user->validateApiModification($values); + return $valid && $this->user->update($values); + } +} diff --git a/app/Auth/Base.php b/app/Auth/Base.php index e174ff8f..ebf6681b 100644 --- a/app/Auth/Base.php +++ b/app/Auth/Base.php @@ -2,20 +2,15 @@ namespace Auth; -use Core\Tool; -use Core\Registry; +use Pimple\Container; /** * Base auth class * * @package auth * @author Frederic Guillot - * - * @property \Model\Acl $acl - * @property \Model\LastLogin $lastLogin - * @property \Model\User $user */ -abstract class Base +abstract class Base extends \Core\Base { /** * Database instance @@ -26,34 +21,14 @@ abstract class Base protected $db; /** - * Registry instance - * - * @access protected - * @var \Core\Registry - */ - protected $registry; - - /** * Constructor * * @access public - * @param \Core\Registry $registry Registry instance - */ - public function __construct(Registry $registry) - { - $this->registry = $registry; - $this->db = $this->registry->shared('db'); - } - - /** - * Load automatically models - * - * @access public - * @param string $name Model name - * @return mixed + * @param \Pimple\Container $container */ - public function __get($name) + public function __construct(Container $container) { - return Tool::loadModel($this->registry, $name); + $this->container = $container; + $this->db = $this->container['db']; } } diff --git a/app/Auth/Database.php b/app/Auth/Database.php index 47dc8e6e..e69f18a9 100644 --- a/app/Auth/Database.php +++ b/app/Auth/Database.php @@ -3,7 +3,7 @@ namespace Auth; use Model\User; -use Core\Request; +use Event\AuthEvent; /** * Database authentication @@ -30,21 +30,16 @@ class Database extends Base */ public function authenticate($username, $password) { - $user = $this->db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne(); - - if ($user && password_verify($password, $user['password'])) { - - // Update user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); - + $user = $this->db + ->table(User::TABLE) + ->eq('username', $username) + ->eq('disable_login_form', 0) + ->eq('is_ldap_user', 0) + ->findOne(); + + if (is_array($user) && password_verify($password, $user['password'])) { + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); return true; } diff --git a/app/Auth/GitHub.php b/app/Auth/GitHub.php index 096d4101..816cc9c1 100644 --- a/app/Auth/GitHub.php +++ b/app/Auth/GitHub.php @@ -2,9 +2,7 @@ namespace Auth; -require __DIR__.'/../../vendor/OAuth/bootstrap.php'; - -use Core\Request; +use Event\AuthEvent; use OAuth\Common\Storage\Session; use OAuth\Common\Consumer\Credentials; use OAuth\Common\Http\Uri\UriFactory; @@ -36,19 +34,9 @@ class GitHub extends Base { $user = $this->user->getByGitHubId($github_id); - if ($user) { - - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); - + if (! empty($user)) { + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); return true; } diff --git a/app/Auth/Google.php b/app/Auth/Google.php index 2bed5b09..9a977037 100644 --- a/app/Auth/Google.php +++ b/app/Auth/Google.php @@ -2,9 +2,7 @@ namespace Auth; -require __DIR__.'/../../vendor/OAuth/bootstrap.php'; - -use Core\Request; +use Event\AuthEvent; use OAuth\Common\Storage\Session; use OAuth\Common\Consumer\Credentials; use OAuth\Common\Http\Uri\UriFactory; @@ -37,19 +35,9 @@ class Google extends Base { $user = $this->user->getByGoogleId($google_id); - if ($user) { - - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); - + if (! empty($user)) { + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); return true; } diff --git a/app/Auth/Ldap.php b/app/Auth/Ldap.php index 4f20998f..3ee6ec9b 100644 --- a/app/Auth/Ldap.php +++ b/app/Auth/Ldap.php @@ -2,7 +2,7 @@ namespace Auth; -use Core\Request; +use Event\AuthEvent; /** * LDAP model @@ -29,13 +29,14 @@ class Ldap extends Base */ public function authenticate($username, $password) { + $username = LDAP_USERNAME_CASE_SENSITIVE ? $username : strtolower($username); $result = $this->findUser($username, $password); if (is_array($result)) { $user = $this->user->getByUsername($username); - if ($user) { + if (! empty($user)) { // There is already a local user with that name if ($user['is_ldap_user'] == 0) { @@ -54,15 +55,8 @@ class Ldap extends Base } // We open the session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); return true; } @@ -104,7 +98,7 @@ class Ldap extends Base { $ldap = $this->connect(); - if ($this->bind($ldap, $username, $password)) { + if (is_resource($ldap) && $this->bind($ldap, $username, $password)) { return $this->search($ldap, $username, $password); } @@ -136,6 +130,12 @@ class Ldap extends Base ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0); + ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 1); + ldap_set_option($ldap, LDAP_OPT_TIMELIMIT, 1); + + if (LDAP_START_TLS && ! @ldap_start_tls($ldap)) { + die('Unable to use ldap_start_tls()'); + } return $ldap; } @@ -200,11 +200,90 @@ class Ldap extends Base return array( 'username' => $username, - 'name' => isset($info[0][LDAP_ACCOUNT_FULLNAME][0]) ? $info[0][LDAP_ACCOUNT_FULLNAME][0] : '', - 'email' => isset($info[0][LDAP_ACCOUNT_EMAIL][0]) ? $info[0][LDAP_ACCOUNT_EMAIL][0] : '', + 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME), + 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL), ); } return false; } + + /** + * Retrieve info on LDAP user + * + * @param string $username Username + * @param string $email Email address + */ + public function lookup($username = null, $email = null) + { + $query = $this->getQuery($username, $email); + if ($query === false) { + return false; + } + + // Connect and attempt anonymous bind + $ldap = $this->connect(); + if (! is_resource($ldap) || ! $this->bind($ldap, null, null)) { + return false; + } + + // Try to find user + $sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, $query, array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL, LDAP_ACCOUNT_ID)); + if ($sr === false) { + return false; + } + + $info = ldap_get_entries($ldap, $sr); + + // User not found + if (count($info) == 0 || $info['count'] == 0) { + return false; + } + + // User id not retrieved: LDAP_ACCOUNT_ID not properly configured + if (empty($username) && ! isset($info[0][LDAP_ACCOUNT_ID][0])) { + return false; + } + + return array( + 'username' => $this->getFromInfo($info, LDAP_ACCOUNT_ID, $username), + 'name' => $this->getFromInfo($info, LDAP_ACCOUNT_FULLNAME), + 'email' => $this->getFromInfo($info, LDAP_ACCOUNT_EMAIL, $email), + ); + } + + /** + * Get the LDAP query to find a user + * + * @param string $username Username + * @param string $email Email address + */ + private function getQuery($username, $email) + { + if ($username && $email) { + return '(&('.sprintf(LDAP_USER_PATTERN, $username).')('.LDAP_ACCOUNT_EMAIL.'='.$email.'))'; + } + else if ($username) { + return sprintf(LDAP_USER_PATTERN, $username); + } + else if ($email) { + return '('.LDAP_ACCOUNT_EMAIL.'='.$email.')'; + } + else { + return false; + } + } + + /** + * Return a value from the LDAP info + * + * @param array $info LDAP info + * @param string $key Key + * @param string $default Default value if key not set in entry + * @return string + */ + private function getFromInfo($info, $key, $default = '') + { + return isset($info[0][$key][0]) ? $info[0][$key][0] : $default; + } } diff --git a/app/Auth/RememberMe.php b/app/Auth/RememberMe.php index 380abbed..e8b20f37 100644 --- a/app/Auth/RememberMe.php +++ b/app/Auth/RememberMe.php @@ -3,8 +3,8 @@ namespace Auth; use Core\Request; +use Event\AuthEvent; use Core\Security; -use Core\Tool; /** * RememberMe model @@ -96,20 +96,19 @@ class RememberMe extends Base // Update the sequence $this->writeCookie( $record['token'], - $this->update($record['token'], $record['sequence']), + $this->update($record['token']), $record['expiration'] ); // Create the session - $this->user->updateSession($this->user->getById($record['user_id'])); - $this->acl->isRememberMe(true); - - // Update last login infos - $this->lastLogin->create( - self::AUTH_NAME, - $this->acl->getUserId(), - Request::getIpAddress(), - Request::getUserAgent() + $this->userSession->refresh($this->user->getById($record['user_id'])); + + // Do not ask 2FA for remember me session + $this->session['2fa_validated'] = true; + + $this->container['dispatcher']->dispatch( + 'auth.success', + new AuthEvent(self::AUTH_NAME, $this->userSession->getId()) ); return true; @@ -137,7 +136,7 @@ class RememberMe extends Base // Update the sequence $this->writeCookie( $record['token'], - $this->update($record['token'], $record['sequence']), + $this->update($record['token']), $record['expiration'] ); } @@ -238,17 +237,15 @@ class RememberMe extends Base * * @access public * @param string $token Session token - * @param string $sequence Sequence token * @return string */ - public function update($token, $sequence) + public function update($token) { $new_sequence = Security::generateToken(); $this->db ->table(self::TABLE) ->eq('token', $token) - ->eq('sequence', $sequence) ->update(array('sequence' => $new_sequence)); return $new_sequence; @@ -311,7 +308,7 @@ class RememberMe extends Base $expiration, BASE_URL_DIRECTORY, null, - Tool::isHTTPS(), + Request::isHTTPS(), true ); } @@ -344,7 +341,7 @@ class RememberMe extends Base time() - 3600, BASE_URL_DIRECTORY, null, - Tool::isHTTPS(), + Request::isHTTPS(), true ); } diff --git a/app/Auth/ReverseProxy.php b/app/Auth/ReverseProxy.php index 5aca881a..c8fd5eec 100644 --- a/app/Auth/ReverseProxy.php +++ b/app/Auth/ReverseProxy.php @@ -2,8 +2,7 @@ namespace Auth; -use Core\Request; -use Core\Security; +use Event\AuthEvent; /** * ReverseProxy backend @@ -33,21 +32,13 @@ class ReverseProxy extends Base $login = $_SERVER[REVERSE_PROXY_USER_HEADER]; $user = $this->user->getByUsername($login); - if (! $user) { + if (empty($user)) { $this->createUser($login); $user = $this->user->getByUsername($login); } - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - self::AUTH_NAME, - $user['id'], - Request::getIpAddress(), - Request::getUserAgent() - ); + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); return true; } @@ -75,6 +66,7 @@ class ReverseProxy extends Base 'username' => $login, 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login, 'is_ldap_user' => 1, + 'disable_login_form' => 1, )); } } diff --git a/app/Console/Base.php b/app/Console/Base.php new file mode 100644 index 00000000..07243080 --- /dev/null +++ b/app/Console/Base.php @@ -0,0 +1,58 @@ +<?php + +namespace Console; + +use Pimple\Container; +use Symfony\Component\Console\Command\Command; + +/** + * Base command class + * + * @package console + * @author Frederic Guillot + * + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\ProjectPermission $projectPermission + * @property \Model\ProjectAnalytic $projectAnalytic + * @property \Model\ProjectDailySummary $projectDailySummary + * @property \Model\SubtaskExport $subtaskExport + * @property \Model\Task $task + * @property \Model\TaskExport $taskExport + * @property \Model\TaskFinder $taskFinder + * @property \Model\Transition $transition + */ +abstract class Base extends Command +{ + /** + * Container instance + * + * @access protected + * @var \Pimple\Container + */ + protected $container; + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + parent::__construct(); + $this->container = $container; + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return $this->container[$name]; + } +} diff --git a/app/Console/ProjectDailySummaryCalculation.php b/app/Console/ProjectDailySummaryCalculation.php new file mode 100644 index 00000000..b2ada1b6 --- /dev/null +++ b/app/Console/ProjectDailySummaryCalculation.php @@ -0,0 +1,27 @@ +<?php + +namespace Console; + +use Model\Project; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ProjectDailySummaryCalculation extends Base +{ + protected function configure() + { + $this + ->setName('projects:daily-summary') + ->setDescription('Calculate daily summary data for all projects'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $projects = $this->project->getAllByStatus(Project::ACTIVE); + + foreach ($projects as $project) { + $output->writeln('Run calculation for '.$project['name']); + $this->projectDailySummary->updateTotals($project['id'], date('Y-m-d')); + } + } +} diff --git a/app/Console/ProjectDailySummaryExport.php b/app/Console/ProjectDailySummaryExport.php new file mode 100644 index 00000000..07841d52 --- /dev/null +++ b/app/Console/ProjectDailySummaryExport.php @@ -0,0 +1,34 @@ +<?php + +namespace Console; + +use Core\Tool; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ProjectDailySummaryExport extends Base +{ + protected function configure() + { + $this + ->setName('export:daily-project-summary') + ->setDescription('Daily project summary CSV export (number of tasks per column and per day)') + ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') + ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') + ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->projectDailySummary->getAggregatedMetrics( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Tool::csv($data); + } + } +} diff --git a/app/Console/SubtaskExport.php b/app/Console/SubtaskExport.php new file mode 100644 index 00000000..167a9225 --- /dev/null +++ b/app/Console/SubtaskExport.php @@ -0,0 +1,34 @@ +<?php + +namespace Console; + +use Core\Tool; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SubtaskExport extends Base +{ + protected function configure() + { + $this + ->setName('export:subtasks') + ->setDescription('Subtasks CSV export') + ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') + ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') + ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->subtaskExport->export( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Tool::csv($data); + } + } +} diff --git a/app/Console/TaskExport.php b/app/Console/TaskExport.php new file mode 100644 index 00000000..2ecd45e5 --- /dev/null +++ b/app/Console/TaskExport.php @@ -0,0 +1,34 @@ +<?php + +namespace Console; + +use Core\Tool; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class TaskExport extends Base +{ + protected function configure() + { + $this + ->setName('export:tasks') + ->setDescription('Tasks CSV export') + ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') + ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') + ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->taskExport->export( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Tool::csv($data); + } + } +} diff --git a/app/Console/TaskOverdueNotification.php b/app/Console/TaskOverdueNotification.php new file mode 100644 index 00000000..3d254ae4 --- /dev/null +++ b/app/Console/TaskOverdueNotification.php @@ -0,0 +1,50 @@ +<?php + +namespace Console; + +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class TaskOverdueNotification extends Base +{ + protected function configure() + { + $this + ->setName('notification:overdue-tasks') + ->setDescription('Send notifications for overdue tasks') + ->addOption('show', null, InputOption::VALUE_NONE, 'Show sent overdue tasks'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $tasks = $this->notification->sendOverdueTaskNotifications(); + + if ($input->getOption('show')) { + $this->showTable($output, $tasks); + } + } + + public function showTable(OutputInterface $output, array $tasks) + { + $rows = array(); + + foreach ($tasks as $task) { + $rows[] = array( + $task['id'], + $task['title'], + date('Y-m-d', $task['date_due']), + $task['project_id'], + $task['project_name'], + $task['assignee_name'] ?: $task['assignee_username'], + ); + } + + $table = new Table($output); + $table + ->setHeaders(array('Id', 'Title', 'Due date', 'Project Id', 'Project name', 'Assignee')) + ->setRows($rows) + ->render(); + } +} diff --git a/app/Console/TransitionExport.php b/app/Console/TransitionExport.php new file mode 100644 index 00000000..ad988c54 --- /dev/null +++ b/app/Console/TransitionExport.php @@ -0,0 +1,34 @@ +<?php + +namespace Console; + +use Core\Tool; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class TransitionExport extends Base +{ + protected function configure() + { + $this + ->setName('export:transitions') + ->setDescription('Task transitions CSV export') + ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') + ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') + ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->transition->export( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Tool::csv($data); + } + } +} diff --git a/app/Controller/Action.php b/app/Controller/Action.php index 714c87f3..cd24453a 100644 --- a/app/Controller/Action.php +++ b/app/Controller/Action.php @@ -17,9 +17,9 @@ class Action extends Base */ public function index() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('action_index', array( + $this->response->html($this->projectLayout('action/index', array( 'values' => array('project_id' => $project['id']), 'project' => $project, 'actions' => $this->action->getAllByProject($project['id']), @@ -27,11 +27,10 @@ class Action extends Base 'available_events' => $this->action->getAvailableEvents(), 'available_params' => $this->action->getAllActionParameters(), 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectPermission->getUsersList($project['id']), + 'users_list' => $this->projectPermission->getMemberList($project['id']), 'projects_list' => $this->project->getList(false), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), - 'menu' => 'projects', 'title' => t('Automatic actions') ))); } @@ -43,18 +42,17 @@ class Action extends Base */ public function event() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); if (empty($values['action_name']) || empty($values['project_id'])) { $this->response->redirect('?controller=action&action=index&project_id='.$project['id']); } - $this->response->html($this->projectLayout('action_event', array( + $this->response->html($this->projectLayout('action/event', array( 'values' => $values, 'project' => $project, 'events' => $this->action->getCompatibleEvents($values['action_name']), - 'menu' => 'projects', 'title' => t('Automatic actions') ))); } @@ -66,7 +64,7 @@ class Action extends Base */ public function params() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); if (empty($values['action_name']) || empty($values['project_id']) || empty($values['event_name'])) { @@ -83,16 +81,15 @@ class Action extends Base $projects_list = $this->project->getList(false); unset($projects_list[$project['id']]); - $this->response->html($this->projectLayout('action_params', array( + $this->response->html($this->projectLayout('action/params', array( 'values' => $values, 'action_params' => $action_params, 'columns_list' => $this->board->getColumnsList($project['id']), - 'users_list' => $this->projectPermission->getUsersList($project['id']), + 'users_list' => $this->projectPermission->getMemberList($project['id']), 'projects_list' => $projects_list, 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($project['id']), 'project' => $project, - 'menu' => 'projects', 'title' => t('Automatic actions') ))); } @@ -104,7 +101,7 @@ class Action extends Base */ public function create() { - $this->doCreation($this->getProjectManagement(), $this->request->getValues()); + $this->doCreation($this->getProject(), $this->request->getValues()); } /** @@ -138,14 +135,13 @@ class Action extends Base */ public function confirm() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('action_remove', array( + $this->response->html($this->projectLayout('action/remove', array( 'action' => $this->action->getById($this->request->getIntegerParam('action_id')), 'available_events' => $this->action->getAvailableEvents(), 'available_actions' => $this->action->getAvailableActions(), 'project' => $project, - 'menu' => 'projects', 'title' => t('Remove an action') ))); } @@ -158,10 +154,10 @@ class Action extends Base public function remove() { $this->checkCSRFParam(); - $project = $this->getProjectManagement(); + $project = $this->getProject(); $action = $this->action->getById($this->request->getIntegerParam('action_id')); - if ($action && $this->action->remove($action['id'])) { + if (! empty($action) && $this->action->remove($action['id'])) { $this->session->flash(t('Action removed successfully.')); } else { $this->session->flashError(t('Unable to remove this action.')); diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php new file mode 100644 index 00000000..f31870e0 --- /dev/null +++ b/app/Controller/Analytic.php @@ -0,0 +1,170 @@ +<?php + +namespace Controller; + +/** + * Project Anaytic controller + * + * @package controller + * @author Frederic Guillot + */ +class Analytic extends Base +{ + /** + * Common layout for analytic views + * + * @access private + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + private function layout($template, array $params) + { + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['content_for_sublayout'] = $this->template->render($template, $params); + + return $this->template->layout('analytic/layout', $params); + } + + /** + * Show tasks distribution graph + * + * @access public + */ + public function tasks() + { + $project = $this->getProject(); + $metrics = $this->projectAnalytic->getTaskRepartition($project['id']); + + if ($this->request->isAjax()) { + $this->response->json(array( + 'metrics' => $metrics, + 'labels' => array( + 'column_title' => t('Column'), + 'nb_tasks' => t('Number of tasks'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/tasks', array( + 'project' => $project, + 'metrics' => $metrics, + 'title' => t('Task repartition for "%s"', $project['name']), + ))); + } + } + + /** + * Show users repartition + * + * @access public + */ + public function users() + { + $project = $this->getProject(); + $metrics = $this->projectAnalytic->getUserRepartition($project['id']); + + if ($this->request->isAjax()) { + $this->response->json(array( + 'metrics' => $metrics, + 'labels' => array( + 'user' => t('User'), + 'nb_tasks' => t('Number of tasks'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/users', array( + 'project' => $project, + 'metrics' => $metrics, + 'title' => t('User repartition for "%s"', $project['name']), + ))); + } + } + + /** + * Show cumulative flow diagram + * + * @access public + */ + public function cfd() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + if ($this->request->isAjax()) { + $this->response->json(array( + 'columns' => array_values($this->board->getColumnsList($project['id'])), + 'metrics' => $this->projectDailySummary->getRawMetrics($project['id'], $from, $to), + 'labels' => array( + 'column' => t('Column'), + 'day' => t('Date'), + 'total' => t('Tasks'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/cfd', array( + 'values' => array( + 'from' => $from, + 'to' => $to, + ), + 'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2, + 'project' => $project, + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Cumulative flow diagram for "%s"', $project['name']), + ))); + } + } + + /** + * Show burndown chart + * + * @access public + */ + public function burndown() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + if ($this->request->isAjax()) { + $this->response->json(array( + 'metrics' => $this->projectDailySummary->getRawMetricsByDay($project['id'], $from, $to), + 'labels' => array( + 'day' => t('Date'), + 'score' => t('Complexity'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/burndown', array( + 'values' => array( + 'from' => $from, + 'to' => $to, + ), + 'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2, + 'project' => $project, + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Burndown chart for "%s"', $project['name']), + ))); + } + } +} diff --git a/app/Controller/App.php b/app/Controller/App.php index feec4221..8a97e8c7 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -2,7 +2,8 @@ namespace Controller; -use Model\Project as ProjectModel; +use Model\Subtask as SubtaskModel; +use Model\Task as TaskModel; /** * Application controller @@ -13,21 +14,117 @@ use Model\Project as ProjectModel; class App extends Base { /** + * Check if the user is connected + * + * @access public + */ + public function status() + { + $this->response->text('OK'); + } + + /** + * User dashboard view for admins + * + * @access public + */ + public function dashboard() + { + $this->index($this->request->getIntegerParam('user_id'), 'dashboard'); + } + + /** * Dashboard for the current user * * @access public */ - public function index() + public function index($user_id = 0, $action = 'index') { - $user_id = $this->acl->getUserId(); - $projects = $this->projectPermission->getAllowedProjects($user_id); - - $this->response->html($this->template->layout('app_index', array( - 'board_selector' => $projects, - 'events' => $this->projectActivity->getProjects(array_keys($projects), 10), - 'tasks' => $this->taskFinder->getAllTasksByUser($user_id), - 'menu' => 'dashboard', + $status = array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS); + $user_id = $user_id ?: $this->userSession->getId(); + $projects = $this->projectPermission->getActiveMemberProjects($user_id); + $project_ids = array_keys($projects); + + $task_paginator = $this->paginator + ->setUrl('app', $action, array('pagination' => 'tasks', 'user_id' => $user_id)) + ->setMax(10) + ->setOrder('tasks.id') + ->setQuery($this->taskFinder->getUserQuery($user_id)) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks'); + + $subtask_paginator = $this->paginator + ->setUrl('app', $action, array('pagination' => 'subtasks', 'user_id' => $user_id)) + ->setMax(10) + ->setOrder('tasks.id') + ->setQuery($this->subtask->getUserQuery($user_id, $status)) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $project_paginator = $this->paginator + ->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id)) + ->setMax(10) + ->setOrder('name') + ->setQuery($this->project->getQueryColumnStats($project_ids)) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'projects'); + + $this->response->html($this->template->layout('app/dashboard', array( 'title' => t('Dashboard'), + 'board_selector' => $this->projectPermission->getAllowedProjects($user_id), + 'events' => $this->projectActivity->getProjects($project_ids, 5), + 'task_paginator' => $task_paginator, + 'subtask_paginator' => $subtask_paginator, + 'project_paginator' => $project_paginator, + 'user_id' => $user_id, ))); } + + /** + * Render Markdown text and reply with the HTML Code + * + * @access public + */ + public function preview() + { + $payload = $this->request->getJson(); + + if (empty($payload['text'])) { + $this->response->html('<p>'.t('Nothing to preview...').'</p>'); + } + + $this->response->html($this->helper->text->markdown($payload['text'])); + } + + /** + * Colors stylesheet + * + * @access public + */ + public function colors() + { + $this->response->css($this->color->getCss()); + } + + /** + * Task autocompletion (Ajax) + * + * @access public + */ + public function autocomplete() + { + $search = $this->request->getStringParam('term'); + + $filter = $this->taskFilter + ->create() + ->filterByProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId())) + ->excludeTasks(array($this->request->getIntegerParam('exclude_task_id'))); + + // Search by task id or by title + if (ctype_digit($search)) { + $filter->filterById($search); + } + else { + $filter->filterByTitle($search); + } + + $this->response->json($filter->toAutoCompletion()); + } } diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php new file mode 100644 index 00000000..24e6e242 --- /dev/null +++ b/app/Controller/Auth.php @@ -0,0 +1,67 @@ +<?php + +namespace Controller; + +/** + * Authentication controller + * + * @package controller + * @author Frederic Guillot + */ +class Auth extends Base +{ + /** + * Display the form login + * + * @access public + */ + public function login(array $values = array(), array $errors = array()) + { + if ($this->userSession->isLogged()) { + $this->response->redirect($this->helper->url->to('app', 'index')); + } + + $this->response->html($this->template->layout('auth/index', array( + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + 'redirect_query' => $this->request->getStringParam('redirect_query'), + 'title' => t('Login') + ))); + } + + /** + * Check credentials + * + * @access public + */ + public function check() + { + $redirect_query = $this->request->getStringParam('redirect_query'); + $values = $this->request->getValues(); + list($valid, $errors) = $this->authentication->validateForm($values); + + if ($valid) { + + if ($redirect_query !== '') { + $this->response->redirect('?'.urldecode($redirect_query)); + } + + $this->response->redirect($this->helper->url->to('app', 'index')); + } + + $this->login($values, $errors); + } + + /** + * Logout and destroy session + * + * @access public + */ + public function logout() + { + $this->authentication->backend('rememberMe')->destroy($this->userSession->getId()); + $this->session->close(); + $this->response->redirect($this->helper->url->to('auth', 'login')); + } +} diff --git a/app/Controller/Base.php b/app/Controller/Base.php index a8e22fd8..fcd07b99 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -2,166 +2,171 @@ namespace Controller; -use Core\Tool; -use Core\Registry; +use Pimple\Container; use Core\Security; +use Core\Request; +use Core\Response; +use Core\Template; +use Core\Session; use Model\LastLogin; +use Symfony\Component\EventDispatcher\Event; /** * Base controller * * @package controller * @author Frederic Guillot - * - * @property \Model\Acl $acl - * @property \Model\Authentication $authentication - * @property \Model\Action $action - * @property \Model\Board $board - * @property \Model\Category $category - * @property \Model\Color $color - * @property \Model\Comment $comment - * @property \Model\Config $config - * @property \Model\File $file - * @property \Model\LastLogin $lastLogin - * @property \Model\Notification $notification - * @property \Model\Project $project - * @property \Model\ProjectPermission $projectPermission - * @property \Model\SubTask $subTask - * @property \Model\Task $task - * @property \Model\TaskHistory $taskHistory - * @property \Model\TaskExport $taskExport - * @property \Model\TaskFinder $taskFinder - * @property \Model\TaskPermission $taskPermission - * @property \Model\TaskValidator $taskValidator - * @property \Model\CommentHistory $commentHistory - * @property \Model\SubtaskHistory $subtaskHistory - * @property \Model\TimeTracking $timeTracking - * @property \Model\User $user - * @property \Model\Webhook $webhook */ -abstract class Base +abstract class Base extends \Core\Base { /** * Request instance * - * @accesss public + * @accesss protected * @var \Core\Request */ - public $request; + protected $request; /** * Response instance * - * @accesss public + * @accesss protected * @var \Core\Response */ - public $response; - - /** - * Template instance - * - * @accesss public - * @var \Core\Template - */ - public $template; - - /** - * Session instance - * - * @accesss public - * @var \Core\Session - */ - public $session; - - /** - * Registry instance - * - * @access private - * @var \Core\Registry - */ - private $registry; + protected $response; /** * Constructor * * @access public - * @param \Core\Registry $registry Registry instance + * @param \Pimple\Container $container */ - public function __construct(Registry $registry) + public function __construct(Container $container) { - $this->registry = $registry; + $this->container = $container; + $this->request = new Request; + $this->response = new Response; + + if (DEBUG) { + $this->container['logger']->debug('START_REQUEST='.$_SERVER['REQUEST_URI']); + } } /** - * Load automatically models + * Destructor * * @access public - * @param string $name Model name - * @return mixed */ - public function __get($name) + public function __destruct() { - return Tool::loadModel($this->registry, $name); + if (DEBUG) { + + foreach ($this->container['db']->getLogMessages() as $message) { + $this->container['logger']->debug($message); + } + + $this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nb_queries)); + $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT'])); + $this->container['logger']->debug('END_REQUEST='.$_SERVER['REQUEST_URI']); + } } /** - * Method executed before each action + * Send HTTP headers * - * @access public + * @access private */ - public function beforeAction($controller, $action) + private function sendHeaders($action) { - // Start the session - $this->session->open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH); - // HTTP secure headers - $this->response->csp(array('style-src' => "'self' 'unsafe-inline'")); + $this->response->csp(array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '*')); $this->response->nosniff(); $this->response->xss(); // Allow the public board iframe inclusion - if ($action !== 'readonly') { + if (ENABLE_XFRAME && $action !== 'readonly') { $this->response->xframe(); } if (ENABLE_HSTS) { $this->response->hsts(); } + } + + /** + * Method executed before each action + * + * @access public + */ + public function beforeAction($controller, $action) + { + // Start the session + $this->session->open(BASE_URL_DIRECTORY); + $this->sendHeaders($action); + $this->container['dispatcher']->dispatch('session.bootstrap', new Event); - $this->config->setupTranslations(); - $this->config->setupTimezone(); + if (! $this->acl->isPublicAction($controller, $action)) { + $this->handleAuthentication(); + $this->handle2FA($controller, $action); + $this->handleAuthorization($controller, $action); - // Authentication - if (! $this->authentication->isAuthenticated($controller, $action)) { - $this->response->redirect('?controller=user&action=login&redirect_query='.urlencode($this->request->getQueryString())); + $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); } + } - // Check if the user is allowed to see this page - if (! $this->acl->isPageAccessAllowed($controller, $action)) { - $this->response->redirect('?controller=user&action=forbidden'); + /** + * Check authentication + * + * @access public + */ + public function handleAuthentication() + { + if (! $this->authentication->isAuthenticated()) { + + if ($this->request->isAjax()) { + $this->response->text('Not Authorized', 401); + } + + $this->response->redirect($this->helper->url->to('auth', 'login', array('redirect_query' => urlencode($this->request->getQueryString())))); } + } - // Attach events - $this->attachEvents(); + /** + * Check 2FA + * + * @access public + */ + public function handle2FA($controller, $action) + { + $ignore = ($controller === 'twofactor' && in_array($action, array('code', 'check'))) || ($controller === 'auth' && $action === 'logout'); + + if ($ignore === false && $this->userSession->has2FA() && ! $this->userSession->check2FA()) { + + if ($this->request->isAjax()) { + $this->response->text('Not Authorized', 401); + } + + $this->response->redirect($this->helper->url->to('twofactor', 'code')); + } } /** - * Attach events + * Check page access and authorization * - * @access private + * @access public */ - private function attachEvents() + public function handleAuthorization($controller, $action) { - $models = array( - 'projectActivity', // Order is important - 'action', - 'project', - 'webhook', - 'notification', - ); - - foreach ($models as $model) { - $this->$model->attachEvents(); + $project_id = $this->request->getIntegerParam('project_id'); + $task_id = $this->request->getIntegerParam('task_id'); + + // Allow urls without "project_id" + if ($task_id > 0 && $project_id === 0) { + $project_id = $this->taskFinder->getProjectId($task_id); + } + + if (! $this->acl->isAllowed($controller, $action, $project_id)) { + $this->forbidden(); } } @@ -173,7 +178,7 @@ abstract class Base */ public function notfound($no_layout = false) { - $this->response->html($this->template->layout('app_notfound', array( + $this->response->html($this->template->layout('app/notfound', array( 'title' => t('Page not found'), 'no_layout' => $no_layout, ))); @@ -187,7 +192,7 @@ abstract class Base */ public function forbidden($no_layout = false) { - $this->response->html($this->template->layout('app_forbidden', array( + $this->response->html($this->template->layout('app/forbidden', array( 'title' => t('Access Forbidden'), 'no_layout' => $no_layout, ))); @@ -206,19 +211,6 @@ abstract class Base } /** - * Check if the current user have access to the given project - * - * @access protected - * @param integer $project_id Project id - */ - protected function checkProjectPermissions($project_id) - { - if ($this->acl->isRegularUser() && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->forbidden(); - } - } - - /** * Redirection when there is no project in the database * * @access protected @@ -239,14 +231,12 @@ abstract class Base */ protected function taskLayout($template, array $params) { - if (isset($params['task']) && $this->taskPermission->canRemoveTask($params['task']) === false) { - $params['hide_remove_menu'] = true; - } - - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['task_content_for_layout'] = $content; + $params['title'] = $params['task']['project_name'].' > '.$params['task']['title']; + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); - return $this->template->layout('task_layout', $params); + return $this->template->layout('task/layout', $params); } /** @@ -257,13 +247,15 @@ abstract class Base * @param array $params Template parameters * @return string */ - protected function projectLayout($template, array $params) + protected function projectLayout($template, array $params, $sidebar_template = 'project/sidebar') { - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['project_content_for_layout'] = $content; - $params['menu'] = 'projects'; + $params['title'] = $params['project']['name'] === $params['title'] ? $params['title'] : $params['project']['name'].' > '.$params['title']; + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['sidebar_template'] = $sidebar_template; - return $this->template->layout('project_layout', $params); + return $this->template->layout('project/layout', $params); } /** @@ -276,12 +268,10 @@ abstract class Base { $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - if (! $task) { + if (empty($task)) { $this->notfound(); } - $this->checkProjectPermissions($task['project_id']); - return $task; } @@ -297,34 +287,11 @@ abstract class Base $project_id = $this->request->getIntegerParam('project_id', $project_id); $project = $this->project->getById($project_id); - if (! $project) { + if (empty($project)) { $this->session->flashError(t('Project not found.')); $this->response->redirect('?controller=project'); } - $this->checkProjectPermissions($project['id']); - - return $project; - } - - /** - * Common method to get a project with administration rights - * - * @access protected - * @return array - */ - protected function getProjectManagement() - { - $project = $this->project->getById($this->request->getIntegerParam('project_id')); - - if (! $project) { - $this->notfound(); - } - - if ($this->acl->isRegularUser() && ! $this->projectPermission->adminAllowed($project['id'], $this->acl->getUserId())) { - $this->forbidden(); - } - return $project; } } diff --git a/app/Controller/Board.php b/app/Controller/Board.php index d49ad021..2b633d82 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -2,10 +2,6 @@ namespace Controller; -use Model\Project as ProjectModel; -use Model\User as UserModel; -use Core\Security; - /** * Board controller * @@ -15,133 +11,6 @@ use Core\Security; class Board extends Base { /** - * Move a column down or up - * - * @access public - */ - public function moveColumn() - { - $this->checkCSRFParam(); - $project = $this->getProjectManagement(); - $column_id = $this->request->getIntegerParam('column_id'); - $direction = $this->request->getStringParam('direction'); - - if ($direction === 'up' || $direction === 'down') { - $this->board->{'move'.$direction}($project['id'], $column_id); - } - - $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); - } - - /** - * Change a task assignee directly from the board - * - * @access public - */ - public function changeAssignee() - { - $task = $this->getTask(); - $project = $this->project->getById($task['project_id']); - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); - $params = array( - 'errors' => array(), - 'values' => $task, - 'users_list' => $this->projectPermission->getUsersList($project['id']), - 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], - ); - - if ($this->request->isAjax()) { - - $this->response->html($this->template->load('board_assignee', $params)); - } - else { - - $this->response->html($this->template->layout('board_assignee', $params + array( - 'menu' => 'boards', - 'title' => t('Change assignee').' - '.$task['title'], - ))); - } - } - - /** - * Validate an assignee modification - * - * @access public - */ - public function updateAssignee() - { - $values = $this->request->getValues(); - $this->checkProjectPermissions($values['project_id']); - - list($valid,) = $this->taskValidator->validateAssigneeModification($values); - - if ($valid && $this->task->update($values)) { - $this->session->flash(t('Task updated successfully.')); - } - else { - $this->session->flashError(t('Unable to update your task.')); - } - - $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); - } - - /** - * Change a task category directly from the board - * - * @access public - */ - public function changeCategory() - { - $task = $this->getTask(); - $project = $this->project->getById($task['project_id']); - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); - $params = array( - 'errors' => array(), - 'values' => $task, - 'categories_list' => $this->category->getList($project['id']), - 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], - ); - - if ($this->request->isAjax()) { - - $this->response->html($this->template->load('board_category', $params)); - } - else { - - $this->response->html($this->template->layout('board_category', $params + array( - 'menu' => 'boards', - 'title' => t('Change category').' - '.$task['title'], - ))); - } - } - - /** - * Validate a category modification - * - * @access public - */ - public function updateCategory() - { - $values = $this->request->getValues(); - $this->checkProjectPermissions($values['project_id']); - - list($valid,) = $this->taskValidator->validateCategoryModification($values); - - if ($valid && $this->task->update($values)) { - $this->session->flash(t('Task updated successfully.')); - } - else { - $this->session->flashError(t('Unable to update your task.')); - } - - $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); - } - - /** * Display the public version of a board * Access checked by a simple token, no user login, read only, auto-refresh * @@ -153,19 +22,25 @@ class Board extends Base $project = $this->project->getByToken($token); // Token verification - if (! $project) { + if (empty($project)) { $this->forbidden(true); } + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project['id']); + // Display the board with a specific layout - $this->response->html($this->template->layout('board_public', array( + $this->response->html($this->template->layout('board/public', array( 'project' => $project, - 'columns' => $this->board->get($project['id']), - 'categories' => $this->category->getList($project['id'], false), + 'swimlanes' => $this->board->getBoard($project['id']), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, 'title' => $project['name'], + 'description' => $project['description'], 'no_layout' => true, 'not_editable' => true, 'board_public_refresh_interval' => $this->config->get('board_public_refresh_interval'), + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), ))); } @@ -176,16 +51,16 @@ class Board extends Base */ public function index() { - $last_seen_project_id = $this->user->getLastSeenProjectId(); - $favorite_project_id = $this->user->getFavoriteProjectId(); + $last_seen_project_id = $this->userSession->getLastSeenProjectId(); + $favorite_project_id = $this->userSession->getFavoriteProjectId(); $project_id = $last_seen_project_id ?: $favorite_project_id; if (! $project_id) { - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId()); if (empty($projects)) { - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin()) { $this->redirectNoProject(); } @@ -207,23 +82,24 @@ class Board extends Base public function show($project_id = 0) { $project = $this->getProject($project_id); - $projects = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects = $this->projectPermission->getAllowedProjects($this->userSession->getId()); $board_selector = $projects; unset($board_selector[$project['id']]); - $this->user->storeLastSeenProjectId($project['id']); + $this->userSession->storeLastSeenProjectId($project['id']); + + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project['id']); - $this->response->html($this->template->layout('board_index', array( - 'users' => $this->projectPermission->getUsersList($project['id'], true, true), - 'filters' => array('user_id' => UserModel::EVERYBODY_ID), + $this->response->html($this->template->layout('board/index', array( + 'users' => $this->projectPermission->getMemberList($project['id'], true, true), 'projects' => $projects, - 'current_project_id' => $project['id'], - 'current_project_name' => $project['name'], - 'board' => $this->board->get($project['id']), - 'categories' => $this->category->getList($project['id'], true, true), - 'menu' => 'boards', + 'project' => $project, + 'swimlanes' => $this->board->getBoard($project['id']), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, 'title' => $project['name'], + 'description' => $project['description'], 'board_selector' => $board_selector, 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), 'board_highlight_period' => $this->config->get('board_highlight_period'), @@ -231,215 +107,264 @@ class Board extends Base } /** - * Display a form to edit a board + * Save the board (Ajax request made by the drag and drop) * * @access public */ - public function edit() + public function save() { - $project = $this->getProjectManagement(); - $columns = $this->board->getColumns($project['id']); - $values = array(); + $project_id = $this->request->getIntegerParam('project_id'); - foreach ($columns as $column) { - $values['title['.$column['id'].']'] = $column['title']; - $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; + if (! $project_id || ! $this->request->isAjax()) { + return $this->response->status(403); } - $this->response->html($this->projectLayout('board_edit', array( - 'errors' => array(), - 'values' => $values + array('project_id' => $project['id']), - 'columns' => $columns, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Edit board') - ))); + if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + $this->response->text('Forbidden', 403); + } + + $values = $this->request->getJson(); + + $result =$this->taskPosition->movePosition( + $project_id, + $values['task_id'], + $values['column_id'], + $values['position'], + $values['swimlane_id'] + ); + + if (! $result) { + return $this->response->status(400); + } + + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project_id); + + $this->response->html( + $this->template->render('board/show', array( + 'project' => $this->project->getById($project_id), + 'swimlanes' => $this->board->getBoard($project_id), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), + )), + 201 + ); } /** - * Validate and update a board + * Check if the board have been changed * * @access public */ - public function update() + public function check() { - $project = $this->getProjectManagement(); - $columns = $this->board->getColumns($project['id']); - $data = $this->request->getValues(); - $values = $columns_list = array(); - - foreach ($columns as $column) { - $columns_list[$column['id']] = $column['title']; - $values['title['.$column['id'].']'] = isset($data['title'][$column['id']]) ? $data['title'][$column['id']] : ''; - $values['task_limit['.$column['id'].']'] = isset($data['task_limit'][$column['id']]) ? $data['task_limit'][$column['id']] : 0; + if (! $this->request->isAjax()) { + return $this->response->status(403); } - list($valid, $errors) = $this->board->validateModification($columns_list, $values); + $project_id = $this->request->getIntegerParam('project_id'); + $timestamp = $this->request->getIntegerParam('timestamp'); - if ($valid) { + if (! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + $this->response->text('Forbidden', 403); + } - if ($this->board->update($data)) { - $this->session->flash(t('Board updated successfully.')); - $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); - } - else { - $this->session->flashError(t('Unable to update this board.')); - } + if (! $this->project->isModifiedSince($project_id, $timestamp)) { + return $this->response->status(304); } - $this->response->html($this->projectLayout('board_edit', array( - 'errors' => $errors, - 'values' => $values + array('project_id' => $project['id']), - 'columns' => $columns, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Edit board') - ))); + list($categories_listing, $categories_description) = $this->category->getBoardCategories($project_id); + + $this->response->html( + $this->template->render('board/show', array( + 'project' => $this->project->getById($project_id), + 'swimlanes' => $this->board->getBoard($project_id), + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), + 'board_highlight_period' => $this->config->get('board_highlight_period'), + )) + ); } /** - * Validate and add a new column + * Get links on mouseover * * @access public */ - public function add() + public function tasklinks() { - $project = $this->getProjectManagement(); - $columns = $this->board->getColumnsList($project['id']); - $data = $this->request->getValues(); - $values = array(); - - foreach ($columns as $column_id => $column_title) { - $values['title['.$column_id.']'] = $column_title; - } - - list($valid, $errors) = $this->board->validateCreation($data); + $task = $this->getTask(); + $this->response->html($this->template->render('board/tasklinks', array( + 'links' => $this->taskLink->getAll($task['id']), + 'task' => $task, + ))); + } - if ($valid) { + /** + * Get subtasks on mouseover + * + * @access public + */ + public function subtasks() + { + $task = $this->getTask(); + $this->response->html($this->template->render('board/subtasks', array( + 'subtasks' => $this->subtask->getAll($task['id']), + 'task' => $task, + ))); + } - if ($this->board->addColumn($project['id'], $data['title'])) { - $this->session->flash(t('Board updated successfully.')); - $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); - } - else { - $this->session->flashError(t('Unable to update this board.')); - } - } + /** + * Display all attachments during the task mouseover + * + * @access public + */ + public function attachments() + { + $task = $this->getTask(); - $this->response->html($this->projectLayout('board_edit', array( - 'errors' => $errors, - 'values' => $values + $data, - 'columns' => $columns, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Edit board') + $this->response->html($this->template->render('board/files', array( + 'files' => $this->file->getAllDocuments($task['id']), + 'images' => $this->file->getAllImages($task['id']), + 'task' => $task, ))); } /** - * Remove a column + * Display comments during a task mouseover * * @access public */ - public function remove() + public function comments() { - $project = $this->getProjectManagement(); + $task = $this->getTask(); - if ($this->request->getStringParam('remove') === 'yes') { + $this->response->html($this->template->render('board/comments', array( + 'comments' => $this->comment->getAll($task['id']) + ))); + } - $this->checkCSRFParam(); - $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); + /** + * Display task description + * + * @access public + */ + public function description() + { + $task = $this->getTask(); - if ($column && $this->board->removeColumn($column['id'])) { - $this->session->flash(t('Column removed successfully.')); - } else { - $this->session->flashError(t('Unable to remove this column.')); - } + $this->response->html($this->template->render('board/description', array( + 'task' => $task + ))); + } - $this->response->redirect('?controller=board&action=edit&project_id='.$project['id']); - } + /** + * Change a task assignee directly from the board + * + * @access public + */ + public function changeAssignee() + { + $task = $this->getTask(); + $project = $this->project->getById($task['project_id']); - $this->response->html($this->projectLayout('board_remove', array( - 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')), + $this->response->html($this->template->render('board/assignee', array( + 'values' => $task, + 'users_list' => $this->projectPermission->getMemberList($project['id']), 'project' => $project, - 'menu' => 'projects', - 'title' => t('Remove a column from a board') ))); } /** - * Save the board (Ajax request made by the drag and drop) + * Validate an assignee modification * * @access public */ - public function save() + public function updateAssignee() { - $project_id = $this->request->getIntegerParam('project_id'); + $values = $this->request->getValues(); - if ($project_id > 0 && $this->request->isAjax()) { + list($valid,) = $this->taskValidator->validateAssigneeModification($values); - if (! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->status(401); - } + if ($valid && $this->taskModification->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } - $values = $this->request->getValues(); + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); + } - if ($this->task->movePosition($project_id, $values['task_id'], $values['column_id'], $values['position'])) { + /** + * Change a task category directly from the board + * + * @access public + */ + public function changeCategory() + { + $task = $this->getTask(); + $project = $this->project->getById($task['project_id']); - $this->response->html( - $this->template->load('board_show', array( - 'current_project_id' => $project_id, - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - )), - 201 - ); - } - else { + $this->response->html($this->template->render('board/category', array( + 'values' => $task, + 'categories_list' => $this->category->getList($project['id']), + 'project' => $project, + ))); + } - $this->response->status(400); - } + /** + * Validate a category modification + * + * @access public + */ + public function updateCategory() + { + $values = $this->request->getValues(); + + list($valid,) = $this->taskValidator->validateCategoryModification($values); + + if ($valid && $this->taskModification->update($values)) { + $this->session->flash(t('Task updated successfully.')); } else { - $this->response->status(401); + $this->session->flashError(t('Unable to update your task.')); } + + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $values['project_id']))); } /** - * Check if the board have been changed + * Screenshot popover * * @access public */ - public function check() + public function screenshot() { - if ($this->request->isAjax()) { + $task = $this->getTask(); - $project_id = $this->request->getIntegerParam('project_id'); - $timestamp = $this->request->getIntegerParam('timestamp'); + $this->response->html($this->template->render('file/screenshot', array( + 'task' => $task, + 'redirect' => 'board', + ))); + } - if ($project_id > 0 && ! $this->projectPermission->isUserAllowed($project_id, $this->acl->getUserId())) { - $this->response->text('Not Authorized', 401); - } + /** + * Get recurrence information on mouseover + * + * @access public + */ + public function recurrence() + { + $task = $this->getTask(); - if ($this->project->isModifiedSince($project_id, $timestamp)) { - $this->response->html( - $this->template->load('board_show', array( - 'current_project_id' => $project_id, - 'board' => $this->board->get($project_id), - 'categories' => $this->category->getList($project_id, false), - 'board_private_refresh_interval' => $this->config->get('board_private_refresh_interval'), - 'board_highlight_period' => $this->config->get('board_highlight_period'), - )) - ); - } - else { - $this->response->status(304); - } - } - else { - $this->response->status(401); - } + $this->response->html($this->template->render('task/recurring_info', array( + 'task' => $task, + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ))); } } diff --git a/app/Controller/Budget.php b/app/Controller/Budget.php new file mode 100644 index 00000000..a2f7e0db --- /dev/null +++ b/app/Controller/Budget.php @@ -0,0 +1,135 @@ +<?php + +namespace Controller; + +/** + * Budget + * + * @package controller + * @author Frederic Guillot + */ +class Budget extends Base +{ + /** + * Budget index page + * + * @access public + */ + public function index() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('budget/index', array( + 'daily_budget' => $this->budget->getDailyBudgetBreakdown($project['id']), + 'project' => $project, + 'title' => t('Budget') + ), 'budget/sidebar')); + } + + /** + * Cost breakdown by users/subtasks/tasks + * + * @access public + */ + public function breakdown() + { + $project = $this->getProject(); + + $paginator = $this->paginator + ->setUrl('budget', 'breakdown', array('project_id' => $project['id'])) + ->setMax(30) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->budget->getSubtaskBreakdown($project['id'])) + ->calculate(); + + $this->response->html($this->projectLayout('budget/breakdown', array( + 'paginator' => $paginator, + 'project' => $project, + 'title' => t('Budget') + ), 'budget/sidebar')); + } + + /** + * Create budget lines + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + if (empty($values)) { + $values['date'] = date('Y-m-d'); + } + + $this->response->html($this->projectLayout('budget/create', array( + 'lines' => $this->budget->getAll($project['id']), + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, + 'project' => $project, + 'title' => t('Budget lines') + ), 'budget/sidebar')); + } + + /** + * Validate and save a new budget + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->budget->validateCreation($values); + + if ($valid) { + + if ($this->budget->create($values['project_id'], $values['amount'], $values['comment'], $values['date'])) { + $this->session->flash(t('The budget line have been created successfully.')); + $this->response->redirect($this->helper->url->to('budget', 'create', array('project_id' => $project['id']))); + } + else { + $this->session->flashError(t('Unable to create the budget line.')); + } + } + + $this->create($values, $errors); + } + + /** + * Confirmation dialog before removing a budget + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('budget/remove', array( + 'project' => $project, + 'budget_id' => $this->request->getIntegerParam('budget_id'), + 'title' => t('Remove a budget line'), + ), 'budget/sidebar')); + } + + /** + * Remove a budget + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + + if ($this->budget->remove($this->request->getIntegerParam('budget_id'))) { + $this->session->flash(t('Budget line removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this budget line.')); + } + + $this->response->redirect($this->helper->url->to('budget', 'create', array('project_id' => $project['id']))); + } +} diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php new file mode 100644 index 00000000..41642a59 --- /dev/null +++ b/app/Controller/Calendar.php @@ -0,0 +1,128 @@ +<?php + +namespace Controller; + +use Model\Task as TaskModel; + +/** + * Project Calendar controller + * + * @package controller + * @author Frederic Guillot + * @author Timo Litzbarski + */ +class Calendar extends Base +{ + /** + * Show calendar view for projects + * + * @access public + */ + public function show() + { + $project = $this->getProject(); + + $this->response->html($this->template->layout('calendar/show', array( + 'check_interval' => $this->config->get('board_private_refresh_interval'), + 'users_list' => $this->projectPermission->getMemberList($project['id'], true, true), + 'categories_list' => $this->category->getList($project['id'], true, true), + 'columns_list' => $this->board->getColumnsList($project['id'], true), + 'swimlanes_list' => $this->swimlane->getList($project['id'], true), + 'colors_list' => $this->color->getList(true), + 'status_list' => $this->taskStatus->getList(true), + 'project' => $project, + 'title' => t('Calendar for "%s"', $project['name']), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + ))); + } + + /** + * Get tasks to display on the calendar (project view) + * + * @access public + */ + public function project() + { + $project_id = $this->request->getIntegerParam('project_id'); + $start = $this->request->getStringParam('start'); + $end = $this->request->getStringParam('end'); + + // Common filter + $filter = $this->taskFilter + ->create() + ->filterByProject($project_id) + ->filterByCategory($this->request->getIntegerParam('category_id', -1)) + ->filterByOwner($this->request->getIntegerParam('owner_id', -1)) + ->filterByColumn($this->request->getIntegerParam('column_id', -1)) + ->filterBySwimlane($this->request->getIntegerParam('swimlane_id', -1)) + ->filterByColor($this->request->getStringParam('color_id')) + ->filterByStatus($this->request->getIntegerParam('is_active', -1)); + + // Tasks + if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { + $events = $filter->copy()->filterByCreationDateRange($start, $end)->toDateTimeCalendarEvents('date_creation', 'date_completed'); + } + else { + $events = $filter->copy()->filterByStartDateRange($start, $end)->toDateTimeCalendarEvents('date_started', 'date_completed'); + } + + // Tasks with due date + $events = array_merge($events, $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents()); + + $this->response->json($events); + } + + /** + * Get tasks to display on the calendar (user view) + * + * @access public + */ + public function user() + { + $user_id = $this->request->getIntegerParam('user_id'); + $start = $this->request->getStringParam('start'); + $end = $this->request->getStringParam('end'); + $filter = $this->taskFilter->create()->filterByOwner($user_id)->filterByStatus(TaskModel::STATUS_OPEN); + + // Task with due date + $events = $filter->copy()->filterByDueDateRange($start, $end)->toAllDayCalendarEvents(); + + // Tasks + if ($this->config->get('calendar_user_tasks', 'date_started') === 'date_creation') { + $events = array_merge($events, $filter->copy()->filterByCreationDateRange($start, $end)->toDateTimeCalendarEvents('date_creation', 'date_completed')); + } + else { + $events = array_merge($events, $filter->copy()->filterByStartDateRange($start, $end)->toDateTimeCalendarEvents('date_started', 'date_completed')); + } + + // Subtasks time tracking + if ($this->config->get('calendar_user_subtasks_time_tracking') == 1) { + $events = array_merge($events, $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end)); + } + + // Subtask estimates + if ($this->config->get('calendar_user_subtasks_forecast') == 1) { + $events = array_merge($events, $this->subtaskForecast->getCalendarEvents($user_id, $end)); + } + + $this->response->json($events); + } + + /** + * Update task due date + * + * @access public + */ + public function save() + { + if ($this->request->isAjax() && $this->request->isPost()) { + + $values = $this->request->getJson(); + + $this->taskModification->update(array( + 'id' => $values['task_id'], + 'date_due' => substr($values['date_due'], 0, 10), + )); + } + } +} diff --git a/app/Controller/Category.php b/app/Controller/Category.php index 38322294..515cc9c8 100644 --- a/app/Controller/Category.php +++ b/app/Controller/Category.php @@ -14,14 +14,14 @@ class Category extends Base * Get the category (common method between actions) * * @access private - * @param $project_id + * @param integer $project_id * @return array */ private function getCategory($project_id) { $category = $this->category->getById($this->request->getIntegerParam('category_id')); - if (! $category) { + if (empty($category)) { $this->session->flashError(t('Category not found.')); $this->response->redirect('?controller=category&action=index&project_id='.$project_id); } @@ -34,28 +34,27 @@ class Category extends Base * * @access public */ - public function index() + public function index(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('category_index', array( + $this->response->html($this->projectLayout('category/index', array( 'categories' => $this->category->getList($project['id'], false), - 'values' => array('project_id' => $project['id']), - 'errors' => array(), + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, 'project' => $project, - 'menu' => 'projects', 'title' => t('Categories') ))); } /** - * Validate and save a new project + * Validate and save a new category * * @access public */ public function save() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); list($valid, $errors) = $this->category->validateCreation($values); @@ -71,14 +70,7 @@ class Category extends Base } } - $this->response->html($this->projectLayout('category_index', array( - 'categories' => $this->category->getList($project['id'], false), - 'values' => $values, - 'errors' => $errors, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Categories') - ))); + $this->index($values, $errors); } /** @@ -86,16 +78,15 @@ class Category extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->projectLayout('category_edit', array( - 'values' => $category, - 'errors' => array(), + $this->response->html($this->projectLayout('category/edit', array( + 'values' => empty($values) ? $category : $values, + 'errors' => $errors, 'project' => $project, - 'menu' => 'projects', 'title' => t('Categories') ))); } @@ -107,7 +98,7 @@ class Category extends Base */ public function update() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues(); list($valid, $errors) = $this->category->validateModification($values); @@ -123,13 +114,7 @@ class Category extends Base } } - $this->response->html($this->projectLayout('category_edit', array( - 'values' => $values, - 'errors' => $errors, - 'project' => $project, - 'menu' => 'projects', - 'title' => t('Categories') - ))); + $this->edit($values, $errors); } /** @@ -139,13 +124,12 @@ class Category extends Base */ public function confirm() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); - $this->response->html($this->projectLayout('category_remove', array( + $this->response->html($this->projectLayout('category/remove', array( 'project' => $project, 'category' => $category, - 'menu' => 'projects', 'title' => t('Remove a category') ))); } @@ -158,7 +142,7 @@ class Category extends Base public function remove() { $this->checkCSRFParam(); - $project = $this->getProjectManagement(); + $project = $this->getProject(); $category = $this->getCategory($project['id']); if ($this->category->remove($category['id'])) { diff --git a/app/Controller/Column.php b/app/Controller/Column.php new file mode 100644 index 00000000..89c495a6 --- /dev/null +++ b/app/Controller/Column.php @@ -0,0 +1,170 @@ +<?php + +namespace Controller; + +/** + * Column controller + * + * @package controller + * @author Frederic Guillot + */ +class Column extends Base +{ + /** + * Display columns list + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $columns = $this->board->getColumns($project['id']); + + foreach ($columns as $column) { + $values['title['.$column['id'].']'] = $column['title']; + $values['description['.$column['id'].']'] = $column['description']; + $values['task_limit['.$column['id'].']'] = $column['task_limit'] ?: null; + } + + $this->response->html($this->projectLayout('column/index', array( + 'errors' => $errors, + 'values' => $values + array('project_id' => $project['id']), + 'columns' => $columns, + 'project' => $project, + 'title' => t('Edit board') + ))); + } + + /** + * Validate and add a new column + * + * @access public + */ + public function create() + { + $project = $this->getProject(); + $columns = $this->board->getColumnsList($project['id']); + $data = $this->request->getValues(); + $values = array(); + + foreach ($columns as $column_id => $column_title) { + $values['title['.$column_id.']'] = $column_title; + } + + list($valid, $errors) = $this->board->validateCreation($data); + + if ($valid) { + + if ($this->board->addColumn($project['id'], $data['title'], $data['task_limit'], $data['description'])) { + $this->session->flash(t('Board updated successfully.')); + $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + } + else { + $this->session->flashError(t('Unable to update this board.')); + } + } + + $this->index($values, $errors); + } + + /** + * Display a form to edit a column + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); + + $this->response->html($this->projectLayout('column/edit', array( + 'errors' => $errors, + 'values' => $values ?: $column, + 'project' => $project, + 'column' => $column, + 'title' => t('Edit column "%s"', $column['title']) + ))); + } + + /** + * Validate and update a column + * + * @access public + */ + public function update() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->board->validateModification($values); + + if ($valid) { + + if ($this->board->updateColumn($values['id'], $values['title'], $values['task_limit'], $values['description'])) { + $this->session->flash(t('Board updated successfully.')); + $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + } + else { + $this->session->flashError(t('Unable to update this board.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Move a column up or down + * + * @access public + */ + public function move() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $column_id = $this->request->getIntegerParam('column_id'); + $direction = $this->request->getStringParam('direction'); + + if ($direction === 'up' || $direction === 'down') { + $this->board->{'move'.$direction}($project['id'], $column_id); + } + + $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + } + + /** + * Confirm column suppression + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('column/remove', array( + 'column' => $this->board->getColumn($this->request->getIntegerParam('column_id')), + 'project' => $project, + 'title' => t('Remove a column from a board') + ))); + } + + /** + * Remove a column + * + * @access public + */ + public function remove() + { + $project = $this->getProject(); + $this->checkCSRFParam(); + $column = $this->board->getColumn($this->request->getIntegerParam('column_id')); + + if (! empty($column) && $this->board->removeColumn($column['id'])) { + $this->session->flash(t('Column removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this column.')); + } + + $this->response->redirect($this->helper->url->to('column', 'index', array('project_id' => $project['id']))); + } +} diff --git a/app/Controller/Comment.php b/app/Controller/Comment.php index a9032ed8..a5f6b1f8 100644 --- a/app/Controller/Comment.php +++ b/app/Controller/Comment.php @@ -20,13 +20,12 @@ class Comment extends Base { $comment = $this->comment->getById($this->request->getIntegerParam('comment_id')); - if (! $comment) { + if (empty($comment)) { $this->notfound(); } - if (! $this->acl->isAdminUser() && $comment['user_id'] != $this->acl->getUserId()) { - $this->response->html($this->template->layout('comment_forbidden', array( - 'menu' => 'tasks', + if (! $this->userSession->isAdmin() && $comment['user_id'] != $this->userSession->getId()) { + $this->response->html($this->template->layout('comment/forbidden', array( 'title' => t('Access Forbidden') ))); } @@ -39,19 +38,32 @@ class Comment extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); - $this->response->html($this->taskLayout('comment_create', array( - 'values' => array( - 'user_id' => $this->acl->getUserId(), + if (empty($values)) { + $values = array( + 'user_id' => $this->userSession->getId(), 'task_id' => $task['id'], - ), - 'errors' => array(), + ); + } + + if ($ajax) { + $this->response->html($this->template->render('comment/create', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'ajax' => $ajax, + ))); + } + + $this->response->html($this->taskLayout('comment/create', array( + 'values' => $values, + 'errors' => $errors, 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Add a comment') + 'title' => t('Add a comment'), ))); } @@ -64,6 +76,7 @@ class Comment extends Base { $task = $this->getTask(); $values = $this->request->getValues(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); list($valid, $errors) = $this->comment->validateCreation($values); @@ -76,16 +89,14 @@ class Comment extends Base $this->session->flashError(t('Unable to create your comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments'); + if ($ajax) { + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); + } + + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments'); } - $this->response->html($this->taskLayout('comment_create', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Add a comment') - ))); + $this->create($values, $errors); } /** @@ -93,17 +104,16 @@ class Comment extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); $comment = $this->getComment(); - $this->response->html($this->taskLayout('comment_edit', array( - 'values' => $comment, - 'errors' => array(), + $this->response->html($this->taskLayout('comment/edit', array( + 'values' => empty($values) ? $comment : $values, + 'errors' => $errors, 'comment' => $comment, 'task' => $task, - 'menu' => 'tasks', 'title' => t('Edit a comment') ))); } @@ -130,17 +140,10 @@ class Comment extends Base $this->session->flashError(t('Unable to update your comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comment-'.$comment['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comment-'.$comment['id']); } - $this->response->html($this->taskLayout('comment_edit', array( - 'values' => $values, - 'errors' => $errors, - 'comment' => $comment, - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Edit a comment') - ))); + $this->edit($values, $errors); } /** @@ -153,10 +156,9 @@ class Comment extends Base $task = $this->getTask(); $comment = $this->getComment(); - $this->response->html($this->taskLayout('comment_remove', array( + $this->response->html($this->taskLayout('comment/remove', array( 'comment' => $comment, 'task' => $task, - 'menu' => 'tasks', 'title' => t('Remove a comment') ))); } @@ -179,6 +181,6 @@ class Comment extends Base $this->session->flashError(t('Unable to remove this comment.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#comments'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#comments'); } } diff --git a/app/Controller/Config.php b/app/Controller/Config.php index 7c8373c3..fbd374ab 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -20,12 +20,12 @@ class Config extends Base */ private function layout($template, array $params) { + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); $params['values'] = $this->config->getAll(); $params['errors'] = array(); - $params['menu'] = 'config'; - $params['config_content_for_layout'] = $this->template->load($template, $params); + $params['config_content_for_layout'] = $this->template->render($template, $params); - return $this->template->layout('config_layout', $params); + return $this->template->layout('config/layout', $params); } /** @@ -38,7 +38,19 @@ class Config extends Base { if ($this->request->isPost()) { - $values = $this->request->getValues(); + $values = $this->request->getValues(); + + switch ($redirect) { + case 'project': + $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0); + break; + case 'integrations': + $values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0, 'integration_gravatar' => 0, 'integration_jabber' => 0); + break; + case 'calendar': + $values += array('calendar_user_subtasks_forecast' => 0, 'calendar_user_subtasks_time_tracking' => 0); + break; + } if ($this->config->save($values)) { $this->config->reload(); @@ -59,9 +71,9 @@ class Config extends Base */ public function index() { - $this->response->html($this->layout('config_about', array( + $this->response->html($this->layout('config/about', array( 'db_size' => $this->config->getDatabaseSize(), - 'title' => t('About'), + 'title' => t('Settings').' > '.t('About'), ))); } @@ -74,11 +86,26 @@ class Config extends Base { $this->common('application'); - $this->response->html($this->layout('config_application', array( - 'title' => t('Application settings'), + $this->response->html($this->layout('config/application', array( 'languages' => $this->config->getLanguages(), 'timezones' => $this->config->getTimezones(), 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Settings').' > '.t('Application settings'), + ))); + } + + /** + * Display the project settings page + * + * @access public + */ + public function project() + { + $this->common('project'); + + $this->response->html($this->layout('config/project', array( + 'default_columns' => implode(', ', $this->board->getDefaultColumns()), + 'title' => t('Settings').' > '.t('Project settings'), ))); } @@ -91,9 +118,36 @@ class Config extends Base { $this->common('board'); - $this->response->html($this->layout('config_board', array( - 'title' => t('Board settings'), - 'default_columns' => implode(', ', $this->board->getDefaultColumns()), + $this->response->html($this->layout('config/board', array( + 'title' => t('Settings').' > '.t('Board settings'), + ))); + } + + /** + * Display the calendar settings page + * + * @access public + */ + public function calendar() + { + $this->common('calendar'); + + $this->response->html($this->layout('config/calendar', array( + 'title' => t('Settings').' > '.t('Calendar settings'), + ))); + } + + /** + * Display the integration settings page + * + * @access public + */ + public function integrations() + { + $this->common('integrations'); + + $this->response->html($this->layout('config/integrations', array( + 'title' => t('Settings').' > '.t('Integrations'), ))); } @@ -106,8 +160,8 @@ class Config extends Base { $this->common('webhook'); - $this->response->html($this->layout('config_webhook', array( - 'title' => t('Webhook settings'), + $this->response->html($this->layout('config/webhook', array( + 'title' => t('Settings').' > '.t('Webhook settings'), ))); } @@ -118,8 +172,8 @@ class Config extends Base */ public function api() { - $this->response->html($this->layout('config_api', array( - 'title' => t('API'), + $this->response->html($this->layout('config/api', array( + 'title' => t('Settings').' > '.t('API'), ))); } diff --git a/app/Controller/Currency.php b/app/Controller/Currency.php new file mode 100644 index 00000000..10fb90da --- /dev/null +++ b/app/Controller/Currency.php @@ -0,0 +1,89 @@ +<?php + +namespace Controller; + +/** + * Currency controller + * + * @package controller + * @author Frederic Guillot + */ +class Currency extends Base +{ + /** + * Common layout for config views + * + * @access private + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + private function layout($template, array $params) + { + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['config_content_for_layout'] = $this->template->render($template, $params); + + return $this->template->layout('config/layout', $params); + } + + /** + * Display all currency rates and form + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $this->response->html($this->layout('currency/index', array( + 'config_values' => array('application_currency' => $this->config->get('application_currency')), + 'values' => $values, + 'errors' => $errors, + 'rates' => $this->currency->getAll(), + 'currencies' => $this->config->getCurrencies(), + 'title' => t('Settings').' > '.t('Currency rates'), + ))); + } + + /** + * Validate and save a new currency rate + * + * @access public + */ + public function create() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->currency->validate($values); + + if ($valid) { + + if ($this->currency->create($values['currency'], $values['rate'])) { + $this->session->flash(t('The currency rate have been added successfully.')); + $this->response->redirect($this->helper->url->to('currency', 'index')); + } + else { + $this->session->flashError(t('Unable to add this currency rate.')); + } + } + + $this->index($values, $errors); + } + + /** + * Save reference currency + * + * @access public + */ + public function reference() + { + $values = $this->request->getValues(); + + if ($this->config->save($values)) { + $this->config->reload(); + $this->session->flash(t('Settings saved successfully.')); + } + else { + $this->session->flashError(t('Unable to save your settings.')); + } + + $this->response->redirect($this->helper->url->to('currency', 'index')); + } +} diff --git a/app/Controller/Export.php b/app/Controller/Export.php new file mode 100644 index 00000000..117fb5ee --- /dev/null +++ b/app/Controller/Export.php @@ -0,0 +1,85 @@ +<?php + +namespace Controller; + +/** + * Export controller + * + * @package controller + * @author Frederic Guillot + */ +class Export extends Base +{ + /** + * Common export method + * + * @access private + */ + private function common($model, $method, $filename, $action, $page_title) + { + $project = $this->getProject(); + $from = $this->request->getStringParam('from'); + $to = $this->request->getStringParam('to'); + + if ($from && $to) { + $data = $this->$model->$method($project['id'], $from, $to); + $this->response->forceDownload($filename.'.csv'); + $this->response->csv($data); + } + + $this->response->html($this->projectLayout('export/'.$action, array( + 'values' => array( + 'controller' => 'export', + 'action' => $action, + 'project_id' => $project['id'], + 'from' => $from, + 'to' => $to, + ), + 'errors' => array(), + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'project' => $project, + 'title' => $page_title, + ), 'export/sidebar')); + } + + /** + * Task export + * + * @access public + */ + public function tasks() + { + $this->common('taskExport', 'export', t('Tasks'), 'tasks', t('Tasks Export')); + } + + /** + * Subtask export + * + * @access public + */ + public function subtasks() + { + $this->common('subtaskExport', 'export', t('Subtasks'), 'subtasks', t('Subtasks Export')); + } + + /** + * Daily project summary export + * + * @access public + */ + public function summary() + { + $this->common('projectDailySummary', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); + } + + /** + * Transition export + * + * @access public + */ + public function transitions() + { + $this->common('transition', 'export', t('Transitions'), 'transitions', t('Task transitions export')); + } +} diff --git a/app/Controller/File.php b/app/Controller/File.php index 3c8c32d1..f0367537 100644 --- a/app/Controller/File.php +++ b/app/Controller/File.php @@ -2,8 +2,6 @@ namespace Controller; -use Model\File as FileModel; - /** * File controller * @@ -13,6 +11,32 @@ use Model\File as FileModel; class File extends Base { /** + * Screenshot + * + * @access public + */ + public function screenshot() + { + $task = $this->getTask(); + + if ($this->request->isPost() && $this->file->uploadScreenshot($task['project_id'], $task['id'], $this->request->getValue('screenshot'))) { + + $this->session->flash(t('Screenshot uploaded successfully.')); + + if ($this->request->getStringParam('redirect') === 'board') { + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + } + + $this->response->html($this->taskLayout('file/screenshot', array( + 'task' => $task, + 'redirect' => 'task', + ))); + } + + /** * File upload form * * @access public @@ -21,11 +45,9 @@ class File extends Base { $task = $this->getTask(); - $this->response->html($this->taskLayout('file_new', array( + $this->response->html($this->taskLayout('file/new', array( 'task' => $task, - 'menu' => 'tasks', 'max_size' => ini_get('upload_max_filesize'), - 'title' => t('Attach a document') ))); } @@ -38,13 +60,11 @@ class File extends Base { $task = $this->getTask(); - if ($this->file->upload($task['project_id'], $task['id'], 'files') === true) { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#attachments'); - } - else { + if (! $this->file->upload($task['project_id'], $task['id'], 'files')) { $this->session->flashError(t('Unable to upload the file.')); - $this->response->redirect('?controller=file&action=create&task_id='.$task['id']); } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } /** @@ -56,14 +76,14 @@ class File extends Base { $task = $this->getTask(); $file = $this->file->getById($this->request->getIntegerParam('file_id')); - $filename = FileModel::BASE_PATH.$file['path']; + $filename = FILES_DIR.$file['path']; if ($file['task_id'] == $task['id'] && file_exists($filename)) { $this->response->forceDownload($file['name']); $this->response->binary(file_get_contents($filename)); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } /** @@ -77,8 +97,9 @@ class File extends Base $file = $this->file->getById($this->request->getIntegerParam('file_id')); if ($file['task_id'] == $task['id']) { - $this->response->html($this->template->load('file_open', array( - 'file' => $file + $this->response->html($this->template->render('file/open', array( + 'file' => $file, + 'task' => $task, ))); } } @@ -92,7 +113,7 @@ class File extends Base { $task = $this->getTask(); $file = $this->file->getById($this->request->getIntegerParam('file_id')); - $filename = FileModel::BASE_PATH.$file['path']; + $filename = FILES_DIR.$file['path']; if ($file['task_id'] == $task['id'] && file_exists($filename)) { $metadata = getimagesize($filename); @@ -105,6 +126,28 @@ class File extends Base } /** + * Return image thumbnails + * + * @access public + */ + public function thumbnail() + { + $task = $this->getTask(); + $file = $this->file->getById($this->request->getIntegerParam('file_id')); + $filename = FILES_DIR.$file['path']; + + if ($file['task_id'] == $task['id'] && file_exists($filename)) { + + $this->response->contentType('image/jpeg'); + $this->file->generateThumbnail( + $filename, + $this->request->getIntegerParam('width'), + $this->request->getIntegerParam('height') + ); + } + } + + /** * Remove a file * * @access public @@ -121,7 +164,7 @@ class File extends Base $this->session->flashError(t('Unable to remove this file.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } /** @@ -134,11 +177,9 @@ class File extends Base $task = $this->getTask(); $file = $this->file->getById($this->request->getIntegerParam('file_id')); - $this->response->html($this->taskLayout('file_remove', array( + $this->response->html($this->taskLayout('file/remove', array( 'task' => $task, 'file' => $file, - 'menu' => 'tasks', - 'title' => t('Remove a file') ))); } } diff --git a/app/Controller/Hourlyrate.php b/app/Controller/Hourlyrate.php new file mode 100644 index 00000000..19650ede --- /dev/null +++ b/app/Controller/Hourlyrate.php @@ -0,0 +1,89 @@ +<?php + +namespace Controller; + +/** + * Hourly Rate controller + * + * @package controller + * @author Frederic Guillot + */ +class Hourlyrate extends User +{ + /** + * Display rate and form + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + $this->response->html($this->layout('hourlyrate/index', array( + 'rates' => $this->hourlyRate->getAllByUser($user['id']), + 'currencies_list' => $this->config->getCurrencies(), + 'values' => $values + array('user_id' => $user['id']), + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Validate and save a new rate + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->hourlyRate->validateCreation($values); + + if ($valid) { + + if ($this->hourlyRate->create($values['user_id'], $values['rate'], $values['currency'], $values['date_effective'])) { + $this->session->flash(t('Hourly rate created successfully.')); + $this->response->redirect($this->helper->url->to('hourlyrate', 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save the hourly rate.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout('hourlyrate/remove', array( + 'rate_id' => $this->request->getIntegerParam('rate_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->hourlyRate->remove($this->request->getIntegerParam('rate_id'))) { + $this->session->flash(t('Rate removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this rate.')); + } + + $this->response->redirect($this->helper->url->to('hourlyrate', 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php new file mode 100644 index 00000000..52e10fa1 --- /dev/null +++ b/app/Controller/Ical.php @@ -0,0 +1,99 @@ +<?php + +namespace Controller; + +use Model\TaskFilter; +use Model\Task as TaskModel; +use Eluceo\iCal\Component\Calendar as iCalendar; + +/** + * iCalendar controller + * + * @package controller + * @author Frederic Guillot + */ +class Ical extends Base +{ + /** + * Get user iCalendar + * + * @access public + */ + public function user() + { + $token = $this->request->getStringParam('token'); + $user = $this->user->getByToken($token); + + // Token verification + if (empty($user)) { + $this->forbidden(true); + } + + // Common filter + $filter = $this->taskFilter + ->create() + ->filterByOwner($user['id']); + + // Calendar properties + $calendar = new iCalendar('Kanboard'); + $calendar->setName($user['name'] ?: $user['username']); + $calendar->setDescription($user['name'] ?: $user['username']); + $calendar->setPublishedTTL('PT1H'); + + $this->renderCalendar($filter, $calendar); + } + + /** + * Get project iCalendar + * + * @access public + */ + public function project() + { + $token = $this->request->getStringParam('token'); + $project = $this->project->getByToken($token); + + // Token verification + if (empty($project)) { + $this->forbidden(true); + } + + // Common filter + $filter = $this->taskFilter + ->create() + ->filterByProject($project['id']); + + // Calendar properties + $calendar = new iCalendar('Kanboard'); + $calendar->setName($project['name']); + $calendar->setDescription($project['name']); + $calendar->setPublishedTTL('PT1H'); + + $this->renderCalendar($filter, $calendar); + } + + /** + * Common method to render iCal events + * + * @access private + */ + private function renderCalendar(TaskFilter $filter, iCalendar $calendar) + { + $start = $this->request->getStringParam('start', strtotime('-1 month')); + $end = $this->request->getStringParam('end', strtotime('+2 months')); + + // Tasks + if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { + $filter->copy()->filterByCreationDateRange($start, $end)->addDateTimeIcalEvents('date_creation', 'date_completed', $calendar); + } + else { + $filter->copy()->filterByStartDateRange($start, $end)->addDateTimeIcalEvents('date_started', 'date_completed', $calendar); + } + + // Tasks with due date + $filter->copy()->filterByDueDateRange($start, $end)->addAllDayIcalEvents('date_due', $calendar); + + $this->response->contentType('text/calendar; charset=utf-8'); + echo $calendar->render(); + } +} diff --git a/app/Controller/Link.php b/app/Controller/Link.php new file mode 100644 index 00000000..482e415c --- /dev/null +++ b/app/Controller/Link.php @@ -0,0 +1,162 @@ +<?php + +namespace Controller; + +/** + * Link controller + * + * @package controller + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Link extends Base +{ + /** + * Common layout for config views + * + * @access private + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + private function layout($template, array $params) + { + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['config_content_for_layout'] = $this->template->render($template, $params); + + return $this->template->layout('config/layout', $params); + } + + /** + * Get the current link + * + * @access private + * @return array + */ + private function getLink() + { + $link = $this->link->getById($this->request->getIntegerParam('link_id')); + + if (empty($link)) { + $this->notfound(); + } + + return $link; + } + + /** + * List of links + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $this->response->html($this->layout('link/index', array( + 'links' => $this->link->getMergedList(), + 'values' => $values, + 'errors' => $errors, + 'title' => t('Settings').' > '.t('Task\'s links'), + ))); + } + + /** + * Validate and save a new link + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->link->validateCreation($values); + + if ($valid) { + + if ($this->link->create($values['label'], $values['opposite_label']) !== false) { + $this->session->flash(t('Link added successfully.')); + $this->response->redirect($this->helper->url->to('link', 'index')); + } + else { + $this->session->flashError(t('Unable to create your link.')); + } + } + + $this->index($values, $errors); + } + + /** + * Edit form + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $link = $this->getLink(); + $link['label'] = t($link['label']); + + $this->response->html($this->layout('link/edit', array( + 'values' => $values ?: $link, + 'errors' => $errors, + 'labels' => $this->link->getList($link['id']), + 'link' => $link, + 'title' => t('Link modification') + ))); + } + + /** + * Edit a link (validate the form and update the database) + * + * @access public + */ + public function update() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->link->validateModification($values); + + if ($valid) { + if ($this->link->update($values)) { + $this->session->flash(t('Link updated successfully.')); + $this->response->redirect($this->helper->url->to('link', 'index')); + } + else { + $this->session->flashError(t('Unable to update your link.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a link + * + * @access public + */ + public function confirm() + { + $link = $this->getLink(); + + $this->response->html($this->layout('link/remove', array( + 'link' => $link, + 'title' => t('Remove a link') + ))); + } + + /** + * Remove a link + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $link = $this->getLink(); + + if ($this->link->remove($link['id'])) { + $this->session->flash(t('Link removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this link.')); + } + + $this->response->redirect($this->helper->url->to('link', 'index')); + } +} diff --git a/app/Controller/Project.php b/app/Controller/Project.php index d749ef53..ba039b7d 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -2,10 +2,8 @@ namespace Controller; -use Model\Task as TaskModel; - /** - * Project controller + * Project controller (Settings + creation/edition) * * @package controller * @author Frederic Guillot @@ -19,25 +17,26 @@ class Project extends Base */ public function index() { - $projects = $this->project->getAll($this->acl->isRegularUser()); - $nb_projects = count($projects); - $active_projects = array(); - $inactive_projects = array(); - - foreach ($projects as $project) { - if ($project['is_active'] == 1) { - $active_projects[] = $project; - } - else { - $inactive_projects[] = $project; - } + if ($this->userSession->isAdmin()) { + $project_ids = $this->project->getAllIds(); } + else { + $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId()); + } + + $nb_projects = count($project_ids); - $this->response->html($this->template->layout('project_index', array( - 'active_projects' => $active_projects, - 'inactive_projects' => $inactive_projects, + $paginator = $this->paginator + ->setUrl('project', 'index') + ->setMax(20) + ->setOrder('name') + ->setQuery($this->project->getQueryColumnStats($project_ids)) + ->calculate(); + + $this->response->html($this->template->layout('project/index', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'paginator' => $paginator, 'nb_projects' => $nb_projects, - 'menu' => 'projects', 'title' => t('Projects').' ('.$nb_projects.')' ))); } @@ -51,54 +50,21 @@ class Project extends Base { $project = $this->getProject(); - $this->response->html($this->projectLayout('project_show', array( + $this->response->html($this->projectLayout('project/show', array( 'project' => $project, - 'stats' => $this->project->getStats($project['id']), + 'stats' => $this->project->getTaskStats($project['id']), 'title' => $project['name'], ))); } /** - * Task export - * - * @access public - */ - public function export() - { - $project = $this->getProjectManagement(); - $from = $this->request->getStringParam('from'); - $to = $this->request->getStringParam('to'); - - if ($from && $to) { - $data = $this->taskExport->export($project['id'], $from, $to); - $this->response->forceDownload('Export_'.date('Y_m_d_H_i_S').'.csv'); - $this->response->csv($data); - } - - $this->response->html($this->projectLayout('project_export', array( - 'values' => array( - 'controller' => 'project', - 'action' => 'export', - 'project_id' => $project['id'], - 'from' => $from, - 'to' => $to, - ), - 'errors' => array(), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'project' => $project, - 'title' => t('Tasks Export') - ))); - } - - /** * Public access management * * @access public */ public function share() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $switch = $this->request->getStringParam('switch'); if ($switch === 'enable' || $switch === 'disable') { @@ -114,24 +80,51 @@ class Project extends Base $this->response->redirect('?controller=project&action=share&project_id='.$project['id']); } - $this->response->html($this->projectLayout('project_share', array( + $this->response->html($this->projectLayout('project/share', array( 'project' => $project, 'title' => t('Public access'), ))); } /** - * Display a form to edit a project + * Integrations page * * @access public */ - public function edit() + public function integration() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); + + if ($this->request->isPost()) { + $params = $this->request->getValues(); + $params += array('hipchat' => 0, 'slack' => 0, 'jabber' => 0); + $this->projectIntegration->saveParameters($project['id'], $params); + } - $this->response->html($this->projectLayout('project_edit', array( + $values = $this->projectIntegration->getParameters($project['id']); + $values += array('hipchat_api_url' => 'https://api.hipchat.com'); + + $this->response->html($this->projectLayout('project/integrations', array( + 'project' => $project, + 'title' => t('Integrations'), + 'webhook_token' => $this->config->get('webhook_token'), + 'values' => $values, 'errors' => array(), - 'values' => $project, + ))); + } + + /** + * Display a form to edit a project + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('project/edit', array( + 'values' => empty($values) ? $project : $values, + 'errors' => $errors, 'project' => $project, 'title' => t('Edit project') ))); @@ -144,8 +137,13 @@ class Project extends Base */ public function update() { - $project = $this->getProjectManagement(); - $values = $this->request->getValues() + array('is_active' => 0); + $project = $this->getProject(); + $values = $this->request->getValues(); + + if ($project['is_private'] == 1 && $this->userSession->isAdmin() && ! isset($values['is_private'])) { + $values += array('is_private' => 0); + } + list($valid, $errors) = $this->project->validateModification($values); if ($valid) { @@ -159,12 +157,7 @@ class Project extends Base } } - $this->response->html($this->projectLayout('project_edit', array( - 'errors' => $errors, - 'values' => $values, - 'project' => $project, - 'title' => t('Edit Project') - ))); + $this->edit($values, $errors); } /** @@ -174,9 +167,9 @@ class Project extends Base */ public function users() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); - $this->response->html($this->projectLayout('project_users', array( + $this->response->html($this->projectLayout('project/users', array( 'project' => $project, 'users' => $this->projectPermission->getAllUsers($project['id']), 'title' => t('Edit project access list') @@ -190,7 +183,7 @@ class Project extends Base */ public function allowEverybody() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); $values = $this->request->getValues() + array('is_everybody_allowed' => 0); list($valid,) = $this->projectPermission->validateProjectModification($values); @@ -219,7 +212,37 @@ class Project extends Base if ($valid) { - if ($this->projectPermission->allowUser($values['project_id'], $values['user_id'])) { + if ($this->projectPermission->addMember($values['project_id'], $values['user_id'])) { + $this->session->flash(t('Project updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update this project.')); + } + } + + $this->response->redirect('?controller=project&action=users&project_id='.$values['project_id']); + } + + /** + * Change the role of a project member + * + * @access public + */ + public function role() + { + $this->checkCSRFParam(); + + $values = array( + 'project_id' => $this->request->getIntegerParam('project_id'), + 'user_id' => $this->request->getIntegerParam('user_id'), + 'is_owner' => $this->request->getIntegerParam('is_owner'), + ); + + list($valid,) = $this->projectPermission->validateUserModification($values); + + if ($valid) { + + if ($this->projectPermission->changeRole($values['project_id'], $values['user_id'], $values['is_owner'])) { $this->session->flash(t('Project updated successfully.')); } else { @@ -248,7 +271,7 @@ class Project extends Base if ($valid) { - if ($this->projectPermission->revokeUser($values['project_id'], $values['user_id'])) { + if ($this->projectPermission->revokeMember($values['project_id'], $values['user_id'])) { $this->session->flash(t('Project updated successfully.')); } else { @@ -266,7 +289,7 @@ class Project extends Base */ public function remove() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('remove') === 'yes') { @@ -281,7 +304,7 @@ class Project extends Base $this->response->redirect('?controller=project'); } - $this->response->html($this->projectLayout('project_remove', array( + $this->response->html($this->projectLayout('project/remove', array( 'project' => $project, 'title' => t('Remove project') ))); @@ -291,17 +314,16 @@ class Project extends Base * Duplicate a project * * @author Antonio Rabelo + * @author Michael Lüpkes * @access public */ public function duplicate() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('duplicate') === 'yes') { - - $this->checkCSRFParam(); - - if ($this->project->duplicate($project['id'])) { + $values = array_keys($this->request->getValues()); + if ($this->projectDuplication->duplicate($project['id'], $values) !== false) { $this->session->flash(t('Project cloned successfully.')); } else { $this->session->flashError(t('Unable to clone this project.')); @@ -310,7 +332,7 @@ class Project extends Base $this->response->redirect('?controller=project'); } - $this->response->html($this->projectLayout('project_duplicate', array( + $this->response->html($this->projectLayout('project/duplicate', array( 'project' => $project, 'title' => t('Clone this project') ))); @@ -323,7 +345,7 @@ class Project extends Base */ public function disable() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('disable') === 'yes') { @@ -338,7 +360,7 @@ class Project extends Base $this->response->redirect('?controller=project&action=show&project_id='.$project['id']); } - $this->response->html($this->projectLayout('project_disable', array( + $this->response->html($this->projectLayout('project/disable', array( 'project' => $project, 'title' => t('Project activation') ))); @@ -351,7 +373,7 @@ class Project extends Base */ public function enable() { - $project = $this->getProjectManagement(); + $project = $this->getProject(); if ($this->request->getStringParam('enable') === 'yes') { @@ -366,7 +388,7 @@ class Project extends Base $this->response->redirect('?controller=project&action=show&project_id='.$project['id']); } - $this->response->html($this->projectLayout('project_enable', array( + $this->response->html($this->projectLayout('project/enable', array( 'project' => $project, 'title' => t('Project activation') ))); @@ -383,115 +405,13 @@ class Project extends Base $project = $this->project->getByToken($token); // Token verification - if (! $project) { + if (empty($project)) { $this->forbidden(true); } - $this->response->xml($this->template->load('project_feed', array( - 'events' => $this->projectActivity->getProject($project['id']), - 'project' => $project, - ))); - } - - /** - * Activity page for a project - * - * @access public - */ - public function activity() - { - $project = $this->getProject(); - - $this->response->html($this->template->layout('project_activity', array( + $this->response->xml($this->template->render('project/feed', array( 'events' => $this->projectActivity->getProject($project['id']), - 'menu' => 'projects', 'project' => $project, - 'title' => t('%s\'s activity', $project['name']) - ))); - } - - /** - * Task search for a given project - * - * @access public - */ - public function search() - { - $project = $this->getProject(); - $search = $this->request->getStringParam('search'); - $direction = $this->request->getStringParam('direction', 'DESC'); - $order = $this->request->getStringParam('order', 'tasks.id'); - $offset = $this->request->getIntegerParam('offset', 0); - $tasks = array(); - $nb_tasks = 0; - $limit = 25; - - if ($search !== '') { - $tasks = $this->taskFinder->search($project['id'], $search, $offset, $limit, $order, $direction); - $nb_tasks = $this->taskFinder->countSearch($project['id'], $search); - } - - $this->response->html($this->template->layout('project_search', array( - 'tasks' => $tasks, - 'nb_tasks' => $nb_tasks, - 'pagination' => array( - 'controller' => 'project', - 'action' => 'search', - 'params' => array('search' => $search, 'project_id' => $project['id']), - 'direction' => $direction, - 'order' => $order, - 'total' => $nb_tasks, - 'offset' => $offset, - 'limit' => $limit, - ), - 'values' => array( - 'search' => $search, - 'controller' => 'project', - 'action' => 'search', - 'project_id' => $project['id'], - ), - 'project' => $project, - 'menu' => 'projects', - 'columns' => $this->board->getColumnsList($project['id']), - 'categories' => $this->category->getList($project['id'], false), - 'title' => $project['name'].($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') - ))); - } - - /** - * List of completed tasks for a given project - * - * @access public - */ - public function tasks() - { - $project = $this->getProject(); - $direction = $this->request->getStringParam('direction', 'DESC'); - $order = $this->request->getStringParam('order', 'tasks.date_completed'); - $offset = $this->request->getIntegerParam('offset', 0); - $limit = 25; - - $tasks = $this->taskFinder->getClosedTasks($project['id'], $offset, $limit, $order, $direction); - $nb_tasks = $this->taskFinder->countByProjectId($project['id'], array(TaskModel::STATUS_CLOSED)); - - $this->response->html($this->template->layout('project_tasks', array( - 'pagination' => array( - 'controller' => 'project', - 'action' => 'tasks', - 'params' => array('project_id' => $project['id']), - 'direction' => $direction, - 'order' => $order, - 'total' => $nb_tasks, - 'offset' => $offset, - 'limit' => $limit, - ), - 'project' => $project, - 'menu' => 'projects', - 'columns' => $this->board->getColumnsList($project['id']), - 'categories' => $this->category->getList($project['id'], false), - 'tasks' => $tasks, - 'nb_tasks' => $nb_tasks, - 'title' => $project['name'].' ('.$nb_tasks.')' ))); } @@ -500,14 +420,16 @@ class Project extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { - $this->response->html($this->template->layout('project_new', array( - 'errors' => array(), - 'values' => array( - 'is_private' => $this->request->getIntegerParam('private', $this->acl->isRegularUser()), - ), - 'title' => t('New project') + $is_private = $this->request->getIntegerParam('private', $this->userSession->isAdmin() ? 0 : 1); + + $this->response->html($this->template->layout('project/new', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'values' => empty($values) ? array('is_private' => $is_private) : $values, + 'errors' => $errors, + 'is_private' => $is_private, + 'title' => $is_private ? t('New private project') : t('New project'), ))); } @@ -523,19 +445,16 @@ class Project extends Base if ($valid) { - if ($this->project->create($values, $this->acl->getUserId())) { + $project_id = $this->project->create($values, $this->userSession->getId(), true); + + if ($project_id > 0) { $this->session->flash(t('Your project have been created successfully.')); - $this->response->redirect('?controller=project'); - } - else { - $this->session->flashError(t('Unable to create your project.')); + $this->response->redirect('?controller=project&action=show&project_id='.$project_id); } + + $this->session->flashError(t('Unable to create your project.')); } - $this->response->html($this->template->layout('project_new', array( - 'errors' => $errors, - 'values' => $values, - 'title' => t('New Project') - ))); + $this->create($values, $errors); } } diff --git a/app/Controller/Projectinfo.php b/app/Controller/Projectinfo.php new file mode 100644 index 00000000..a9498f43 --- /dev/null +++ b/app/Controller/Projectinfo.php @@ -0,0 +1,97 @@ +<?php + +namespace Controller; + +/** + * Project Info controller (ActivityStream + completed tasks) + * + * @package controller + * @author Frederic Guillot + */ +class Projectinfo extends Base +{ + /** + * Activity page for a project + * + * @access public + */ + public function activity() + { + $project = $this->getProject(); + + $this->response->html($this->template->layout('projectinfo/activity', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'events' => $this->projectActivity->getProject($project['id']), + 'project' => $project, + 'title' => t('%s\'s activity', $project['name']) + ))); + } + + /** + * Task search for a given project + * + * @access public + */ + public function search() + { + $project = $this->getProject(); + $search = $this->request->getStringParam('search'); + $nb_tasks = 0; + + $paginator = $this->paginator + ->setUrl('projectinfo', 'search', array('search' => $search, 'project_id' => $project['id'])) + ->setMax(30) + ->setOrder('tasks.id') + ->setDirection('DESC'); + + if ($search !== '') { + + $paginator + ->setQuery($this->taskFinder->getSearchQuery($project['id'], $search)) + ->calculate(); + + $nb_tasks = $paginator->getTotal(); + } + + $this->response->html($this->template->layout('projectinfo/search', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'values' => array( + 'search' => $search, + 'controller' => 'projectinfo', + 'action' => 'search', + 'project_id' => $project['id'], + ), + 'paginator' => $paginator, + 'project' => $project, + 'columns' => $this->board->getColumnsList($project['id']), + 'categories' => $this->category->getList($project['id'], false), + 'title' => t('Search in the project "%s"', $project['name']).($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') + ))); + } + + /** + * List of completed tasks for a given project + * + * @access public + */ + public function tasks() + { + $project = $this->getProject(); + $paginator = $this->paginator + ->setUrl('projectinfo', 'tasks', array('project_id' => $project['id'])) + ->setMax(30) + ->setOrder('tasks.id') + ->setDirection('DESC') + ->setQuery($this->taskFinder->getClosedTaskQuery($project['id'])) + ->calculate(); + + $this->response->html($this->template->layout('projectinfo/tasks', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'project' => $project, + 'columns' => $this->board->getColumnsList($project['id']), + 'categories' => $this->category->getList($project['id'], false), + 'paginator' => $paginator, + 'title' => t('Completed tasks for "%s"', $project['name']).' ('.$paginator->getTotal().')' + ))); + } +} diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php index 48f0d6e2..5baa6004 100644 --- a/app/Controller/Subtask.php +++ b/app/Controller/Subtask.php @@ -2,8 +2,10 @@ namespace Controller; +use Model\Subtask as SubtaskModel; + /** - * SubTask controller + * Subtask controller * * @package controller * @author Frederic Guillot @@ -18,9 +20,9 @@ class Subtask extends Base */ private function getSubtask() { - $subtask = $this->subTask->getById($this->request->getIntegerParam('subtask_id')); + $subtask = $this->subtask->getById($this->request->getIntegerParam('subtask_id')); - if (! $subtask) { + if (empty($subtask)) { $this->notfound(); } @@ -32,20 +34,22 @@ class Subtask extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { $task = $this->getTask(); - $this->response->html($this->taskLayout('subtask_create', array( - 'values' => array( + if (empty($values)) { + $values = array( 'task_id' => $task['id'], 'another_subtask' => $this->request->getIntegerParam('another_subtask', 0) - ), - 'errors' => array(), - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), + ); + } + + $this->response->html($this->taskLayout('subtask/create', array( + 'values' => $values, + 'errors' => $errors, + 'users_list' => $this->projectPermission->getMemberList($task['project_id']), 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Add a sub-task') ))); } @@ -59,11 +63,11 @@ class Subtask extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subTask->validateCreation($values); + list($valid, $errors) = $this->subtask->validateCreation($values); if ($valid) { - if ($this->subTask->create($values)) { + if ($this->subtask->create($values)) { $this->session->flash(t('Sub-task added successfully.')); } else { @@ -71,20 +75,13 @@ class Subtask extends Base } if (isset($values['another_subtask']) && $values['another_subtask'] == 1) { - $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1'); + $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id'].'&another_subtask=1&project_id='.$task['project_id']); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } - $this->response->html($this->taskLayout('subtask_create', array( - 'values' => $values, - 'errors' => $errors, - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Add a sub-task') - ))); + $this->create($values, $errors); } /** @@ -92,20 +89,18 @@ class Subtask extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); $subtask = $this->getSubTask(); - $this->response->html($this->taskLayout('subtask_edit', array( - 'values' => $subtask, - 'errors' => array(), - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), - 'status_list' => $this->subTask->getStatusList(), + $this->response->html($this->taskLayout('subtask/edit', array( + 'values' => empty($values) ? $subtask : $values, + 'errors' => $errors, + 'users_list' => $this->projectPermission->getMemberList($task['project_id']), + 'status_list' => $this->subtask->getStatusList(), 'subtask' => $subtask, 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Edit a sub-task') ))); } @@ -117,33 +112,24 @@ class Subtask extends Base public function update() { $task = $this->getTask(); - $subtask = $this->getSubtask(); + $this->getSubtask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->subTask->validateModification($values); + list($valid, $errors) = $this->subtask->validateModification($values); if ($valid) { - if ($this->subTask->update($values)) { + if ($this->subtask->update($values)) { $this->session->flash(t('Sub-task updated successfully.')); } else { $this->session->flashError(t('Unable to update your sub-task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } - $this->response->html($this->taskLayout('subtask_edit', array( - 'values' => $values, - 'errors' => $errors, - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), - 'status_list' => $this->subTask->getStatusList(), - 'subtask' => $subtask, - 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Edit a sub-task') - ))); + $this->edit($values, $errors); } /** @@ -156,11 +142,9 @@ class Subtask extends Base $task = $this->getTask(); $subtask = $this->getSubtask(); - $this->response->html($this->taskLayout('subtask_remove', array( + $this->response->html($this->taskLayout('subtask/remove', array( 'subtask' => $subtask, 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Remove a sub-task') ))); } @@ -175,14 +159,14 @@ class Subtask extends Base $task = $this->getTask(); $subtask = $this->getSubtask(); - if ($this->subTask->remove($subtask['id'])) { + if ($this->subtask->remove($subtask['id'])) { $this->session->flash(t('Sub-task removed successfully.')); } else { $this->session->flashError(t('Unable to remove this sub-task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id'].'#subtasks'); } /** @@ -194,17 +178,103 @@ class Subtask extends Base { $task = $this->getTask(); $subtask = $this->getSubtask(); + $redirect = $this->request->getStringParam('redirect', 'task'); + + $this->subtask->toggleStatus($subtask['id']); + + if ($redirect === 'board') { + + $this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId()); + + $this->response->html($this->template->render('board/subtasks', array( + 'subtasks' => $this->subtask->getAll($task['id']), + 'task' => $task, + ))); + } + + $this->toggleRedirect($task, $redirect); + } + + /** + * Handle subtask restriction (popover) + * + * @access public + */ + public function subtaskRestriction() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); - $value = array( + $this->response->html($this->template->render('subtask/restriction_change_status', array( + 'status_list' => array( + SubtaskModel::STATUS_TODO => t('Todo'), + SubtaskModel::STATUS_DONE => t('Done'), + ), + 'subtask_inprogress' => $this->subtask->getSubtaskInProgress($this->userSession->getId()), + 'subtask' => $subtask, + 'task' => $task, + 'redirect' => $this->request->getStringParam('redirect'), + ))); + } + + /** + * Change status of the in progress subtask and the other subtask + * + * @access public + */ + public function changeRestrictionStatus() + { + $task = $this->getTask(); + $subtask = $this->getSubtask(); + $values = $this->request->getValues(); + + // Change status of the previous in progress subtask + $this->subtask->update(array( + 'id' => $values['id'], + 'status' => $values['status'], + )); + + // Set the current subtask to in pogress + $this->subtask->update(array( 'id' => $subtask['id'], - 'status' => ($subtask['status'] + 1) % 3, - 'task_id' => $task['id'], - ); + 'status' => SubtaskModel::STATUS_INPROGRESS, + )); - if (! $this->subTask->update($value)) { - $this->session->flashError(t('Unable to update your sub-task.')); + $this->toggleRedirect($task, $values['redirect']); + } + + /** + * Redirect to the right page + * + * @access private + */ + private function toggleRedirect(array $task, $redirect) + { + switch ($redirect) { + case 'board': + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); + case 'dashboard': + $this->response->redirect($this->helper->url->to('app', 'index')); + default: + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } + } + + /** + * Move subtask position + * + * @access public + */ + public function movePosition() + { + $this->checkCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + $task_id = $this->request->getIntegerParam('task_id'); + $subtask_id = $this->request->getIntegerParam('subtask_id'); + $direction = $this->request->getStringParam('direction'); + $method = $direction === 'up' ? 'moveUp' : 'moveDown'; - $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks'); + $this->subtask->$method($task_id, $subtask_id); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id)).'#subtasks'); } } diff --git a/app/Controller/Swimlane.php b/app/Controller/Swimlane.php new file mode 100644 index 00000000..c6862d47 --- /dev/null +++ b/app/Controller/Swimlane.php @@ -0,0 +1,256 @@ +<?php + +namespace Controller; + +use Model\Swimlane as SwimlaneModel; + +/** + * Swimlanes + * + * @package controller + * @author Frederic Guillot + */ +class Swimlane extends Base +{ + /** + * Get the swimlane (common method between actions) + * + * @access private + * @param integer $project_id + * @return array + */ + private function getSwimlane($project_id) + { + $swimlane = $this->swimlane->getById($this->request->getIntegerParam('swimlane_id')); + + if (empty($swimlane)) { + $this->session->flashError(t('Swimlane not found.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project_id); + } + + return $swimlane; + } + + /** + * List of swimlanes for a given project + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->projectLayout('swimlane/index', array( + 'default_swimlane' => $this->swimlane->getDefault($project['id']), + 'active_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::ACTIVE), + 'inactive_swimlanes' => $this->swimlane->getAllByStatus($project['id'], SwimlaneModel::INACTIVE), + 'values' => $values + array('project_id' => $project['id']), + 'errors' => $errors, + 'project' => $project, + 'title' => t('Swimlanes') + ))); + } + + /** + * Validate and save a new swimlane + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->swimlane->validateCreation($values); + + if ($valid) { + + if ($this->swimlane->create($project['id'], $values['name'])) { + $this->session->flash(t('Your swimlane have been created successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to create your swimlane.')); + } + } + + $this->index($values, $errors); + } + + /** + * Change the default swimlane + * + * @access public + */ + public function change() + { + $project = $this->getProject(); + + $values = $this->request->getValues() + array('show_default_swimlane' => 0); + list($valid,) = $this->swimlane->validateDefaultModification($values); + + if ($valid) { + + if ($this->swimlane->updateDefault($values)) { + $this->session->flash(t('The default swimlane have been updated successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + } + + $this->index(); + } + + /** + * Edit a swimlane (display the form) + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + $swimlane = $this->getSwimlane($project['id']); + + $this->response->html($this->projectLayout('swimlane/edit', array( + 'values' => empty($values) ? $swimlane : $values, + 'errors' => $errors, + 'project' => $project, + 'title' => t('Swimlanes') + ))); + } + + /** + * Edit a swimlane (validate the form and update the database) + * + * @access public + */ + public function update() + { + $project = $this->getProject(); + + $values = $this->request->getValues(); + list($valid, $errors) = $this->swimlane->validateModification($values); + + if ($valid) { + + if ($this->swimlane->rename($values['id'], $values['name'])) { + $this->session->flash(t('Swimlane updated successfully.')); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a swimlane + * + * @access public + */ + public function confirm() + { + $project = $this->getProject(); + $swimlane = $this->getSwimlane($project['id']); + + $this->response->html($this->projectLayout('swimlane/remove', array( + 'project' => $project, + 'swimlane' => $swimlane, + 'title' => t('Remove a swimlane') + ))); + } + + /** + * Remove a swimlane + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->remove($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane removed successfully.')); + } else { + $this->session->flashError(t('Unable to remove this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Disable a swimlane + * + * @access public + */ + public function disable() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->disable($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Enable a swimlane + * + * @access public + */ + public function enable() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + if ($this->swimlane->enable($project['id'], $swimlane_id)) { + $this->session->flash(t('Swimlane updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this swimlane.')); + } + + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Move up a swimlane + * + * @access public + */ + public function moveup() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->swimlane->moveUp($project['id'], $swimlane_id); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } + + /** + * Move down a swimlane + * + * @access public + */ + public function movedown() + { + $this->checkCSRFParam(); + $project = $this->getProject(); + $swimlane_id = $this->request->getIntegerParam('swimlane_id'); + + $this->swimlane->moveDown($project['id'], $swimlane_id); + $this->response->redirect('?controller=swimlane&action=index&project_id='.$project['id']); + } +} diff --git a/app/Controller/Task.php b/app/Controller/Task.php index 1b20cf15..dc83f7b1 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -22,20 +22,21 @@ class Task extends Base $project = $this->project->getByToken($this->request->getStringParam('token')); // Token verification - if (! $project) { + if (empty($project)) { $this->forbidden(true); } $task = $this->taskFinder->getDetails($this->request->getIntegerParam('task_id')); - if (! $task) { + if (empty($task)) { $this->notfound(true); } - $this->response->html($this->template->layout('task_public', array( + $this->response->html($this->template->layout('task/public', array( 'project' => $project, 'comments' => $this->comment->getAll($task['id']), - 'subtasks' => $this->subTask->getAll($task['id']), + 'subtasks' => $this->subtask->getAll($task['id']), + 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), 'task' => $task, 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->color->getList(), @@ -54,7 +55,7 @@ class Task extends Base public function show() { $task = $this->getTask(); - $subtasks = $this->subTask->getAll($task['id']); + $subtasks = $this->subtask->getAll($task['id']); $values = array( 'id' => $task['id'], @@ -65,20 +66,41 @@ class Task extends Base $this->dateParser->format($values, array('date_started')); - $this->response->html($this->taskLayout('task_show', array( + $this->response->html($this->taskLayout('task/show', array( 'project' => $this->project->getById($task['project_id']), - 'files' => $this->file->getAll($task['id']), + 'files' => $this->file->getAllDocuments($task['id']), + 'images' => $this->file->getAllImages($task['id']), 'comments' => $this->comment->getAll($task['id']), 'subtasks' => $subtasks, + 'links' => $this->taskLink->getAllGroupedByLabel($task['id']), 'task' => $task, 'values' => $values, - 'timesheet' => $this->timeTracking->getTaskTimesheet($task, $subtasks), + 'link_label_list' => $this->link->getList(0, false), 'columns_list' => $this->board->getColumnsList($task['project_id']), 'colors_list' => $this->color->getList(), 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', + 'title' => $task['project_name'].' > '.$task['title'], + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ))); + } + + /** + * Display task activities + * + * @access public + */ + public function activites() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('task/activity', array( 'title' => $task['title'], + 'task' => $task, + 'ajax' => $this->request->isAjax(), + 'events' => $this->projectActivity->getTask($task['id']), ))); } @@ -87,29 +109,36 @@ class Task extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { - $project_id = $this->request->getIntegerParam('project_id'); - $this->checkProjectPermissions($project_id); + $project = $this->getProject(); + $method = $this->request->isAjax() ? 'render' : 'layout'; + $swimlanes_list = $this->swimlane->getList($project['id'], false, true); - $this->response->html($this->template->layout('task_new', array( - 'errors' => array(), - 'values' => array( - 'project_id' => $project_id, + if (empty($values)) { + + $values = array( + 'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanes_list)), 'column_id' => $this->request->getIntegerParam('column_id'), 'color_id' => $this->request->getStringParam('color_id'), 'owner_id' => $this->request->getIntegerParam('owner_id'), 'another_task' => $this->request->getIntegerParam('another_task'), - ), + ); + } + + $this->response->html($this->template->$method('task/new', array( + 'ajax' => $this->request->isAjax(), + 'errors' => $errors, + 'values' => $values + array('project_id' => $project['id']), 'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE), - 'columns_list' => $this->board->getColumnsList($project_id), - 'users_list' => $this->projectPermission->getUsersList($project_id), + 'columns_list' => $this->board->getColumnsList($project['id']), + 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true), 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($project_id), + 'categories_list' => $this->category->getList($project['id']), + 'swimlanes_list' => $swimlanes_list, 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', - 'title' => t('New task') + 'title' => $project['name'].' > '.t('New task') ))); } @@ -120,16 +149,15 @@ class Task extends Base */ public function save() { + $project = $this->getProject(); $values = $this->request->getValues(); - $values['creator_id'] = $this->acl->getUserId(); - - $this->checkProjectPermissions($values['project_id']); + $values['creator_id'] = $this->userSession->getId(); list($valid, $errors) = $this->taskValidator->validateCreation($values); if ($valid) { - if ($this->task->create($values)) { + if ($this->taskCreation->create($values)) { $this->session->flash(t('Task created successfully.')); if (isset($values['another_task']) && $values['another_task'] == 1) { @@ -138,7 +166,7 @@ class Task extends Base $this->response->redirect('?controller=task&action=create&'.http_build_query($values)); } else { - $this->response->redirect('?controller=board&action=show&project_id='.$values['project_id']); + $this->response->redirect('?controller=board&action=show&project_id='.$project['id']); } } else { @@ -146,19 +174,7 @@ class Task extends Base } } - $this->response->html($this->template->layout('task_new', array( - 'errors' => $errors, - 'values' => $values, - 'projects_list' => $this->project->getListByStatus(ProjectModel::ACTIVE), - 'columns_list' => $this->board->getColumnsList($values['project_id']), - 'users_list' => $this->projectPermission->getUsersList($values['project_id']), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($values['project_id']), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', - 'title' => t('New task') - ))); + $this->create($values, $errors); } /** @@ -166,32 +182,34 @@ class Task extends Base * * @access public */ - public function edit() + public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); $ajax = $this->request->isAjax(); - $this->dateParser->format($task, array('date_due')); + if (empty($values)) { + $values = $task; + } + + $this->dateParser->format($values, array('date_due')); $params = array( - 'values' => $task, - 'errors' => array(), + 'values' => $values, + 'errors' => $errors, 'task' => $task, - 'users_list' => $this->projectPermission->getUsersList($task['project_id']), + 'users_list' => $this->projectPermission->getMemberList($task['project_id']), 'colors_list' => $this->color->getList(), 'categories_list' => $this->category->getList($task['project_id']), 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), 'ajax' => $ajax, - 'menu' => 'tasks', - 'title' => t('Edit a task') ); if ($ajax) { - $this->response->html($this->template->load('task_edit', $params)); + $this->response->html($this->template->render('task/edit', $params)); } else { - $this->response->html($this->taskLayout('task_edit', $params)); + $this->response->html($this->taskLayout('task/edit', $params)); } } @@ -209,14 +227,14 @@ class Task extends Base if ($valid) { - if ($this->task->update($values)) { + if ($this->taskModification->update($values)) { $this->session->flash(t('Task updated successfully.')); if ($this->request->getIntegerParam('ajax')) { $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } else { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } } else { @@ -224,20 +242,7 @@ class Task extends Base } } - $this->response->html($this->taskLayout('task_edit', array( - 'values' => $values, - 'errors' => $errors, - 'task' => $task, - 'columns_list' => $this->board->getColumnsList($values['project_id']), - 'users_list' => $this->projectPermission->getUsersList($values['project_id']), - 'colors_list' => $this->color->getList(), - 'categories_list' => $this->category->getList($values['project_id']), - 'date_format' => $this->config->get('application_date_format'), - 'date_formats' => $this->dateParser->getAvailableFormats(), - 'menu' => 'tasks', - 'title' => t('Edit a task'), - 'ajax' => $this->request->isAjax(), - ))); + $this->edit($values, $errors); } /** @@ -250,16 +255,16 @@ class Task extends Base $task = $this->getTask(); $values = $this->request->getValues(); - list($valid, $errors) = $this->taskValidator->validateTimeModification($values); + list($valid,) = $this->taskValidator->validateTimeModification($values); - if ($valid && $this->task->update($values)) { + if ($valid && $this->taskModification->update($values)) { $this->session->flash(t('Task updated successfully.')); } else { $this->session->flashError(t('Unable to update your task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } /** @@ -270,24 +275,35 @@ class Task extends Base public function close() { $task = $this->getTask(); + $redirect = $this->request->getStringParam('redirect'); if ($this->request->getStringParam('confirmation') === 'yes') { $this->checkCSRFParam(); - if ($this->task->close($task['id'])) { + if ($this->taskStatus->close($task['id'])) { $this->session->flash(t('Task closed successfully.')); } else { $this->session->flashError(t('Unable to close this task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + if ($redirect === 'board') { + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); } - $this->response->html($this->taskLayout('task_close', array( + if ($this->request->isAjax()) { + $this->response->html($this->template->render('task/close', array( + 'task' => $task, + 'redirect' => $redirect, + ))); + } + + $this->response->html($this->taskLayout('task/close', array( 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Close a task') + 'redirect' => $redirect, ))); } @@ -304,19 +320,17 @@ class Task extends Base $this->checkCSRFParam(); - if ($this->task->open($task['id'])) { + if ($this->taskStatus->open($task['id'])) { $this->session->flash(t('Task opened successfully.')); } else { $this->session->flashError(t('Unable to open this task.')); } - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } - $this->response->html($this->taskLayout('task_open', array( + $this->response->html($this->taskLayout('task/open', array( 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Open a task') ))); } @@ -346,10 +360,8 @@ class Task extends Base $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } - $this->response->html($this->taskLayout('task_remove', array( + $this->response->html($this->taskLayout('task/remove', array( 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Remove a task') ))); } @@ -365,21 +377,19 @@ class Task extends Base if ($this->request->getStringParam('confirmation') === 'yes') { $this->checkCSRFParam(); - $task_id = $this->task->duplicateToSameProject($task); + $task_id = $this->taskDuplication->duplicate($task['id']); if ($task_id) { $this->session->flash(t('Task created successfully.')); - $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$task['project_id']); } else { $this->session->flashError(t('Unable to create this task.')); - $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=duplicate&task_id='.$task['id'].'&project_id='.$task['project_id']); } } - $this->response->html($this->taskLayout('task_duplicate', array( + $this->response->html($this->taskLayout('task/duplicate', array( 'task' => $task, - 'menu' => 'tasks', - 'title' => t('Duplicate a task') ))); } @@ -401,7 +411,7 @@ class Task extends Base if ($valid) { - if ($this->task->update($values)) { + if ($this->taskModification->update($values)) { $this->session->flash(t('Task updated successfully.')); } else { @@ -412,7 +422,7 @@ class Task extends Base $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); } else { - $this->response->redirect('?controller=task&action=show&task_id='.$task['id']); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); } } } @@ -426,49 +436,123 @@ class Task extends Base 'errors' => $errors, 'task' => $task, 'ajax' => $ajax, - 'menu' => 'tasks', - 'title' => t('Edit the description'), ); if ($ajax) { - $this->response->html($this->template->load('task_edit_description', $params)); + $this->response->html($this->template->render('task/edit_description', $params)); } else { - $this->response->html($this->taskLayout('task_edit_description', $params)); + $this->response->html($this->taskLayout('task/edit_description', $params)); } } /** - * Move a task to another project + * Edit recurrence form * * @access public */ - public function move() + public function recurrence() { - $this->toAnotherProject('move'); + $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateEditRecurrence($values); + + if ($valid) { + + if ($this->taskModification->update($values)) { + $this->session->flash(t('Task updated successfully.')); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + + if ($ajax) { + $this->response->redirect('?controller=board&action=show&project_id='.$task['project_id']); + } + else { + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$task['project_id']); + } + } + } + else { + $values = $task; + $errors = array(); + } + + $params = array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'ajax' => $ajax, + 'recurrence_status_list' => $this->task->getRecurrenceStatusList(), + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ); + + if ($ajax) { + $this->response->html($this->template->render('task/edit_recurrence', $params)); + } + else { + $this->response->html($this->taskLayout('task/edit_recurrence', $params)); + } } /** - * Duplicate a task to another project + * Move a task to another project * * @access public */ - public function copy() + public function move() { - $this->toAnotherProject('duplicate'); + $task = $this->getTask(); + $values = $task; + $errors = array(); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); + + unset($projects_list[$task['project_id']]); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + list($valid, $errors) = $this->taskValidator->validateProjectModification($values); + + if ($valid) { + + if ($this->taskDuplication->moveToProject($task['id'], $values['project_id'])) { + $this->session->flash(t('Task updated successfully.')); + $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'&project_id='.$values['project_id']); + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + } + } + + $this->response->html($this->taskLayout('task/move_project', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'projects_list' => $projects_list, + ))); } /** - * Common methods between the actions "move" and "copy" + * Duplicate a task to another project * - * @access private + * @access public */ - private function toAnotherProject($action) + public function copy() { $task = $this->getTask(); $values = $task; $errors = array(); - $projects_list = $this->projectPermission->getAllowedProjects($this->acl->getUserId()); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); unset($projects_list[$task['project_id']]); @@ -478,10 +562,10 @@ class Task extends Base list($valid, $errors) = $this->taskValidator->validateProjectModification($values); if ($valid) { - $task_id = $this->task->{$action.'ToAnotherProject'}($values['project_id'], $task); + $task_id = $this->taskDuplication->duplicateToProject($task['id'], $values['project_id']); if ($task_id) { $this->session->flash(t('Task created successfully.')); - $this->response->redirect('?controller=task&action=show&task_id='.$task_id); + $this->response->redirect('?controller=task&action=show&task_id='.$task_id.'&project_id='.$values['project_id']); } else { $this->session->flashError(t('Unable to create your task.')); @@ -489,13 +573,49 @@ class Task extends Base } } - $this->response->html($this->taskLayout('task_'.$action.'_project', array( + $this->response->html($this->taskLayout('task/duplicate_project', array( 'values' => $values, 'errors' => $errors, 'task' => $task, 'projects_list' => $projects_list, - 'menu' => 'tasks', - 'title' => t(ucfirst($action).' the task to another project') + ))); + } + + /** + * Display the time tracking details + * + * @access public + */ + public function timesheet() + { + $task = $this->getTask(); + + $subtask_paginator = $this->paginator + ->setUrl('task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'pagination' => 'subtasks')) + ->setMax(15) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->subtaskTimeTracking->getTaskQuery($task['id'])) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $this->response->html($this->taskLayout('task/time_tracking', array( + 'task' => $task, + 'subtask_paginator' => $subtask_paginator, + ))); + } + + /** + * Display the task transitions + * + * @access public + */ + public function transitions() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('task/transitions', array( + 'task' => $task, + 'transitions' => $this->transition->getAllByTask($task['id']), ))); } } diff --git a/app/Controller/Tasklink.php b/app/Controller/Tasklink.php new file mode 100644 index 00000000..dd076802 --- /dev/null +++ b/app/Controller/Tasklink.php @@ -0,0 +1,179 @@ +<?php + +namespace Controller; + +/** + * TaskLink controller + * + * @package controller + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Tasklink extends Base +{ + /** + * Get the current link + * + * @access private + * @return array + */ + private function getTaskLink() + { + $link = $this->taskLink->getById($this->request->getIntegerParam('link_id')); + + if (empty($link)) { + $this->notfound(); + } + + return $link; + } + + /** + * Creation form + * + * @access public + */ + public function create(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + if ($ajax && empty($errors)) { + $this->response->html($this->template->render('tasklink/create', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'labels' => $this->link->getList(0, false), + 'title' => t('Add a new link'), + 'ajax' => $ajax, + ))); + } + + $this->response->html($this->taskLayout('tasklink/create', array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'labels' => $this->link->getList(0, false), + 'title' => t('Add a new link') + ))); + } + + /** + * Validation and creation + * + * @access public + */ + public function save() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + list($valid, $errors) = $this->taskLink->validateCreation($values); + + if ($valid) { + + if ($this->taskLink->create($values['task_id'], $values['opposite_task_id'], $values['link_id'])) { + $this->session->flash(t('Link added successfully.')); + + if ($ajax) { + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + } + + $errors = array('title' => array(t('The exact same link already exists'))); + $this->session->flashError(t('Unable to create your link.')); + } + + $this->create($values, $errors); + } + + /** + * Edit form + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + $task_link = $this->getTaskLink(); + + if (empty($values)) { + $opposite_task = $this->taskFinder->getById($task_link['opposite_task_id']); + $values = $task_link; + $values['title'] = '#'.$opposite_task['id'].' - '.$opposite_task['title']; + } + + $this->response->html($this->taskLayout('tasklink/edit', array( + 'values' => $values, + 'errors' => $errors, + 'task_link' => $task_link, + 'task' => $task, + 'labels' => $this->link->getList(0, false), + 'title' => t('Edit link') + ))); + } + + /** + * Validation and update + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskLink->validateModification($values); + + if ($valid) { + + if ($this->taskLink->update($values['id'], $values['task_id'], $values['opposite_task_id'], $values['link_id'])) { + $this->session->flash(t('Link updated successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + } + + $this->session->flashError(t('Unable to update your link.')); + } + + $this->edit($values, $errors); + } + + /** + * Confirmation dialog before removing a link + * + * @access public + */ + public function confirm() + { + $task = $this->getTask(); + $link = $this->getTaskLink(); + + $this->response->html($this->taskLayout('tasklink/remove', array( + 'link' => $link, + 'task' => $task, + ))); + } + + /** + * Remove a link + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $task = $this->getTask(); + + if ($this->taskLink->remove($this->request->getIntegerParam('link_id'))) { + $this->session->flash(t('Link removed successfully.')); + } + else { + $this->session->flashError(t('Unable to remove this link.')); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])).'#links'); + } +} diff --git a/app/Controller/Timetable.php b/app/Controller/Timetable.php new file mode 100644 index 00000000..65edb44c --- /dev/null +++ b/app/Controller/Timetable.php @@ -0,0 +1,39 @@ +<?php + +namespace Controller; + +use DateTime; + +/** + * Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetable extends User +{ + /** + * Display timetable for the user + * + * @access public + */ + public function index() + { + $user = $this->getUser(); + $from = $this->request->getStringParam('from', date('Y-m-d')); + $to = $this->request->getStringParam('to', date('Y-m-d', strtotime('next week'))); + $timetable = $this->timetable->calculate($user['id'], new DateTime($from), new DateTime($to)); + + $this->response->html($this->layout('timetable/index', array( + 'user' => $user, + 'timetable' => $timetable, + 'values' => array( + 'from' => $from, + 'to' => $to, + 'controller' => 'timetable', + 'action' => 'index', + 'user_id' => $user['id'], + ), + ))); + } +} diff --git a/app/Controller/Timetableday.php b/app/Controller/Timetableday.php new file mode 100644 index 00000000..c8f7ac8a --- /dev/null +++ b/app/Controller/Timetableday.php @@ -0,0 +1,88 @@ +<?php + +namespace Controller; + +/** + * Day Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableday extends User +{ + /** + * Display timetable for the user + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + $this->response->html($this->layout('timetable_day/index', array( + 'timetable' => $this->timetableDay->getByUser($user['id']), + 'values' => $values + array('user_id' => $user['id']), + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Validate and save + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->timetableDay->validateCreation($values); + + if ($valid) { + + if ($this->timetableDay->create($values['user_id'], $values['start'], $values['end'])) { + $this->session->flash(t('Time slot created successfully.')); + $this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save this time slot.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout('timetable_day/remove', array( + 'slot_id' => $this->request->getIntegerParam('slot_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->timetableDay->remove($this->request->getIntegerParam('slot_id'))) { + $this->session->flash(t('Time slot removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this time slot.')); + } + + $this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Timetableextra.php b/app/Controller/Timetableextra.php new file mode 100644 index 00000000..7c6fe265 --- /dev/null +++ b/app/Controller/Timetableextra.php @@ -0,0 +1,16 @@ +<?php + +namespace Controller; + +/** + * Over-time Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableextra extends Timetableoff +{ + protected $model = 'timetableExtra'; + protected $controller_url = 'timetableextra'; + protected $template_dir = 'timetable_extra'; +} diff --git a/app/Controller/Timetableoff.php b/app/Controller/Timetableoff.php new file mode 100644 index 00000000..585014a3 --- /dev/null +++ b/app/Controller/Timetableoff.php @@ -0,0 +1,107 @@ +<?php + +namespace Controller; + +/** + * Time-off Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableoff extends User +{ + protected $model = 'timetableOff'; + protected $controller_url = 'timetableoff'; + protected $template_dir = 'timetable_off'; + + /** + * Display timetable for the user + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + $paginator = $this->paginator + ->setUrl($this->controller_url, 'index', array('user_id' => $user['id'])) + ->setMax(10) + ->setOrder('date') + ->setDirection('desc') + ->setQuery($this->{$this->model}->getUserQuery($user['id'])) + ->calculate(); + + $this->response->html($this->layout($this->template_dir.'/index', array( + 'values' => $values + array('user_id' => $user['id']), + 'errors' => $errors, + 'paginator' => $paginator, + 'user' => $user, + ))); + } + + /** + * Validate and save + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->{$this->model}->validateCreation($values); + + if ($valid) { + + if ($this->{$this->model}->create( + $values['user_id'], + $values['date'], + isset($values['all_day']) && $values['all_day'] == 1, + $values['start'], + $values['end'], + $values['comment'])) { + + $this->session->flash(t('Time slot created successfully.')); + $this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save this time slot.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout($this->template_dir.'/remove', array( + 'slot_id' => $this->request->getIntegerParam('slot_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->{$this->model}->remove($this->request->getIntegerParam('slot_id'))) { + $this->session->flash(t('Time slot removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this time slot.')); + } + + $this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Timetableweek.php b/app/Controller/Timetableweek.php new file mode 100644 index 00000000..b8ce00e7 --- /dev/null +++ b/app/Controller/Timetableweek.php @@ -0,0 +1,99 @@ +<?php + +namespace Controller; + +/** + * Week Timetable controller + * + * @package controller + * @author Frederic Guillot + */ +class Timetableweek extends User +{ + /** + * Display timetable for the user + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $user = $this->getUser(); + + if (empty($values)) { + + $day = $this->timetableDay->getByUser($user['id']); + + $values = array( + 'user_id' => $user['id'], + 'start' => isset($day[0]['start']) ? $day[0]['start'] : null, + 'end' => isset($day[0]['end']) ? $day[0]['end'] : null, + ); + } + + $this->response->html($this->layout('timetable_week/index', array( + 'timetable' => $this->timetableWeek->getByUser($user['id']), + 'values' => $values, + 'errors' => $errors, + 'user' => $user, + ))); + } + + /** + * Validate and save + * + * @access public + */ + public function save() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->timetableWeek->validateCreation($values); + + if ($valid) { + + if ($this->timetableWeek->create($values['user_id'], $values['day'], $values['start'], $values['end'])) { + $this->session->flash(t('Time slot created successfully.')); + $this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $values['user_id']))); + } + else { + $this->session->flashError(t('Unable to save this time slot.')); + } + } + + $this->index($values, $errors); + } + + /** + * Confirmation dialag box to remove a row + * + * @access public + */ + public function confirm() + { + $user = $this->getUser(); + + $this->response->html($this->layout('timetable_week/remove', array( + 'slot_id' => $this->request->getIntegerParam('slot_id'), + 'user' => $user, + ))); + } + + /** + * Remove a row + * + * @access public + */ + public function remove() + { + $this->checkCSRFParam(); + $user = $this->getUser(); + + if ($this->timetableWeek->remove($this->request->getIntegerParam('slot_id'))) { + $this->session->flash(t('Time slot removed successfully.')); + } + else { + $this->session->flash(t('Unable to remove this time slot.')); + } + + $this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $user['id']))); + } +} diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php new file mode 100644 index 00000000..a8b0351f --- /dev/null +++ b/app/Controller/Twofactor.php @@ -0,0 +1,167 @@ +<?php + +namespace Controller; + +use Otp\Otp; +use Otp\GoogleAuthenticator; +use Base32\Base32; + +/** + * Two Factor Auth controller + * + * @package controller + * @author Frederic Guillot + */ +class Twofactor extends User +{ + /** + * Only the current user can access to 2FA settings + * + * @access private + */ + private function checkCurrentUser(array $user) + { + if ($user['id'] != $this->userSession->getId()) { + $this->forbidden(); + } + } + + /** + * Index + * + * @access public + */ + public function index() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $label = $user['email'] ?: $user['username']; + + $this->response->html($this->layout('twofactor/index', array( + 'user' => $user, + 'qrcode_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getQrCodeUrl('totp', $label, $user['twofactor_secret']) : '', + 'key_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getKeyUri('totp', $label, $user['twofactor_secret']) : '', + ))); + } + + /** + * Enable/disable 2FA + * + * @access public + */ + public function save() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $values = $this->request->getValues(); + + if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) { + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 1, + 'twofactor_secret' => GoogleAuthenticator::generateRandom(), + )); + } + else { + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 0, + 'twofactor_secret' => '', + )); + } + + // Allow the user to test or disable the feature + $_SESSION['user']['twofactor_activated'] = false; + + $this->session->flash(t('User updated successfully.')); + $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + } + + /** + * Test 2FA + * + * @access public + */ + public function test() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $otp = new Otp; + $values = $this->request->getValues(); + + if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) { + $this->session->flash(t('The two factor authentication code is valid.')); + } + else { + $this->session->flashError(t('The two factor authentication code is not valid.')); + } + + $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + } + + /** + * Check 2FA + * + * @access public + */ + public function check() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $otp = new Otp; + $values = $this->request->getValues(); + + if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) { + $this->session['2fa_validated'] = true; + $this->session->flash(t('The two factor authentication code is valid.')); + $this->response->redirect($this->helper->url->to('app', 'index')); + } + else { + $this->session->flashError(t('The two factor authentication code is not valid.')); + $this->response->redirect($this->helper->url->to('twofactor', 'code')); + } + } + + /** + * Ask the 2FA code + * + * @access public + */ + public function code() + { + $this->response->html($this->template->layout('twofactor/check', array( + 'title' => t('Check two factor authentication code'), + ))); + } + + /** + * Disable 2FA for a user + * + * @access public + */ + public function disable() + { + $user = $this->getUser(); + + if ($this->request->getStringParam('disable') === 'yes') { + + $this->checkCSRFParam(); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 0, + 'twofactor_secret' => '', + )); + + $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user['id']))); + } + + $this->response->html($this->layout('twofactor/disable', array( + 'user' => $user, + ))); + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php index 834b2379..4cea06b1 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -11,103 +11,41 @@ namespace Controller; class User extends Base { /** - * Logout and destroy session - * - * @access public - */ - public function logout() - { - $this->checkCSRFParam(); - $this->authentication->backend('rememberMe')->destroy($this->acl->getUserId()); - $this->session->close(); - $this->response->redirect('?controller=user&action=login'); - } - - /** - * Display the form login - * - * @access public - */ - public function login() - { - if ($this->acl->isLogged()) { - $this->response->redirect('?controller=app'); - } - - $this->response->html($this->template->layout('user_login', array( - 'errors' => array(), - 'values' => array(), - 'no_layout' => true, - 'redirect_query' => $this->request->getStringParam('redirect_query'), - 'title' => t('Login') - ))); - } - - /** - * Check credentials - * - * @access public - */ - public function check() - { - $redirect_query = $this->request->getStringParam('redirect_query'); - $values = $this->request->getValues(); - list($valid, $errors) = $this->authentication->validateForm($values); - - if ($valid) { - if ($redirect_query !== '') { - $this->response->redirect('?'.$redirect_query); - } - else { - $this->response->redirect('?controller=app'); - } - } - - $this->response->html($this->template->layout('user_login', array( - 'errors' => $errors, - 'values' => $values, - 'no_layout' => true, - 'redirect_query' => $redirect_query, - 'title' => t('Login') - ))); - } - - /** * Common layout for user views * - * @access private + * @access protected * @param string $template Template name * @param array $params Template parameters * @return string */ - private function layout($template, array $params) + protected function layout($template, array $params) { - $content = $this->template->load($template, $params); + $content = $this->template->render($template, $params); $params['user_content_for_layout'] = $content; - $params['menu'] = 'users'; + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); if (isset($params['user'])) { - $params['title'] = $params['user']['name'] ?: $params['user']['username']; + $params['title'] = ($params['user']['name'] ?: $params['user']['username']).' (#'.$params['user']['id'].')'; } - return $this->template->layout('user_layout', $params); + return $this->template->layout('user/layout', $params); } /** * Common method to get the user * - * @access private + * @access protected * @return array */ - private function getUser() + protected function getUser() { $user = $this->user->getById($this->request->getIntegerParam('user_id')); - if (! $user) { + if (empty($user)) { $this->notfound(); } - if ($this->acl->isRegularUser() && $this->acl->getUserId() != $user['id']) { + if (! $this->userSession->isAdmin() && $this->userSession->getId() != $user['id']) { $this->forbidden(); } @@ -121,16 +59,19 @@ class User extends Base */ public function index() { - $users = $this->user->getAll(); - $nb_users = count($users); + $paginator = $this->paginator + ->setUrl('user', 'index') + ->setMax(30) + ->setOrder('username') + ->setQuery($this->user->getQuery()) + ->calculate(); $this->response->html( - $this->template->layout('user_index', array( + $this->template->layout('user/index', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'projects' => $this->project->getList(), - 'users' => $users, - 'nb_users' => $nb_users, - 'menu' => 'users', - 'title' => t('Users').' ('.$nb_users.')' + 'title' => t('Users').' ('.$paginator->getTotal().')', + 'paginator' => $paginator, ))); } @@ -139,13 +80,15 @@ class User extends Base * * @access public */ - public function create() + public function create(array $values = array(), array $errors = array()) { - $this->response->html($this->template->layout('user_new', array( + $this->response->html($this->template->layout('user/new', array( + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), 'projects' => $this->project->getList(), - 'errors' => array(), - 'values' => array(), - 'menu' => 'users', + 'errors' => $errors, + 'values' => $values, 'title' => t('New user') ))); } @@ -162,22 +105,18 @@ class User extends Base if ($valid) { - if ($this->user->create($values)) { + $user_id = $this->user->create($values); + + if ($user_id !== false) { $this->session->flash(t('User created successfully.')); - $this->response->redirect('?controller=user'); + $this->response->redirect($this->helper->url->to('user', 'show', array('user_id' => $user_id))); } else { $this->session->flashError(t('Unable to create your user.')); } } - $this->response->html($this->template->layout('user_new', array( - 'projects' => $this->project->getList(), - 'errors' => $errors, - 'values' => $values, - 'menu' => 'users', - 'title' => t('New user') - ))); + $this->create($values, $errors); } /** @@ -188,9 +127,48 @@ class User extends Base public function show() { $user = $this->getUser(); - $this->response->html($this->layout('user_show', array( + $this->response->html($this->layout('user/show', array( 'projects' => $this->projectPermission->getAllowedProjects($user['id']), 'user' => $user, + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), + ))); + } + + /** + * Display user calendar + * + * @access public + */ + public function calendar() + { + $user = $this->getUser(); + + $this->response->html($this->layout('user/calendar', array( + 'user' => $user, + ))); + } + + /** + * Display timesheet + * + * @access public + */ + public function timesheet() + { + $user = $this->getUser(); + + $subtask_paginator = $this->paginator + ->setUrl('user', 'timesheet', array('user_id' => $user['id'], 'pagination' => 'subtasks')) + ->setMax(20) + ->setOrder('start') + ->setDirection('DESC') + ->setQuery($this->subtaskTimeTracking->getUserQuery($user['id'])) + ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); + + $this->response->html($this->layout('user/timesheet', array( + 'subtask_paginator' => $subtask_paginator, + 'user' => $user, ))); } @@ -202,7 +180,7 @@ class User extends Base public function last() { $user = $this->getUser(); - $this->response->html($this->layout('user_last', array( + $this->response->html($this->layout('user/last', array( 'last_logins' => $this->lastLogin->getAll($user['id']), 'user' => $user, ))); @@ -216,7 +194,7 @@ class User extends Base public function sessions() { $user = $this->getUser(); - $this->response->html($this->layout('user_sessions', array( + $this->response->html($this->layout('user/sessions', array( 'sessions' => $this->authentication->backend('rememberMe')->getAll($user['id']), 'user' => $user, ))); @@ -251,7 +229,7 @@ class User extends Base $this->response->redirect('?controller=user&action=notifications&user_id='.$user['id']); } - $this->response->html($this->layout('user_notifications', array( + $this->response->html($this->layout('user/notifications', array( 'projects' => $this->projectPermission->getAllowedProjects($user['id']), 'notifications' => $this->notification->readSettings($user['id']), 'user' => $user, @@ -266,13 +244,42 @@ class User extends Base public function external() { $user = $this->getUser(); - $this->response->html($this->layout('user_external', array( + $this->response->html($this->layout('user/external', array( 'last_logins' => $this->lastLogin->getAll($user['id']), 'user' => $user, ))); } /** + * Public access management + * + * @access public + */ + public function share() + { + $user = $this->getUser(); + $switch = $this->request->getStringParam('switch'); + + if ($switch === 'enable' || $switch === 'disable') { + + $this->checkCSRFParam(); + + if ($this->user->{$switch.'PublicAccess'}($user['id'])) { + $this->session->flash(t('User updated successfully.')); + } else { + $this->session->flashError(t('Unable to update this user.')); + } + + $this->response->redirect($this->helper->url->to('user', 'share', array('user_id' => $user['id']))); + } + + $this->response->html($this->layout('user/share', array( + 'user' => $user, + 'title' => t('Public access'), + ))); + } + + /** * Password modification * * @access public @@ -301,7 +308,7 @@ class User extends Base } } - $this->response->html($this->layout('user_password', array( + $this->response->html($this->layout('user/password', array( 'values' => $values, 'errors' => $errors, 'user' => $user, @@ -323,9 +330,9 @@ class User extends Base if ($this->request->isPost()) { - $values = $this->request->getValues(); + $values = $this->request->getValues() + array('disable_login_form' => 0); - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin()) { $values += array('is_admin' => 0); } else { @@ -350,11 +357,13 @@ class User extends Base } } - $this->response->html($this->layout('user_edit', array( + $this->response->html($this->layout('user/edit', array( 'values' => $values, 'errors' => $errors, 'projects' => $this->projectPermission->filterProjects($this->project->getList(), $user['id']), 'user' => $user, + 'timezones' => $this->config->getTimezones(true), + 'languages' => $this->config->getLanguages(true), ))); } @@ -380,7 +389,7 @@ class User extends Base $this->response->redirect('?controller=user'); } - $this->response->html($this->layout('user_remove', array( + $this->response->html($this->layout('user/remove', array( 'user' => $user, ))); } @@ -401,22 +410,22 @@ class User extends Base if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { - if ($this->authentication->backend('google')->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('google')->updateUser($this->userSession->getId(), $profile)) { $this->session->flash(t('Your Google Account is linked to your profile successfully.')); } else { $this->session->flashError(t('Unable to link your Google Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } else if ($this->authentication->backend('google')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); } else { - $this->response->html($this->template->layout('user_login', array( + $this->response->html($this->template->layout('auth/index', array( 'errors' => array('login' => t('Google authentication failed')), 'values' => array(), 'no_layout' => true, @@ -438,14 +447,14 @@ class User extends Base public function unlinkGoogle() { $this->checkCSRFParam(); - if ($this->authentication->backend('google')->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('google')->unlink($this->userSession->getId())) { $this->session->flash(t('Your Google Account is not linked anymore to your profile.')); } else { $this->session->flashError(t('Unable to unlink your Google Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } /** @@ -453,7 +462,7 @@ class User extends Base * * @access public */ - public function gitHub() + public function github() { $code = $this->request->getStringParam('code'); @@ -463,22 +472,22 @@ class User extends Base if (is_array($profile)) { // If the user is already logged, link the account otherwise authenticate - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { - if ($this->authentication->backend('gitHub')->updateUser($this->acl->getUserId(), $profile)) { + if ($this->authentication->backend('gitHub')->updateUser($this->userSession->getId(), $profile)) { $this->session->flash(t('Your GitHub account was successfully linked to your profile.')); } else { $this->session->flashError(t('Unable to link your GitHub Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } else if ($this->authentication->backend('gitHub')->authenticate($profile['id'])) { $this->response->redirect('?controller=app'); } else { - $this->response->html($this->template->layout('user_login', array( + $this->response->html($this->template->layout('auth/index', array( 'errors' => array('login' => t('GitHub authentication failed')), 'values' => array(), 'no_layout' => true, @@ -497,19 +506,19 @@ class User extends Base * * @access public */ - public function unlinkGitHub() + public function unlinkGithub() { $this->checkCSRFParam(); $this->authentication->backend('gitHub')->revokeGitHubAccess(); - if ($this->authentication->backend('gitHub')->unlink($this->acl->getUserId())) { + if ($this->authentication->backend('gitHub')->unlink($this->userSession->getId())) { $this->session->flash(t('Your GitHub account is no longer linked to your profile.')); } else { $this->session->flashError(t('Unable to unlink your GitHub Account.')); } - $this->response->redirect('?controller=user&action=external&user_id='.$this->acl->getUserId()); + $this->response->redirect('?controller=user&action=external&user_id='.$this->userSession->getId()); } } diff --git a/app/Controller/Webhook.php b/app/Controller/Webhook.php index 71acab08..10a24e47 100644 --- a/app/Controller/Webhook.php +++ b/app/Controller/Webhook.php @@ -35,7 +35,7 @@ class Webhook extends Base list($valid,) = $this->taskValidator->validateCreation($values); - if ($valid && $this->task->create($values)) { + if ($valid && $this->taskCreation->create($values)) { $this->response->text('OK'); } @@ -55,9 +55,91 @@ class Webhook extends Base $this->githubWebhook->setProjectId($this->request->getIntegerParam('project_id')); - $this->githubWebhook->parsePayload( + $result = $this->githubWebhook->parsePayload( $this->request->getHeader('X-Github-Event'), - $this->request->getBody() + $this->request->getJson() ?: array() ); + + echo $result ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Gitlab webhooks + * + * @access public + */ + public function gitlab() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + $this->gitlabWebhook->setProjectId($this->request->getIntegerParam('project_id')); + + $result = $this->gitlabWebhook->parsePayload( + $this->request->getJson() ?: array() + ); + + echo $result ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Bitbucket webhooks + * + * @access public + */ + public function bitbucket() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + $this->bitbucketWebhook->setProjectId($this->request->getIntegerParam('project_id')); + + $result = $this->bitbucketWebhook->parsePayload(json_decode(@$_POST['payload'], true) ?: array()); + + echo $result ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Postmark webhooks + * + * @access public + */ + public function postmark() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + echo $this->postmark->receiveEmail($this->request->getJson() ?: array()) ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Mailgun webhooks + * + * @access public + */ + public function mailgun() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + echo $this->mailgun->receiveEmail($_POST) ? 'PARSED' : 'IGNORED'; + } + + /** + * Handle Sendgrid webhooks + * + * @access public + */ + public function sendgrid() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + echo $this->sendgridWebhook->parsePayload($_POST) ? 'PARSED' : 'IGNORED'; } } diff --git a/app/Core/Base.php b/app/Core/Base.php new file mode 100644 index 00000000..6cb87cbc --- /dev/null +++ b/app/Core/Base.php @@ -0,0 +1,112 @@ +<?php + +namespace Core; + +use Pimple\Container; + +/** + * Base class + * + * @package core + * @author Frederic Guillot + * + * @property \Core\Helper $helper + * @property \Core\EmailClient $emailClient + * @property \Core\HttpClient $httpClient + * @property \Core\Paginator $paginator + * @property \Core\Request $request + * @property \Core\Session $session + * @property \Core\Template $template + * @property \Integration\BitbucketWebhook $bitbucketWebhook + * @property \Integration\GithubWebhook $githubWebhook + * @property \Integration\GitlabWebhook $gitlabWebhook + * @property \Integration\HipchatWebhook $hipchatWebhook + * @property \Integration\Jabber $jabber + * @property \Integration\Mailgun $mailgun + * @property \Integration\Postmark $postmark + * @property \Integration\SendgridWebhook $sendgridWebhook + * @property \Integration\SlackWebhook $slackWebhook + * @property \Integration\Smtp $smtp + * @property \Model\Acl $acl + * @property \Model\Action $action + * @property \Model\Authentication $authentication + * @property \Model\Board $board + * @property \Model\Budget $budget + * @property \Model\Category $category + * @property \Model\Color $color + * @property \Model\Comment $comment + * @property \Model\Config $config + * @property \Model\Currency $currency + * @property \Model\DateParser $dateParser + * @property \Model\File $file + * @property \Model\HourlyRate $hourlyRate + * @property \Model\LastLogin $lastLogin + * @property \Model\Link $link + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\ProjectActivity $projectActivity + * @property \Model\ProjectAnalytic $projectAnalytic + * @property \Model\ProjectDuplication $projectDuplication + * @property \Model\ProjectDailySummary $projectDailySummary + * @property \Model\ProjectIntegration $projectIntegration + * @property \Model\ProjectPermission $projectPermission + * @property \Model\Subtask $subtask + * @property \Model\SubtaskExport $subtaskExport + * @property \Model\SubtaskForecast $subtaskForecast + * @property \Model\SubtaskTimeTracking $subtaskTimeTracking + * @property \Model\Swimlane $swimlane + * @property \Model\Task $task + * @property \Model\TaskCreation $taskCreation + * @property \Model\TaskDuplication $taskDuplication + * @property \Model\TaskExport $taskExport + * @property \Model\TaskFinder $taskFinder + * @property \Model\TaskFilter $taskFilter + * @property \Model\TaskLink $taskLink + * @property \Model\TaskModification $taskModification + * @property \Model\TaskPermission $taskPermission + * @property \Model\TaskPosition $taskPosition + * @property \Model\TaskStatus $taskStatus + * @property \Model\TaskValidator $taskValidator + * @property \Model\Timetable $timetable + * @property \Model\TimetableDay $timetableDay + * @property \Model\TimetableExtra $timetableExtra + * @property \Model\TimetableOff $timetableOff + * @property \Model\TimetableWeek $timetableWeek + * @property \Model\Transition $transition + * @property \Model\User $user + * @property \Model\UserSession $userSession + * @property \Model\Webhook $webhook + */ +abstract class Base +{ + /** + * Container instance + * + * @access protected + * @var \Pimple\Container + */ + protected $container; + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return $this->container[$name]; + } +} diff --git a/app/Core/Cache.php b/app/Core/Cache.php new file mode 100644 index 00000000..670a76e0 --- /dev/null +++ b/app/Core/Cache.php @@ -0,0 +1,58 @@ +<?php + +namespace Core; + +use Pimple\Container; + +abstract class Cache +{ + /** + * Container instance + * + * @access protected + * @var \Pimple\Container + */ + protected $container; + + abstract public function init(); + abstract public function set($key, $value); + abstract public function get($key); + abstract public function flush(); + abstract public function remove($key); + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + $this->init(); + } + + /** + * Proxy cache + * + * Note: Arguments must be scalar types + * + * @access public + * @param string $container Container name + * @param string $method Container method + * @return mixed + */ + public function proxy($container, $method) + { + $args = func_get_args(); + $key = 'proxy_'.implode('_', $args); + $result = $this->get($key); + + if ($result === null) { + $result = call_user_func_array(array($this->container[$container], $method), array_splice($args, 2)); + $this->set($key, $result); + } + + return $result; + } +} diff --git a/app/Core/Cli.php b/app/Core/Cli.php deleted file mode 100644 index 13533b9a..00000000 --- a/app/Core/Cli.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php - -namespace Core; - -use Closure; - -/** - * CLI class - * - * @package core - * @author Frederic Guillot - */ -class Cli -{ - /** - * Default command name - * - * @access public - * @var string - */ - public $default_command = 'help'; - - /** - * List of registered commands - * - * @access private - * @var array - */ - private $commands = array(); - - /** - * - * - * @access public - * @param string $command Command name - * @param Closure $callback Command callback - */ - public function register($command, Closure $callback) - { - $this->commands[$command] = $callback; - } - - /** - * Execute a command - * - * @access public - * @param string $command Command name - */ - public function call($command) - { - if (isset($this->commands[$command])) { - $this->commands[$command](); - exit; - } - } - - /** - * Determine which command to execute - * - * @access public - */ - public function execute() - { - if (php_sapi_name() !== 'cli') { - die('This script work only from the command line.'); - } - - if ($GLOBALS['argc'] === 1) { - $this->call($this->default_command); - } - - $this->call($GLOBALS['argv'][1]); - $this->call($this->default_command); - } -} diff --git a/app/Core/EmailClient.php b/app/Core/EmailClient.php new file mode 100644 index 00000000..07687c42 --- /dev/null +++ b/app/Core/EmailClient.php @@ -0,0 +1,46 @@ +<?php + +namespace Core; + +/** + * Mail client + * + * @package core + * @author Frederic Guillot + */ +class EmailClient extends Base +{ + /** + * Send a HTML email + * + * @access public + * @param string $email + * @param string $name + * @param string $subject + * @param string $html + */ + public function send($email, $name, $subject, $html) + { + $this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); + + $start_time = microtime(true); + $author = 'Kanboard'; + + if (Session::isOpen() && $this->userSession->isLogged()) { + $author = e('%s via Kanboard', $this->user->getFullname($this->session['user'])); + } + + switch (MAIL_TRANSPORT) { + case 'mailgun': + $this->mailgun->sendEmail($email, $name, $subject, $html, $author); + break; + case 'postmark': + $this->postmark->sendEmail($email, $name, $subject, $html, $author); + break; + default: + $this->smtp->sendEmail($email, $name, $subject, $html, $author); + } + + $this->container['logger']->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + } +} diff --git a/app/Core/Event.php b/app/Core/Event.php deleted file mode 100644 index a32499d8..00000000 --- a/app/Core/Event.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php - -namespace Core; - -/** - * Event dispatcher class - * - * @package core - * @author Frederic Guillot - */ -class Event -{ - /** - * Contains all listeners - * - * @access private - * @var array - */ - private $listeners = array(); - - /** - * The last listener executed - * - * @access private - * @var string - */ - private $lastListener = ''; - - /** - * The last triggered event - * - * @access private - * @var string - */ - private $lastEvent = ''; - - /** - * Triggered events list - * - * @access private - * @var array - */ - private $events = array(); - - /** - * Attach a listener object to an event - * - * @access public - * @param string $eventName Event name - * @param Listener $listener Object that implements the Listener interface - */ - public function attach($eventName, Listener $listener) - { - if (! isset($this->listeners[$eventName])) { - $this->listeners[$eventName] = array(); - } - - $this->listeners[$eventName][] = $listener; - } - - /** - * Trigger an event - * - * @access public - * @param string $eventName Event name - * @param array $data Event data - */ - public function trigger($eventName, array $data) - { - if (! $this->isEventTriggered($eventName)) { - - $this->events[] = $eventName; - - if (isset($this->listeners[$eventName])) { - - foreach ($this->listeners[$eventName] as $listener) { - - $this->lastEvent = $eventName; - - if ($listener->execute($data)) { - $this->lastListener = get_class($listener); - } - } - } - } - } - - /** - * Get the last listener executed - * - * @access public - * @return string Event name - */ - public function getLastListenerExecuted() - { - return $this->lastListener; - } - - /** - * Get the last fired event - * - * @access public - * @return string Event name - */ - public function getLastTriggeredEvent() - { - return $this->lastEvent; - } - - /** - * Get a list of triggered events - * - * @access public - * @return array - */ - public function getTriggeredEvents() - { - return $this->events; - } - - /** - * Check if an event have been triggered - * - * @access public - * @param string $eventName Event name - * @return bool - */ - public function isEventTriggered($eventName) - { - return in_array($eventName, $this->events); - } - - /** - * Flush the list of triggered events - * - * @access public - */ - public function clearTriggeredEvents() - { - $this->events = array(); - $this->lastEvent = ''; - } - - /** - * Check if a listener bind to an event - * - * @access public - * @param string $eventName Event name - * @param mixed $instance Instance name or object itself - * @return bool Yes or no - */ - public function hasListener($eventName, $instance) - { - if (isset($this->listeners[$eventName])) { - foreach ($this->listeners[$eventName] as $listener) { - if ($listener instanceof $instance) { - return true; - } - } - } - - return false; - } -} diff --git a/app/Core/Helper.php b/app/Core/Helper.php new file mode 100644 index 00000000..53084a7e --- /dev/null +++ b/app/Core/Helper.php @@ -0,0 +1,60 @@ +<?php + +namespace Core; + +/** + * Helper base class + * + * @package core + * @author Frederic Guillot + * + * @property \Helper\App $app + * @property \Helper\Asset $asset + * @property \Helper\Datetime $datetime + * @property \Helper\File $file + * @property \Helper\Form $form + * @property \Helper\Subtask $subtask + * @property \Helper\Task $task + * @property \Helper\Text $text + * @property \Helper\Url $url + * @property \Helper\User $user + */ +class Helper extends Base +{ + /** + * Helper instances + * + * @static + * @access private + * @var array + */ + private static $helpers = array(); + + /** + * Load automatically helpers + * + * @access public + * @param string $name Helper name + * @return mixed + */ + public function __get($name) + { + if (! isset(self::$helpers[$name])) { + $class = '\Helper\\'.ucfirst($name); + self::$helpers[$name] = new $class($this->container); + } + + return self::$helpers[$name]; + } + + /** + * HTML escaping + * + * @param string $value Value to escape + * @return string + */ + public function e($value) + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); + } +} diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php new file mode 100644 index 00000000..2f280a1e --- /dev/null +++ b/app/Core/HttpClient.php @@ -0,0 +1,117 @@ +<?php + +namespace Core; + +/** + * HTTP client + * + * @package core + * @author Frederic Guillot + */ +class HttpClient extends Base +{ + /** + * HTTP connection timeout in seconds + * + * @var integer + */ + const HTTP_TIMEOUT = 5; + + /** + * Number of maximum redirections for the HTTP client + * + * @var integer + */ + const HTTP_MAX_REDIRECTS = 2; + + /** + * HTTP client user agent + * + * @var string + */ + const HTTP_USER_AGENT = 'Kanboard'; + + /** + * Send a POST HTTP request encoded in JSON + * + * @access public + * @param string $url + * @param array $data + * @param array $headers + * @return string + */ + public function postJson($url, array $data, array $headers = array()) + { + return $this->doRequest( + $url, + json_encode($data), + array_merge(array('Content-type: application/json'), $headers) + ); + } + + /** + * Send a POST HTTP request encoded in www-form-urlencoded + * + * @access public + * @param string $url + * @param array $data + * @param array $headers + * @return string + */ + public function postForm($url, array $data, array $headers = array()) + { + return $this->doRequest( + $url, + http_build_query($data), + array_merge(array('Content-type: application/x-www-form-urlencoded'), $headers) + ); + } + + /** + * Make the HTTP request + * + * @access private + * @param string $url + * @param array $content + * @param array $headers + * @return string + */ + private function doRequest($url, $content, array $headers) + { + if (empty($url)) { + return ''; + } + + $headers = array_merge(array('User-Agent: '.self::HTTP_USER_AGENT, 'Connection: close'), $headers); + + $context = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'protocol_version' => 1.1, + 'timeout' => self::HTTP_TIMEOUT, + 'max_redirects' => self::HTTP_MAX_REDIRECTS, + 'header' => implode("\r\n", $headers), + 'content' => $content + ) + )); + + $stream = @fopen(trim($url), 'r', false, $context); + $response = ''; + + if (is_resource($stream)) { + $response = stream_get_contents($stream); + } + else { + $this->container['logger']->error('HttpClient: request failed'); + } + + if (DEBUG) { + $this->container['logger']->debug('HttpClient: url='.$url); + $this->container['logger']->debug('HttpClient: payload='.$content); + $this->container['logger']->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true)); + $this->container['logger']->debug('HttpClient: response='.$response); + } + + return $response; + } +} diff --git a/app/Core/Listener.php b/app/Core/Listener.php deleted file mode 100644 index 9c96cd57..00000000 --- a/app/Core/Listener.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php - -namespace Core; - -/** - * Event listener interface - * - * @package core - * @author Frederic Guillot - */ -interface Listener -{ - /** - * Execute the listener - * - * @access public - * @param array $data Event data - * @return boolean - */ - public function execute(array $data); -} diff --git a/app/Core/Loader.php b/app/Core/Loader.php deleted file mode 100644 index 151081c1..00000000 --- a/app/Core/Loader.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -namespace Core; - -/** - * Loader class - * - * @package core - * @author Frederic Guillot - */ -class Loader -{ - /** - * List of paths - * - * @access private - * @var array - */ - private $paths = array(); - - /** - * Load the missing class - * - * @access public - * @param string $class Class name with namespace - */ - public function load($class) - { - foreach ($this->paths as $path) { - - $filename = $path.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; - - if (file_exists($filename)) { - require $filename; - break; - } - } - } - - /** - * Register the autoloader - * - * @access public - */ - public function execute() - { - spl_autoload_register(array($this, 'load')); - } - - /** - * Register a new path - * - * @access public - * @param string $path Path - * @return Core\Loader - */ - public function setPath($path) - { - $this->paths[] = $path; - return $this; - } -} diff --git a/app/Core/Markdown.php b/app/Core/Markdown.php new file mode 100644 index 00000000..fa4e8080 --- /dev/null +++ b/app/Core/Markdown.php @@ -0,0 +1,47 @@ +<?php + +namespace Core; + +use Parsedown; +use Helper\Url; + +/** + * Specific Markdown rules for Kanboard + * + * @package core + * @author norcnorc + * @author Frederic Guillot + */ +class Markdown extends Parsedown +{ + private $link; + private $helper; + + public function __construct($link, Url $helper) + { + $this->link = $link; + $this->helper = $helper; + $this->InlineTypes['#'][] = 'TaskLink'; + $this->inlineMarkerList .= '#'; + } + + protected function inlineTaskLink($Excerpt) + { + // Replace task #123 by a link to the task + if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) { + + $url = $this->helper->href( + $this->link['controller'], + $this->link['action'], + $this->link['params'] + array('task_id' => $matches[1]) + ); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[0], + 'attributes' => array('href' => $url))); + } + } +} diff --git a/app/Core/MemoryCache.php b/app/Core/MemoryCache.php new file mode 100644 index 00000000..f80a66ef --- /dev/null +++ b/app/Core/MemoryCache.php @@ -0,0 +1,32 @@ +<?php + +namespace Core; + +class MemoryCache extends Cache +{ + private $storage = array(); + + public function init() + { + } + + public function set($key, $value) + { + $this->storage[$key] = $value; + } + + public function get($key) + { + return isset($this->storage[$key]) ? $this->storage[$key] : null; + } + + public function flush() + { + $this->storage = array(); + } + + public function remove($key) + { + unset($this->storage[$key]); + } +} diff --git a/app/Core/Paginator.php b/app/Core/Paginator.php new file mode 100644 index 00000000..12cc05a1 --- /dev/null +++ b/app/Core/Paginator.php @@ -0,0 +1,461 @@ +<?php + +namespace Core; + +use Pimple\Container; +use PicoDb\Table; + +/** + * Paginator helper + * + * @package core + * @author Frederic Guillot + */ +class Paginator +{ + /** + * Container instance + * + * @access private + * @var \Pimple\Container + */ + private $container; + + /** + * Total number of items + * + * @access private + * @var integer + */ + private $total = 0; + + /** + * Page number + * + * @access private + * @var integer + */ + private $page = 1; + + /** + * Offset + * + * @access private + * @var integer + */ + private $offset = 0; + + /** + * Limit + * + * @access private + * @var integer + */ + private $limit = 0; + + /** + * Sort by this column + * + * @access private + * @var string + */ + private $order = ''; + + /** + * Sorting direction + * + * @access private + * @var string + */ + private $direction = 'ASC'; + + /** + * Slice of items + * + * @access private + * @var array + */ + private $items = array(); + + /** + * PicoDb Table instance + * + * @access private + * @var \Picodb\Table + */ + private $query = null; + + /** + * Controller name + * + * @access private + * @var string + */ + private $controller = ''; + + /** + * Action name + * + * @access private + * @var string + */ + private $action = ''; + + /** + * Url params + * + * @access private + * @var array + */ + private $params = array(); + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + } + + /** + * Set a PicoDb query + * + * @access public + * @param \PicoDb\Table + * @return Paginator + */ + public function setQuery(Table $query) + { + $this->query = $query; + $this->total = $this->query->count(); + return $this; + } + + /** + * Execute a PicoDb query + * + * @access public + * @return array + */ + public function executeQuery() + { + if ($this->query !== null) { + return $this->query + ->offset($this->offset) + ->limit($this->limit) + ->orderBy($this->order, $this->direction) + ->findAll(); + } + + return array(); + } + + /** + * Set url parameters + * + * @access public + * @param string $controller + * @param string $action + * @param array $params + * @return Paginator + */ + public function setUrl($controller, $action, array $params = array()) + { + $this->controller = $controller; + $this->action = $action; + $this->params = $params; + return $this; + } + + /** + * Add manually items + * + * @access public + * @param array $items + * @return Paginator + */ + public function setCollection(array $items) + { + $this->items = $items; + return $this; + } + + /** + * Return the items + * + * @access public + * @return array + */ + public function getCollection() + { + return $this->items ?: $this->executeQuery(); + } + + /** + * Set the total number of items + * + * @access public + * @param integer $total + * @return Paginator + */ + public function setTotal($total) + { + $this->total = $total; + return $this; + } + + /** + * Get the total number of items + * + * @access public + * @return integer + */ + public function getTotal() + { + return $this->total; + } + + /** + * Set the default page number + * + * @access public + * @param integer $page + * @return Paginator + */ + public function setPage($page) + { + $this->page = $page; + return $this; + } + + /** + * Set the default column order + * + * @access public + * @param string $order + * @return Paginator + */ + public function setOrder($order) + { + $this->order = $order; + return $this; + } + + /** + * Set the default sorting direction + * + * @access public + * @param string $direction + * @return Paginator + */ + public function setDirection($direction) + { + $this->direction = $direction; + return $this; + } + + /** + * Set the maximum number of items per page + * + * @access public + * @param integer $limit + * @return Paginator + */ + public function setMax($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Return true if the collection is empty + * + * @access public + * @return boolean + */ + public function isEmpty() + { + return $this->total === 0; + } + + /** + * Execute the offset calculation only if the $condition is true + * + * @access public + * @param boolean $condition + * @return Paginator + */ + public function calculateOnlyIf($condition) + { + if ($condition) { + $this->calculate(); + } + + return $this; + } + + /** + * Calculate the offset value accoring to url params and the page number + * + * @access public + * @return Paginator + */ + public function calculate() + { + $this->page = $this->container['request']->getIntegerParam('page', 1); + $this->direction = $this->container['request']->getStringParam('direction', $this->direction); + $this->order = $this->container['request']->getStringParam('order', $this->order); + + if ($this->page < 1) { + $this->page = 1; + } + + $this->offset = ($this->page - 1) * $this->limit; + + return $this; + } + + /** + * Get url params for link generation + * + * @access public + * @param integer $page + * @param string $order + * @param string $direction + * @return string + */ + public function getUrlParams($page, $order, $direction) + { + $params = array( + 'page' => $page, + 'order' => $order, + 'direction' => $direction, + ); + + return array_merge($this->params, $params); + } + + /** + * Generate the previous link + * + * @access public + * @return string + */ + public function generatePreviousLink() + { + $html = '<span class="pagination-previous">'; + + if ($this->offset > 0) { + $html .= $this->container['helper']->url->link( + '← '.t('Previous'), + $this->controller, + $this->action, + $this->getUrlParams($this->page - 1, $this->order, $this->direction) + ); + } + else { + $html .= '← '.t('Previous'); + } + + $html .= '</span>'; + + return $html; + } + + /** + * Generate the next link + * + * @access public + * @return string + */ + public function generateNextLink() + { + $html = '<span class="pagination-next">'; + + if (($this->total - $this->offset) > $this->limit) { + $html .= $this->container['helper']->url->link( + t('Next').' →', + $this->controller, + $this->action, + $this->getUrlParams($this->page + 1, $this->order, $this->direction) + ); + } + else { + $html .= t('Next').' →'; + } + + $html .= '</span>'; + + return $html; + } + + /** + * Return true if there is no pagination to show + * + * @access public + * @return boolean + */ + public function hasNothingtoShow() + { + return $this->offset === 0 && ($this->total - $this->offset) <= $this->limit; + } + + /** + * Generation pagination links + * + * @access public + * @return string + */ + public function toHtml() + { + $html = ''; + + if (! $this->hasNothingtoShow()) { + $html .= '<div class="pagination">'; + $html .= $this->generatePreviousLink(); + $html .= $this->generateNextLink(); + $html .= '</div>'; + } + + return $html; + } + + /** + * Magic method to output pagination links + * + * @access public + * @return string + */ + public function __toString() + { + return $this->toHtml(); + } + + /** + * Column sorting + * + * @param string $label Column title + * @param string $column SQL column name + * @return string + */ + public function order($label, $column) + { + $prefix = ''; + $direction = 'ASC'; + + if ($this->order === $column) { + $prefix = $this->direction === 'DESC' ? '▼ ' : '▲ '; + $direction = $this->direction === 'DESC' ? 'ASC' : 'DESC'; + } + + return $prefix.$this->container['helper']->url->link( + $label, + $this->controller, + $this->action, + $this->getUrlParams($this->page, $column, $direction) + ); + } +} diff --git a/app/Core/Registry.php b/app/Core/Registry.php deleted file mode 100644 index d8b9063e..00000000 --- a/app/Core/Registry.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php - -namespace Core; - -use RuntimeException; - -/** - * The registry class is a dependency injection container - * - * @property mixed db - * @property mixed event - * @package core - * @author Frederic Guillot - */ -class Registry -{ - /** - * Contains all dependencies - * - * @access private - * @var array - */ - private $container = array(); - - /** - * Contains all instances - * - * @access private - * @var array - */ - private $instances = array(); - - /** - * Set a dependency - * - * @access public - * @param string $name Unique identifier for the service/parameter - * @param mixed $value The value of the parameter or a closure to define an object - */ - public function __set($name, $value) - { - $this->container[$name] = $value; - } - - /** - * Get a dependency - * - * @access public - * @param string $name Unique identifier for the service/parameter - * @return mixed The value of the parameter or an object - * @throws RuntimeException If the identifier is not found - */ - public function __get($name) - { - if (isset($this->container[$name])) { - - if (is_callable($this->container[$name])) { - return $this->container[$name](); - } - else { - return $this->container[$name]; - } - } - - throw new \RuntimeException('Identifier not found in the registry: '.$name); - } - - /** - * Return a shared instance of a dependency - * - * @access public - * @param string $name Unique identifier for the service/parameter - * @return mixed Same object instance of the dependency - */ - public function shared($name) - { - if (! isset($this->instances[$name])) { - $this->instances[$name] = $this->$name; - } - - return $this->instances[$name]; - } -} diff --git a/app/Core/Request.php b/app/Core/Request.php index a4c426f0..c7ca3184 100644 --- a/app/Core/Request.php +++ b/app/Core/Request.php @@ -76,6 +76,17 @@ class Request } /** + * Get the Json request body + * + * @access public + * @return array + */ + public function getJson() + { + return json_decode($this->getBody(), true); + } + + /** * Get the content of an uploaded file * * @access public @@ -114,6 +125,20 @@ class Request } /** + * Check if the page is requested through HTTPS + * + * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS + * + * @static + * @access public + * @return boolean + */ + public static function isHTTPS() + { + return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; + } + + /** * Return a HTTP header value * * @access public diff --git a/app/Core/Response.php b/app/Core/Response.php index 347cdde7..d42a8f1e 100644 --- a/app/Core/Response.php +++ b/app/Core/Response.php @@ -168,6 +168,23 @@ class Response } /** + * Send a css response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function css($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/css; charset=utf-8'); + echo $data; + + exit; + } + + /** * Send a binary response * * @access public @@ -195,24 +212,7 @@ class Response $policies['default-src'] = "'self'"; $values = ''; - foreach ($policies as $policy => $hosts) { - - if (is_array($hosts)) { - - $acl = ''; - - foreach ($hosts as &$host) { - - if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) { - $acl .= $host.' '; - } - } - } - else { - - $acl = $hosts; - } - + foreach ($policies as $policy => $acl) { $values .= $policy.' '.trim($acl).'; '; } @@ -246,7 +246,7 @@ class Response */ public function hsts() { - if (Tool::isHTTPS()) { + if (Request::isHTTPS()) { header('Strict-Transport-Security: max-age=31536000'); } } diff --git a/app/Core/Router.php b/app/Core/Router.php index c9af6e2c..36c11a0a 100644 --- a/app/Core/Router.php +++ b/app/Core/Router.php @@ -2,6 +2,8 @@ namespace Core; +use Pimple\Container; + /** * Router class * @@ -27,24 +29,24 @@ class Router private $action = ''; /** - * Registry instance + * Container instance * * @access private - * @var \Core\Registry + * @var \Pimple\Container */ - private $registry; + private $container; /** * Constructor * * @access public - * @param Registry $registry Registry instance - * @param string $controller Controller name - * @param string $action Action name + * @param \Pimple\Container $container Container instance + * @param string $controller Controller name + * @param string $action Action name */ - public function __construct(Registry $registry, $controller = '', $action = '') + public function __construct(Container $container, $controller = '', $action = '') { - $this->registry = $registry; + $this->container = $container; $this->controller = empty($_GET['controller']) ? $controller : $_GET['controller']; $this->action = empty($_GET['action']) ? $action : $_GET['action']; } @@ -81,11 +83,7 @@ class Router return false; } - $instance = new $class($this->registry); - $instance->request = new Request; - $instance->response = new Response; - $instance->session = new Session; - $instance->template = new Template; + $instance = new $class($this->container); $instance->beforeAction($this->controller, $this->action); $instance->$method(); diff --git a/app/Core/Session.php b/app/Core/Session.php index 6028f0b9..c35014cd 100644 --- a/app/Core/Session.php +++ b/app/Core/Session.php @@ -2,13 +2,15 @@ namespace Core; +use ArrayAccess; + /** * Session class * * @package core * @author Frederic Guillot */ -class Session +class Session implements ArrayAccess { /** * Sesion lifetime @@ -36,32 +38,32 @@ class Session * * @access public * @param string $base_path Cookie path - * @param string $save_path Custom session save path */ - public function open($base_path = '/', $save_path = '') + public function open($base_path = '/') { - if ($save_path !== '') { - session_save_path($save_path); - } - + $base_path = str_replace('\\', '/', $base_path); + // HttpOnly and secure flags for session cookie session_set_cookie_params( self::SESSION_LIFETIME, $base_path ?: '/', null, - Tool::isHTTPS(), + Request::isHTTPS(), true ); // Avoid session id in the URL ini_set('session.use_only_cookies', '1'); + // Enable strict mode + ini_set('session.use_strict_mode', '1'); + // Ensure session ID integrity ini_set('session.entropy_file', '/dev/urandom'); ini_set('session.entropy_length', '32'); ini_set('session.hash_bits_per_character', 6); - // If session was autostarted with session.auto_start = 1 in php.ini destroy it, otherwise we cannot login + // If the session was autostarted with session.auto_start = 1 in php.ini destroy it if (isset($_SESSION)) { session_destroy(); } @@ -90,19 +92,17 @@ class Session $_SESSION = array(); // Destroy the session cookie - if (ini_get('session.use_cookies')) { - $params = session_get_cookie_params(); - - setcookie( - session_name(), - '', - time() - 42000, - $params['path'], - $params['domain'], - $params['secure'], - $params['httponly'] - ); - } + $params = session_get_cookie_params(); + + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); // Destroy session data session_destroy(); @@ -129,4 +129,24 @@ class Session { $_SESSION['flash_error_message'] = $message; } + + public function offsetSet($offset, $value) + { + $_SESSION[$offset] = $value; + } + + public function offsetExists($offset) + { + return isset($_SESSION[$offset]); + } + + public function offsetUnset($offset) + { + unset($_SESSION[$offset]); + } + + public function offsetGet($offset) + { + return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; + } } diff --git a/app/Core/Template.php b/app/Core/Template.php index f21e8a6d..9688c2a5 100644 --- a/app/Core/Template.php +++ b/app/Core/Template.php @@ -10,28 +10,28 @@ use LogicException; * @package core * @author Frederic Guillot */ -class Template +class Template extends Helper { /** * Template path * * @var string */ - const PATH = 'app/Templates/'; + const PATH = 'app/Template/'; /** - * Load a template + * Render a template * * Example: * - * $template->load('template_name', ['bla' => 'value']); + * $template->render('template_name', ['bla' => 'value']); * * @access public * @params string $__template_name Template name * @params array $__template_args Key/Value map of template variables * @return string */ - public function load($__template_name, array $__template_args = array()) + public function render($__template_name, array $__template_args = array()) { $__template_file = self::PATH.$__template_name.'.php'; @@ -57,9 +57,9 @@ class Template */ public function layout($template_name, array $template_args = array(), $layout_name = 'layout') { - return $this->load( + return $this->render( $layout_name, - $template_args + array('content_for_layout' => $this->load($template_name, $template_args)) + $template_args + array('content_for_layout' => $this->render($template_name, $template_args)) ); } } diff --git a/app/Core/Tool.php b/app/Core/Tool.php index e54a0d3b..84e42ba8 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -33,35 +33,22 @@ class Tool } /** - * Load and register a model + * Get the mailbox hash from an email address * * @static * @access public - * @param Core\Registry $registry DPI container - * @param string $name Model name - * @return mixed + * @param string $email + * @return string */ - public static function loadModel(Registry $registry, $name) + public static function getMailboxHash($email) { - if (! isset($registry->$name)) { - $class = '\Model\\'.ucfirst($name); - $registry->$name = new $class($registry); + if (! strpos($email, '@') || ! strpos($email, '+')) { + return ''; } - return $registry->shared($name); - } + list($local_part,) = explode('@', $email); + list(,$identifier) = explode('+', $local_part); - /** - * Check if the page is requested through HTTPS - * - * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS - * - * @static - * @access public - * @return boolean - */ - public static function isHTTPS() - { - return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; + return $identifier; } } diff --git a/app/Core/Translator.php b/app/Core/Translator.php index 43e934a9..e3d19692 100644 --- a/app/Core/Translator.php +++ b/app/Core/Translator.php @@ -11,14 +11,14 @@ namespace Core; class Translator { /** - * Locales path + * Locale path * * @var string */ - const PATH = 'app/Locales/'; + const PATH = 'app/Locale/'; /** - * Locales + * Locale * * @static * @access private @@ -27,6 +27,31 @@ class Translator private static $locales = array(); /** + * Instance + * + * @static + * @access private + * @var Translator + */ + private static $instance = null; + + /** + * Get instance + * + * @static + * @access public + * @return Translator + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self; + } + + return self::$instance; + } + + /** * Get a translation * * $translator->translate('I have %d kids', 5); @@ -181,5 +206,8 @@ class Translator if (file_exists($filename)) { self::$locales = require $filename; } + else { + self::$locales = array(); + } } } diff --git a/app/Event/AuthEvent.php b/app/Event/AuthEvent.php new file mode 100644 index 00000000..ec1bec99 --- /dev/null +++ b/app/Event/AuthEvent.php @@ -0,0 +1,27 @@ +<?php + +namespace Event; + +use Symfony\Component\EventDispatcher\Event as BaseEvent; + +class AuthEvent extends BaseEvent +{ + private $auth_name; + private $user_id; + + public function __construct($auth_name, $user_id) + { + $this->auth_name = $auth_name; + $this->user_id = $user_id; + } + + public function getUserId() + { + return $this->user_id; + } + + public function getAuthType() + { + return $this->auth_name; + } +} diff --git a/app/Event/Base.php b/app/Event/Base.php deleted file mode 100644 index 745871a5..00000000 --- a/app/Event/Base.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php - -namespace Event; - -use Core\Listener; -use Core\Registry; -use Core\Tool; - -/** - * Base Listener - * - * @package event - * @author Frederic Guillot - * - * @property \Model\Comment $comment - * @property \Model\Project $project - * @property \Model\ProjectActivity $projectActivity - * @property \Model\SubTask $subTask - * @property \Model\Task $task - * @property \Model\TaskFinder $taskFinder - */ -abstract class Base implements Listener -{ - /** - * Registry instance - * - * @access protected - * @var \Core\Registry - */ - protected $registry; - - /** - * Constructor - * - * @access public - * @param \Core\Registry $registry Regsitry instance - */ - public function __construct(Registry $registry) - { - $this->registry = $registry; - } - - /** - * Return class information - * - * @access public - * @return string - */ - public function __toString() - { - return get_called_class(); - } - - /** - * Load automatically models - * - * @access public - * @param string $name Model name - * @return mixed - */ - public function __get($name) - { - return Tool::loadModel($this->registry, $name); - } - - /** - * Get event namespace - * - * Event = task.close | Namespace = task - * - * @access public - * @return string - */ - public function getEventNamespace() - { - $event_name = $this->registry->event->getLastTriggeredEvent(); - return substr($event_name, 0, strpos($event_name, '.')); - } -} diff --git a/app/Event/CommentEvent.php b/app/Event/CommentEvent.php new file mode 100644 index 00000000..75a132d7 --- /dev/null +++ b/app/Event/CommentEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Event; + +class CommentEvent extends GenericEvent +{ +} diff --git a/app/Event/FileEvent.php b/app/Event/FileEvent.php new file mode 100644 index 00000000..81bd83c9 --- /dev/null +++ b/app/Event/FileEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Event; + +class FileEvent extends GenericEvent +{ +} diff --git a/app/Event/GenericEvent.php b/app/Event/GenericEvent.php new file mode 100644 index 00000000..b29d8f32 --- /dev/null +++ b/app/Event/GenericEvent.php @@ -0,0 +1,45 @@ +<?php + +namespace Event; + +use ArrayAccess; +use Symfony\Component\EventDispatcher\Event as BaseEvent; + +class GenericEvent extends BaseEvent implements ArrayAccess +{ + private $container = array(); + + public function __construct(array $values = array()) + { + $this->container = $values; + } + + public function getAll() + { + return $this->container; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return isset($this->container[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->container[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->container[$offset]) ? $this->container[$offset] : null; + } +} diff --git a/app/Event/NotificationListener.php b/app/Event/NotificationListener.php deleted file mode 100644 index 3c049327..00000000 --- a/app/Event/NotificationListener.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php - -namespace Event; - -/** - * Notification listener - * - * @package event - * @author Frederic Guillot - */ -class NotificationListener extends Base -{ - /** - * Template name - * - * @accesss private - * @var string - */ - private $template = ''; - - /** - * Set template name - * - * @access public - * @param string $template Template name - */ - public function setTemplate($template) - { - $this->template = $template; - } - - /** - * Execute the action - * - * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed - */ - public function execute(array $data) - { - $values = $this->getTemplateData($data); - $users = $this->notification->getUsersList($values['task']['project_id']); - - if ($users) { - $this->notification->sendEmails($this->template, $users, $values); - return true; - } - - return false; - } - - /** - * Fetch data for the mail template - * - * @access public - * @param array $data Event data - * @return array - */ - public function getTemplateData(array $data) - { - $values = array(); - - switch ($this->getEventNamespace()) { - case 'task': - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - break; - case 'subtask': - $values['subtask'] = $this->subtask->getById($data['id'], true); - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - break; - case 'file': - $values['file'] = $data; - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - break; - case 'comment': - $values['comment'] = $this->comment->getById($data['id']); - $values['task'] = $this->taskFinder->getDetails($values['comment']['task_id']); - break; - } - - return $values; - } -} diff --git a/app/Event/ProjectActivityListener.php b/app/Event/ProjectActivityListener.php deleted file mode 100644 index 8958bd2b..00000000 --- a/app/Event/ProjectActivityListener.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php - -namespace Event; - -/** - * Project activity listener - * - * @package event - * @author Frederic Guillot - */ -class ProjectActivityListener extends Base -{ - /** - * Execute the action - * - * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed - */ - public function execute(array $data) - { - if (isset($data['task_id'])) { - - $values = $this->getValues($data); - - return $this->projectActivity->createEvent( - $values['task']['project_id'], - $values['task']['id'], - $this->acl->getUserId(), - $this->registry->event->getLastTriggeredEvent(), - $values - ); - } - - return false; - } - - /** - * Get event activity data - * - * @access private - * @param array $data Event data dictionary - * @return array - */ - private function getValues(array $data) - { - $values = array(); - $values['task'] = $this->taskFinder->getDetails($data['task_id']); - - switch ($this->getEventNamespace()) { - case 'subtask': - $values['subtask'] = $this->subTask->getById($data['id'], true); - break; - case 'comment': - $values['comment'] = $this->comment->getById($data['id']); - break; - } - - return $values; - } -} diff --git a/app/Event/ProjectModificationDateListener.php b/app/Event/ProjectModificationDateListener.php deleted file mode 100644 index abc176b0..00000000 --- a/app/Event/ProjectModificationDateListener.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php - -namespace Event; - -/** - * Project modification date listener - * - * Update the "last_modified" field for a project - * - * @package event - * @author Frederic Guillot - */ -class ProjectModificationDateListener extends Base -{ - /** - * Execute the action - * - * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed - */ - public function execute(array $data) - { - if (isset($data['project_id'])) { - return $this->project->updateModificationDate($data['project_id']); - } - - return false; - } -} diff --git a/app/Event/SubtaskEvent.php b/app/Event/SubtaskEvent.php new file mode 100644 index 00000000..229db860 --- /dev/null +++ b/app/Event/SubtaskEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Event; + +class SubtaskEvent extends GenericEvent +{ +} diff --git a/app/Event/TaskEvent.php b/app/Event/TaskEvent.php new file mode 100644 index 00000000..e2fb30fe --- /dev/null +++ b/app/Event/TaskEvent.php @@ -0,0 +1,7 @@ +<?php + +namespace Event; + +class TaskEvent extends GenericEvent +{ +} diff --git a/app/Event/WebhookListener.php b/app/Event/WebhookListener.php deleted file mode 100644 index f7e23e07..00000000 --- a/app/Event/WebhookListener.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -namespace Event; - -/** - * Webhook task events - * - * @package event - * @author Frederic Guillot - */ -class WebhookListener extends Base -{ - /** - * Url to call - * - * @access private - * @var string - */ - private $url = ''; - - /** - * Set webhook url - * - * @access public - * @param string $url URL to call - */ - public function setUrl($url) - { - $this->url = $url; - } - - /** - * Execute the action - * - * @access public - * @param array $data Event data dictionary - * @return bool True if the action was executed or false when not executed - */ - public function execute(array $data) - { - $this->webhook->notify($this->url, $data); - return true; - } -} diff --git a/app/Helper/App.php b/app/Helper/App.php new file mode 100644 index 00000000..8f591143 --- /dev/null +++ b/app/Helper/App.php @@ -0,0 +1,56 @@ +<?php + +namespace Helper; + +/** + * Application helpers + * + * @package helper + * @author Frederic Guillot + */ +class App extends \Core\Base +{ + /** + * Get javascript language code + * + * @access public + * @return string + */ + public function jsLang() + { + return $this->config->getJsLanguageCode(); + } + + /** + * Get current timezone + * + * @access public + * @return string + */ + public function getTimezone() + { + return $this->config->getCurrentTimezone(); + } + + /** + * Get session flash message + * + * @access public + * @return string + */ + public function flashMessage() + { + $html = ''; + + if (isset($this->session['flash_message'])) { + $html = '<div class="alert alert-success alert-fade-out">'.$this->helper->e($this->session['flash_message']).'</div>'; + unset($this->session['flash_message']); + } + else if (isset($this->session['flash_error_message'])) { + $html = '<div class="alert alert-error">'.$this->helper->e($this->session['flash_error_message']).'</div>'; + unset($this->session['flash_error_message']); + } + + return $html; + } +} diff --git a/app/Helper/Asset.php b/app/Helper/Asset.php new file mode 100644 index 00000000..fe285081 --- /dev/null +++ b/app/Helper/Asset.php @@ -0,0 +1,51 @@ +<?php + +namespace Helper; + +/** + * Assets helpers + * + * @package helper + * @author Frederic Guillot + */ +class Asset extends \Core\Base +{ + /** + * Add a Javascript asset + * + * @param string $filename Filename + * @return string + */ + public function js($filename) + { + return '<script type="text/javascript" src="'.$filename.'?'.filemtime($filename).'"></script>'; + } + + /** + * Add a stylesheet asset + * + * @param string $filename Filename + * @param boolean $is_file Add file timestamp + * @param string $media Media + * @return string + */ + public function css($filename, $is_file = true, $media = 'screen') + { + return '<link rel="stylesheet" href="'.$filename.($is_file ? '?'.filemtime($filename) : '').'" media="'.$media.'">'; + } + + /** + * Get custom css + * + * @access public + * @return string + */ + public function customCss() + { + if ($this->config->get('application_stylesheet')) { + return '<style>'.$this->config->get('application_stylesheet').'</style>'; + } + + return ''; + } +} diff --git a/app/Helper/Datetime.php b/app/Helper/Datetime.php new file mode 100644 index 00000000..3a9c4c48 --- /dev/null +++ b/app/Helper/Datetime.php @@ -0,0 +1,61 @@ +<?php + +namespace Helper; + +/** + * DateTime helpers + * + * @package helper + * @author Frederic Guillot + */ +class Datetime extends \Core\Base +{ + /** + * Get all hours for day + * + * @access public + * @return array + */ + public function getDayHours() + { + $values = array(); + + foreach (range(0, 23) as $hour) { + foreach (array(0, 30) as $minute) { + $time = sprintf('%02d:%02d', $hour, $minute); + $values[$time] = $time; + } + } + + return $values; + } + + /** + * Get all days of a week + * + * @access public + * @return array + */ + public function getWeekDays() + { + $values = array(); + + foreach (range(1, 7) as $day) { + $values[$day] = $this->getWeekDay($day); + } + + return $values; + } + + /** + * Get the localized day name from the day number + * + * @access public + * @param integer $day Day number + * @return string + */ + public function getWeekDay($day) + { + return dt('%A', strtotime('next Monday +'.($day - 1).' days')); + } +} diff --git a/app/Helper/File.php b/app/Helper/File.php new file mode 100644 index 00000000..a35e4283 --- /dev/null +++ b/app/Helper/File.php @@ -0,0 +1,56 @@ +<?php + +namespace Helper; + +/** + * File helpers + * + * @package helper + * @author Frederic Guillot + */ +class File extends \Core\Base +{ + /** + * Get file icon + * + * @access public + * @param string $filename Filename + * @return string Font-Awesome-Icon-Name + */ + public function icon($filename){ + + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + switch ($extension) { + case 'jpeg': + case 'jpg': + case 'png': + case 'gif': + return 'fa-file-image-o'; + case 'xls': + case 'xlsx': + return 'fa-file-excel-o'; + case 'doc': + case 'docx': + return 'fa-file-word-o'; + case 'ppt': + case 'pptx': + return 'fa-file-powerpoint-o'; + case 'zip': + case 'rar': + return 'fa-file-archive-o'; + case 'mp3': + return 'fa-audio-o'; + case 'avi': + return 'fa-video-o'; + case 'php': + case 'html': + case 'css': + return 'fa-code-o'; + case 'pdf': + return 'fa-file-pdf-o'; + } + + return 'fa-file-o'; + } +} diff --git a/app/Helper/Form.php b/app/Helper/Form.php new file mode 100644 index 00000000..83e3b113 --- /dev/null +++ b/app/Helper/Form.php @@ -0,0 +1,323 @@ +<?php + +namespace Helper; + +use Core\Security; + +/** + * Form helpers + * + * @package helper + * @author Frederic Guillot + */ +class Form extends \Core\Base +{ + /** + * Hidden CSRF token field + * + * @access public + * @return string + */ + public function csrf() + { + return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>'; + } + + /** + * Display a hidden form field + * + * @access public + * @param string $name Field name + * @param array $values Form values + * @return string + */ + public function hidden($name, array $values = array()) + { + return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).'/>'; + } + + /** + * Display a select field + * + * @access public + * @param string $name Field name + * @param array $options Options + * @param array $values Form values + * @param array $errors Form errors + * @param string $class CSS class + * @return string + */ + public function select($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '.implode(' ', $attributes).'>'; + + foreach ($options as $id => $value) { + + $html .= '<option value="'.$this->helper->e($id).'"'; + + if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"'; + if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"'; + + $html .= '>'.$this->helper->e($value).'</option>'; + } + + $html .= '</select>'; + $html .= $this->errorList($errors, $name); + + return $html; + } + + /** + * Display a radio field group + * + * @access public + * @param string $name Field name + * @param array $options Options + * @param array $values Form values + * @return string + */ + public function radios($name, array $options, array $values = array()) + { + $html = ''; + + foreach ($options as $value => $label) { + $html .= $this->radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value); + } + + return $html; + } + + /** + * Display a radio field + * + * @access public + * @param string $name Field name + * @param string $label Form label + * @param string $value Form value + * @param boolean $selected Field selected or not + * @param string $class CSS class + * @return string + */ + public function radio($name, $label, $value, $selected = false, $class = '') + { + return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($selected ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>'; + } + + /** + * Display a checkbox field + * + * @access public + * @param string $name Field name + * @param string $label Form label + * @param string $value Form value + * @param boolean $checked Field selected or not + * @param string $class CSS class + * @return string + */ + public function checkbox($name, $label, $value, $checked = false, $class = '') + { + return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.$this->helper->e($value).'" '.($checked ? 'checked="checked"' : '').'> '.$this->helper->e($label).'</label>'; + } + + /** + * Display a form label + * + * @access public + * @param string $name Field name + * @param string $label Form label + * @param array $attributes HTML attributes + * @return string + */ + public function label($label, $name, array $attributes = array()) + { + return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.$this->helper->e($label).'</label>'; + } + + /** + * Display a textarea + * + * @access public + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + $class .= $this->errorClass($errors, $name); + + $html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '; + $html .= implode(' ', $attributes).'>'; + $html .= isset($values->$name) ? $this->helper->e($values->$name) : isset($values[$name]) ? $values[$name] : ''; + $html .= '</textarea>'; + $html .= $this->errorList($errors, $name); + + return $html; + } + + /** + * Display a input field + * + * @access public + * @param string $type HMTL input tag type + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + $class .= $this->errorClass($errors, $name); + + $html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.$this->formValue($values, $name).' class="'.$class.'" '; + $html .= implode(' ', $attributes).'>'; + + if (in_array('required', $attributes)) { + $html .= '<span class="form-required">*</span>'; + } + + $html .= $this->errorList($errors, $name); + + return $html; + } + + /** + * Display a text field + * + * @access public + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->input('text', $name, $values, $errors, $attributes, $class); + } + + /** + * Display a password field + * + * @access public + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->input('password', $name, $values, $errors, $attributes, $class); + } + + /** + * Display an email field + * + * @access public + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->input('email', $name, $values, $errors, $attributes, $class); + } + + /** + * Display a number field + * + * @access public + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->input('number', $name, $values, $errors, $attributes, $class); + } + + /** + * Display a numeric field (allow decimal number) + * + * @access public + * @param string $name Field name + * @param array $values Form values + * @param array $errors Form errors + * @param array $attributes HTML attributes + * @param string $class CSS class + * @return string + */ + public function numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') + { + return $this->input('text', $name, $values, $errors, $attributes, $class.' form-numeric'); + } + + /** + * Display the form error class + * + * @access private + * @param array $errors Error list + * @param string $name Field name + * @return string + */ + private function errorClass(array $errors, $name) + { + return ! isset($errors[$name]) ? '' : ' form-error'; + } + + /** + * Display a list of form errors + * + * @access private + * @param array $errors List of errors + * @param string $name Field name + * @return string + */ + private function errorList(array $errors, $name) + { + $html = ''; + + if (isset($errors[$name])) { + + $html .= '<ul class="form-errors">'; + + foreach ($errors[$name] as $error) { + $html .= '<li>'.$this->helper->e($error).'</li>'; + } + + $html .= '</ul>'; + } + + return $html; + } + + /** + * Get an escaped form value + * + * @access private + * @param mixed $values Values + * @param string $name Field name + * @return string + */ + private function formValue($values, $name) + { + if (isset($values->$name)) { + return 'value="'.$this->helper->e($values->$name).'"'; + } + + return isset($values[$name]) ? 'value="'.$this->helper->e($values[$name]).'"' : ''; + } +} diff --git a/app/Helper/Subtask.php b/app/Helper/Subtask.php new file mode 100644 index 00000000..6348ebd1 --- /dev/null +++ b/app/Helper/Subtask.php @@ -0,0 +1,42 @@ +<?php + +namespace Helper; + +/** + * Subtask helpers + * + * @package helper + * @author Frederic Guillot + */ +class Subtask extends \Core\Base +{ + /** + * Get the link to toggle subtask status + * + * @access public + * @param array $subtask + * @param string $redirect + * @return string + */ + public function toggleStatus(array $subtask, $redirect) + { + if ($subtask['status'] == 0 && isset($this->session['has_subtask_inprogress']) && $this->session['has_subtask_inprogress'] === true) { + + return $this->helper->url->link( + trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']), + 'subtask', + 'subtaskRestriction', + array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect), + false, + 'popover task-board-popover' + ); + } + + return $this->helper->url->link( + trim($this->template->render('subtask/icons', array('subtask' => $subtask))) . $this->helper->e($subtask['title']), + 'subtask', + 'toggleStatus', + array('task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'redirect' => $redirect) + ); + } +} diff --git a/app/Helper/Task.php b/app/Helper/Task.php new file mode 100644 index 00000000..b3931cdb --- /dev/null +++ b/app/Helper/Task.php @@ -0,0 +1,59 @@ +<?php + +namespace Helper; + +/** + * Task helpers + * + * @package helper + * @author Frederic Guillot + */ +class Task extends \Core\Base +{ + /** + * Get the age of an item in quasi human readable format. + * It's in this format: <1h , NNh, NNd + * + * @access public + * @param integer $timestamp Unix timestamp of the artifact for which age will be calculated + * @param integer $now Compare with this timestamp (Default value is the current unix timestamp) + * @return string + */ + public function age($timestamp, $now = null) + { + if ($now === null) { + $now = time(); + } + + $diff = $now - $timestamp; + + if ($diff < 3600) { + return t('<1h'); + } + else if ($diff < 86400) { + return t('%dh', $diff / 3600); + } + + return t('%dd', ($now - $timestamp) / 86400); + } + + public function recurrenceTriggers() + { + return $this->task->getRecurrenceTriggerList(); + } + + public function recurrenceTimeframes() + { + return $this->task->getRecurrenceTimeframeList(); + } + + public function recurrenceBasedates() + { + return $this->task->getRecurrenceBasedateList(); + } + + public function canRemove(array $task) + { + return $this->taskPermission->canRemoveTask($task); + } +} diff --git a/app/Helper/Text.php b/app/Helper/Text.php new file mode 100644 index 00000000..cfb557b1 --- /dev/null +++ b/app/Helper/Text.php @@ -0,0 +1,91 @@ +<?php + +namespace Helper; + +use Core\Markdown; + +/** + * Text helpers + * + * @package helper + * @author Frederic Guillot + */ +class Text extends \Core\Base +{ + /** + * Markdown transformation + * + * @param string $text Markdown content + * @param array $link Link parameters for replacement + * @return string + */ + public function markdown($text, array $link = array()) + { + $parser = new Markdown($link, $this->helper->url); + $parser->setMarkupEscaped(MARKDOWN_ESCAPE_HTML); + return $parser->text($text); + } + + /** + * Format a file size + * + * @param integer $size Size in bytes + * @param integer $precision Precision + * @return string + */ + public function bytes($size, $precision = 2) + { + $base = log($size) / log(1024); + $suffixes = array('', 'k', 'M', 'G', 'T'); + + return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)]; + } + + /** + * Truncate a long text + * + * @param string $value Text + * @param integer $max_length Max Length + * @param string $end Text end + * @return string + */ + public function truncate($value, $max_length = 85, $end = '[...]') + { + $length = strlen($value); + + if ($length > $max_length) { + return substr($value, 0, $max_length).' '.$end; + } + + return $value; + } + + /** + * Return true if needle is contained in the haystack + * + * @param string $haystack Haystack + * @param string $needle Needle + * @return boolean + */ + public function contains($haystack, $needle) + { + return strpos($haystack, $needle) !== false; + } + + /** + * Return a value from a dictionary + * + * @param mixed $id Key + * @param array $listing Dictionary + * @param string $default_value Value displayed when the key doesn't exists + * @return string + */ + public function in($id, array $listing, $default_value = '?') + { + if (isset($listing[$id])) { + return $this->helper->e($listing[$id]); + } + + return $default_value; + } +} diff --git a/app/Helper/Url.php b/app/Helper/Url.php new file mode 100644 index 00000000..64b2c83f --- /dev/null +++ b/app/Helper/Url.php @@ -0,0 +1,117 @@ +<?php + +namespace Helper; + +use Core\Request; +use Core\Security; + +/** + * Url helpers + * + * @package helper + * @author Frederic Guillot + */ +class Url extends \Core\Base +{ + /** + * HTML Link tag + * + * @access public + * @param string $label Link label + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters + * @param boolean $csrf Add a CSRF token + * @param string $class CSS class attribute + * @param boolean $new_tab Open the link in a new tab + * @param string $anchor Link Anchor + * @return string + */ + public function link($label, $controller, $action, array $params = array(), $csrf = false, $class = '', $title = '', $new_tab = false, $anchor = '') + { + return '<a href="'.$this->href($controller, $action, $params, $csrf, $anchor).'" class="'.$class.'" title="'.$title.'" '.($new_tab ? 'target="_blank"' : '').'>'.$label.'</a>'; + } + + /** + * Hyperlink + * + * @access public + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters + * @param boolean $csrf Add a CSRF token + * @param string $anchor Link Anchor + * @return string + */ + public function href($controller, $action, array $params = array(), $csrf = false, $anchor = '') + { + $values = array( + 'controller' => $controller, + 'action' => $action, + ); + + if ($csrf) { + $params['csrf_token'] = Security::getCSRFToken(); + } + + $values += $params; + + return '?'.http_build_query($values, '', '&').(empty($anchor) ? '' : '#'.$anchor); + } + + /** + * Generate controller/action url + * + * @access public + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters + * @return string + */ + public function to($controller, $action, array $params = array()) + { + $values = array( + 'controller' => $controller, + 'action' => $action, + ); + + $values += $params; + + return '?'.http_build_query($values, '', '&'); + } + + /** + * Get application base url + * + * @access public + * @return string + */ + public function base() + { + $application_url = $this->config->get('application_url'); + + if (! empty($application_url)) { + return $application_url; + } + + return $this->server(); + } + + /** + * Get current server base url + * + * @access public + * @return string + */ + public function server() + { + $self = str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])); + + $url = Request::isHTTPS() ? 'https://' : 'http://'; + $url .= $_SERVER['SERVER_NAME']; + $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; + $url .= $self !== '/' ? $self.'/' : '/'; + + return $url; + } +} diff --git a/app/Helper/User.php b/app/Helper/User.php new file mode 100644 index 00000000..1cad6042 --- /dev/null +++ b/app/Helper/User.php @@ -0,0 +1,104 @@ +<?php + +namespace Helper; + +/** + * User helpers + * + * @package helper + * @author Frederic Guillot + */ +class User extends \Core\Base +{ + /** + * Get user id + * + * @access public + * @return integer + */ + public function getId() + { + return $this->userSession->getId(); + } + + /** + * Get user profile + * + * @access public + * @return string + */ + public function getProfileLink() + { + return $this->helper->url->link( + $this->helper->e($this->getFullname()), + 'user', + 'show', + array('user_id' => $this->userSession->getId()) + ); + } + /** + * Check if the given user_id is the connected user + * + * @param integer $user_id User id + * @return boolean + */ + public function isCurrentUser($user_id) + { + return $this->userSession->getId() == $user_id; + } + + /** + * Return if the logged user is admin + * + * @access public + * @return boolean + */ + public function isAdmin() + { + return $this->userSession->isAdmin(); + } + + /** + * Proxy cache helper for acl::isManagerActionAllowed() + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isManager($project_id) + { + if ($this->userSession->isAdmin()) { + return true; + } + + return $this->memoryCache->proxy('acl', 'isManagerActionAllowed', $project_id); + } + + /** + * Return the user full name + * + * @param array $user User properties + * @return string + */ + public function getFullname(array $user = array()) + { + return $this->user->getFullname(empty($user) ? $_SESSION['user'] : $user); + } + + /** + * Display gravatar image + * + * @access public + * @param string $email + * @param string $alt + * @return string + */ + public function avatar($email, $alt = '') + { + if (! empty($email) && $this->config->get('integration_gravatar') == 1) { + return '<img class="avatar" src="https://www.gravatar.com/avatar/'.md5(strtolower($email)).'?s=25" alt="'.$this->helper->e($alt).'" title="'.$this->helper->e($alt).'">'; + } + + return ''; + } +} diff --git a/app/Integration/BitbucketWebhook.php b/app/Integration/BitbucketWebhook.php new file mode 100644 index 00000000..75fc1c81 --- /dev/null +++ b/app/Integration/BitbucketWebhook.php @@ -0,0 +1,97 @@ +<?php + +namespace Integration; + +use Event\TaskEvent; +use Model\Task; + +/** + * Bitbucket Webhook + * + * @package integration + * @author Frederic Guillot + */ +class BitbucketWebhook extends \Core\Base +{ + /** + * Events + * + * @var string + */ + const EVENT_COMMIT = 'bitbucket.webhook.commit'; + + /** + * Project id + * + * @access private + * @var integer + */ + private $project_id = 0; + + /** + * Set the project id + * + * @access public + * @param integer $project_id Project id + */ + public function setProjectId($project_id) + { + $this->project_id = $project_id; + } + + /** + * Parse events + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function parsePayload(array $payload) + { + if (! empty($payload['commits'])) { + + foreach ($payload['commits'] as $commit) { + + if ($this->handleCommit($commit)) { + return true; + } + } + } + + return false; + } + + /** + * Parse commit + * + * @access public + * @param array $commit Gitlab commit + * @return boolean + */ + public function handleCommit(array $commit) + { + $task_id = $this->task->getTaskIdFromText($commit['message']); + + if (! $task_id) { + return false; + } + + $task = $this->taskFinder->getById($task_id); + + if (empty($task)) { + return false; + } + + if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) { + + $this->container['dispatcher']->dispatch( + self::EVENT_COMMIT, + new TaskEvent(array('task_id' => $task_id) + $task) + ); + + return true; + } + + return false; + } +} diff --git a/app/Model/GithubWebhook.php b/app/Integration/GithubWebhook.php index 6624a782..d95eba78 100644 --- a/app/Model/GithubWebhook.php +++ b/app/Integration/GithubWebhook.php @@ -1,14 +1,17 @@ <?php -namespace Model; +namespace Integration; + +use Event\GenericEvent; +use Model\Task; /** - * Github Webhook model + * Github Webhook * - * @package model + * @package integration * @author Frederic Guillot */ -class GithubWebhook extends Base +class GithubWebhook extends \Core\Base { /** * Events @@ -47,18 +50,21 @@ class GithubWebhook extends Base * * @access public * @param string $type Github event type - * @param string $payload Raw Github event (JSON) + * @param array $payload Github event + * @return boolean */ - public function parsePayload($type, $payload) + public function parsePayload($type, array $payload) { - $payload = json_decode($payload, true); - switch ($type) { case 'push': return $this->parsePushEvent($payload); case 'issues': return $this->parseIssueEvent($payload); + case 'issue_comment': + return $this->parseCommentIssueEvent($payload); } + + return false; } /** @@ -66,6 +72,7 @@ class GithubWebhook extends Base * * @access public * @param array $payload Event data + * @return boolean */ public function parsePushEvent(array $payload) { @@ -79,14 +86,19 @@ class GithubWebhook extends Base $task = $this->taskFinder->getById($task_id); - if (! $task) { + if (empty($task)) { continue; } - if ($task['is_active'] == Task::STATUS_OPEN) { - $this->event->trigger(self::EVENT_COMMIT, array('task_id' => $task_id) + $task); + if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) { + $this->container['dispatcher']->dispatch( + self::EVENT_COMMIT, + new GenericEvent(array('task_id' => $task_id) + $task) + ); } } + + return true; } /** @@ -94,32 +106,61 @@ class GithubWebhook extends Base * * @access public * @param array $payload Event data + * @return boolean */ public function parseIssueEvent(array $payload) { switch ($payload['action']) { case 'opened': - $this->handleIssueOpened($payload['issue']); - break; + return $this->handleIssueOpened($payload['issue']); case 'closed': - $this->handleIssueClosed($payload['issue']); - break; + return $this->handleIssueClosed($payload['issue']); case 'reopened': - $this->handleIssueReopened($payload['issue']); - break; + return $this->handleIssueReopened($payload['issue']); case 'assigned': - $this->handleIssueAssigned($payload['issue']); - break; + return $this->handleIssueAssigned($payload['issue']); case 'unassigned': - $this->handleIssueUnassigned($payload['issue']); - break; + return $this->handleIssueUnassigned($payload['issue']); case 'labeled': - $this->handleIssueLabeled($payload['issue'], $payload['label']); - break; + return $this->handleIssueLabeled($payload['issue'], $payload['label']); case 'unlabeled': - $this->handleIssueUnlabeled($payload['issue'], $payload['label']); - break; + return $this->handleIssueUnlabeled($payload['issue'], $payload['label']); + } + + return false; + } + + /** + * Parse comment issue events + * + * @access public + * @param array $payload Event data + * @return boolean + */ + public function parseCommentIssueEvent(array $payload) + { + $task = $this->taskFinder->getByReference($this->project_id, $payload['issue']['number']); + $user = $this->user->getByUsername($payload['comment']['user']['login']); + + if (! empty($task) && ! empty($user)) { + + $event = array( + 'project_id' => $this->project_id, + 'reference' => $payload['comment']['id'], + 'comment' => $payload['comment']['body'], + 'user_id' => $user['id'], + 'task_id' => $task['id'], + ); + + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_COMMENT, + new GenericEvent($event) + ); + + return true; } + + return false; } /** @@ -127,6 +168,7 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueOpened(array $issue) { @@ -137,7 +179,12 @@ class GithubWebhook extends Base 'description' => $issue['body']."\n\n[".t('Github Issue').']('.$issue['html_url'].')', ); - $this->event->trigger(self::EVENT_ISSUE_OPENED, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_OPENED, + new GenericEvent($event) + ); + + return true; } /** @@ -145,20 +192,28 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueClosed(array $issue) { - $task = $this->taskFinder->getByReference($issue['number']); + $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - if ($task) { + if (! empty($task)) { $event = array( 'project_id' => $this->project_id, 'task_id' => $task['id'], 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_CLOSED, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_CLOSED, + new GenericEvent($event) + ); + + return true; } + + return false; } /** @@ -166,20 +221,28 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueReopened(array $issue) { - $task = $this->taskFinder->getByReference($issue['number']); + $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - if ($task) { + if (! empty($task)) { $event = array( 'project_id' => $this->project_id, 'task_id' => $task['id'], 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_REOPENED, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_REOPENED, + new GenericEvent($event) + ); + + return true; } + + return false; } /** @@ -187,13 +250,14 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueAssigned(array $issue) { $user = $this->user->getByUsername($issue['assignee']['login']); - $task = $this->taskFinder->getByReference($issue['number']); + $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - if ($user && $task) { + if (! empty($user) && ! empty($task)) { $event = array( 'project_id' => $this->project_id, @@ -202,8 +266,15 @@ class GithubWebhook extends Base 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_ASSIGNEE_CHANGE, + new GenericEvent($event) + ); + + return true; } + + return false; } /** @@ -211,12 +282,13 @@ class GithubWebhook extends Base * * @access public * @param array $issue Issue data + * @return boolean */ public function handleIssueUnassigned(array $issue) { - $task = $this->taskFinder->getByReference($issue['number']); + $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - if ($task) { + if (! empty($task)) { $event = array( 'project_id' => $this->project_id, @@ -225,8 +297,15 @@ class GithubWebhook extends Base 'reference' => $issue['number'], ); - $this->event->trigger(self::EVENT_ISSUE_ASSIGNEE_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_ASSIGNEE_CHANGE, + new GenericEvent($event) + ); + + return true; } + + return false; } /** @@ -235,12 +314,13 @@ class GithubWebhook extends Base * @access public * @param array $issue Issue data * @param array $label Label data + * @return boolean */ public function handleIssueLabeled(array $issue, array $label) { - $task = $this->taskFinder->getByReference($issue['number']); + $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - if ($task) { + if (! empty($task)) { $event = array( 'project_id' => $this->project_id, @@ -249,8 +329,15 @@ class GithubWebhook extends Base 'label' => $label['name'], ); - $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_LABEL_CHANGE, + new GenericEvent($event) + ); + + return true; } + + return false; } /** @@ -259,12 +346,13 @@ class GithubWebhook extends Base * @access public * @param array $issue Issue data * @param array $label Label data + * @return boolean */ public function handleIssueUnlabeled(array $issue, array $label) { - $task = $this->taskFinder->getByReference($issue['number']); + $task = $this->taskFinder->getByReference($this->project_id, $issue['number']); - if ($task) { + if (! empty($task)) { $event = array( 'project_id' => $this->project_id, @@ -274,7 +362,14 @@ class GithubWebhook extends Base 'category_id' => 0, ); - $this->event->trigger(self::EVENT_ISSUE_LABEL_CHANGE, $event); + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_LABEL_CHANGE, + new GenericEvent($event) + ); + + return true; } + + return false; } } diff --git a/app/Integration/GitlabWebhook.php b/app/Integration/GitlabWebhook.php new file mode 100644 index 00000000..8a11f5c6 --- /dev/null +++ b/app/Integration/GitlabWebhook.php @@ -0,0 +1,213 @@ +<?php + +namespace Integration; + +use Event\GenericEvent; +use Event\TaskEvent; +use Model\Task; + +/** + * Gitlab Webhook + * + * @package integration + * @author Frederic Guillot + */ +class GitlabWebhook extends \Core\Base +{ + /** + * Events + * + * @var string + */ + const EVENT_ISSUE_OPENED = 'gitlab.webhook.issue.opened'; + const EVENT_ISSUE_CLOSED = 'gitlab.webhook.issue.closed'; + const EVENT_COMMIT = 'gitlab.webhook.commit'; + + /** + * Supported webhook events + * + * @var string + */ + const TYPE_PUSH = 'push'; + const TYPE_ISSUE = 'issue'; + + /** + * Project id + * + * @access private + * @var integer + */ + private $project_id = 0; + + /** + * Set the project id + * + * @access public + * @param integer $project_id Project id + */ + public function setProjectId($project_id) + { + $this->project_id = $project_id; + } + + /** + * Parse events + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function parsePayload(array $payload) + { + switch ($this->getType($payload)) { + case self::TYPE_PUSH: + return $this->handlePushEvent($payload); + case self::TYPE_ISSUE; + return $this->handleIssueEvent($payload); + } + + return false; + } + + /** + * Get event type + * + * @access public + * @param array $payload Gitlab event + * @return string + */ + public function getType(array $payload) + { + if (isset($payload['object_kind']) && $payload['object_kind'] === 'issue') { + return self::TYPE_ISSUE; + } + + if (isset($payload['commits'])) { + return self::TYPE_PUSH; + } + + return ''; + } + + /** + * Parse push event + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function handlePushEvent(array $payload) + { + foreach ($payload['commits'] as $commit) { + $this->handleCommit($commit); + } + + return true; + } + + /** + * Parse commit + * + * @access public + * @param array $commit Gitlab commit + * @return boolean + */ + public function handleCommit(array $commit) + { + $task_id = $this->task->getTaskIdFromText($commit['message']); + + if (! $task_id) { + return false; + } + + $task = $this->taskFinder->getById($task_id); + + if (empty($task)) { + return false; + } + + if ($task['is_active'] == Task::STATUS_OPEN && $task['project_id'] == $this->project_id) { + + $this->container['dispatcher']->dispatch( + self::EVENT_COMMIT, + new TaskEvent(array('task_id' => $task_id) + $task) + ); + + return true; + } + + return false; + } + + /** + * Parse issue event + * + * @access public + * @param array $payload Gitlab event + * @return boolean + */ + public function handleIssueEvent(array $payload) + { + switch ($payload['object_attributes']['action']) { + case 'open': + return $this->handleIssueOpened($payload['object_attributes']); + case 'close': + return $this->handleIssueClosed($payload['object_attributes']); + } + + return false; + } + + /** + * Handle new issues + * + * @access public + * @param array $issue Issue data + * @return boolean + */ + public function handleIssueOpened(array $issue) + { + $event = array( + 'project_id' => $this->project_id, + 'reference' => $issue['id'], + 'title' => $issue['title'], + 'description' => $issue['description']."\n\n[".t('Gitlab Issue').']('.$issue['url'].')', + ); + + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_OPENED, + new GenericEvent($event) + ); + + return true; + } + + /** + * Handle issue closing + * + * @access public + * @param array $issue Issue data + * @return boolean + */ + public function handleIssueClosed(array $issue) + { + $task = $this->taskFinder->getByReference($this->project_id, $issue['id']); + + if (! empty($task)) { + $event = array( + 'project_id' => $this->project_id, + 'task_id' => $task['id'], + 'reference' => $issue['id'], + ); + + $this->container['dispatcher']->dispatch( + self::EVENT_ISSUE_CLOSED, + new GenericEvent($event) + ); + + return true; + } + + return false; + } +} diff --git a/app/Integration/HipchatWebhook.php b/app/Integration/HipchatWebhook.php new file mode 100644 index 00000000..f1be0f34 --- /dev/null +++ b/app/Integration/HipchatWebhook.php @@ -0,0 +1,95 @@ +<?php + +namespace Integration; + +/** + * Hipchat webhook + * + * @package integration + * @author Frederic Guillot + */ +class HipchatWebhook extends \Core\Base +{ + /** + * Return true if Hipchat is enabled for this project or globally + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isActivated($project_id) + { + return $this->config->get('integration_hipchat') == 1 || $this->projectIntegration->hasValue($project_id, 'hipchat', 1); + } + + /** + * Get API parameters + * + * @access public + * @param integer $project_id + * @return array + */ + public function getParameters($project_id) + { + if ($this->config->get('integration_hipchat') == 1) { + return array( + 'api_url' => $this->config->get('integration_hipchat_api_url'), + 'room_id' => $this->config->get('integration_hipchat_room_id'), + 'room_token' => $this->config->get('integration_hipchat_room_token'), + ); + } + + $options = $this->projectIntegration->getParameters($project_id); + + return array( + 'api_url' => $options['hipchat_api_url'], + 'room_id' => $options['hipchat_room_id'], + 'room_token' => $options['hipchat_room_token'], + ); + } + + /** + * Send the notification if activated + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $event_name Event name + * @param array $event Event data + */ + public function notify($project_id, $task_id, $event_name, array $event) + { + if ($this->isActivated($project_id)) { + + $params = $this->getParameters($project_id); + $project = $this->project->getbyId($project_id); + + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); + + $html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>'; + $html .= '<strong>'.$project['name'].'</strong>'.(isset($event['task']['title']) ? '<br/>'.$event['task']['title'] : '').'<br/>'; + $html .= $this->projectActivity->getTitle($event); + + if ($this->config->get('application_url')) { + $html .= '<br/><a href="'.$this->config->get('application_url'); + $html .= $this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)).'">'; + $html .= t('view the task on Kanboard').'</a>'; + } + + $payload = array( + 'message' => $html, + 'color' => 'yellow', + ); + + $url = sprintf( + '%s/v2/room/%s/notification?auth_token=%s', + $params['api_url'], + $params['room_id'], + $params['room_token'] + ); + + $this->httpClient->postJson($url, $payload); + } + } +} diff --git a/app/Integration/Jabber.php b/app/Integration/Jabber.php new file mode 100644 index 00000000..a1191662 --- /dev/null +++ b/app/Integration/Jabber.php @@ -0,0 +1,130 @@ +<?php + +namespace Integration; + +use Exception; +use Fabiang\Xmpp\Options; +use Fabiang\Xmpp\Client; +use Fabiang\Xmpp\Protocol\Message; +use Fabiang\Xmpp\Protocol\Presence; + +/** + * Jabber + * + * @package integration + * @author Frederic Guillot + */ +class Jabber extends \Core\Base +{ + /** + * Return true if Jabber is enabled for this project or globally + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isActivated($project_id) + { + return $this->config->get('integration_jabber') == 1 || $this->projectIntegration->hasValue($project_id, 'jabber', 1); + } + + /** + * Get connection parameters + * + * @access public + * @param integer $project_id + * @return array + */ + public function getParameters($project_id) + { + if ($this->config->get('integration_jabber') == 1) { + return array( + 'server' => $this->config->get('integration_jabber_server'), + 'domain' => $this->config->get('integration_jabber_domain'), + 'username' => $this->config->get('integration_jabber_username'), + 'password' => $this->config->get('integration_jabber_password'), + 'nickname' => $this->config->get('integration_jabber_nickname'), + 'room' => $this->config->get('integration_jabber_room'), + ); + } + + $options = $this->projectIntegration->getParameters($project_id); + + return array( + 'server' => $options['jabber_server'], + 'domain' => $options['jabber_domain'], + 'username' => $options['jabber_username'], + 'password' => $options['jabber_password'], + 'nickname' => $options['jabber_nickname'], + 'room' => $options['jabber_room'], + ); + } + + /** + * Build and send the message + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $event_name Event name + * @param array $event Event data + */ + public function notify($project_id, $task_id, $event_name, array $event) + { + if ($this->isActivated($project_id)) { + + $project = $this->project->getbyId($project_id); + + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); + + $payload = '['.$project['name'].'] '.str_replace('"', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : ''); + + if ($this->config->get('application_url')) { + $payload .= ' '.$this->config->get('application_url'); + $payload .= $this->helper->url->to('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)); + } + + $this->sendMessage($project_id, $payload); + } + } + + /** + * Send message to the XMPP server + * + * @access public + * @param integer $project_id + * @param string $payload + */ + public function sendMessage($project_id, $payload) + { + try { + + $params = $this->getParameters($project_id); + + $options = new Options($params['server']); + $options->setUsername($params['username']); + $options->setPassword($params['password']); + $options->setTo($params['domain']); + $options->setLogger($this->container['logger']); + + $client = new Client($options); + + $channel = new Presence; + $channel->setTo($params['room'])->setNickName($params['nickname']); + $client->send($channel); + + $message = new Message; + $message->setMessage($payload) + ->setTo($params['room']) + ->setType(Message::TYPE_GROUPCHAT); + + $client->send($message); + + $client->disconnect(); + } + catch (Exception $e) { + $this->container['logger']->error('Jabber error: '.$e->getMessage()); + } + } +} diff --git a/app/Integration/Mailgun.php b/app/Integration/Mailgun.php new file mode 100644 index 00000000..1451b211 --- /dev/null +++ b/app/Integration/Mailgun.php @@ -0,0 +1,97 @@ +<?php + +namespace Integration; + +use HTML_To_Markdown; +use Core\Tool; + +/** + * Mailgun Integration + * + * @package integration + * @author Frederic Guillot + */ +class Mailgun extends \Core\Base +{ + /** + * Send a HTML email + * + * @access public + * @param string $email + * @param string $name + * @param string $subject + * @param string $html + * @param string $author + */ + public function sendEmail($email, $name, $subject, $html, $author) + { + $headers = array( + 'Authorization: Basic '.base64_encode('api:'.MAILGUN_API_TOKEN) + ); + + $payload = array( + 'from' => sprintf('%s <%s>', $author, MAIL_FROM), + 'to' => sprintf('%s <%s>', $name, $email), + 'subject' => $subject, + 'html' => $html, + ); + + $this->httpClient->postForm('https://api.mailgun.net/v3/'.MAILGUN_DOMAIN.'/messages', $payload, $headers); + } + + /** + * Parse incoming email + * + * @access public + * @param array $payload Incoming email + * @return boolean + */ + public function receiveEmail(array $payload) + { + if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient'])) { + return false; + } + + // The user must exists in Kanboard + $user = $this->user->getByEmail($payload['sender']); + + if (empty($user)) { + $this->container['logger']->debug('Mailgun: ignored => user not found'); + return false; + } + + // The project must have a short name + $project = $this->project->getByIdentifier(Tool::getMailboxHash($payload['recipient'])); + + if (empty($project)) { + $this->container['logger']->debug('Mailgun: ignored => project not found'); + return false; + } + + // The user must be member of the project + if (! $this->projectPermission->isMember($project['id'], $user['id'])) { + $this->container['logger']->debug('Mailgun: ignored => user is not member of the project'); + return false; + } + + // Get the Markdown contents + if (! empty($payload['stripped-html'])) { + $markdown = new HTML_To_Markdown($payload['stripped-html'], array('strip_tags' => true)); + $description = $markdown->output(); + } + else if (! empty($payload['stripped-text'])) { + $description = $payload['stripped-text']; + } + else { + $description = ''; + } + + // Finally, we create the task + return (bool) $this->taskCreation->create(array( + 'project_id' => $project['id'], + 'title' => $payload['subject'], + 'description' => $description, + 'creator_id' => $user['id'], + )); + } +} diff --git a/app/Integration/Postmark.php b/app/Integration/Postmark.php new file mode 100644 index 00000000..dbb70aee --- /dev/null +++ b/app/Integration/Postmark.php @@ -0,0 +1,97 @@ +<?php + +namespace Integration; + +use HTML_To_Markdown; + +/** + * Postmark integration + * + * @package integration + * @author Frederic Guillot + */ +class Postmark extends \Core\Base +{ + /** + * Send a HTML email + * + * @access public + * @param string $email + * @param string $name + * @param string $subject + * @param string $html + * @param string $author + */ + public function sendEmail($email, $name, $subject, $html, $author) + { + $headers = array( + 'Accept: application/json', + 'X-Postmark-Server-Token: '.POSTMARK_API_TOKEN, + ); + + $payload = array( + 'From' => sprintf('%s <%s>', $author, MAIL_FROM), + 'To' => sprintf('%s <%s>', $name, $email), + 'Subject' => $subject, + 'HtmlBody' => $html, + ); + + $this->httpClient->postJson('https://api.postmarkapp.com/email', $payload, $headers); + } + + /** + * Parse incoming email + * + * @access public + * @param array $payload Incoming email + * @return boolean + */ + public function receiveEmail(array $payload) + { + if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash'])) { + return false; + } + + // The user must exists in Kanboard + $user = $this->user->getByEmail($payload['From']); + + if (empty($user)) { + $this->container['logger']->debug('Postmark: ignored => user not found'); + return false; + } + + // The project must have a short name + $project = $this->project->getByIdentifier($payload['MailboxHash']); + + if (empty($project)) { + $this->container['logger']->debug('Postmark: ignored => project not found'); + return false; + } + + // The user must be member of the project + if (! $this->projectPermission->isMember($project['id'], $user['id'])) { + $this->container['logger']->debug('Postmark: ignored => user is not member of the project'); + return false; + } + + // Get the Markdown contents + if (! empty($payload['HtmlBody'])) { + $markdown = new HTML_To_Markdown($payload['HtmlBody'], array('strip_tags' => true)); + $description = $markdown->output(); + } + else if (! empty($payload['TextBody'])) { + $description = $payload['TextBody']; + } + else { + $description = ''; + } + + // Finally, we create the task + return (bool) $this->taskCreation->create(array( + 'project_id' => $project['id'], + 'title' => $payload['Subject'], + 'description' => $description, + 'creator_id' => $user['id'], + )); + } +} diff --git a/app/Integration/SendgridWebhook.php b/app/Integration/SendgridWebhook.php new file mode 100644 index 00000000..9125f00b --- /dev/null +++ b/app/Integration/SendgridWebhook.php @@ -0,0 +1,74 @@ +<?php + +namespace Integration; + +use HTML_To_Markdown; +use Core\Tool; + +/** + * Sendgrid Webhook + * + * @package integration + * @author Frederic Guillot + */ +class SendgridWebhook extends \Core\Base +{ + /** + * Parse incoming email + * + * @access public + * @param array $payload Incoming email + * @return boolean + */ + public function parsePayload(array $payload) + { + if (empty($payload['envelope']) || empty($payload['subject'])) { + return false; + } + + $envelope = json_decode($payload['envelope'], true); + $sender = isset($envelope['to'][0]) ? $envelope['to'][0] : ''; + + // The user must exists in Kanboard + $user = $this->user->getByEmail($envelope['from']); + + if (empty($user)) { + $this->container['logger']->debug('SendgridWebhook: ignored => user not found'); + return false; + } + + // The project must have a short name + $project = $this->project->getByIdentifier(Tool::getMailboxHash($sender)); + + if (empty($project)) { + $this->container['logger']->debug('SendgridWebhook: ignored => project not found'); + return false; + } + + // The user must be member of the project + if (! $this->projectPermission->isMember($project['id'], $user['id'])) { + $this->container['logger']->debug('SendgridWebhook: ignored => user is not member of the project'); + return false; + } + + // Get the Markdown contents + if (! empty($payload['html'])) { + $markdown = new HTML_To_Markdown($payload['html'], array('strip_tags' => true)); + $description = $markdown->output(); + } + else if (! empty($payload['text'])) { + $description = $payload['text']; + } + else { + $description = ''; + } + + // Finally, we create the task + return (bool) $this->taskCreation->create(array( + 'project_id' => $project['id'], + 'title' => $payload['subject'], + 'description' => $description, + 'creator_id' => $user['id'], + )); + } +} diff --git a/app/Integration/SlackWebhook.php b/app/Integration/SlackWebhook.php new file mode 100644 index 00000000..975ea21f --- /dev/null +++ b/app/Integration/SlackWebhook.php @@ -0,0 +1,75 @@ +<?php + +namespace Integration; + +/** + * Slack Webhook + * + * @package integration + * @author Frederic Guillot + */ +class SlackWebhook extends \Core\Base +{ + /** + * Return true if Slack is enabled for this project or globally + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isActivated($project_id) + { + return $this->config->get('integration_slack_webhook') == 1 || $this->projectIntegration->hasValue($project_id, 'slack', 1); + } + + /** + * Get wehbook url + * + * @access public + * @param integer $project_id + * @return string + */ + public function getWebhookUrl($project_id) + { + if ($this->config->get('integration_slack_webhook') == 1) { + return $this->config->get('integration_slack_webhook_url'); + } + + $options = $this->projectIntegration->getParameters($project_id); + return $options['slack_webhook_url']; + } + + /** + * Send message to the incoming Slack webhook + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $event_name Event name + * @param array $event Event data + */ + public function notify($project_id, $task_id, $event_name, array $event) + { + if ($this->isActivated($project_id)) { + + $project = $this->project->getbyId($project_id); + + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); + + $payload = array( + 'text' => '*['.$project['name'].']* '.str_replace('"', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : ''), + 'username' => 'Kanboard', + 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', + ); + + if ($this->config->get('application_url')) { + $payload['text'] .= ' - <'.$this->config->get('application_url'); + $payload['text'] .= $this->helper->url->href('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)); + $payload['text'] .= '|'.t('view the task on Kanboard').'>'; + } + + $this->httpClient->postJson($this->getWebhookUrl($project_id), $payload); + } + } +} diff --git a/app/Integration/Smtp.php b/app/Integration/Smtp.php new file mode 100644 index 00000000..ad2f30f8 --- /dev/null +++ b/app/Integration/Smtp.php @@ -0,0 +1,71 @@ +<?php + +namespace Integration; + +use Swift_Message; +use Swift_Mailer; +use Swift_MailTransport; +use Swift_SendmailTransport; +use Swift_SmtpTransport; +use Swift_TransportException; + +/** + * Smtp + * + * @package integration + * @author Frederic Guillot + */ +class Smtp extends \Core\Base +{ + /** + * Send a HTML email + * + * @access public + * @param string $email + * @param string $name + * @param string $subject + * @param string $html + * @param string $author + */ + public function sendEmail($email, $name, $subject, $html, $author) + { + try { + + $message = Swift_Message::newInstance() + ->setSubject($subject) + ->setFrom(array(MAIL_FROM => $author)) + ->setBody($html, 'text/html') + ->setTo(array($email => $name)); + + Swift_Mailer::newInstance($this->getTransport())->send($message); + } + catch (Swift_TransportException $e) { + $this->container['logger']->error($e->getMessage()); + } + } + + /** + * Get SwiftMailer transport + * + * @access private + * @return \Swift_Transport + */ + private function getTransport() + { + switch (MAIL_TRANSPORT) { + case 'smtp': + $transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT); + $transport->setUsername(MAIL_SMTP_USERNAME); + $transport->setPassword(MAIL_SMTP_PASSWORD); + $transport->setEncryption(MAIL_SMTP_ENCRYPTION); + break; + case 'sendmail': + $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND); + break; + default: + $transport = Swift_MailTransport::newInstance(); + } + + return $transport; + } +} diff --git a/app/Library/password.php b/app/Library/password.php new file mode 100644 index 00000000..c6e84cbd --- /dev/null +++ b/app/Library/password.php @@ -0,0 +1,227 @@ +<?php +/** + * A Compatibility library with PHP 5.5's simplified password hashing API. + * + * @author Anthony Ferrara <ircmaxell@php.net> + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @copyright 2012 The Authors + */ + +if (!defined('PASSWORD_BCRYPT')) { + + define('PASSWORD_BCRYPT', 1); + define('PASSWORD_DEFAULT', PASSWORD_BCRYPT); + + if (version_compare(PHP_VERSION, '5.3.7', '<')) { + + define('PASSWORD_PREFIX', '$2a$'); + } + else { + + define('PASSWORD_PREFIX', '$2y$'); + } + + /** + * Hash the password using the specified algorithm + * + * @param string $password The password to hash + * @param int $algo The algorithm to use (Defined by PASSWORD_* constants) + * @param array $options The options for the algorithm to use + * + * @return string|false The hashed password, or false on error. + */ + function password_hash($password, $algo, array $options = array()) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING); + return null; + } + if (!is_string($password)) { + trigger_error("password_hash(): Password must be a string", E_USER_WARNING); + return null; + } + if (!is_int($algo)) { + trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING); + return null; + } + switch ($algo) { + case PASSWORD_BCRYPT: + // Note that this is a C constant, but not exposed to PHP, so we don't define it here. + $cost = 10; + if (isset($options['cost'])) { + $cost = $options['cost']; + if ($cost < 4 || $cost > 31) { + trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING); + return null; + } + } + $required_salt_len = 22; + $hash_format = sprintf("%s%02d$", PASSWORD_PREFIX, $cost); + break; + default: + trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING); + return null; + } + if (isset($options['salt'])) { + switch (gettype($options['salt'])) { + case 'NULL': + case 'boolean': + case 'integer': + case 'double': + case 'string': + $salt = (string) $options['salt']; + break; + case 'object': + if (method_exists($options['salt'], '__tostring')) { + $salt = (string) $options['salt']; + break; + } + case 'array': + case 'resource': + default: + trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING); + return null; + } + if (strlen($salt) < $required_salt_len) { + trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING); + return null; + } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) { + $salt = str_replace('+', '.', base64_encode($salt)); + } + } else { + $buffer = ''; + $raw_length = (int) ($required_salt_len * 3 / 4 + 1); + $buffer_valid = false; + if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) { + $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) { + $buffer = openssl_random_pseudo_bytes($raw_length); + if ($buffer) { + $buffer_valid = true; + } + } + if (!$buffer_valid && is_readable('/dev/urandom')) { + $f = fopen('/dev/urandom', 'r'); + $read = strlen($buffer); + while ($read < $raw_length) { + $buffer .= fread($f, $raw_length - $read); + $read = strlen($buffer); + } + fclose($f); + if ($read >= $raw_length) { + $buffer_valid = true; + } + } + if (!$buffer_valid || strlen($buffer) < $raw_length) { + $bl = strlen($buffer); + for ($i = 0; $i < $raw_length; $i++) { + if ($i < $bl) { + $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255)); + } else { + $buffer .= chr(mt_rand(0, 255)); + } + } + } + $salt = str_replace('+', '.', base64_encode($buffer)); + + } + $salt = substr($salt, 0, $required_salt_len); + + $hash = $hash_format . $salt; + + $ret = crypt($password, $hash); + + if (!is_string($ret) || strlen($ret) <= 13) { + return false; + } + + return $ret; + } + + /** + * Get information about the password hash. Returns an array of the information + * that was used to generate the password hash. + * + * array( + * 'algo' => 1, + * 'algoName' => 'bcrypt', + * 'options' => array( + * 'cost' => 10, + * ), + * ) + * + * @param string $hash The password hash to extract info from + * + * @return array The array of information about the hash. + */ + function password_get_info($hash) { + $return = array( + 'algo' => 0, + 'algoName' => 'unknown', + 'options' => array(), + ); + if (substr($hash, 0, 4) == PASSWORD_PREFIX && strlen($hash) == 60) { + $return['algo'] = PASSWORD_BCRYPT; + $return['algoName'] = 'bcrypt'; + list($cost) = sscanf($hash, PASSWORD_PREFIX."%d$"); + $return['options']['cost'] = $cost; + } + return $return; + } + + /** + * Determine if the password hash needs to be rehashed according to the options provided + * + * If the answer is true, after validating the password using password_verify, rehash it. + * + * @param string $hash The hash to test + * @param int $algo The algorithm used for new password hashes + * @param array $options The options array passed to password_hash + * + * @return boolean True if the password needs to be rehashed. + */ + function password_needs_rehash($hash, $algo, array $options = array()) { + $info = password_get_info($hash); + if ($info['algo'] != $algo) { + return true; + } + switch ($algo) { + case PASSWORD_BCRYPT: + $cost = isset($options['cost']) ? $options['cost'] : 10; + if ($cost != $info['options']['cost']) { + return true; + } + break; + } + return false; + } + + /** + * Verify a password against a hash using a timing attack resistant approach + * + * @param string $password The password to verify + * @param string $hash The hash to verify against + * + * @return boolean If the password matches the hash + */ + function password_verify($password, $hash) { + if (!function_exists('crypt')) { + trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING); + return false; + } + $ret = crypt($password, $hash); + if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) { + return false; + } + + $status = 0; + for ($i = 0; $i < strlen($ret); $i++) { + $status |= (ord($ret[$i]) ^ ord($hash[$i])); + } + + return $status === 0; + } +} diff --git a/app/Locales/da_DK/translations.php b/app/Locale/da_DK/translations.php index 546926af..535d77b8 100644 --- a/app/Locales/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -1,6 +1,8 @@ <?php return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', 'None' => 'Ingen', 'edit' => 'rediger', 'Edit' => 'Rediger', @@ -182,18 +184,19 @@ return array( 'Change assignee' => 'Ændre ansvarlig', 'Change assignee for the task "%s"' => 'Ændre ansvarlig for opgaven: "%s"', 'Timezone' => 'Tidszone', - 'Sorry, I didn\'t found this information in my database!' => 'Denne information kunne ikke findes i databasen!', + 'Sorry, I didn\'t find this information in my database!' => 'Denne information kunne ikke findes i databasen!', 'Page not found' => 'Siden er ikke fundet', 'Complexity' => 'Kompleksitet', 'limit' => 'Begrænsning', 'Task limit' => 'Opgave begrænsning', + // 'Task count' => '', 'This value must be greater than %d' => 'Denne værdi skal være større end %d', 'Edit project access list' => 'Rediger adgangstilladelser for projektet', 'Edit users access' => 'Rediger brugertilladelser', 'Allow this user' => 'Tillad denne bruger', 'Only those users have access to this project:' => 'Kunne disse brugere har adgang til dette projekt:', 'Don\'t forget that administrators have access to everything.' => 'Glem ikke at administratorer har adgang til alt.', - 'revoke' => 'fjern', + 'Revoke' => 'Fjern', 'List of authorized users' => 'Liste over autoriserede brugere', 'User' => 'Bruger', 'Nobody have access to this project.' => 'Ingen har adgang til dette projekt.', @@ -212,6 +215,7 @@ return array( 'Invalid date' => 'Ugyldig dato', 'Must be done before %B %e, %Y' => 'Skal være fuldført inden %d.%m.%Y', '%B %e, %Y' => '%d.%m.%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Automatiske handlinger', 'Your automatic action have been created successfully.' => 'Din automatiske handling er oprettet.', 'Unable to create your automatic action.' => 'Din automatiske handling kunne ikke oprettes.', @@ -385,8 +389,6 @@ return array( 'Creator' => 'Skaber', 'Modification date' => 'Ændringsdato', 'Completion date' => 'Afslutningsdato', - 'Webhook URL for task creation' => 'Webhook URL for opgave oprettelse', - 'Webhook URL for task modification' => 'Webhook URL opgave redigering', 'Clone' => 'Kopier', 'Clone Project' => 'Kopier projekt', 'Project cloned successfully.' => 'Projektet er kopieret.', @@ -406,15 +408,13 @@ return array( 'Comment updated' => 'Kommentar opdateret', 'New comment posted by %s' => 'Ny kommentar af %s', 'List of due tasks for the project "%s"' => 'Udestående opgaver for projektet "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][Ny vedhæftning] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Ny kommentar] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Kommentar opdateret] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Ny under-opgave] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Under-opgave opdateret] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Ny opgave] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Opgave opdateret] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Opgave lukket] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Opgave åbnet] %s (#%d)', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', '[%s][Due tasks]' => 'Udestående opgaver', '[Kanboard] Notification' => '[Kanboard] Notifikation', 'I want to receive notifications only for those projects:' => 'Jeg vil kun have notifikationer for disse projekter:', @@ -467,18 +467,18 @@ return array( 'Unable to change the password.' => 'Adgangskoden kunne ikke ændres.', 'Change category for the task "%s"' => 'Skift kategori for opgaven "%s"', 'Change category' => 'Skift kategori', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s opdatert opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s åben opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s flyt opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a> til positionen #%d i kolonnen "%s"', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s flyttede opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a> til kolonnen "%s"', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s oprettede opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s oprettede en under-opgave for opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s opdaterede en under-opgave for opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a>', + '%s updated the task %s' => '%s opdatert opgaven %s', + '%s opened the task %s' => '%s åben opgaven %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s flyt opgaven %s til positionen #%d i kolonnen "%s"', + '%s moved the task %s to the column "%s"' => '%s flyttede opgaven %s til kolonnen "%s"', + '%s created the task %s' => '%s oprettede opgaven %s', + // '%s closed the task %s' => '', + '%s created a subtask for the task %s' => '%s oprettede en under-opgave for opgaven %s', + '%s updated a subtask for the task %s' => '%s opdaterede en under-opgave for opgaven %s', 'Assigned to %s with an estimate of %s/%sh' => 'Tildelt til %s med en estimering på %s/%sh', 'Not assigned, estimate of %sh' => 'Ikke tildelt, estimeret til %sh', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s opdateret en kommentar på opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s har kommenteret opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a>', + '%s updated a comment on the task %s' => '%s opdateret en kommentar på opgaven %s', + '%s commented the task %s' => '%s har kommenteret opgaven %s', '%s\'s activity' => '%s\'s aktvitet', 'No activity.' => 'Ingen aktivitet', 'RSS feed' => 'RSS feed', @@ -497,10 +497,10 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Standard kolonne for nye projekter (kommasepareret)', 'Task assignee change' => 'Opgaven ansvarlig ændring', '%s change the assignee of the task #%d to %s' => '%s skrift ansvarlig for opgaven #%d til %s', - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '%s skift ansvarlig for opgaven <a href="?controller=task&action=show&task_id=%d">#%d</a> til %s', - '[%s][Column Change] %s (#%d)' => '[%s][Kolonne Skift] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Position Skift] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Ansvarlig Skift] %s (#%d)', + '%s changed the assignee of the task %s to %s' => '%s skift ansvarlig for opgaven %s til %s', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', 'New password for the user "%s"' => 'Ny adgangskode for brugeren', 'Choose an event' => 'Vælg et event', 'Github commit received' => 'Github commit modtaget', @@ -551,4 +551,375 @@ return array( 'Confirmation' => 'Bekræftelse', // 'Allow everybody to access to this project' => '', // 'Everybody have access to this project.' => '', + // 'Webhooks' => '', + // 'API' => '', + // 'Integration' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', + // 'Configure' => '', + // 'Project management' => '', + // 'My projects' => '', + // 'Columns' => '', + // 'Task' => '', + // 'Your are not member of any project.' => '', + // 'Percentage' => '', + // 'Number of tasks' => '', + // 'Task distribution' => '', + // 'Reportings' => '', + // 'Task repartition for "%s"' => '', + // 'Analytics' => '', + // 'Subtask' => '', + // 'My subtasks' => '', + // 'User repartition' => '', + // 'User repartition for "%s"' => '', + // 'Clone this project' => '', + // 'Column removed successfully.' => '', + // 'Edit Project' => '', + // 'Github Issue' => '', + // 'Not enough data to show the graph.' => '', + // 'Previous' => '', + // 'The id must be an integer' => '', + // 'The project id must be an integer' => '', + // 'The status must be an integer' => '', + // 'The subtask id is required' => '', + // 'The subtask id must be an integer' => '', + // 'The task id is required' => '', + // 'The task id must be an integer' => '', + // 'The user id must be an integer' => '', + // 'This value is required' => '', + // 'This value must be numeric' => '', + // 'Unable to create this task.' => '', + // 'Cumulative flow diagram' => '', + // 'Cumulative flow diagram for "%s"' => '', + // 'Daily project summary' => '', + // 'Daily project summary export' => '', + // 'Daily project summary export for "%s"' => '', + // 'Exports' => '', + // 'This export contains the number of tasks per column grouped per day.' => '', + // 'Nothing to preview...' => '', + // 'Preview' => '', + // 'Write' => '', + // 'Active swimlanes' => '', + // 'Add a new swimlane' => '', + // 'Change default swimlane' => '', + // 'Default swimlane' => '', + // 'Do you really want to remove this swimlane: "%s"?' => '', + // 'Inactive swimlanes' => '', + // 'Set project manager' => '', + // 'Set project member' => '', + // 'Remove a swimlane' => '', + // 'Rename' => '', + // 'Show default swimlane' => '', + // 'Swimlane modification for the project "%s"' => '', + // 'Swimlane not found.' => '', + // 'Swimlane removed successfully.' => '', + // 'Swimlanes' => '', + // 'Swimlane updated successfully.' => '', + // 'The default swimlane have been updated successfully.' => '', + // 'Unable to create your swimlane.' => '', + // 'Unable to remove this swimlane.' => '', + // 'Unable to update this swimlane.' => '', + // 'Your swimlane have been created successfully.' => '', + // 'Example: "Bug, Feature Request, Improvement"' => '', + // 'Default categories for new projects (Comma-separated)' => '', + // 'Gitlab commit received' => '', + // 'Gitlab issue opened' => '', + // 'Gitlab issue closed' => '', + // 'Gitlab webhooks' => '', + // 'Help on Gitlab webhooks' => '', + // 'Integrations' => '', + // 'Integration with third-party services' => '', + // 'Role for this project' => '', + // 'Project manager' => '', + // 'Project member' => '', + // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', + // 'Gitlab Issue' => '', + // 'Subtask Id' => '', + // 'Subtasks' => '', + // 'Subtasks Export' => '', + // 'Subtasks exportation for "%s"' => '', + // 'Task Title' => '', + // 'Untitled' => '', + // 'Application default' => '', + // 'Language:' => '', + // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', + // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', + // 'Compact view' => '', + // 'Horizontal scrolling' => '', + // 'Compact/wide view' => '', + // 'No results match:' => '', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + // 'Hourly rates' => '', + // 'Hourly rate' => '', + // 'Currency' => '', + // 'Effective date' => '', + // 'Add new rate' => '', + // 'Rate removed successfully.' => '', + // 'Unable to remove this rate.' => '', + // 'Unable to save the hourly rate.' => '', + // 'Hourly rate created successfully.' => '', + // 'Start time' => '', + // 'End time' => '', + // 'Comment' => '', + // 'All day' => '', + // 'Day' => '', + // 'Manage timetable' => '', + // 'Overtime timetable' => '', + // 'Time off timetable' => '', + // 'Timetable' => '', + // 'Work timetable' => '', + // 'Week timetable' => '', + // 'Day timetable' => '', + // 'From' => '', + // 'To' => '', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + // 'Remove time slot' => '', + // 'Add new time slot' => '', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + // 'Files' => '', + // 'Images' => '', + // 'Private project' => '', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + // 'Budget' => '', + // 'Budget line' => '', + // 'Budget line removed successfully.' => '', + // 'Budget lines' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + // 'Cost' => '', + // 'Cost breakdown' => '', + // 'Custom Stylesheet' => '', + // 'download' => '', + // 'Do you really want to remove this budget line?' => '', + // 'EUR - Euro' => '', + // 'Expenses' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'New budget line' => '', + // 'NZD - New Zealand Dollar' => '', + // 'Remove a budget line' => '', + // 'Remove budget line' => '', + // 'RSD - Serbian dinar' => '', + // 'The budget line have been created successfully.' => '', + // 'Unable to create the budget line.' => '', + // 'Unable to remove this budget line.' => '', + // 'USD - US Dollar' => '', + // 'Remaining' => '', + // 'Destination column' => '', + // 'Move the task to another column when assigned to a user' => '', + // 'Move the task to another column when assignee is cleared' => '', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', ); diff --git a/app/Locales/de_DE/translations.php b/app/Locale/de_DE/translations.php index f280a802..b56469de 100644 --- a/app/Locales/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1,6 +1,8 @@ <?php return array( + 'number.decimals_separator' => ',', + 'number.thousands_separator' => '.', 'None' => 'Keines', 'edit' => 'Bearbeiten', 'Edit' => 'Bearbeiten', @@ -35,8 +37,8 @@ return array( 'Users' => 'Benutzer', 'No user' => 'Kein Benutzer', 'Forbidden' => 'Verboten', - 'Access Forbidden' => 'Zugang verboten', - 'Only administrators can access to this page.' => 'Nur Administratoren haben Zugang zu dieser Seite.', + 'Access Forbidden' => 'Zugriff verboten', + 'Only administrators can access to this page.' => 'Nur Administratoren haben Zugriff zu dieser Seite.', 'Edit user' => 'Benutzer bearbeiten', 'Logout' => 'Abmelden', 'Bad username or password' => 'Falscher Benutzername oder Passwort', @@ -49,7 +51,7 @@ return array( 'No project' => 'Keine Projekte', 'Project' => 'Projekt', 'Status' => 'Status', - 'Tasks' => 'Aufgabe', + 'Tasks' => 'Aufgaben', 'Board' => 'Pinnwand', 'Actions' => 'Aktionen', 'Inactive' => 'Inaktiv', @@ -57,7 +59,7 @@ return array( 'Column %d' => 'Spalte %d', 'Add this column' => 'Diese Spalte hinzufügen', '%d tasks on the board' => '%d Aufgaben auf dieser Pinnwand', - '%d tasks in total' => '%d Aufgaben gesamt', + '%d tasks in total' => '%d Aufgaben insgesamt', 'Unable to update this board.' => 'Ändern dieser Pinnwand nicht möglich.', 'Edit board' => 'Pinnwand bearbeiten', 'Disable' => 'Deaktivieren', @@ -94,7 +96,7 @@ return array( 'User settings' => 'Benutzereinstellungen', 'My default project:' => 'Standardprojekt:', 'Close a task' => 'Aufgabe abschließen', - 'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich abgeschlossen werden: "%s"?', + 'Do you really want to close this task: "%s"?' => 'Soll diese Aufgabe wirklich geschlossen werden: "%s"?', 'Edit a task' => 'Aufgabe bearbeiten', 'Column' => 'Spalte', 'Color' => 'Farbe', @@ -108,8 +110,8 @@ return array( 'There is nobody assigned' => 'Die Aufgabe wurde niemandem zugewiesen', 'Column on the board:' => 'Spalte:', 'Status is open' => 'Status ist geöffnet', - 'Status is closed' => 'Status ist abgeschlossen', - 'Close this task' => 'Aufgabe abschließen', + 'Status is closed' => 'Status ist geschlossen', + 'Close this task' => 'Aufgabe schließen', 'Open this task' => 'Aufgabe wieder öffnen', 'There is no description.' => 'Keine Beschreibung vorhanden.', 'Add a new task' => 'Neue Aufgabe hinzufügen', @@ -147,9 +149,9 @@ return array( 'Project disabled successfully.' => 'Projekt erfolgreich deaktiviert.', 'Unable to disable this project.' => 'Deaktivieren des Projekts nicht möglich.', 'Unable to open this task.' => 'Wiedereröffnung der Aufgabe nicht möglich.', - 'Task opened successfully.' => 'Aufgabe erfolgreich wieder eröffnet.', + 'Task opened successfully.' => 'Aufgabe erfolgreich wieder geöffnet.', 'Unable to close this task.' => 'Abschließen der Aufgabe nicht möglich.', - 'Task closed successfully.' => 'Aufgabe erfolgreich abgeschlossen.', + 'Task closed successfully.' => 'Aufgabe erfolgreich geschlossen.', 'Unable to update your task.' => 'Aktualisieren der Aufgabe nicht möglich.', 'Task updated successfully.' => 'Aufgabe erfolgreich aktualisiert.', 'Unable to create your task.' => 'Erstellen der Aufgabe nicht möglich.', @@ -182,18 +184,19 @@ return array( 'Change assignee' => 'Zuständigkeit ändern', 'Change assignee for the task "%s"' => 'Zuständigkeit für diese Aufgabe ändern: "%s"', 'Timezone' => 'Zeitzone', - 'Sorry, I didn\'t found this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', + 'Sorry, I didn\'t find this information in my database!' => 'Diese Information wurde in der Datenbank nicht gefunden!', 'Page not found' => 'Seite nicht gefunden', - // 'Complexity' => '', + 'Complexity' => 'Komplexität', 'limit' => 'Limit', 'Task limit' => 'Maximale Anzahl von Aufgaben', + 'Task count' => 'Aufgabenanzahl', 'This value must be greater than %d' => 'Dieser Wert muss größer sein als %d', 'Edit project access list' => 'Zugriffsberechtigungen des Projektes bearbeiten', 'Edit users access' => 'Benutzerzugriff ändern', 'Allow this user' => 'Diesen Benutzer autorisieren', - 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugang zum Projekt:', - 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugang.', - 'revoke' => 'entfernen', + 'Only those users have access to this project:' => 'Nur diese Benutzer haben Zugriff zum Projekt:', + 'Don\'t forget that administrators have access to everything.' => 'Nicht vergessen: Administratoren haben überall Zugriff.', + 'Revoke' => 'Entfernen', 'List of authorized users' => 'Liste der autorisierten Benutzer', 'User' => 'Benutzer', 'Nobody have access to this project.' => 'Niemand hat Zugriff auf dieses Projekt.', @@ -201,9 +204,9 @@ return array( 'Comments' => 'Kommentare', 'Post comment' => 'Kommentieren', 'Write your text in Markdown' => 'Schreibe deinen Text in Markdown-Syntax', - 'Leave a comment' => 'Kommentar eingeben...', + 'Leave a comment' => 'Kommentar eingeben', 'Comment is required' => 'Ein Kommentar wird benötigt', - 'Leave a description' => 'Beschreibung eingeben...', + 'Leave a description' => 'Beschreibung eingeben', 'Comment added successfully.' => 'Kommentar erfolgreich hinzugefügt.', 'Unable to create your comment.' => 'Hinzufügen eines Kommentars nicht möglich.', 'The description is required' => 'Eine Beschreibung wird benötigt', @@ -212,8 +215,9 @@ return array( 'Invalid date' => 'Ungültiges Datum', 'Must be done before %B %e, %Y' => 'Muss vor dem %d.%m.%Y erledigt werden', '%B %e, %Y' => '%d.%m.%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Automatische Aktionen', - 'Your automatic action have been created successfully.' => 'Die Automatische Aktion wurde erfolgreich erstellt.', + 'Your automatic action have been created successfully.' => 'Die automatische Aktion wurde erfolgreich erstellt.', 'Unable to create your automatic action.' => 'Erstellen der automatischen Aktion nicht möglich.', 'Remove an action' => 'Aktion löschen', 'Unable to remove this action.' => 'Löschen der Aktion nicht möglich.', @@ -221,8 +225,8 @@ return array( 'Automatic actions for the project "%s"' => 'Automatische Aktionen für das Projekt "%s"', 'Defined actions' => 'Definierte Aktionen', 'Add an action' => 'Aktion hinzufügen', - 'Event name' => 'Ereignis', - 'Action name' => 'Aktion', + 'Event name' => 'Ereignisname', + 'Action name' => 'Aktionsname', 'Action parameters' => 'Aktionsparameter', 'Action' => 'Aktion', 'Event' => 'Ereignis', @@ -291,7 +295,7 @@ return array( 'Email address invalid' => 'Ungültige E-Mail-Adresse', 'Your Google Account is not linked anymore to your profile.' => 'Google Account nicht mehr mit dem Profil verbunden.', 'Unable to unlink your Google Account.' => 'Trennung der Verbindung zum Google Account nicht möglich.', - 'Google authentication failed' => 'Zugang mit Google fehl geschlagen', + 'Google authentication failed' => 'Zugriff mit Google fehlgeschlagen', 'Unable to link your Google Account.' => 'Verbindung mit diesem Google Account nicht möglich.', 'Your Google Account is linked to your profile successfully.' => 'Der Google Account wurde erfolgreich verbunden.', 'Email' => 'E-Mail', @@ -330,7 +334,7 @@ return array( 'Remove a file' => 'Datei löschen', 'Unable to remove this file.' => 'Löschen der Datei nicht möglich.', 'File removed successfully.' => 'Datei erfolgreich gelöscht.', - 'Attach a document' => 'Datei anhängen', + 'Attach a document' => 'Dokument anhängen', 'Do you really want to remove this file: "%s"?' => 'Soll diese Datei wirklich gelöscht werden: "%s"?', 'open' => 'öffnen', 'Attachments' => 'Anhänge', @@ -342,33 +346,33 @@ return array( 'Time tracking' => 'Zeiterfassung', 'Estimate:' => 'Geschätzt:', 'Spent:' => 'Aufgewendet:', - 'Do you really want to remove this sub-task?' => 'Soll diese Unteraufgabe wirklich gelöscht werden: "%s"?', + 'Do you really want to remove this sub-task?' => 'Soll diese Teilaufgabe wirklich gelöscht werden: "%s"?', 'Remaining:' => 'Verbleibend:', 'hours' => 'Stunden', 'spent' => 'aufgewendet', 'estimated' => 'geschätzt', - 'Sub-Tasks' => 'Unteraufgaben', - 'Add a sub-task' => 'Unteraufgabe anlegen', + 'Sub-Tasks' => 'Teilaufgaben', + 'Add a sub-task' => 'Teilaufgabe anlegen', 'Original estimate' => 'Geschätzter Aufwand', - 'Create another sub-task' => 'Weitere Unteraufgabe anlegen', + 'Create another sub-task' => 'Weitere Teilaufgabe anlegen', 'Time spent' => 'Aufgewendete Zeit', - 'Edit a sub-task' => 'Unteraufgabe bearbeiten', - 'Remove a sub-task' => 'Unteraufgabe löschen', + 'Edit a sub-task' => 'Teilaufgabe bearbeiten', + 'Remove a sub-task' => 'Teilaufgabe löschen', 'The time must be a numeric value' => 'Zeit nur als nummerische Angabe', 'Todo' => 'Nicht gestartet', 'In progress' => 'In Bearbeitung', - 'Sub-task removed successfully.' => 'Unteraufgabe erfolgreich gelöscht.', - 'Unable to remove this sub-task.' => 'Löschen der Unteraufgabe nicht möglich.', - 'Sub-task updated successfully.' => 'Unteraufgabe erfolgreich aktualisiert.', - 'Unable to update your sub-task.' => 'Aktualisieren der Unteraufgabe nicht möglich.', - 'Unable to create your sub-task.' => 'Erstellen der Unteraufgabe nicht möglich.', - 'Sub-task added successfully.' => 'Unteraufgabe erfolgreich angelegt.', + 'Sub-task removed successfully.' => 'Teilaufgabe erfolgreich gelöscht.', + 'Unable to remove this sub-task.' => 'Löschen der Teilaufgabe nicht möglich.', + 'Sub-task updated successfully.' => 'Teilaufgabe erfolgreich aktualisiert.', + 'Unable to update your sub-task.' => 'Aktualisieren der Teilaufgabe nicht möglich.', + 'Unable to create your sub-task.' => 'Erstellen der Teilaufgabe nicht möglich.', + 'Sub-task added successfully.' => 'Teilaufgabe erfolgreich angelegt.', 'Maximum size: ' => 'Maximalgröße: ', 'Unable to upload the file.' => 'Hochladen der Datei nicht möglich.', - 'Display another project' => 'Zu Projekt wechseln...', + 'Display another project' => 'Zu Projekt wechseln', 'Your GitHub account was successfully linked to your profile.' => 'GitHub Account erfolgreich mit dem Profil verbunden.', 'Unable to link your GitHub Account.' => 'Verbindung mit diesem GitHub Account nicht möglich.', - 'GitHub authentication failed' => 'Zugang mit GitHub fehl geschlagen', + 'GitHub authentication failed' => 'Zugriff mit GitHub fehlgeschlagen', 'Your GitHub account is no longer linked to your profile.' => 'GitHub Account nicht mehr mit dem Profil verbunden.', 'Unable to unlink your GitHub Account.' => 'Trennung der Verbindung zum GitHub Account nicht möglich.', 'Login with my GitHub Account' => 'Anmelden mit meinem GitHub Account', @@ -385,8 +389,6 @@ return array( 'Creator' => 'Erstellt von', 'Modification date' => 'Änderungsdatum', 'Completion date' => 'Abschlussdatum', - 'Webhook URL for task creation' => 'Webhook URL zur Aufgabenerstellung', - 'Webhook URL for task modification' => 'Webhook URL zur Aufgabenbearbeitung', 'Clone' => 'duplizieren', 'Clone Project' => 'Projekt duplizieren', 'Project cloned successfully.' => 'Projekt wurde dupliziert.', @@ -396,30 +398,28 @@ return array( 'Task position:' => 'Position der Aufgabe', 'The task #%d have been opened.' => 'Die Aufgabe #%d wurde geöffnet.', 'The task #%d have been closed.' => 'Die Aufgabe #%d wurde geschlossen.', - 'Sub-task updated' => 'Unteraufgabe aktualisiert', + 'Sub-task updated' => 'Teilaufgabe aktualisiert', 'Title:' => 'Titel', 'Status:' => 'Status', 'Assignee:' => 'Zuständigkeit:', 'Time tracking:' => 'Zeittracking', - 'New sub-task' => 'Neue Unteraufgabe', + 'New sub-task' => 'Neue Teilaufgabe', 'New attachment added "%s"' => 'Neuer Anhang "%s" wurde hinzugefügt.', 'Comment updated' => 'Kommentar wurde aktualisiert', 'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s', - // 'List of due tasks for the project "%s"' => '', - // '[%s][New attachment] %s (#%d)' => '', - // '[%s][New comment] %s (#%d)' => '', - // '[%s][Comment updated] %s (#%d)' => '', - // '[%s][New subtask] %s (#%d)' => '', - // '[%s][Subtask updated] %s (#%d)' => '', - // '[%s][New task] %s (#%d)' => '', - // '[%s][Task updated] %s (#%d)' => '', - // '[%s][Task closed] %s (#%d)' => '', - // '[%s][Task opened] %s (#%d)' => '', - // '[%s][Due tasks]' => '', - // '[Kanboard] Notification' => '', + 'List of due tasks for the project "%s"' => 'Liste der fälligen Aufgaben für das Projekt "%s"', + 'New attachment' => 'Neuer Anhang', + 'New comment' => 'Neuer Kommentar', + 'New subtask' => 'Neue Teilaufgabe', + 'Subtask updated' => 'Teilaufgabe aktualisiert', + 'Task updated' => 'Aufgabe aktualisiert', + 'Task closed' => 'Aufgabe geschlossen', + 'Task opened' => 'Aufgabe geöffnet', + '[%s][Due tasks]' => '[%s][Fällige Aufgaben]', + '[Kanboard] Notification' => '[Kanboard] Benachrichtigung', 'I want to receive notifications only for those projects:' => 'Ich möchte nur für diese Projekte Benachrichtigungen erhalten:', 'view the task on Kanboard' => 'diese Aufgabe auf dem Kanboard zeigen', - 'Public access' => 'Öffentlich', + 'Public access' => 'Öffentlicher Zugriff', 'Category management' => 'Kategorien verwalten', 'User management' => 'Benutzer verwalten', 'Active tasks' => 'Aktive Aufgaben', @@ -447,12 +447,12 @@ return array( 'Username:' => 'Benutzername', 'Name:' => 'Name', 'Email:' => 'E-Mail', - 'Default project:' => 'Standardprojekt', - 'Notifications:' => 'Benachrichtigungen', + 'Default project:' => 'Standardprojekt:', + 'Notifications:' => 'Benachrichtigungen:', 'Notifications' => 'Benachrichtigungen', 'Group:' => 'Gruppe', 'Regular user' => 'Standardbenutzer', - 'Account type:' => 'Accounttyp', + 'Account type:' => 'Accounttyp:', 'Edit profile' => 'Profil bearbeiten', 'Change password' => 'Passwort ändern', 'Password modification' => 'Passwortänderung', @@ -467,25 +467,25 @@ return array( 'Unable to change the password.' => 'Passwort konnte nicht geändert werden.', 'Change category for the task "%s"' => 'Kategorie der Aufgabe "%s" ändern', 'Change category' => 'Kategorie ändern', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> aktualisiert', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> geöffnet', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s hat die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> auf die Position #%d in der Spalte "%s" verschoben', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s hat die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> in die Spalte "%s" verschoben', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> angelegt', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> geschlossen', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat eine Unteraufgabe für die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> angelegt', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat eine Unteraufgabe der Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> verändert', + '%s updated the task %s' => '%s hat die Aufgabe %s aktualisiert', + '%s opened the task %s' => '%s hat die Aufgabe %s geöffnet', + '%s moved the task %s to the position #%d in the column "%s"' => '%s hat die Aufgabe %s auf die Position #%d in der Spalte "%s" verschoben', + '%s moved the task %s to the column "%s"' => '%s hat die Aufgabe %s in die Spalte "%s" verschoben', + '%s created the task %s' => '%s hat die Aufgabe %s angelegt', + '%s closed the task %s' => '%s hat die Aufgabe %s geschlossen', + '%s created a subtask for the task %s' => '%s hat eine Teilaufgabe für die Aufgabe %s angelegt', + '%s updated a subtask for the task %s' => '%s hat eine Teilaufgabe der Aufgabe %s verändert', 'Assigned to %s with an estimate of %s/%sh' => 'An %s zugewiesen mit einer Schätzung von %s/%s Stunden', 'Not assigned, estimate of %sh' => 'Nicht zugewiesen, Schätzung von %s Stunden', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat einen Kommentat der Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> aktualisiert', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s hat die Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> kommentiert', + '%s updated a comment on the task %s' => '%s hat einen Kommentat der Aufgabe %s aktualisiert', + '%s commented the task %s' => '%s hat die Aufgabe %s kommentiert', '%s\'s activity' => '%s\'s Aktivität', 'No activity.' => 'Keine Aktivität.', - // 'RSS feed' => '', + 'RSS feed' => 'RSS Feed', '%s updated a comment on the task #%d' => '%s hat einen Kommentar der Aufgabe #%d aktualisiert', '%s commented on the task #%d' => '%s hat die Aufgabe #%d kommentiert', - '%s updated a subtask for the task #%d' => '%s hat eine Unteraufgabe der Aufgabe #%d aktualisiert', - '%s created a subtask for the task #%d' => '%s hat eine Unteraufgabe der Aufgabe #%d angelegt', + '%s updated a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d aktualisiert', + '%s created a subtask for the task #%d' => '%s hat eine Teilaufgabe der Aufgabe #%d angelegt', '%s updated the task #%d' => '%s hat die Aufgabe #%d aktualisiert', '%s created the task #%d' => '%s hat die Aufgabe #%d angelegt', '%s closed the task #%d' => '%s hat die Aufgabe #%d geschlossen', @@ -497,10 +497,10 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Standardspalten für neue Projekte (komma-getrennt)', 'Task assignee change' => 'Zuständigkeit geändert', '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '%s hat die Zuständigkeit der Aufgabe <a href="?controller=task&action=show&task_id=%d">#%d</a> geändert um %s', - '[%s][Column Change] %s (#%d)' => '[%s][Spaltenänderung] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Positionsänderung] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Zuständigkeitsänderung] %s (#%d)', + '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s', + 'Column Change' => 'Spalte ändern', + 'Position Change' => 'Position ändern', + 'Assignee Change' => 'Zuordnung ändern', 'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"', 'Choose an event' => 'Aktion wählen', 'Github commit received' => 'Github commit empfangen', @@ -510,7 +510,7 @@ return array( 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert', 'Github issue label change' => 'Github Fehlerkennzeichnung verändert', 'Create a task from an external provider' => 'Eine Aufgabe durch einen externen Provider hinzufügen', - 'Change the assignee based on an external username' => '', + 'Change the assignee based on an external username' => 'Zuordnung ändern basierend auf externem Benutzernamen', 'Change the category based on an external label' => 'Kategorie basierend auf einer externen Kennzeichnung ändern', 'Reference' => 'Referenz', 'Reference: %s' => 'Referenz: %s', @@ -537,18 +537,389 @@ return array( 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO Format wird immer akzeptiert, z.B.: "%s" und "%s"', 'New private project' => 'Neues privates Projekt', 'This project is private' => 'Dieses Projekt ist privat', - 'Type here to create a new sub-task' => 'Hier tippen, um eine neue Unteraufgabe zu erstellen', + 'Type here to create a new sub-task' => 'Hier tippen, um eine neue Teilaufgabe zu erstellen', 'Add' => 'Hinzufügen', 'Estimated time: %s hours' => 'Geplante Zeit: %s Stunden', 'Time spent: %s hours' => 'Aufgewendete Zeit: %s Stunden', 'Started on %B %e, %Y' => 'Gestartet am %B %e %Y', 'Start date' => 'Startdatum', - 'Time estimated' => 'Geplante Zeit', - 'There is nothing assigned to you.' => 'Es ist nichts an Sie zugewiesen.', + 'Time estimated' => 'Geschätzte Zeit', + 'There is nothing assigned to you.' => 'Ihnen ist nichts zugewiesen.', 'My tasks' => 'Meine Aufgaben', 'Activity stream' => 'Letzte Aktivitäten', 'Dashboard' => 'Dashboard', 'Confirmation' => 'Wiederholung', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', + 'Allow everybody to access to this project' => 'Jedem Zugriff zu diesem Projekt gewähren', + 'Everybody have access to this project.' => 'Jeder hat Zugriff zu diesem Projekt', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Integration', + 'Github webhooks' => 'Github Webhook', + 'Help on Github webhooks' => 'Hilfe für Github Webhooks', + 'Create a comment from an external provider' => 'Kommentar eines externen Providers hinzufügen', + 'Github issue comment created' => 'Github Fehler Kommentar hinzugefügt', + 'Configure' => 'Einstellungen', + 'Project management' => 'Projektmanagement', + 'My projects' => 'Meine Projekte', + 'Columns' => 'Spalten', + 'Task' => 'Aufgabe', + 'Your are not member of any project.' => 'Sie sind nicht Mitglied eines Projekts.', + 'Percentage' => 'Prozentsatz', + 'Number of tasks' => 'Anzahl an Aufgaben', + 'Task distribution' => 'Aufgabenverteilung', + 'Reportings' => 'Berichte', + 'Task repartition for "%s"' => 'Aufgabenzuweisung für "%s"', + 'Analytics' => 'Analyse', + 'Subtask' => 'Teilaufgabe', + 'My subtasks' => 'Meine Teilaufgaben', + 'User repartition' => 'Benutzerverteilung', + 'User repartition for "%s"' => 'Benutzerverteilung für "%s"', + 'Clone this project' => 'Projekt kopieren', + 'Column removed successfully.' => 'Spalte erfolgreich entfernt.', + 'Edit Project' => 'Projekt bearbeiten', + 'Github Issue' => 'Github Issue', + 'Not enough data to show the graph.' => 'Nicht genügend Daten, um die Grafik zu zeigen.', + 'Previous' => 'Vorherige', + 'The id must be an integer' => 'Die Id muss eine ganze Zahl sein', + 'The project id must be an integer' => 'Der Projektid muss eine ganze Zahl sein', + 'The status must be an integer' => 'Der Status muss eine ganze Zahl sein', + 'The subtask id is required' => 'Die Teilaufgabenid ist benötigt', + 'The subtask id must be an integer' => 'Die Teilaufgabenid muss eine ganze Zahl sein', + 'The task id is required' => 'Die Aufgabenid ist benötigt', + 'The task id must be an integer' => 'Die Aufgabenid muss eine ganze Zahl sein', + 'The user id must be an integer' => 'Die Userid muss eine ganze Zahl sein', + 'This value is required' => 'Dieser Wert ist erforderlich', + 'This value must be numeric' => 'Dieser Wert muss numerisch sein', + 'Unable to create this task.' => 'Diese Aufgabe kann nicht erstellt werden', + 'Cumulative flow diagram' => 'Kumulatives Flussdiagramm', + 'Cumulative flow diagram for "%s"' => 'Kumulatives Flussdiagramm für "%s"', + 'Daily project summary' => 'Tägliche Projektzusammenfassung', + 'Daily project summary export' => 'Export der täglichen Projektzusammenfassung', + 'Daily project summary export for "%s"' => 'Export der täglichen Projektzusammenfassung für "%s"', + 'Exports' => 'Exporte', + 'This export contains the number of tasks per column grouped per day.' => 'Dieser Export enthält die Anzahl der Aufgaben pro Spalte nach Tagen gruppiert.', + 'Nothing to preview...' => 'Nichts in der Vorschau anzuzeigen ...', + 'Preview' => 'Vorschau', + 'Write' => 'Ändern', + 'Active swimlanes' => 'Aktive Swimlane', + 'Add a new swimlane' => 'Eine neue Swimlane hinzufügen', + 'Change default swimlane' => 'Standard Swimlane ändern', + 'Default swimlane' => 'Standard Swimlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', + 'Inactive swimlanes' => 'Inaktive Swimlane', + 'Set project manager' => 'zum Projektmanager machen', + 'Set project member' => 'zum Projektmitglied machen', + 'Remove a swimlane' => 'Swimlane entfernen', + 'Rename' => 'umbenennen', + 'Show default swimlane' => 'Standard Swimlane anzeigen', + 'Swimlane modification for the project "%s"' => 'Swimlane Änderung für das Projekt "% s"', + 'Swimlane not found.' => 'Swimlane nicht gefunden', + 'Swimlane removed successfully.' => 'Swimlane erfolgreich entfernt.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane erfolgreich geändert.', + 'The default swimlane have been updated successfully.' => 'Die standard Swimlane wurden erfolgreich aktualisiert. Die standard Swimlane wurden erfolgreich aktualisiert.', + 'Unable to create your swimlane.' => 'Es ist nicht möglich die Swimlane zu erstellen.', + 'Unable to remove this swimlane.' => 'Es ist nicht möglich die Swimlane zu entfernen.', + 'Unable to update this swimlane.' => 'Es ist nicht möglich die Swimöane zu ändern.', + 'Your swimlane have been created successfully.' => 'Die Swimlane wurde erfolgreich angelegt.', + 'Example: "Bug, Feature Request, Improvement"' => 'Beispiel: "Bug, Funktionswünsche, Verbesserung"', + 'Default categories for new projects (Comma-separated)' => 'Standard Kategorien für neue Projekte (Komma-getrennt)', + 'Gitlab commit received' => 'Gitlab commit erhalten', + 'Gitlab issue opened' => 'Gitlab Fehler eröffnet', + 'Gitlab issue closed' => 'Gitlab Fehler geschlossen', + 'Gitlab webhooks' => 'Gitlab Webhook', + 'Help on Gitlab webhooks' => 'Hilfe für Gitlab Webhooks', + 'Integrations' => 'Integration', + 'Integration with third-party services' => 'Integration von Fremdleistungen', + 'Role for this project' => 'Rolle für dieses Projekt', + 'Project manager' => 'Projektmanager', + 'Project member' => 'Projektmitglied', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Ein Projektmanager kann die Projekteinstellungen ändern und hat mehr Rechte als ein normaler Benutzer.', + 'Gitlab Issue' => 'Gitlab Fehler', + 'Subtask Id' => 'Teilaufgaben Id', + 'Subtasks' => 'Teilaufgaben', + 'Subtasks Export' => 'Teilaufgaben Export', + 'Subtasks exportation for "%s"' => 'Teilaufgaben Export für "%s"', + 'Task Title' => 'Aufgaben Titel', + 'Untitled' => 'unbetitelt', + 'Application default' => 'Anwendungsstandard', + 'Language:' => 'Sprache:', + 'Timezone:' => 'Zeitzone:', + 'All columns' => 'Alle Spalten', + 'Calendar for "%s"' => 'Kalender für "%s"', + 'Filter by column' => 'Spalte filtern', + 'Filter by status' => 'Status filtern', + 'Calendar' => 'Kalender', + 'Next' => 'Nächste', + // '#%d' => '', + 'Filter by color' => 'Farbe filtern', + 'Filter by swimlane' => 'Swimlane filtern', + 'All swimlanes' => 'Alle Swimlanes', + 'All colors' => 'Alle Farben', + 'All status' => 'Alle Status', + 'Add a comment logging moving the task between columns' => 'Kommentar hinzufügen wenn die Aufgabe verschoben wird', + 'Moved to column %s' => 'In Spalte %s verschoben', + 'Change description' => 'Beschreibung ändern', + 'User dashboard' => 'Benutzer Dashboard', + 'Allow only one subtask in progress at the same time for a user' => 'Erlaube nur eine Teilaufgabe pro Benutzer zu bearbeiten', + 'Edit column "%s"' => 'Spalte "%s" bearbeiten', + 'Enable time tracking for subtasks' => 'Aktiviere Zeiterfassung für Teilaufgaben', + 'Select the new status of the subtask: "%s"' => 'Wähle einen neuen Status für Teilaufgabe: "%s"', + 'Subtask timesheet' => 'Teilaufgaben Zeiterfassung', + 'There is nothing to show.' => 'Es ist nichts zum Anzeigen vorhanden.', + 'Time Tracking' => 'Zeiterfassung', + 'You already have one subtask in progress' => 'Bereits eine Teilaufgabe in bearbeitung', + 'Which parts of the project do you want to duplicate?' => 'Welcher Teil des Projekts soll kopiert werden?', + 'Change dashboard view' => 'Dashboardansicht ändern', + 'Show/hide activities' => 'Aktivitäten anzeigen/verbergen', + 'Show/hide projects' => 'Projekte anzeigen/verbergen', + 'Show/hide subtasks' => 'Teilaufgaben anzeigen/verbergen', + 'Show/hide tasks' => 'Aufgaben anzeigen/verbergen', + 'Disable login form' => 'Anmeldeformular deaktivieren', + 'Show/hide calendar' => 'Kalender anzeigen/verbergen', + 'User calendar' => 'Benutzer Kalender', + 'Bitbucket commit received' => 'Bitbucket commit erhalten', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket webhooks', + 'Start' => 'Start', + 'End' => 'Ende', + 'Task age in days' => 'Aufgabenalter in Tagen', + 'Days in this column' => 'Tage in dieser Spalte', + '%dd' => '%dT', + 'Add a link' => 'Verbindung hinzufügen', + 'Add a new link' => 'Neue Verbindung hinzufügen', + 'Do you really want to remove this link: "%s"?' => 'Die Verbindung "%s" wirklich löschen?', + 'Do you really want to remove this link with task #%d?' => 'Die Verbindung mit der Aufgabe #%d wirklich löschen?', + 'Field required' => 'Feld erforderlich', + 'Link added successfully.' => 'Verbindung erfolgreich hinzugefügt.', + 'Link updated successfully.' => 'Verbindung erfolgreich aktualisiert.', + 'Link removed successfully.' => 'Verbindung erfolgreich gelöscht.', + 'Link labels' => 'Verbindungsbeschriftung', + 'Link modification' => 'Verbindung ändern', + 'Links' => 'Verbindungen', + 'Link settings' => 'Verbindungseinstellungen', + 'Opposite label' => 'Gegenteil', + 'Remove a link' => 'Verbindung entfernen', + 'Task\'s links' => 'Aufgaben Verbindungen', + 'The labels must be different' => 'Die Beschriftung muss unterschiedlich sein', + 'There is no link.' => 'Es gibt keine Verbindung', + 'This label must be unique' => 'Die Beschriftung muss einzigartig sein', + 'Unable to create your link.' => 'Verbindung kann nicht erstellt werden.', + 'Unable to update your link.' => 'Verbindung kann nicht aktualisiert werden.', + 'Unable to remove this link.' => 'Verbindung kann nicht entfernt werden', + 'relates to' => 'gehört zu', + 'blocks' => 'blockiert', + 'is blocked by' => 'ist blockiert von', + 'duplicates' => 'doppelt', + 'is duplicated by' => 'ist gedoppelt von', + 'is a child of' => 'ist untergeordnet', + 'is a parent of' => 'ist übergeordnet', + 'targets milestone' => 'betrifft Meilenstein', + 'is a milestone of' => 'ist ein Meilenstein von', + 'fixes' => 'behebt', + 'is fixed by' => 'wird behoben von', + 'This task' => 'Diese Aufgabe', + '<1h' => '<1Std', + '%dh' => '%dStd', + // '%b %e' => '', + 'Expand tasks' => 'Aufgaben aufklappen', + 'Collapse tasks' => 'Aufgaben zusammenklappen', + 'Expand/collapse tasks' => 'Aufgaben auf/zuklappen', + 'Close dialog box' => 'Dialog schließen', + 'Submit a form' => 'Formular abschicken', + 'Board view' => 'Pinnwand Ansicht', + 'Keyboard shortcuts' => 'Tastaturkürzel', + 'Open board switcher' => 'Pinnwandauswahl öffnen', + 'Application' => 'Anwendung', + 'Filter recently updated' => 'Zuletzt geänderte anzeigen', + 'since %B %e, %Y at %k:%M %p' => 'seit %B %e, %Y um %k:%M %p', + 'More filters' => 'Mehr Filter', + 'Compact view' => 'Kompaktansicht', + 'Horizontal scrolling' => 'Horizontales Scrollen', + 'Compact/wide view' => 'Kompakt/Breite-Ansicht', + 'No results match:' => 'Keine Ergebnisse:', + 'Remove hourly rate' => 'Stundensatz entfernen', + 'Do you really want to remove this hourly rate?' => 'Diesen Stundensatz wirklich entfernen?', + 'Hourly rates' => 'Stundensätze', + 'Hourly rate' => 'Stundensatz', + 'Currency' => 'Währung', + 'Effective date' => 'Inkraftsetzung', + 'Add new rate' => 'Neue Rate hinzufügen', + 'Rate removed successfully.' => 'Rate erfolgreich entfernt', + 'Unable to remove this rate.' => 'Nicht in der Lage, diese Rate zu entfernen.', + 'Unable to save the hourly rate.' => 'Nicht in der Lage, diese Rate zu speichern', + 'Hourly rate created successfully.' => 'Stundensatz erfolgreich angelegt.', + 'Start time' => 'Startzeit', + 'End time' => 'Endzeit', + 'Comment' => 'Kommentar', + 'All day' => 'ganztägig', + 'Day' => 'Tag', + 'Manage timetable' => 'Zeitplan verwalten', + 'Overtime timetable' => 'Überstunden Zeitplan', + 'Time off timetable' => 'Freizeit Zeitplan', + 'Timetable' => 'Zeitplan', + 'Work timetable' => 'Arbeitszeitplan', + 'Week timetable' => 'Wochenzeitplan', + 'Day timetable' => 'Tageszeitplan', + 'From' => 'von', + 'To' => 'bis', + 'Time slot created successfully.' => 'Zeitfenster erfolgreich erstellt.', + 'Unable to save this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu speichern.', + 'Time slot removed successfully.' => 'Zeitfenster erfolgreich entfernt.', + 'Unable to remove this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu entfernen', + 'Do you really want to remove this time slot?' => 'Soll diese Zeitfenster wirklich gelöscht werden?', + 'Remove time slot' => 'Zeitfenster entfernen', + 'Add new time slot' => 'Neues Zeitfenster hinzufügen', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Dieses Zeitfenster wird verwendet, wenn die Checkbox "gantägig" für Freizeit und Überstunden angeklickt ist.', + 'Files' => 'Dateien', + 'Images' => 'Bilder', + 'Private project' => 'privates Projekt', + 'Amount' => 'Betrag', + // 'AUD - Australian Dollar' => '', + 'Budget' => 'Budget', + 'Budget line' => 'Budgetlinie', + 'Budget line removed successfully.' => 'Budgetlinie erfolgreich entfernt', + 'Budget lines' => 'Budgetlinien', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + 'Cost' => 'Kosten', + 'Cost breakdown' => 'Kostenaufschlüsselung', + 'Custom Stylesheet' => 'benutzerdefiniertes Stylesheet', + 'download' => 'Download', + 'Do you really want to remove this budget line?' => 'Soll diese Budgetlinie wirklich entfernt werden?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Kosten', + 'GBP - British Pound' => 'GBP - Britische Pfung', + 'INR - Indian Rupee' => 'INR - Indische Rupien', + 'JPY - Japanese Yen' => 'JPY - Japanischer Yen', + 'New budget line' => 'Neue Budgetlinie', + 'NZD - New Zealand Dollar' => 'NZD - Neuseeland-Dollar', + 'Remove a budget line' => 'Budgetlinie entfernen', + 'Remove budget line' => 'Budgetlinie entfernen', + 'RSD - Serbian dinar' => 'RSD - Serbische Dinar', + 'The budget line have been created successfully.' => 'Die Budgetlinie wurde erfolgreich angelegt.', + 'Unable to create the budget line.' => 'Budgetlinie konnte nicht erstellt werden.', + 'Unable to remove this budget line.' => 'Budgetlinie konnte nicht gelöscht werden.', + 'USD - US Dollar' => 'USD - US Dollar', + 'Remaining' => 'Verbleibend', + 'Destination column' => 'Zielspalte', + 'Move the task to another column when assigned to a user' => 'Aufgabe in eine andere Spalte verschieben, wenn ein User zugeordnet wurde.', + 'Move the task to another column when assignee is cleared' => 'Aufgabe in eine andere Spalte verschieben, wenn die Zuordnung gelöscht wurde.', + 'Source column' => 'Quellspalte', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => 'Übergänge', + 'Executer' => 'Ausführender', + 'Time spent in the column' => 'Zeit in Spalte verbracht', + 'Task transitions' => 'Aufgaben Übergänge', + 'Task transitions export' => 'Aufgaben Übergänge exportieren', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Diese Auswertung enthält alle Spaltenbewegungen für jede Aufgabe mit Datum, Benutzer und Zeit vor jedem Wechsel.', + 'Currency rates' => 'Währungskurse', + 'Rate' => 'Kurse', + 'Change reference currency' => 'Referenzwährung ändern', + 'Add a new currency rate' => 'Neuen Währungskurs hinzufügen', + 'Currency rates are used to calculate project budget.' => 'Währungskurse werden verwendet um das Projektbudget zu berechnen.', + 'Reference currency' => 'Referenzwährung', + 'The currency rate have been added successfully.' => 'Der Währungskurs wurde erfolgreich hinzugefügt.', + 'Unable to add this currency rate.' => 'Währungskurs konnte nicht hinzugefügt werden', + 'Send notifications to a Slack channel' => 'Benachrichtigung an einen Slack-Kanal senden', + 'Webhook URL' => 'Webhook URL', + 'Help on Slack integration' => 'Hilfe für Slack integration.', + '%s remove the assignee of the task %s' => '%s Zuordnung für die Aufgabe %s entfernen', + 'Send notifications to Hipchat' => 'Sende Benachrichtigung an Hipchat', + 'API URL' => 'API URL', + 'Room API ID or name' => 'Raum API ID oder Name', + 'Room notification token' => 'Raum Benachrichtigungstoken', + 'Help on Hipchat integration' => 'Hilfe bei Hipchat Integration', + 'Enable Gravatar images' => 'Aktiviere Gravatar Bilder', + 'Information' => 'Information', + 'Check two factor authentication code' => 'Prüfe Zwei-Faktor-Authentifizierungscode', + 'The two factor authentication code is not valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist ungültig.', + 'The two factor authentication code is valid.' => 'Der Zwei-Faktor-Authentifizierungscode ist gültig.', + 'Code' => 'Code', + 'Two factor authentication' => 'Zwei-Faktor-Authentifizierung', + 'Enable/disable two factor authentication' => 'Aktiviere/Deaktiviere Zwei-Faktor-Authentifizierung', + 'This QR code contains the key URI: ' => 'Dieser QR-Code beinhaltet die Schlüssel-URI', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Speichere den geheimen Schlüssel in deiner TOTP software (z.B. Google Authenticator oder FreeOTP).', + 'Check my code' => 'Überprüfe meinen Code', + 'Secret key: ' => 'Geheimer Schlüssel', + 'Test your device' => 'Teste dein Gerät', + 'Assign a color when the task is moved to a specific column' => 'Weise eine Farbe zu, wenn die Aufgabe zu einer bestimmten Spalte bewegt wird', + '%s via Kanboard' => '%s via Kanboard', + 'uploaded by: %s' => 'Hochgeladen von: %s', + 'uploaded on: %s' => 'Hochgeladen am: %s', + 'size: %s' => 'Größe: %s', + 'Burndown chart for "%s"' => 'Burndown-Chart für "%s"', + 'Burndown chart' => 'Burndown-Chart', + 'This chart show the task complexity over the time (Work Remaining).' => 'Dieses Diagramm zeigt die Aufgabenkomplexität über den Faktor Zeit (Verbleibende Arbeit).', + 'Screenshot taken %s' => 'Screenshot aufgenommen %s ', + 'Add a screenshot' => 'Füge einen Screenshot hinzu', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Nimm einen Screenshot auf und drücke STRG+V oder ⌘+V um ihn hier einzufügen.', + 'Screenshot uploaded successfully.' => 'Screenshot erfolgreich hochgeladen.', + 'SEK - Swedish Krona' => 'SEK - Schwedische Kronen', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Der Projektidentifikator ist ein optionaler alphanumerischer Code, der das Projekt identifiziert.', + 'Identifier' => 'Identifikator', + 'Postmark (incoming emails)' => 'Postmark (Eingehende E-Mails)', + 'Help on Postmark integration' => 'Hilfe bei Postmark-Integration', + 'Mailgun (incoming emails)' => 'Mailgun (Eingehende E-Mails)', + 'Help on Mailgun integration' => 'Hilfe bei Mailgun-Integration', + 'Sendgrid (incoming emails)' => 'Sendgrid (Eingehende E-Mails)', + 'Help on Sendgrid integration' => 'Hilfe bei Sendgrid-Integration', + 'Disable two factor authentication' => 'Deaktiviere Zwei-Faktor-Authentifizierung', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', ); diff --git a/app/Locales/es_ES/translations.php b/app/Locale/es_ES/translations.php index d24cdfcf..2c215390 100644 --- a/app/Locales/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -1,6 +1,8 @@ <?php return array( + 'number.decimals_separator' => ',', + 'number.thousands_separator' => '.', 'None' => 'Ninguno', 'edit' => 'modificar', 'Edit' => 'Modificar', @@ -83,7 +85,7 @@ return array( 'Settings' => 'Preferencias', 'Application settings' => 'Parámetros de la aplicación', 'Language' => 'Idioma', - 'Webhook token:' => 'Ficha de seguridad (token) para los webhooks :', + 'Webhook token:' => 'Ficha de seguridad (token) para los disparadores Web (webhooks):', 'API token:' => 'Ficha de seguridad (token) para API:', 'More information' => 'Más informaciones', 'Database size:' => 'Tamaño de la base de datos:', @@ -182,21 +184,22 @@ return array( 'Change assignee' => 'Cambiar la persona asignada', 'Change assignee for the task "%s"' => 'Cambiar la persona asignada por la tarea « %s »', 'Timezone' => 'Zona horaria', - 'Sorry, I didn\'t found this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', + 'Sorry, I didn\'t find this information in my database!' => 'Lo siento no he encontrado información en la base de datos!', 'Page not found' => 'Página no encontrada', 'Complexity' => 'Complejidad', 'limit' => 'límite', 'Task limit' => 'Número máximo de tareas', + 'Task count' => 'Contador de tareas', 'This value must be greater than %d' => 'Este valor no debe de ser más grande que %d', 'Edit project access list' => 'Editar los permisos del proyecto', 'Edit users access' => 'Editar los permisos de usuario', 'Allow this user' => 'Autorizar este usuario', 'Only those users have access to this project:' => 'Solo estos usuarios tienen acceso a este proyecto:', 'Don\'t forget that administrators have access to everything.' => 'No olvide que los administradores tienen acceso a todo.', - 'revoke' => 'revocar', + 'Revoke' => 'Revocar', 'List of authorized users' => 'Lista de los usuarios autorizados', 'User' => 'Usuario', - // 'Nobody have access to this project.' => '', + 'Nobody have access to this project.' => 'Nadie tiene acceso a este proyecto', 'You are not allowed to access to this project.' => 'No está autorizado a acceder a este proyecto.', 'Comments' => 'Comentarios', 'Post comment' => 'Commentar', @@ -212,6 +215,7 @@ return array( 'Invalid date' => 'Fecha no válida', 'Must be done before %B %e, %Y' => 'Debe de estar hecho antes del %d/%m/%Y', '%B %e, %Y' => '%d/%m/%Y', + // '%b %e, %Y' => '', 'Automatic actions' => 'Acciones automatizadas', 'Your automatic action have been created successfully.' => 'La acción automatizada ha sido creada correctamente.', 'Unable to create your automatic action.' => 'No se puede crear esta acción automatizada.', @@ -385,8 +389,6 @@ return array( 'Creator' => 'Creador', 'Modification date' => 'Fecha de modificación', 'Completion date' => 'Fecha de terminación', - 'Webhook URL for task creation' => 'Webhook para la creación de tareas', - 'Webhook URL for task modification' => 'Webhook para la modificación de tareas', 'Clone' => 'Clonar', 'Clone Project' => 'Clonar proyecto', 'Project cloned successfully.' => 'Proyecto clonado correctamente', @@ -406,15 +408,13 @@ return array( 'Comment updated' => 'Comentario actualizado', 'New comment posted by %s' => 'Nuevo comentario agregado por %s', 'List of due tasks for the project "%s"' => 'Lista de tareas para el proyecto "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][uevo adjunto] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Nuevo comentario] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Comentario actualizado] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Nueva subtarea] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Subtarea actualizada] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Nueva tarea] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Tarea actualizada] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Tarea cerrada] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Tarea abierta] %s (#%d)', + 'New attachment' => 'Nuevo adjunto', + 'New comment' => 'Nuevo comentario', + 'New subtask' => 'Nueva subtarea', + 'Subtask updated' => 'Subtarea actualizada', + 'Task updated' => 'Tarea actualizada', + 'Task closed' => 'Tarea cerrada', + 'Task opened' => 'Tarea abierta', '[%s][Due tasks]' => '[%s][Tareas vencidas]', '[Kanboard] Notification' => '[Kanboard] Notificación', 'I want to receive notifications only for those projects:' => 'Quiero recibir notificaciones sólo de estos proyectos:', @@ -449,9 +449,9 @@ return array( 'Email:' => 'Correo electrónico:', 'Default project:' => 'Proyecto por defecto:', 'Notifications:' => 'Notificaciones:', - // 'Notifications' => '', + 'Notifications' => 'Notificaciones', 'Group:' => 'Grupo:', - 'Regular user' => 'Usuario regular:', + 'Regular user' => 'Usuario regular', 'Account type:' => 'Tipo de Cuenta:', 'Edit profile' => 'Editar perfil', 'Change password' => 'Cambiar contraseña', @@ -467,18 +467,18 @@ return array( 'Unable to change the password.' => 'No pude cambiar la contraseña.', 'Change category for the task "%s"' => 'Cambiar la categoría de la tarea "%s"', 'Change category' => 'Cambiar categoría', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s actualizó la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s abrió la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s movió la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a> a la posición #%d de la columna "%s"', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s movió la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a> a la columna "%s"', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s creó la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s cerró la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s creó una subtarea para la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s actualizó una subtarea para la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', + '%s updated the task %s' => '%s actualizó la tarea %s', + '%s opened the task %s' => '%s abrió la tarea %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s movió la tarea %s a la posición #%d de la columna "%s"', + '%s moved the task %s to the column "%s"' => '%s movió la tarea %s a la columna "%s"', + '%s created the task %s' => '%s creó la tarea %s', + '%s closed the task %s' => '%s cerró la tarea %s', + '%s created a subtask for the task %s' => '%s creó una subtarea para la tarea %s', + '%s updated a subtask for the task %s' => '%s actualizó una subtarea para la tarea %s', 'Assigned to %s with an estimate of %s/%sh' => 'Asignada a %s con una estimación de %s/%sh', 'Not assigned, estimate of %sh' => 'No asignada, se estima en %sh', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s actualizó un comentario de la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s comentó la tarea <a href="?controller=task&action=show&task_id=%d">#%d</a>', + '%s updated a comment on the task %s' => '%s actualizó un comentario de la tarea %s', + '%s commented the task %s' => '%s comentó la tarea %s', '%s\'s activity' => 'Actividad de %s', 'No activity.' => 'Sin actividad', 'RSS feed' => 'Fichero RSS', @@ -496,59 +496,430 @@ return array( 'Default values are "%s"' => 'Los valores por defecto son "%s"', 'Default columns for new projects (Comma-separated)' => 'Columnas por defecto de los nuevos proyectos (Separadas mediante comas)', 'Task assignee change' => 'Cambiar persona asignada a la tarea', - // '%s change the assignee of the task #%d to %s' => '', - // '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '', - '[%s][Column Change] %s (#%d)' => '[%s][Cambia Columna] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Cambia Posición] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Cambia Persona Asignada] %s (#%d)', + '%s change the assignee of the task #%d to %s' => '%s cambió el asignado de la tarea #%d por %s', + '%s changed the assignee of the task %s to %s' => '%s cambió el asignado de la tarea %s por %s', + 'Column Change' => 'Cambio de Columna', + 'Position Change' => 'Cambio de Posición', + 'Assignee Change' => 'Cambio de Asignado', 'New password for the user "%s"' => 'Nueva contraseña para el usuario "%s"', - // 'Choose an event' => '', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', - // 'Reference' => '', - // 'Reference: %s' => '', - // 'Label' => '', - // 'Database' => '', - // 'About' => '', - // 'Database driver:' => '', - // 'Board settings' => '', - // 'URL and token' => '', - // 'Webhook settings' => '', - // 'URL for task creation:' => '', - // 'Reset token' => '', - // 'API endpoint:' => '', - // 'Refresh interval for private board' => '', - // 'Refresh interval for public board' => '', - // 'Task highlight period' => '', - // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', - // 'Frequency in second (60 seconds by default)' => '', - // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', - // 'Application URL' => '', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', - // 'Date format' => '', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', - // 'New private project' => '', - // 'This project is private' => '', - // 'Type here to create a new sub-task' => '', - // 'Add' => '', - // 'Estimated time: %s hours' => '', - // 'Time spent: %s hours' => '', - // 'Started on %B %e, %Y' => '', - // 'Start date' => '', - // 'Time estimated' => '', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', + 'Choose an event' => 'Escoga un evento', + 'Github commit received' => 'Envío a Github recibido', + 'Github issue opened' => 'Problema en Github abierto', + 'Github issue closed' => 'Problema en Github cerrado', + 'Github issue reopened' => 'Problema en Github reabierto', + 'Github issue assignee change' => 'Cambio en signación de problema en Github', + 'Github issue label change' => 'Cambio en etiqueta del problema', + 'Create a task from an external provider' => 'Crear una tarea a partir de un proveedor externo', + 'Change the assignee based on an external username' => 'Cambiar la asignación basado en un nombre de usuario externo', + 'Change the category based on an external label' => 'Cambiar la categoría basado en una etiqueta externa', + 'Reference' => 'Referencia', + 'Reference: %s' => 'Referencia: %s', + 'Label' => 'Etiqueta', + 'Database' => 'Base de Datos', + 'About' => 'Acerca de', + 'Database driver:' => 'Driver de la base de datos', + 'Board settings' => 'Configuraciones del Tablero', + 'URL and token' => 'URL y token', + 'Webhook settings' => 'Configuraciones del Disparador Web (Webhook)', + 'URL for task creation:' => 'URL para la creación de tareas', + 'Reset token' => 'Resetear token', + 'API endpoint:' => 'Punto final del API', + 'Refresh interval for private board' => 'Intervalo de refrescamiento del tablero privado', + 'Refresh interval for public board' => 'Intervalo de refrescamiento del tablero público', + 'Task highlight period' => 'Periodo del realce de la tarea', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (en segundos) para considerar que una tarea fué modificada recientemente (0 para deshabilitar, 2 días por defecto)', + 'Frequency in second (60 seconds by default)' => 'Frecuencia en segundos (60 segundos por defecto)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frecuencia en segundos (0 para deshabilitar esta característica, 10 segundos por defecto)', + 'Application URL' => 'URL de la aplicación', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Ejemplo: http://ejemplo.kanboard.net/ (usado por las notificaciones de correo)', + 'Token regenerated.' => 'Token regenerado', + 'Date format' => 'Formato de la fecha', + 'ISO format is always accepted, example: "%s" and "%s"' => 'El formato ISO siempre es aceptado, ejemplo: "%s" y "%s"', + 'New private project' => 'Nuevo proyecto privado', + 'This project is private' => 'Este proyecto es privado', + 'Type here to create a new sub-task' => 'Escriba aquí para crear una nueva sub-tarea', + 'Add' => 'Añadir', + 'Estimated time: %s hours' => 'Tiempo estimado: % horas', + 'Time spent: %s hours' => 'Tiempo invertido: %s horas', + 'Started on %B %e, %Y' => 'Iniciado en %B %e, %Y', + 'Start date' => 'Fecha de inicio', + 'Time estimated' => 'Tiempo estimado', + 'There is nothing assigned to you.' => 'Esto no le está asignado', + 'My tasks' => 'Mis tareas', + 'Activity stream' => 'Flujo de actividad', + 'Dashboard' => 'Tablero', + 'Confirmation' => 'Confirmación', + 'Allow everybody to access to this project' => 'Permitir a cualquier acceder a este proyecto', + 'Everybody have access to this project.' => 'Cualquier tiene acceso a este proyecto', + 'Webhooks' => 'Disparadores Web (Webhooks)', + 'API' => 'API', + 'Integration' => 'Integración', + 'Github webhooks' => 'Disparadores Web (Webhooks) de Github', + 'Help on Github webhooks' => 'Ayuda con los Disparadores Web (Webhook) de Github', + 'Create a comment from an external provider' => 'Crear un comentario a partir de un proveedor externo', + 'Github issue comment created' => 'Creado el comentario del problema en Github', + 'Configure' => 'Configurar', + 'Project management' => 'Administración del proyecto', + 'My projects' => 'Mis proyectos', + 'Columns' => 'Columnas', + 'Task' => 'Tarea', + 'Your are not member of any project.' => 'No es miembro de ningún proyecto', + 'Percentage' => 'Porcentaje', + 'Number of tasks' => 'Número de tareas', + 'Task distribution' => 'Distribución de tareas', + 'Reportings' => 'Reportes', + 'Task repartition for "%s"' => 'Repartición de tareas para "%s"', + 'Analytics' => 'Analítica', + 'Subtask' => 'Subtarea', + 'My subtasks' => 'Mis subtareas', + 'User repartition' => 'Repartición de usuarios', + 'User repartition for "%s"' => 'Repartición para "%s"', + 'Clone this project' => 'Clonar este proyecto', + 'Column removed successfully.' => 'Columna removida correctamente', + 'Edit Project' => 'Editar Proyecto', + 'Github Issue' => 'Problema Github', + 'Not enough data to show the graph.' => 'No hay suficiente información para mostrar el gráfico', + 'Previous' => 'Anterior', + 'The id must be an integer' => 'El id debe ser un entero', + 'The project id must be an integer' => 'El id del proyecto debe ser un entero', + 'The status must be an integer' => 'El estado debe ser un entero', + 'The subtask id is required' => 'El id de la subtarea es requerido', + 'The subtask id must be an integer' => 'El id de la subtarea debe ser un entero', + 'The task id is required' => 'El id de la tarea es requerido', + 'The task id must be an integer' => 'El id de la tarea debe ser un entero', + 'The user id must be an integer' => 'El id del usuario debe ser un entero', + 'This value is required' => 'El valor es requerido', + 'This value must be numeric' => 'Este valor debe ser numérico', + 'Unable to create this task.' => 'Imposible crear esta tarea', + 'Cumulative flow diagram' => 'Diagrama de flujo acumulativo', + 'Cumulative flow diagram for "%s"' => 'Diagrama de flujo acumulativo para "%s"', + 'Daily project summary' => 'Sumario diario del proyecto', + 'Daily project summary export' => 'Exportar sumario diario del proyecto', + 'Daily project summary export for "%s"' => 'Exportar sumario diario del proyecto para "%s"', + 'Exports' => 'Exportar', + 'This export contains the number of tasks per column grouped per day.' => 'Esta exportación contiene el número de tereas por columna agrupada por día', + 'Nothing to preview...' => 'Nada que previsualizar...', + 'Preview' => 'Previsualizar', + 'Write' => 'Escribir', + 'Active swimlanes' => 'Carriles activos', + 'Add a new swimlane' => 'Añadir nuevo carril', + 'Change default swimlane' => 'Cambiar el carril por defecto', + 'Default swimlane' => 'Carril por defecto', + 'Do you really want to remove this swimlane: "%s"?' => '¿Realmente quiere remover este carril: "%s"?', + 'Inactive swimlanes' => 'Carriles inactivos', + 'Set project manager' => 'Asignar administrador del proyecto', + 'Set project member' => 'Asignar miembro del proyecto', + 'Remove a swimlane' => 'Remover un carril', + 'Rename' => 'Renombrar', + 'Show default swimlane' => 'Mostrar carril por defecto', + // 'Swimlane modification for the project "%s"' => '', + 'Swimlane not found.' => 'Carril no encontrado', + 'Swimlane removed successfully.' => 'Carril removido correctamente', + 'Swimlanes' => 'Carriles', + 'Swimlane updated successfully.' => 'Carril actualizado correctamente', + 'The default swimlane have been updated successfully.' => 'El carril por defecto ha sido actualizado correctamente', + 'Unable to create your swimlane.' => 'Imposible crear su carril', + 'Unable to remove this swimlane.' => 'Imposible remover este carril', + 'Unable to update this swimlane.' => 'Imposible actualizar este carril', + 'Your swimlane have been created successfully.' => 'Su carril ha sido creado correctamente', + 'Example: "Bug, Feature Request, Improvement"' => 'Ejemplo: "Error, Solicitud de característica, Mejora', + 'Default categories for new projects (Comma-separated)' => 'Categorías por defecto para nuevos proyectos (separadas por comas)', + 'Gitlab commit received' => 'Recibido envío desde Gitlab', + 'Gitlab issue opened' => 'Abierto asunto de Gitlab', + 'Gitlab issue closed' => 'Cerrado asunto de Gitlab', + 'Gitlab webhooks' => 'Disparadores Web (Webhooks) de Gitlab', + 'Help on Gitlab webhooks' => 'Ayuda sobre Disparadores Web (Webhooks) de Gitlab', + 'Integrations' => 'Integraciones', + 'Integration with third-party services' => 'Integración con servicios de terceros', + 'Role for this project' => 'Papel de este proyecto', + 'Project manager' => 'Gestor de proyecto', + 'Project member' => 'Miembro de proyecto', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un gestor de proyecto puede cambiar sus valores y tener más privilegios que un usuario estándar.', + 'Gitlab Issue' => 'Asunto Gitlab', + 'Subtask Id' => 'Id de Subtarea', + 'Subtasks' => 'Subtareas', + 'Subtasks Export' => 'Exportación de Subtareas', + 'Subtasks exportation for "%s"' => 'Exportación de subtareas para "%s"', + 'Task Title' => 'Título de la tarea', + 'Untitled' => 'Sin título', + 'Application default' => 'Predefinido de la aplicación', + 'Language:' => 'Idioma', + 'Timezone:' => 'Zona horaria', + 'All columns' => 'Todas las columnas', + 'Calendar for "%s"' => 'Calendario para "%s"', + 'Filter by column' => 'Filtrar por columna', + 'Filter by status' => 'Filtrar por estado', + 'Calendar' => 'Calendario', + 'Next' => 'Siguiente', + // '#%d' => '', + 'Filter by color' => 'Filtrar por color', + 'Filter by swimlane' => 'Filtrar por carril', + 'All swimlanes' => 'Todos los carriles', + 'All colors' => 'Todos los colores', + 'All status' => 'Todos los estados', + 'Add a comment logging moving the task between columns' => 'Añadir un cometario de historial moviendo la tarea entre columnas', + 'Moved to column %s' => 'Movido a columna %s', + 'Change description' => 'Cambiar descripción', + 'User dashboard' => 'Tablero de usuario', + 'Allow only one subtask in progress at the same time for a user' => 'Permitir sólo una subtarea en progreso a la vez para cada usuario', + 'Edit column "%s"' => 'Editar columna %s', + 'Enable time tracking for subtasks' => 'Activar seguimiento temporal para subtareas', + 'Select the new status of the subtask: "%s"' => 'Seleccionar el nuevo estado de la subtarea: "%s"', + 'Subtask timesheet' => 'Hoja temporal de subtarea', + 'There is nothing to show.' => 'Nada que mostrar', + 'Time Tracking' => 'Seguimiento Temporal', + 'You already have one subtask in progress' => 'Ya dispones de una subtarea en progreso', + 'Which parts of the project do you want to duplicate?' => '¿Qué partes del proyecto deseas duplicar?', + 'Change dashboard view' => 'Cambiar vista de tablero', + 'Show/hide activities' => 'Mostrar/ocultar actividades', + 'Show/hide projects' => 'Mostrar/ocultar proyectos', + 'Show/hide subtasks' => 'Mostrar/Ocultar subtareas', + 'Show/hide tasks' => 'Mostrar/ocultar tareas', + 'Disable login form' => 'Desactivar formulario de ingreso', + 'Show/hide calendar' => 'Mostrar/ocultar calendario', + 'User calendar' => 'Calendario de usuario', + 'Bitbucket commit received' => 'Recibida envío desde Bitbucket', + 'Bitbucket webhooks' => 'Disparadores Web (webhooks) de Bitbucket', + 'Help on Bitbucket webhooks' => 'Ayuda sobre disparadores web (webhooks) de Bitbucket', + 'Start' => 'Inicio', + 'End' => 'Fin', + 'Task age in days' => 'Edad de la tarea en días', + 'Days in this column' => 'Días en esta columna', + // '%dd' => '', + 'Add a link' => 'Añadir enlace', + 'Add a new link' => 'Añadir nuevo enlace', + 'Do you really want to remove this link: "%s"?' => '¿Realmente quieres quitar este enlace: "%s"?', + 'Do you really want to remove this link with task #%d?' => '¿Realmente quieres quitar este enlace con esta tarea: #%d?', + 'Field required' => 'Es necesario el campo', + 'Link added successfully.' => 'Enlace añadido con éxito.', + 'Link updated successfully.' => 'Enlace actualizado con éxito', + 'Link removed successfully.' => 'Enlace quitado con éxito', + 'Link labels' => 'etiquetas de enlace', + 'Link modification' => 'Modificación de enlace', + 'Links' => 'Enlaces', + 'Link settings' => 'Preferencias de enlace', + 'Opposite label' => 'Etiqueta opuesta', + 'Remove a link' => 'Quitar un enlace', + 'Task\'s links' => 'Enlaces de tareas', + 'The labels must be different' => 'Las etiquetas han de ser diferentes', + 'There is no link.' => 'No hay enlace', + 'This label must be unique' => 'Esta etiqueta ha de ser única', + 'Unable to create your link.' => 'No puedo crea su enlace.', + 'Unable to update your link.' => 'No puedo actualizar su enlace.', + 'Unable to remove this link.' => 'No puedo quitar este enlace.', + 'relates to' => 'se refiere a', + 'blocks' => 'bloques', + 'is blocked by' => 'bloqueado por', + 'duplicates' => 'duplica', + 'is duplicated by' => 'está duplicado por', + 'is a child of' => 'es un hijo de', + 'is a parent of' => 'es un padre de', + 'targets milestone' => 'hito de objetivos', + 'is a milestone of' => 'es un hito de', + 'fixes' => 'arregla', + 'is fixed by' => 'arreglado por', + 'This task' => 'Esta tarea', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + 'Expand tasks' => 'Espande tareas', + 'Collapse tasks' => 'Colapsa tareas', + 'Expand/collapse tasks' => 'Expande/colapasa tareas', + 'Close dialog box' => 'Cerrar caja de diálogo', + 'Submit a form' => 'Enviar formulario', + 'Board view' => 'Vista de tablero', + 'Keyboard shortcuts' => 'Atajos de teclado', + 'Open board switcher' => 'Abrir conmutador de tablero', + 'Application' => 'Aplicación', + 'Filter recently updated' => 'Filtro actualizado recientemente', + 'since %B %e, %Y at %k:%M %p' => 'desde %B %e, %Y a las %k:%M %p', + 'More filters' => 'Más filtros', + 'Compact view' => 'Compactar vista', + 'Horizontal scrolling' => 'Desplazamiento horizontal', + 'Compact/wide view' => 'Vista compacta/amplia', + 'No results match:' => 'No hay resultados coincidentes:', + 'Remove hourly rate' => 'Quitar cobro horario', + 'Do you really want to remove this hourly rate?' => '¿Realmente quires quitar el cobro horario?', + 'Hourly rates' => 'Cobros horarios', + 'Hourly rate' => 'Cobro horario', + 'Currency' => 'Moneda', + 'Effective date' => 'Fecha efectiva', + 'Add new rate' => 'Añadir nuevo cobro', + 'Rate removed successfully.' => 'Cobro quitado con éxito.', + 'Unable to remove this rate.' => 'No pude quitar este cobro.', + 'Unable to save the hourly rate.' => 'No pude grabar el cobro horario.', + 'Hourly rate created successfully.' => 'Cobro horario creado con éxito', + 'Start time' => 'Tiempo de inicio', + 'End time' => 'Tiempo de fin', + 'Comment' => 'Comentario', + 'All day' => 'Todos los días', + 'Day' => 'Día', + 'Manage timetable' => 'Gestionar horario', + 'Overtime timetable' => 'Horario de tiempo extra', + 'Time off timetable' => 'Horario de tiempo libre', + 'Timetable' => 'Horario', + 'Work timetable' => 'Horario de trabajo', + 'Week timetable' => 'Horario semanal', + 'Day timetable' => 'Horario de día', + 'From' => 'Desde', + 'To' => 'Hasta', + 'Time slot created successfully.' => 'Tiempo asignado creado con éxito.', + 'Unable to save this time slot.' => 'No pude grabar este tiempo asignado.', + 'Time slot removed successfully.' => 'Tiempo asignado quitado con éxito.', + 'Unable to remove this time slot.' => 'No pude quitar este tiempo asignado.', + 'Do you really want to remove this time slot?' => '¿Realmente quieres quitar este tiempo asignado?', + 'Remove time slot' => 'Quitar tiempo asignado', + 'Add new time slot' => 'Añadir nuevo tiempo asignado', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Este horario se usa cuando se marca la casilla "todos los días" para calendario de tiempo libre y horas extras.', + 'Files' => 'Ficheros', + 'Images' => 'Imágenes', + 'Private project' => 'Proyecto privado', + 'Amount' => 'Cantidad', + 'AUD - Australian Dollar' => 'AUD - Dólar australiano', + 'Budget' => 'Presupuesto', + 'Budget line' => 'Línea de presupuesto', + 'Budget line removed successfully.' => 'Línea de presupuesto quitada con éxito', + 'Budget lines' => 'Líneas de presupuesto', + 'CAD - Canadian Dollar' => 'CAD - Dólar canadiense', + 'CHF - Swiss Francs' => 'CHF - Francos suizos', + 'Cost' => 'Costo', + 'Cost breakdown' => 'Desglose de costes', + 'Custom Stylesheet' => 'Hoja de Estilo habitual', + 'download' => 'descargar', + 'Do you really want to remove this budget line?' => '¿Realmente quieres quitar esta línea de presupuesto?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Gastos', + 'GBP - British Pound' => 'GBP - Libra británica', + 'INR - Indian Rupee' => 'INR - Rupias indúes', + 'JPY - Japanese Yen' => 'JPY - Yen japonés', + 'New budget line' => 'Nueva línea de presupuesto', + 'NZD - New Zealand Dollar' => 'NZD - Dóloar neocelandés', + 'Remove a budget line' => 'Quitar una línea de presupuesto', + 'Remove budget line' => 'Quitar línea de presupuesto', + 'RSD - Serbian dinar' => 'RSD - Dinar serbio', + 'The budget line have been created successfully.' => 'Se ha creado la línea de presupuesto con éxito.', + 'Unable to create the budget line.' => 'No pude crear la línea de presupuesto.', + 'Unable to remove this budget line.' => 'No pude quitar esta línea de presupuesto.', + 'USD - US Dollar' => 'USD - Dólar americano', + 'Remaining' => 'Restante', + 'Destination column' => 'Columna destino', + 'Move the task to another column when assigned to a user' => 'Mover la tarea a otra columna al asignarse al usuario', + 'Move the task to another column when assignee is cleared' => 'Mover la tarea a otra columna al quitar el asignado', + 'Source column' => 'Columna fuente', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => 'Transiciones', + 'Executer' => 'Ejecutor', + 'Time spent in the column' => 'Tiempo transcurrido en la columna', + 'Task transitions' => 'Transiciones de tarea', + 'Task transitions export' => 'Eportar transiciones de tarea', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Este informe contiene todos los movimientos de columna para cada tarea con la fecha, el usuario y el tiempo transcurrido en cada trasición.', + 'Currency rates' => 'Cambio de monedas', + 'Rate' => 'Cambio', + 'Change reference currency' => 'Cambiar moneda de referencia', + 'Add a new currency rate' => 'Añadir nuevo cambio de moneda', + 'Currency rates are used to calculate project budget.' => 'Se usan los cambios de moneda para calcular el presupuesto del proyecto.', + 'Reference currency' => 'Moneda de referencia', + 'The currency rate have been added successfully.' => 'Se ha añadido el cambio de moneda con éxito', + 'Unable to add this currency rate.' => 'No pude añadir este cambio de moneda.', + 'Send notifications to a Slack channel' => 'Enviar notificaciones a un canal Desatendido', + 'Webhook URL' => 'URL de Disparador Web (webhook)', + 'Help on Slack integration' => 'Ayuda sobre integración Desatendida', + '%s remove the assignee of the task %s' => '%s quita el asignado de la tarea %s', + 'Send notifications to Hipchat' => 'Enviar notificaciones a Hipchat', + 'API URL' => 'URL de API', + 'Room API ID or name' => 'ID de API de habitación o nombre', + 'Room notification token' => 'Notificación de ficha de Habitación', + 'Help on Hipchat integration' => 'Ayuda sobre integración de Hipchat', + 'Enable Gravatar images' => 'Activar imágenes Gravatar', + 'Information' => 'Información', + 'Check two factor authentication code' => 'Revisar código de autenticación de dos factores', + 'The two factor authentication code is not valid.' => 'El código de autenticación de dos factores no es válido', + 'The two factor authentication code is valid.' => 'El código de autenticación de dos factores es válido', + 'Code' => 'Código', + 'Two factor authentication' => 'Autenticación de dos factores', + 'Enable/disable two factor authentication' => 'Activar/desactivar autenticación de dos factores', + 'This QR code contains the key URI: ' => 'Este código QR contiene la clave URI: ', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Guarda la clave secreta en tu software TOTP (por ejemplo Autenticación Google o FreeOTP).', + 'Check my code' => 'Revisar mi código', + 'Secret key: ' => 'Clave secreta: ', + 'Test your device' => 'Probar tu dispositivo', + 'Assign a color when the task is moved to a specific column' => 'Asignar un color al mover la tarea a una columna específica', + '%s via Kanboard' => '%s vía Kanboard', + 'uploaded by: %s' => 'cargado por: %s', + 'uploaded on: %s' => 'cargado en: %s', + 'size: %s' => 'tamaño: %s', + 'Burndown chart for "%s"' => 'Trabajo pendiente para "%s"', + 'Burndown chart' => 'Trabajo pendiente', + 'This chart show the task complexity over the time (Work Remaining).' => 'Este diagrama mestra la complejidad de las tareas a lo largo del tiempo (Trabajo restante)', + 'Screenshot taken %s' => 'Pantallazo tomado el %s', + 'Add a screenshot' => 'Añadir un pantallazo', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Capture un patallazo y pulse CTRL+V o ⌘+V para pegar aquí.', + 'Screenshot uploaded successfully.' => 'Pantallazo cargado con éxito', + 'SEK - Swedish Krona' => 'SEK - Corona sueca', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'El identificador del proyecto us un código opcional alfanumérico que se usa para identificar tu proyecto.', + 'Identifier' => 'Identificador', + 'Postmark (incoming emails)' => 'Matasellos (emails entrantes)', + 'Help on Postmark integration' => 'Ayuda sobre la integración de Matasellos', + 'Mailgun (incoming emails)' => 'Mailgun (emails entrantes)', + 'Help on Mailgun integration' => 'Ayuda sobre la integración con Mailgun', + 'Sendgrid (incoming emails)' => 'Sendgrid (emails entrantes)', + 'Help on Sendgrid integration' => 'Ayuda sobre la integración con Sendgrid', + 'Disable two factor authentication' => 'Desactivar la autenticación de dos factores', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => '¿Realmentes quieres desactuvar la autenticación de dos factores para este usuario: "%s?"', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php new file mode 100644 index 00000000..2d1edbc7 --- /dev/null +++ b/app/Locale/fi_FI/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => 'Ei mikään', + 'edit' => 'muokkaa', + 'Edit' => 'Muokkaa', + 'remove' => 'poista', + 'Remove' => 'Poista', + 'Update' => 'Päivitä', + 'Yes' => 'Kyllä', + 'No' => 'Ei', + 'cancel' => 'peruuta', + 'or' => 'tai', + 'Yellow' => 'Keltainen', + 'Blue' => 'Sininen', + 'Green' => 'Vihreä', + 'Purple' => 'Violetti', + 'Red' => 'Punainen', + 'Orange' => 'Oranssi', + 'Grey' => 'Harmaa', + 'Save' => 'Tallenna', + 'Login' => 'Sisäänkirjautuminen', + 'Official website:' => 'Virallinen verkkosivu:', + 'Unassigned' => 'Ei suorittajaa', + 'View this task' => 'Näytä tämä tehtävä', + 'Remove user' => 'Poista käyttäjä', + 'Do you really want to remove this user: "%s"?' => 'Oletko varma että haluat poistaa käyttäjän "%s"?', + 'New user' => 'Uusi käyttäjä', + 'All users' => 'Kaikki käyttäjät', + 'Username' => 'Käyttäjänimi', + 'Password' => 'Salasana', + 'Default project' => 'Oletusprojekti', + 'Administrator' => 'Ylläpitäjä', + 'Sign in' => 'Kirjaudu sisään', + 'Users' => 'Käyttäjät', + 'No user' => 'Ei käyttäjää', + 'Forbidden' => 'Estetty', + 'Access Forbidden' => 'Pääsy estetty', + 'Only administrators can access to this page.' => 'Vain ylläpitäjillä on pääsy tälle sivulle.', + 'Edit user' => 'Muokkaa käyttäjää', + 'Logout' => 'Kirjaudu ulos', + 'Bad username or password' => 'Väärä käyttäjätunnus tai salasana', + 'users' => 'käyttäjät', + 'projects' => 'projektit', + 'Edit project' => 'Muokkaa projektia', + 'Name' => 'Nimi', + 'Activated' => 'Aktivoitu', + 'Projects' => 'Projektit', + 'No project' => 'Ei projektia', + 'Project' => 'Projekti', + 'Status' => 'Status', + 'Tasks' => 'Tehtävät', + 'Board' => 'Taulu', + 'Actions' => 'Toiminnot', + 'Inactive' => 'Ei aktiivinen', + 'Active' => 'Aktiivinen', + 'Column %d' => 'Sarake %d', + 'Add this column' => 'Lisää tämä sarake', + '%d tasks on the board' => '%d tehtävää taululla', + '%d tasks in total' => '%d tehtävää yhteensä', + 'Unable to update this board.' => 'Taulun muuttaminen ei onnistunut.', + 'Edit board' => 'Muuta taulua', + 'Disable' => 'Disabloi', + 'Enable' => 'Aktivoi', + 'New project' => 'Uusi projekti', + 'Do you really want to remove this project: "%s"?' => 'Haluatko varmasti poistaa projektin: "%s"?', + 'Remove project' => 'Poista projekti', + 'Boards' => 'Taulut', + 'Edit the board for "%s"' => 'Muokkaa taulua projektille "%s"', + 'All projects' => 'Kaikki projektit', + 'Change columns' => 'Muokkaa sarakkeita', + 'Add a new column' => 'Lisää uusi sarake', + 'Title' => 'Nimi', + 'Add Column' => 'Lisää sarake', + 'Project "%s"' => 'Projekti "%s"', + 'Nobody assigned' => 'Ei suorittajaa', + 'Assigned to %s' => 'Tekijä: %s', + 'Remove a column' => 'Poista sarake', + 'Remove a column from a board' => 'Poista sarake taulusta', + 'Unable to remove this column.' => 'Sarakkeen poistaminen ei onnistunut.', + 'Do you really want to remove this column: "%s"?' => 'Haluatko varmasti poistaa sarakkeen "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Tämä toiminto POISTAA KAIKKI TEHTÄVÄT tästä sarakkeesta!', + 'Settings' => 'Asetukset', + 'Application settings' => 'Ohjelman asetukset', + 'Language' => 'Kieli', + 'Webhook token:' => 'Webhooks avain:', + // 'API token:' => '', + 'More information' => 'Lisätietoja', + 'Database size:' => 'Tietokannan koko:', + 'Download the database' => 'Lataa tietokanta', + 'Optimize the database' => 'Optimoi tietokanta', + '(VACUUM command)' => '(VACUUM-komento)', + '(Gzip compressed Sqlite file)' => '(Gzip-pakattu Sqlite-tiedosto)', + 'User settings' => 'Käyttäjän asetukset', + 'My default project:' => 'Oletusprojektini: ', + 'Close a task' => 'Sulje tehtävä', + 'Do you really want to close this task: "%s"?' => 'Haluatko varmasti sulkea tehtävän: "%s"?', + 'Edit a task' => 'Muokkaa tehtävää', + 'Column' => 'Sarake', + 'Color' => 'Väri', + 'Assignee' => 'Suorittaja', + 'Create another task' => 'Luo toinen tehtävä', + 'New task' => 'Uusi tehtävä', + 'Open a task' => 'Avaa tehtävä', + 'Do you really want to open this task: "%s"?' => 'Haluatko varmasti avata tehtävän: "%s"?', + 'Back to the board' => 'Takaisin tauluun', + 'Created on %B %e, %Y at %k:%M %p' => 'Luotu %d.%m.%Y kello %H:%M', + 'There is nobody assigned' => 'Ei suorittajaa', + 'Column on the board:' => 'Sarake taululla: ', + 'Status is open' => 'Status on avoin', + 'Status is closed' => 'Status on suljettu', + 'Close this task' => 'Sulje tämä tehtävä', + 'Open this task' => 'Avaa tämä tehtävä', + 'There is no description.' => 'Ei kuvausta.', + 'Add a new task' => 'Lisää uusi tehtävä', + 'The username is required' => 'Käyttäjätunnut vaaditaan', + 'The maximum length is %d characters' => 'Maksimipituus on %d merkkiä', + 'The minimum length is %d characters' => 'Vähimmäispituus on %d merkkiä', + 'The password is required' => 'Salasana vaaditaan', + 'This value must be an integer' => 'Tämän arvon täytyy olla numero', + 'The username must be unique' => 'Käyttäjänimi täytyy olla uniikki', + 'The username must be alphanumeric' => 'Käyttäjänimen täytyy olla alfanumeerinen', + 'The user id is required' => 'Käyttäjän id on pakollinen', + // 'Passwords don\'t match' => '', + 'The confirmation is required' => 'Varmistus vaaditaan', + 'The column is required' => 'Sarake on pakollinen', + 'The project is required' => 'Projekti on pakollinen', + 'The color is required' => 'Väri on pakollinen', + 'The id is required' => 'ID vaaditaan', + 'The project id is required' => 'Projektin ID on pakollinen', + 'The project name is required' => 'Projektin nimi on pakollinen', + 'This project must be unique' => 'Projektin nimi täytyy olla uniikki', + 'The title is required' => 'Otsikko vaaditaan', + 'The language is required' => 'Kieli on pakollinen', + 'There is no active project, the first step is to create a new project.' => 'Aktiivista projektia ei ole, ensimmäinen vaihe on luoda uusi projekti.', + 'Settings saved successfully.' => 'Asetukset tallennettu onnistuneesti.', + 'Unable to save your settings.' => 'Asetusten tallentaminen epäonnistui.', + 'Database optimization done.' => 'Tietokannan optimointi suoritettu.', + 'Your project have been created successfully.' => 'Projekti luotiin onnistuneesti.', + 'Unable to create your project.' => 'Projektin luominen epäonnistui.', + 'Project updated successfully.' => 'Projekti päivitettiin onnistuneesti.', + 'Unable to update this project.' => 'Projektin muuttaminen epäonnistui.', + 'Unable to remove this project.' => 'Projektin poistaminen epäonnistui.', + 'Project removed successfully.' => 'Projekti poistettiin onnistuneesti.', + 'Project activated successfully.' => 'Projekti aktivoitiin onnistuneesti.', + 'Unable to activate this project.' => 'Projektin aktivoiminen epäonnistui.', + 'Project disabled successfully.' => 'Projektin disabloiminen onnistui.', + 'Unable to disable this project.' => 'Projektin disabloiminen epäonnistui.', + 'Unable to open this task.' => 'Tehtävän avaus epäonnistui.', + 'Task opened successfully.' => 'Tehtävä avattiin onnistuneesti.', + 'Unable to close this task.' => 'Tehtävän sulkeminen epäonnistui.', + 'Task closed successfully.' => 'Tehtävä suljettiin onnistuneesti.', + 'Unable to update your task.' => 'Tehtävän muokkaaminen epäonnistui.', + 'Task updated successfully.' => 'Tehtävä päivitettiin onnistuneesti.', + 'Unable to create your task.' => 'Tehtävän luominen epäonnistui.', + 'Task created successfully.' => 'Tehtävä luotiin onnistuneesti.', + 'User created successfully.' => 'Käyttäjä lisättiin onnistuneesti.', + 'Unable to create your user.' => 'Käyttäjän lisäys epäonnistui.', + 'User updated successfully.' => 'Käyttäjätietojen päivitys onnistui.', + 'Unable to update your user.' => 'Käyttäjätietojen päivitys epäonnistui.', + 'User removed successfully.' => 'Käyttäjä poistettiin onnistuneesti.', + 'Unable to remove this user.' => 'Käyttäjän poistaminen epäonnistui.', + 'Board updated successfully.' => 'Taulu päivitettiin onnistuneesti.', + 'Ready' => 'Valmis', + 'Backlog' => 'Tehtäväjono', + 'Work in progress' => 'Työnalla', + 'Done' => 'Tehty', + 'Application version:' => 'Ohjelman versio:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Valmistunut %d.%m.%Y kello %H:%M', + '%B %e, %Y at %k:%M %p' => '%d.%m.%Y kello %H:%M', + 'Date created' => 'Luomispäivä', + 'Date completed' => 'Valmistumispäivä', + 'Id' => 'Id', + 'No task' => 'Ei tehtävää', + 'Completed tasks' => 'Valmiit tehtävät', + 'List of projects' => 'Projektit', + 'Completed tasks for "%s"' => 'Suoritetut tehtävät projektille %s', + '%d closed tasks' => '%d suljettua tehtävää', + 'No task for this project' => 'Ei tehtävää tälle projektille', + 'Public link' => 'Julkinen linkki', + 'There is no column in your project!' => 'Projektilta puuttuu sarakkeet!', + 'Change assignee' => 'Vaihda suorittajaa', + 'Change assignee for the task "%s"' => 'Vaihda suorittajaa tehtävälle %s', + 'Timezone' => 'Aikavyöhyke', + 'Sorry, I didn\'t find this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani', + 'Page not found' => 'Sivua ei löydy', + 'Complexity' => 'Monimutkaisuus', + 'limit' => 'raja', + 'Task limit' => 'Tehtävien maksimimäärä', + 'Task count' => 'Tehtävien määrä', + 'This value must be greater than %d' => 'Arvon täytyy olla suurempi kuin %d', + 'Edit project access list' => 'Muuta projektin käyttäjiä', + 'Edit users access' => 'Muuta käyttäjien pääsyä', + 'Allow this user' => 'Salli tämä projekti', + 'Only those users have access to this project:' => 'Vain näillä käyttäjillä on pääsy projektiin:', + 'Don\'t forget that administrators have access to everything.' => 'Muista että ylläpitäjät pääsevät kaikkialle.', + 'Revoke' => 'Poista', + 'List of authorized users' => 'Sallittujen käyttäjien lista', + 'User' => 'Käyttäjät', + // 'Nobody have access to this project.' => '', + 'You are not allowed to access to this project.' => 'Sinulla ei ole pääsyä tähän projektiin.', + 'Comments' => 'Kommentit', + 'Post comment' => 'Lisää kommentti', + 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla', + 'Leave a comment' => 'Lisää kommentti', + 'Comment is required' => 'Kommentti vaaditaan', + 'Leave a description' => 'Lisää kuvaus', + 'Comment added successfully.' => 'Kommentti lisättiin onnistuneesti.', + 'Unable to create your comment.' => 'Kommentin lisäys epäonnistui.', + 'The description is required' => 'Kuvaus vaaditaan', + 'Edit this task' => 'Muokkaa tehtävää', + 'Due Date' => 'Deadline', + 'Invalid date' => 'Virheellinen päiväys', + 'Must be done before %B %e, %Y' => 'Täytyy suorittaa ennen %d.%m.%Y', + '%B %e, %Y' => '%d.%m.%Y', + // '%b %e, %Y' => '', + 'Automatic actions' => 'Automaattiset toiminnot', + 'Your automatic action have been created successfully.' => 'Toiminto suoritettiin onnistuneesti.', + 'Unable to create your automatic action.' => 'Automaattisen toiminnon luominen epäonnistui.', + 'Remove an action' => 'Poista toiminto', + 'Unable to remove this action.' => 'Toiminnon poistaminen epäonnistui.', + 'Action removed successfully.' => 'Toiminto poistettiin onnistuneesti.', + 'Automatic actions for the project "%s"' => 'Automaattiset toiminnot projektille "%s"', + 'Defined actions' => 'Määritellyt toiminnot', + // 'Add an action' => '', + 'Event name' => 'Tapahtuman nimi', + 'Action name' => 'Toiminnon nimi', + 'Action parameters' => 'Toiminnon parametrit', + 'Action' => 'Toiminto', + 'Event' => 'Tapahtuma', + 'When the selected event occurs execute the corresponding action.' => 'Kun valittu tapahtuma tapahtuu, suorita vastaava toiminto.', + 'Next step' => 'Seuraava vaihe', + 'Define action parameters' => 'Määrittele toiminnon parametrit', + 'Save this action' => 'Tallenna toiminto', + 'Do you really want to remove this action: "%s"?' => 'Oletko varma että haluat poistaa toiminnon "%s"?', + 'Remove an automatic action' => 'Poista automaattintn toiminto', + 'Close the task' => 'Sulje tehtävä', + 'Assign the task to a specific user' => 'Osoita tehtävä käyttäjälle', + 'Assign the task to the person who does the action' => 'Määritä suorittaja tehtävälle', + 'Duplicate the task to another project' => 'Monista tehtävä toiselle projektille', + 'Move a task to another column' => 'Siirrä tehtävä toiseen sarakkeeseen', + 'Move a task to another position in the same column' => 'Siirrä tehtävä eri järjestykseen samassa sarakkeessa', + 'Task modification' => 'Tehtävän muokkaus', + 'Task creation' => 'Tehtävän luominen', + 'Open a closed task' => 'Avaa jo suljettu tehtävä', + 'Closing a task' => 'Tehtävää suljetaan', + 'Assign a color to a specific user' => 'Valitse väri käyttäjälle', + 'Column title' => 'Sarakkeen nimi', + 'Position' => 'Positio', + 'Move Up' => 'Siirrä ylös', + 'Move Down' => 'Siirrä alas', + 'Duplicate to another project' => 'Kopioi toiseen projektiin', + 'Duplicate' => 'Monista', + 'link' => 'linkki', + 'Update this comment' => 'Muuta projektia', + 'Comment updated successfully.' => 'Kommentti päivitettiin onnistuneesti.', + 'Unable to update your comment.' => 'Kommentin päivitys epäonnistui.', + 'Remove a comment' => 'Poista kommentti', + 'Comment removed successfully.' => 'Kommentti poistettiin onnistuneesti.', + 'Unable to remove this comment.' => 'Kommentin poistaminen epäonnistui.', + 'Do you really want to remove this comment?' => 'Haluatko varmasti poistaa tämän kommentin?', + 'Only administrators or the creator of the comment can access to this page.' => 'Vain ylläpitäjillä tai kommentin jättäjällä on pääsy tälle sivulle.', + 'Details' => 'Tiedot', + 'Current password for the user "%s"' => 'Käyttäjän "%s" salasana', + 'The current password is required' => 'Salasana vaaditaan', + 'Wrong password' => 'Väärä salasana', + 'Reset all tokens' => 'Resetoi kaikki tokenit', + 'All tokens have been regenerated.' => 'Kaikki tokenit luotiin uudelleen.', + 'Unknown' => 'Tuntematon', + 'Last logins' => 'Viimeisimmät kirjautumiset', + 'Login date' => 'Kirjautumispäivä', + 'Authentication method' => 'Autentikointimenetelmä', + 'IP address' => 'IP-Osoite', + 'User agent' => 'Selain', + 'Persistent connections' => 'Voimassa olevat yhteydet', + 'No session.' => 'Ei sessioita.', + 'Expiration date' => 'Vanhentumispäivä', + 'Remember Me' => 'Muista minut', + 'Creation date' => 'Luomispäivä', + 'Filter by user' => 'Rajaa käyttäjän mukaan', + 'Filter by due date' => 'Rajaa deadlinen mukaan', + 'Everybody' => 'Kaikki', + 'Open' => 'Avoin', + 'Closed' => 'Suljettu', + 'Search' => 'Etsi', + 'Nothing found.' => 'Ei löytynyt.', + 'Search in the project "%s"' => 'Etsi projektista "%s"', + 'Due date' => 'Deadline', + 'Others formats accepted: %s and %s' => 'Muut hyväksytyt muodot: %s ja %s', + 'Description' => 'Kuvaus', + '%d comments' => '%d kommenttia', + '%d comment' => '%d kommentti', + 'Email address invalid' => 'Email ei kelpaa', + 'Your Google Account is not linked anymore to your profile.' => 'Google tunnustasi ei ole enää linkattu profiiliisi', + 'Unable to unlink your Google Account.' => 'Google tunnuksen linkkaamisen poistaminen epäonnistui.', + 'Google authentication failed' => 'Google autentikointi epäonnistui', + 'Unable to link your Google Account.' => 'Google tunnuksen linkkaaminen epäonnistui.', + 'Your Google Account is linked to your profile successfully.' => 'Google tunnuksesi linkitettiin profiiliisi onnistuneesti.', + 'Email' => 'Sähköposti', + 'Link my Google Account' => 'Linkitä Google-tili', + 'Unlink my Google Account' => 'Poista Google-tilin linkitys', + 'Login with my Google Account' => 'Kirjaudu Google tunnuksella', + 'Project not found.' => 'Projektia ei löytynyt.', + 'Task #%d' => 'Tehtävä #%d', + 'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.', + 'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.', + 'Remove a task' => 'Poista tehtävä', + 'Do you really want to remove this task: "%s"?' => 'Haluatko varmasti poistaa tehtävän: "%s"?', + 'Assign automatically a color based on a category' => 'Aseta väri automaattisesti kategorian mukaan', + 'Assign automatically a category based on a color' => 'Aseta kategoria automaattisesti värin mukaan', + 'Task creation or modification' => 'Tehtävän luonti tai muuttaminen', + 'Category' => 'Kategoria', + 'Category:' => 'Kategoria:', + 'Categories' => 'Kategoriat', + 'Category not found.' => 'Kategoriaa ei löytynyt.', + 'Your category have been created successfully.' => 'Kategoria luotiin onnistuneesti.', + 'Unable to create your category.' => 'Kategorian luonti epäonnistui.', + 'Your category have been updated successfully.' => 'Kategoriaa muokattiin onnistuneesti.', + 'Unable to update your category.' => 'Kategorian muokkaaminen epäonnistui.', + 'Remove a category' => 'Poista kategoria', + 'Category removed successfully.' => 'Kategoria poistettu onnistuneesti.', + 'Unable to remove this category.' => 'Kategorian poisto epäonnistui.', + 'Category modification for the project "%s"' => 'Kategorian muutos projektissa "%s"', + 'Category Name' => 'Kategorian nimi', + 'Categories for the project "%s"' => 'Kategoriat projektille "%s"', + 'Add a new category' => 'Lisää uusi kategoria', + 'Do you really want to remove this category: "%s"?' => 'Haluatko varmasti poistaa kategorian: "%s"?', + 'Filter by category' => 'Rajaa kategorian mukaan', + 'All categories' => 'Kaikki kategoriat', + 'No category' => 'Kategoriaa ei löydy', + 'The name is required' => 'Nimi vaaditaan', + 'Remove a file' => 'Poista tiedosto', + 'Unable to remove this file.' => 'Tiedoston poistaminen epäonnistui.', + 'File removed successfully.' => 'Tiedosto poistettiin onnistuneesti.', + 'Attach a document' => 'Liitä dokumentti', + 'Do you really want to remove this file: "%s"?' => 'Haluatko varmasti poistaa tiedoston: "%s"?', + 'open' => 'avaa', + 'Attachments' => 'Liitteet', + 'Edit the task' => 'Muokkaa tehtävää', + 'Edit the description' => 'Muokkaa kuvausta', + 'Add a comment' => 'Lisää kommentti', + 'Edit a comment' => 'Muokkaa kommenttia', + 'Summary' => 'Yhteenveto', + 'Time tracking' => 'Ajan seuranta', + 'Estimate:' => 'Arvio:', + 'Spent:' => 'Käytetty:', + 'Do you really want to remove this sub-task?' => 'Haluatko varmasti poistaa tämän alitehtävän?', + 'Remaining:' => 'Jäljellä', + 'hours' => 'tuntia', + 'spent' => 'käytetty', + 'estimated' => 'estimoitu', + 'Sub-Tasks' => 'Alitehtävät', + 'Add a sub-task' => 'Lisää alitehtävä', + 'Original estimate' => 'Alkuperäinen estimaatti', + 'Create another sub-task' => 'Lisää toinen alitehtävä', + 'Time spent' => 'Käytetty aika', + 'Edit a sub-task' => 'Muokkaa alitehtävää', + 'Remove a sub-task' => 'Poista alitehtävä', + 'The time must be a numeric value' => 'Ajan pitää olla numero', + 'Todo' => 'Todo', + 'In progress' => 'Työnalla', + 'Sub-task removed successfully.' => 'Alitehtävä poistettu onnistuneesti.', + 'Unable to remove this sub-task.' => 'Alitehtävän poistaminen epäonnistui.', + 'Sub-task updated successfully.' => 'Alitehtävä päivitettiin onnistuneesti.', + 'Unable to update your sub-task.' => 'Alitehtävän päivitys epäonnistui.', + 'Unable to create your sub-task.' => 'Alitehtävän luonti epäonnistui.', + 'Sub-task added successfully.' => 'Alitehtävä luotiin onnistuneesti.', + 'Maximum size: ' => 'Maksimikoko: ', + 'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.', + 'Display another project' => 'Näytä toinen projekti', + 'Your GitHub account was successfully linked to your profile.' => 'Github-tilisi on onnistuneesti liitetty profiiliisi', + 'Unable to link your GitHub Account.' => 'Github-tilin liittäminen epäonnistui', + 'GitHub authentication failed' => 'Github-todennus epäonnistui', + 'Your GitHub account is no longer linked to your profile.' => 'Github-tiliäsi ei ole enää liitetty profiiliisi.', + 'Unable to unlink your GitHub Account.' => 'Github-tilisi liitoksen poisto epäonnistui', + 'Login with my GitHub Account' => 'Kirjaudu sisään Github-tililläni', + 'Link my GitHub Account' => 'Liitä Github-tilini', + 'Unlink my GitHub Account' => 'Poista liitos Github-tiliini', + 'Created by %s' => 'Luonut: %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Viimeksi muokattu %B %e, %Y kello %H:%M', + 'Tasks Export' => 'Tehtävien vienti', + 'Tasks exportation for "%s"' => 'Tehtävien vienti projektilta "%s"', + 'Start Date' => 'Aloituspäivä', + 'End Date' => 'Lopetuspäivä', + 'Execute' => 'Suorita', + 'Task Id' => 'Tehtävän ID', + 'Creator' => 'Luonut', + 'Modification date' => 'Muokkauspäivä', + 'Completion date' => 'Valmistumispäivä', + 'Clone' => 'Kahdenna', + 'Clone Project' => 'Kahdenna projekti', + 'Project cloned successfully.' => 'Projekti kahdennettu onnistuneesti', + 'Unable to clone this project.' => 'Projektin kahdennus epäonnistui', + 'Email notifications' => 'Sähköposti-ilmoitukset', + 'Enable email notifications' => 'Ota käyttöön sähköposti-ilmoitukset', + 'Task position:' => 'Tehtävän sijainti', + 'The task #%d have been opened.' => 'Tehtävä #%d on avattu', + 'The task #%d have been closed.' => 'Tehtävä #%d on suljettu', + 'Sub-task updated' => 'Alitehtävä päivitetty', + 'Title:' => 'Otsikko:', + 'Status:' => 'Tila:', + 'Assignee:' => 'Vastaanottaja:', + 'Time tracking:' => 'Ajan seuranta:', + 'New sub-task' => 'Uusi alitehtävä', + 'New attachment added "%s"' => 'Uusi liite lisätty "%s"', + 'Comment updated' => 'Kommentti päivitetty', + 'New comment posted by %s' => '%s lisäsi uuden kommentin', + // 'List of due tasks for the project "%s"' => '', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', + // '[%s][Due tasks]' => '', + // '[Kanboard] Notification' => '', + 'I want to receive notifications only for those projects:' => 'Haluan vastaanottaa ilmoituksia ainoastaan näistä projekteista:', + 'view the task on Kanboard' => 'katso tehtävää Kanboardissa', + 'Public access' => 'Julkinen käyttöoikeus', + 'Category management' => 'Kategorioiden hallinta', + 'User management' => 'Käyttäjähallinta', + 'Active tasks' => 'Aktiiviset tehtävät', + 'Disable public access' => 'Poista käytöstä julkinen käyttöoikeus', + 'Enable public access' => 'Ota käyttöön ', + 'Active projects' => 'Aktiiviset projektit', + 'Inactive projects' => 'Passiiviset projektit', + 'Public access disabled' => 'Julkinen käyttöoikeus ei ole käytössä', + 'Do you really want to disable this project: "%s"?' => 'Haluatko varmasti tehdä projektista "%s" passiivisen?', + 'Do you really want to duplicate this project: "%s"?' => 'Haluatko varmasti kahdentaa projektin "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Haluatko varmasti aktivoida projektinen "%s"', + 'Project activation' => 'Projektin aktivointi', + 'Move the task to another project' => 'Siirrä tehtävä toiseen projektiin', + 'Move to another project' => 'Siirrä toiseen projektiin', + 'Do you really want to duplicate this task?' => 'Haluatko varmasti kahdentaa tämän tehtävän?', + 'Duplicate a task' => 'Kahdenna tehtävä', + 'External accounts' => 'Muut tilit', + 'Account type' => 'Tilin tyyppi', + 'Local' => 'Paikallinen', + 'Remote' => 'Etä', + 'Enabled' => 'Käytössä', + 'Disabled' => 'Pois käytöstä', + 'Google account linked' => 'Google-tili liitetty', + 'Github account linked' => 'Github-tili liitetty', + 'Username:' => 'Käyttäjänimi:', + 'Name:' => 'Nimi:', + 'Email:' => 'Sähköpostiosoite:', + 'Default project:' => 'Oletusprojekti:', + 'Notifications:' => 'Ilmoitukset:', + 'Notifications' => 'Ilmoitukset', + 'Group:' => 'Ryhmä:', + 'Regular user' => 'Peruskäyttäjä', + 'Account type:' => 'Tilin tyyppi:', + 'Edit profile' => 'Muokkaa profiilia', + 'Change password' => 'Vaihda salasana', + 'Password modification' => 'Salasanan vaihto', + 'External authentications' => 'Muut tunnistautumistavat', + 'Google Account' => 'Google-tili', + 'Github Account' => 'Github-tili', + 'Never connected.' => 'Ei koskaan liitetty.', + 'No account linked.' => 'Tiliä ei ole liitetty.', + 'Account linked.' => 'Tili on liitetty.', + 'No external authentication enabled.' => 'Muita tunnistautumistapoja ei ole otettu käyttöön.', + 'Password modified successfully.' => 'Salasana vaihdettu onnistuneesti.', + 'Unable to change the password.' => 'Salasanan vaihto epäonnistui.', + 'Change category for the task "%s"' => 'Vaihda tehtävän "%s" kategoria', + 'Change category' => 'Vaihda kategoria', + '%s updated the task %s' => '%s päivitti tehtävän %s', + '%s opened the task %s' => '%s avasi tehtävän %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s siirsi tehtävän %s %d. sarakkeessa "%s"', + '%s moved the task %s to the column "%s"' => '%s siirsi tehtävän %s sarakkeeseen "%s"', + '%s created the task %s' => '%s loi tehtävän %s', + '%s closed the task %s' => '%s sulki tehtävän %s', + '%s created a subtask for the task %s' => '%s loi alitehtävän tehtävälle %s', + '%s updated a subtask for the task %s' => '%s päivitti tehtävän %s alitehtävää', + 'Assigned to %s with an estimate of %s/%sh' => 'Annettu henkilölle %s arviolla %s/%sh', + 'Not assigned, estimate of %sh' => 'Ei annettu kenellekään, arvio %sh', + '%s updated a comment on the task %s' => '%s päivitti kommentia tehtävässä %s', + '%s commented the task %s' => '%s kommentoi tehtävää %s', + '%s\'s activity' => 'Henkilön %s toiminta', + 'No activity.' => 'Ei toimintaa.', + 'RSS feed' => 'RSS-syöte', + '%s updated a comment on the task #%d' => '%s päivitti kommenttia tehtävässä #%d', + '%s commented on the task #%d' => '%s kommentoi tehtävää #%d', + '%s updated a subtask for the task #%d' => '%s päivitti tehtävän #%d alitehtävää', + '%s created a subtask for the task #%d' => '%s loi alitehtävän tehtävälle #%d', + '%s updated the task #%d' => '%s päivitti tehtävää #%d', + '%s created the task #%d' => '%s loi tehtävän #%d', + '%s closed the task #%d' => '%s sulki tehtävän #%d', + '%s open the task #%d' => '%s avasi tehtävän #%d', + '%s moved the task #%d to the column "%s"' => '%s siirsi tehtävän #%d sarakkeeseen "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s siirsi tehtävän #%d %d. sarakkeessa %s', + 'Activity' => 'Toiminta', + 'Default values are "%s"' => 'Oletusarvot ovat "%s"', + 'Default columns for new projects (Comma-separated)' => 'Oletussarakkeet uusille projekteille', + 'Task assignee change' => 'Tehtävän saajan vaihto', + '%s change the assignee of the task #%d to %s' => '%s vaihtoi tehtävän #%d saajaksi %s', + '%s changed the assignee of the task %s to %s' => '%s vaihtoi tehtävän %s saajaksi %s', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', + 'New password for the user "%s"' => 'Uusi salasana käyttäjälle "%s"', + 'Choose an event' => 'Valitse toiminta', + 'Github commit received' => 'Github-kommitti vastaanotettu', + 'Github issue opened' => 'Github-issue avattu', + 'Github issue closed' => 'Github-issue suljettu', + 'Github issue reopened' => 'Github-issue uudelleenavattu', + 'Github issue assignee change' => 'Github-issuen saajan vaihto', + 'Github issue label change' => 'Github-issuen labelin vaihto', + 'Create a task from an external provider' => 'Luo tehtävä ulkoiselta tarjoajalta', + 'Change the assignee based on an external username' => 'Vaihda tehtävän saajaa perustuen ulkoiseen käyttäjänimeen', + 'Change the category based on an external label' => 'Vaihda kategoriaa perustuen ulkoiseen labeliin', + 'Reference' => 'Viite', + 'Reference: %s' => 'Viite: %s', + 'Label' => 'Label', + 'Database' => 'Tietokanta', + 'About' => 'Tietoja', + 'Database driver:' => 'Tietokantaohjelmisto:', + 'Board settings' => 'Taulun asetukset', + 'URL and token' => 'URL ja token', + 'Webhook settings' => 'Webhookin asetukset', + 'URL for task creation:' => 'URL tehtävän luomiseksi:', + 'Reset token' => 'Vaihda token', + 'API endpoint:' => 'API päätepiste:', + 'Refresh interval for private board' => 'Päivitystiheys yksityisille tauluille', + 'Refresh interval for public board' => 'Päivitystiheys julkisille tauluille', + 'Task highlight period' => 'Tehtävän korostusaika', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Aika (sekunteina) kuinka kauan tehtävä voidaan katsoa äskettäin muokatuksi (0 poistaa toiminnon käytöstä, oletuksena 2 päivää)', + 'Frequency in second (60 seconds by default)' => 'Päivitystiheys sekunteina (60 sekuntia oletuksena)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Päivitystiheys sekunteina (0 poistaa toiminnon käytöstä, oletuksena 10 sekuntia)', + 'Application URL' => 'Sovelluksen URL', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Esimerkiksi: http://example.kanboard.net/ (käytetään sähköposti-ilmoituksissa)', + 'Token regenerated.' => 'Token uudelleenluotu.', + 'Date format' => 'Päiväyksen muoto', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO-muoto on aina hyväksytty, esimerkiksi %s ja %s', + 'New private project' => 'Uusi yksityinen projekti', + 'This project is private' => 'Tämä projekti on yksityinen', + 'Type here to create a new sub-task' => 'Kirjoita tähän luodaksesi uuden alitehtävän', + 'Add' => 'Lisää', + 'Estimated time: %s hours' => 'Arvioitu aika: %s tuntia', + 'Time spent: %s hours' => 'Aikaa kulunut: %s tuntia', + 'Started on %B %e, %Y' => 'Aloitettu %B %e, %Y', + 'Start date' => 'Aloituspäivä', + 'Time estimated' => 'Arvioitu aika', + 'There is nothing assigned to you.' => 'Ei tehtäviä, joihin sinut olisi merkitty tekijäksi.', + 'My tasks' => 'Minun tehtävät', + 'Activity stream' => 'Toiminta', + 'Dashboard' => 'Työpöytä', + 'Confirmation' => 'Vahvistus', + 'Allow everybody to access to this project' => 'Anna kaikille käyttöoikeus tähän projektiin', + 'Everybody have access to this project.' => 'Kaikilla on käyttöoikeus projektiin.', + // 'Webhooks' => '', + // 'API' => '', + 'Integration' => 'Integraatio', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', + 'Configure' => 'Konfiguroi', + 'Project management' => 'Projektin hallinta', + 'My projects' => 'Minun projektini', + 'Columns' => 'Sarakkeet', + 'Task' => 'Tehtävät', + 'Your are not member of any project.' => 'Et ole minkään projektin jäsen.', + 'Percentage' => 'Prosentti', + 'Number of tasks' => 'Tehtävien määrä', + 'Task distribution' => 'Tehtävien jakauma', + 'Reportings' => 'Raportoinnit', + // 'Task repartition for "%s"' => '', + 'Analytics' => 'Analytiikka', + 'Subtask' => 'Alitehtävä', + 'My subtasks' => 'Minun alitehtäväni', + // 'User repartition' => '', + // 'User repartition for "%s"' => '', + 'Clone this project' => 'Kahdenna projekti', + 'Column removed successfully.' => 'Sarake poistettu onnstuneesti.', + 'Edit Project' => 'Muokkaa projektia', + 'Github Issue' => 'Github-issue', + 'Not enough data to show the graph.' => 'Ei riittävästi dataa graafin näyttämiseksi.', + 'Previous' => 'Edellinen', + 'The id must be an integer' => 'ID:n on oltava kokonaisluku', + 'The project id must be an integer' => 'Projektin ID:n on oltava kokonaisluku', + 'The status must be an integer' => 'Tilan on oltava kokonaisluku', + 'The subtask id is required' => 'Alitehtävän ID vaaditaan', + 'The subtask id must be an integer' => 'Alitehtävän ID:ntulee olla kokonaisluku', + 'The task id is required' => 'Tehtävän ID vaaditaan', + 'The task id must be an integer' => 'Tehtävän ID on oltava kokonaisluku', + 'The user id must be an integer' => 'Käyttäjän ID on oltava kokonaisluku', + 'This value is required' => 'Tämä arvo on pakollinen', + 'This value must be numeric' => 'Tämän arvon tulee olla numeerinen', + 'Unable to create this task.' => 'Tehtävän luonti epäonnistui', + 'Cumulative flow diagram' => 'Kumulatiivinen vuokaavio', + 'Cumulative flow diagram for "%s"' => 'Kumulatiivinen vuokaavio kohteelle "%s"', + 'Daily project summary' => 'Päivittäinen yhteenveto', + 'Daily project summary export' => 'Päivittäisen yhteenvedon vienti', + 'Daily project summary export for "%s"' => 'Päivittäisen yhteenvedon vienti kohteeseen "%s"', + 'Exports' => 'Viennit', + 'This export contains the number of tasks per column grouped per day.' => 'Tämä tiedosto sisältää tehtäviä sarakkeisiin päiväkohtaisesti ryhmilteltyinä', + 'Nothing to preview...' => 'Ei esikatselua...', + 'Preview' => 'Ei esikatselua', + 'Write' => 'Kirjoita', + 'Active swimlanes' => 'Aktiiviset kaistat', + 'Add a new swimlane' => 'Lisää uusi kaista', + 'Change default swimlane' => 'Vaihda oletuskaistaa', + 'Default swimlane' => 'Oletuskaista', + 'Do you really want to remove this swimlane: "%s"?' => 'Haluatko varmasti poistaa tämän kaistan: "%s"?', + 'Inactive swimlanes' => 'Passiiviset kaistat', + // 'Set project manager' => '', + // 'Set project member' => '', + 'Remove a swimlane' => 'Poista kaista', + 'Rename' => 'Uudelleennimeä', + 'Show default swimlane' => 'Näytä oletuskaista', + 'Swimlane modification for the project "%s"' => 'Kaistamuutos projektille "%s"', + 'Swimlane not found.' => 'Kaistaa ei löydy', + 'Swimlane removed successfully.' => 'Kaista poistettu onnistuneesti.', + 'Swimlanes' => 'Kaistat', + 'Swimlane updated successfully.' => 'Kaista päivitetty onnistuneesti.', + 'The default swimlane have been updated successfully.' => 'Oletuskaista päivitetty onnistuneesti.', + 'Unable to create your swimlane.' => 'Kaistan luonti epäonnistui.', + 'Unable to remove this swimlane.' => 'Kaistan poisto epäonnistui.', + 'Unable to update this swimlane.' => 'Kaistan päivittäminen epäonnistui.', + 'Your swimlane have been created successfully.' => 'Kaista luotu onnistuneesti.', + 'Example: "Bug, Feature Request, Improvement"' => 'Esimerkiksi: "Bugit, Ominaisuuspyynnöt, Parannukset"', + 'Default categories for new projects (Comma-separated)' => 'Oletuskategoriat uusille projekteille (pilkuin eroteltu)', + // 'Gitlab commit received' => '', + // 'Gitlab issue opened' => '', + // 'Gitlab issue closed' => '', + // 'Gitlab webhooks' => '', + // 'Help on Gitlab webhooks' => '', + // 'Integrations' => '', + // 'Integration with third-party services' => '', + // 'Role for this project' => '', + // 'Project manager' => '', + // 'Project member' => '', + // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', + // 'Gitlab Issue' => '', + // 'Subtask Id' => '', + // 'Subtasks' => '', + // 'Subtasks Export' => '', + // 'Subtasks exportation for "%s"' => '', + // 'Task Title' => '', + // 'Untitled' => '', + // 'Application default' => '', + // 'Language:' => '', + // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', + // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', + // 'Compact view' => '', + // 'Horizontal scrolling' => '', + // 'Compact/wide view' => '', + // 'No results match:' => '', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + // 'Hourly rates' => '', + // 'Hourly rate' => '', + // 'Currency' => '', + // 'Effective date' => '', + // 'Add new rate' => '', + // 'Rate removed successfully.' => '', + // 'Unable to remove this rate.' => '', + // 'Unable to save the hourly rate.' => '', + // 'Hourly rate created successfully.' => '', + // 'Start time' => '', + // 'End time' => '', + // 'Comment' => '', + // 'All day' => '', + // 'Day' => '', + // 'Manage timetable' => '', + // 'Overtime timetable' => '', + // 'Time off timetable' => '', + // 'Timetable' => '', + // 'Work timetable' => '', + // 'Week timetable' => '', + // 'Day timetable' => '', + // 'From' => '', + // 'To' => '', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + // 'Remove time slot' => '', + // 'Add new time slot' => '', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + // 'Files' => '', + // 'Images' => '', + // 'Private project' => '', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + // 'Budget' => '', + // 'Budget line' => '', + // 'Budget line removed successfully.' => '', + // 'Budget lines' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + // 'Cost' => '', + // 'Cost breakdown' => '', + // 'Custom Stylesheet' => '', + // 'download' => '', + // 'Do you really want to remove this budget line?' => '', + // 'EUR - Euro' => '', + // 'Expenses' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'New budget line' => '', + // 'NZD - New Zealand Dollar' => '', + // 'Remove a budget line' => '', + // 'Remove budget line' => '', + // 'RSD - Serbian dinar' => '', + // 'The budget line have been created successfully.' => '', + // 'Unable to create the budget line.' => '', + // 'Unable to remove this budget line.' => '', + // 'USD - US Dollar' => '', + // 'Remaining' => '', + // 'Destination column' => '', + // 'Move the task to another column when assigned to a user' => '', + // 'Move the task to another column when assignee is cleared' => '', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locales/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index f4eaab11..f5c97759 100644 --- a/app/Locales/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -1,6 +1,8 @@ <?php return array( + 'number.decimals_separator' => ',', + 'number.thousands_separator' => ' ', 'None' => 'Aucun', 'edit' => 'modifier', 'Edit' => 'Modifier', @@ -182,18 +184,19 @@ return array( 'Change assignee' => 'Changer la personne assignée', 'Change assignee for the task "%s"' => 'Changer la personne assignée pour la tâche « %s »', 'Timezone' => 'Fuseau horaire', - 'Sorry, I didn\'t found this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', + 'Sorry, I didn\'t find this information in my database!' => 'Désolé, je n\'ai pas trouvé cette information dans ma base de données !', 'Page not found' => 'Page introuvable', 'Complexity' => 'Complexité', 'limit' => 'limite', - 'Task limit' => 'Nombre maximum de tâches', + 'Task limit' => 'Tâches Max.', + 'Task count' => 'Nombre de tâches', 'This value must be greater than %d' => 'Cette valeur doit être plus grande que %d', 'Edit project access list' => 'Modifier l\'accès au projet', 'Edit users access' => 'Modifier les utilisateurs autorisés', 'Allow this user' => 'Autoriser cet utilisateur', 'Only those users have access to this project:' => 'Seulement ces utilisateurs ont accès à ce projet :', 'Don\'t forget that administrators have access to everything.' => 'N\'oubliez pas que les administrateurs ont accès à tout.', - 'revoke' => 'révoquer', + 'Revoke' => 'Révoquer', 'List of authorized users' => 'Liste des utilisateurs autorisés', 'User' => 'Utilisateur', 'Nobody have access to this project.' => 'Personne n\'est autorisé à accéder au projet.', @@ -212,6 +215,7 @@ return array( 'Invalid date' => 'Date invalide', 'Must be done before %B %e, %Y' => 'Doit être fait avant le %d/%m/%Y', '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d/%m/%Y', 'Automatic actions' => 'Actions automatisées', 'Your automatic action have been created successfully.' => 'Votre action automatisée a été ajouté avec succès.', 'Unable to create your automatic action.' => 'Impossible de créer votre action automatisée.', @@ -236,7 +240,7 @@ return array( 'Assign the task to a specific user' => 'Assigner la tâche à un utilisateur spécifique', 'Assign the task to the person who does the action' => 'Assigner la tâche à la personne qui fait l\'action', 'Duplicate the task to another project' => 'Dupliquer la tâche vers un autre projet', - 'Move a task to another column' => 'Déplacement d\'une tâche vers un autre colonne', + 'Move a task to another column' => 'Déplacement d\'une tâche vers une autre colonne', 'Move a task to another position in the same column' => 'Déplacement d\'une tâche à une autre position mais dans la même colonne', 'Task modification' => 'Modification d\'une tâche', 'Task creation' => 'Création d\'une tâche', @@ -276,7 +280,7 @@ return array( 'Remember Me' => 'Connexion automatique', 'Creation date' => 'Date de création', 'Filter by user' => 'Filtrer par utilisateur', - 'Filter by due date' => 'Filtrer par date d\'échéance', + 'Filter by due date' => 'Avec une date d\'échéance', 'Everybody' => 'Tout le monde', 'Open' => 'Ouvert', 'Closed' => 'Fermé', @@ -339,7 +343,7 @@ return array( 'Add a comment' => 'Ajouter un commentaire', 'Edit a comment' => 'Modifier un commentaire', 'Summary' => 'Résumé', - 'Time tracking' => 'Gestion du temps', + 'Time tracking' => 'Suivi du temps', 'Estimate:' => 'Estimation :', 'Spent:' => 'Passé :', 'Do you really want to remove this sub-task?' => 'Voulez-vous vraiment supprimer cette sous-tâche ?', @@ -385,8 +389,6 @@ return array( 'Creator' => 'Créateur', 'Modification date' => 'Date de modification', 'Completion date' => 'Date de complétion', - 'Webhook URL for task creation' => 'URL du webhook pour la création de tâche', - 'Webhook URL for task modification' => 'URL du webhook pour la modification de tâche', 'Clone' => 'Clone', 'Clone Project' => 'Cloner le projet', 'Project cloned successfully.' => 'Projet cloné avec succès.', @@ -406,15 +408,15 @@ return array( 'Comment updated' => 'Commentaire ajouté', 'New comment posted by %s' => 'Nouveau commentaire ajouté par « %s »', 'List of due tasks for the project "%s"' => 'Liste des tâches expirées pour le projet « %s »', - '[%s][New attachment] %s (#%d)' => '[%s][Pièce-jointe] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Nouveau commentaire] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Commentaire mis à jour] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Nouvelle sous-tâche] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Sous-tâche mise à jour] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Nouvelle tâche] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Tâche mise à jour] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Tâche fermée] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Tâche ouverte] %s (#%d)', + 'New attachment' => 'Nouveau document', + 'New comment' => 'Nouveau commentaire', + 'Comment updated' => 'Commentaire mis à jour', + 'New subtask' => 'Nouvelle sous-tâche', + 'Subtask updated' => 'Sous-tâche mise à jour', + 'New task' => 'Nouvelle tâche', + 'Task updated' => 'Tâche mise à jour', + 'Task closed' => 'Tâche fermée', + 'Task opened' => 'Tâche ouverte', '[%s][Due tasks]' => '[%s][Tâches expirées]', '[Kanboard] Notification' => '[Kanboard] Notification', 'I want to receive notifications only for those projects:' => 'Je souhaite reçevoir les notifications uniquement pour les projets sélectionnés :', @@ -456,7 +458,7 @@ return array( 'Edit profile' => 'Modifier le profil', 'Change password' => 'Changer le mot de passe', 'Password modification' => 'Changement de mot de passe', - 'External authentications' => 'Authentifications externe', + 'External authentications' => 'Authentifications externes', 'Google Account' => 'Compte Google', 'Github Account' => 'Compte Github', 'Never connected.' => 'Jamais connecté.', @@ -467,18 +469,18 @@ return array( 'Unable to change the password.' => 'Impossible de changer le mot de passe.', 'Change category for the task "%s"' => 'Changer la catégorie pour la tâche « %s »', 'Change category' => 'Changer de catégorie', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a mis à jour la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a ouvert la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s a déplacé la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a> à la position n°%d dans la colonne « %s »', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s a déplacé la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a> dans la colonne « %s »', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a créé la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a fermé la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a créé une sous-tâche pour la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a mis à jour une sous-tâche appartenant à la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', + '%s updated the task %s' => '%s a mis à jour la tâche %s', + '%s opened the task %s' => '%s a ouvert la tâche %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s a déplacé la tâche %s à la position n°%d dans la colonne « %s »', + '%s moved the task %s to the column "%s"' => '%s a déplacé la tâche %s dans la colonne « %s »', + '%s created the task %s' => '%s a créé la tâche %s', + '%s closed the task %s' => '%s a fermé la tâche %s', + '%s created a subtask for the task %s' => '%s a créé une sous-tâche pour la tâche %s', + '%s updated a subtask for the task %s' => '%s a mis à jour une sous-tâche appartenant à la tâche %s', 'Assigned to %s with an estimate of %s/%sh' => 'Assigné à %s avec un estimé de %s/%sh', 'Not assigned, estimate of %sh' => 'Personne assigné, estimé de %sh', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a mis à jour un commentaire appartenant à la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s a ajouté un commentaire sur la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a>', + '%s updated a comment on the task %s' => '%s a mis à jour un commentaire appartenant à la tâche %s', + '%s commented the task %s' => '%s a ajouté un commentaire sur la tâche %s', '%s\'s activity' => 'Activité du projet %s', 'No activity.' => 'Aucune activité.', 'RSS feed' => 'Flux RSS', @@ -496,11 +498,11 @@ return array( 'Default values are "%s"' => 'Les valeurs par défaut sont « %s »', 'Default columns for new projects (Comma-separated)' => 'Colonnes par défaut pour les nouveaux projets (séparé par des virgules)', 'Task assignee change' => 'Modification de la personne assignée sur une tâche', - '%s change the assignee of the task #%d to %s' => '%s a changé la personne assignée sur la tâche #%d pour %s', - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '%s a changé la personne assignée sur la tâche <a href="?controller=task&action=show&task_id=%d">n°%d</a> pour %s', - '[%s][Column Change] %s (#%d)' => '[%s][Changement de colonne] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Changement de position] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Changement d\'assigné] %s (#%d)', + '%s change the assignee of the task #%d to %s' => '%s a changé la personne assignée sur la tâche n˚%d pour %s', + '%s changed the assignee of the task %s to %s' => '%s a changé la personne assignée sur la tâche %s pour %s', + 'Column Change' => 'Changement de colonne', + 'Position Change' => 'Changement de position', + 'Assignee Change' => 'Changement d\'assigné', 'New password for the user "%s"' => 'Nouveau mot de passe pour l\'utilisateur « %s »', 'Choose an event' => 'Choisir un événement', 'Github commit received' => '« Commit » reçu via Github', @@ -551,4 +553,375 @@ return array( 'Confirmation' => 'Confirmation', 'Allow everybody to access to this project' => 'Autoriser tout le monde à accéder à ce projet', 'Everybody have access to this project.' => 'Tout le monde a acccès à ce projet.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Intégration', + 'Github webhooks' => 'Webhook Github', + 'Help on Github webhooks' => 'Aide sur les webhooks Github', + 'Create a comment from an external provider' => 'Créer un commentaire depuis un fournisseur externe', + 'Github issue comment created' => 'Commentaire créé sur un ticket Github', + 'Configure' => 'Configurer', + 'Project management' => 'Gestion des projets', + 'My projects' => 'Mes projets', + 'Columns' => 'Colonnes', + 'Task' => 'Tâche', + 'Your are not member of any project.' => 'Vous n\'êtes membre d\'aucun projet.', + 'Percentage' => 'Pourcentage', + 'Number of tasks' => 'Nombre de tâches', + 'Task distribution' => 'Répartition des tâches', + 'Reportings' => 'Rapports', + 'Task repartition for "%s"' => 'Répartition des tâches pour « %s »', + 'Analytics' => 'Analytique', + 'Subtask' => 'Sous-tâche', + 'My subtasks' => 'Mes sous-tâches', + 'User repartition' => 'Répartition des utilisateurs', + 'User repartition for "%s"' => 'Répartition des utilisateurs pour « %s »', + 'Clone this project' => 'Cloner ce projet', + 'Column removed successfully.' => 'Colonne supprimée avec succès.', + 'Edit Project' => 'Modifier le projet', + 'Github Issue' => 'Ticket Github', + 'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique.', + 'Previous' => 'Précédent', + 'The id must be an integer' => 'L\'id doit être un entier', + 'The project id must be an integer' => 'L\'id du projet doit être un entier', + 'The status must be an integer' => 'Le status doit être un entier', + 'The subtask id is required' => 'L\'id de la sous-tâche est obligatoire', + 'The subtask id must be an integer' => 'L\'id de la sous-tâche doit être en entier', + 'The task id is required' => 'L\'id de la tâche est obligatoire', + 'The task id must be an integer' => 'L\'id de la tâche doit être en entier', + 'The user id must be an integer' => 'L\'id de l\'utilisateur doit être en entier', + 'This value is required' => 'Cette valeur est obligatoire', + 'This value must be numeric' => 'Cette valeur doit être numérique', + 'Unable to create this task.' => 'Impossible de créer cette tâche', + 'Cumulative flow diagram' => 'Diagramme de flux cumulé', + 'Cumulative flow diagram for "%s"' => 'Diagramme de flux cumulé pour « %s »', + 'Daily project summary' => 'Résumé journalier du projet', + 'Daily project summary export' => 'Export du résumé journalier du projet', + 'Daily project summary export for "%s"' => 'Export du résumé quotidien du projet pour « %s »', + 'Exports' => 'Exports', + 'This export contains the number of tasks per column grouped per day.' => 'Cet export contient le nombre de tâches par colonne groupé par jour.', + 'Nothing to preview...' => 'Rien à prévisualiser...', + 'Preview' => 'Prévisualiser', + 'Write' => 'Écrire', + 'Active swimlanes' => 'Swimlanes actives', + 'Add a new swimlane' => 'Ajouter une nouvelle swimlane', + 'Change default swimlane' => 'Modifier la swimlane par défaut', + 'Default swimlane' => 'Swimlane par défaut', + 'Do you really want to remove this swimlane: "%s"?' => 'Voulez-vous vraiment supprimer cette swimlane : « %s » ?', + 'Inactive swimlanes' => 'Swimlanes inactives', + 'Set project manager' => 'Mettre chef de projet', + 'Set project member' => 'Mettre membre du projet', + 'Remove a swimlane' => 'Supprimer une swimlane', + 'Rename' => 'Renommer', + 'Show default swimlane' => 'Afficher la swimlane par défaut', + 'Swimlane modification for the project "%s"' => 'Modification d\'une swimlane pour le projet « %s »', + 'Swimlane not found.' => 'Cette swimlane est introuvable.', + 'Swimlane removed successfully.' => 'Swimlane supprimée avec succès.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane mise à jour avec succès.', + 'The default swimlane have been updated successfully.' => 'La swimlane par défaut a été mise à jour avec succès.', + 'Unable to create your swimlane.' => 'Impossible de créer votre swimlane.', + 'Unable to remove this swimlane.' => 'Impossible de supprimer cette swimlane.', + 'Unable to update this swimlane.' => 'Impossible de mettre à jour cette swimlane.', + 'Your swimlane have been created successfully.' => 'Votre swimlane a été créée avec succès.', + 'Example: "Bug, Feature Request, Improvement"' => 'Exemple: « Incident, Demande de fonctionnalité, Amélioration »', + 'Default categories for new projects (Comma-separated)' => 'Catégories par défaut pour les nouveaux projets (séparé par des virgules)', + 'Gitlab commit received' => '« Commit » reçu via Gitlab', + 'Gitlab issue opened' => 'Ouverture d\'un ticket sur Gitlab', + 'Gitlab issue closed' => 'Fermeture d\'un ticket sur Gitlab', + 'Gitlab webhooks' => 'Webhook Gitlab', + 'Help on Gitlab webhooks' => 'Aide sur les webhooks Gitlab', + 'Integrations' => 'Intégrations', + 'Integration with third-party services' => 'Intégration avec des services externes', + 'Role for this project' => 'Rôle pour ce projet', + 'Project manager' => 'Chef de projet', + 'Project member' => 'Membre du projet', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un chef de projet peut changer les paramètres du projet et possède plus de privilèges qu\'un utilisateur standard.', + 'Gitlab Issue' => 'Ticket Gitlab', + 'Subtask Id' => 'Identifiant de la sous-tâche', + 'Subtasks' => 'Sous-tâches', + 'Subtasks Export' => 'Exportation des sous-tâches', + 'Subtasks exportation for "%s"' => 'Exportation des sous-tâches pour le projet « %s »', + 'Task Title' => 'Titre de la tâche', + 'Untitled' => 'Sans nom', + 'Application default' => 'Valeur par défaut de l\'application', + 'Language:' => 'Langue :', + 'Timezone:' => 'Fuseau horaire :', + 'All columns' => 'Toutes les colonnes', + 'Calendar for "%s"' => 'Agenda pour le projet « %s »', + 'Filter by column' => 'Filtrer par colonne', + 'Filter by status' => 'Filtrer par status', + 'Calendar' => 'Agenda', + 'Next' => 'Suivant', + '#%d' => 'n˚%d', + 'Filter by color' => 'Filtrer par couleur', + 'Filter by swimlane' => 'Filtrer par swimlanes', + 'All swimlanes' => 'Toutes les swimlanes', + 'All colors' => 'Toutes les couleurs', + 'All status' => 'Tous les états', + 'Add a comment logging moving the task between columns' => 'Ajouter un commentaire de log lorsqu\'une tâche est déplacée dans une autre colonne', + 'Moved to column %s' => 'Tâche déplacée à la colonne %s', + 'Change description' => 'Changer la description', + 'User dashboard' => 'Tableau de bord de l\'utilisateur', + 'Allow only one subtask in progress at the same time for a user' => 'Autoriser une seule sous-tâche en progrès en même temps pour un utilisateur', + 'Edit column "%s"' => 'Modifier la colonne « %s »', + 'Enable time tracking for subtasks' => 'Activer la feuille de temps pour les sous-tâches', + 'Select the new status of the subtask: "%s"' => 'Selectionnez le nouveau statut de la sous-tâche : « %s »', + 'Subtask timesheet' => 'Feuille de temps des sous-tâches', + 'There is nothing to show.' => 'Il n\'y a rien à montrer.', + 'Time Tracking' => 'Feuille de temps', + 'You already have one subtask in progress' => 'Vous avez déjà une sous-tâche en progrès', + 'Which parts of the project do you want to duplicate?' => 'Quelles parties du projet voulez-vous dupliquer ?', + 'Change dashboard view' => 'Changer la vue du tableau de bord', + 'Show/hide activities' => 'Afficher/cacher les activités', + 'Show/hide projects' => 'Afficher/cacher les projets', + 'Show/hide subtasks' => 'Afficher/cacher les sous-tâches', + 'Show/hide tasks' => 'Afficher/cacher les tâches', + 'Disable login form' => 'Désactiver le formulaire d\'authentification', + 'Show/hide calendar' => 'Afficher/cacher le calendrier', + 'User calendar' => 'Calendrier de l\'utilisateur', + 'Bitbucket commit received' => '« Commit » reçu via Bitbucket', + 'Bitbucket webhooks' => 'Webhook Bitbucket', + 'Help on Bitbucket webhooks' => 'Aide sur les webhooks Bitbucket', + 'Start' => 'Début', + 'End' => 'Fin', + 'Task age in days' => 'Age de la tâche en jours', + 'Days in this column' => 'Jours dans cette colonne', + '%dd' => '%dj', + 'Add a link' => 'Ajouter un lien', + 'Add a new link' => 'Ajouter un nouveau lien', + 'Do you really want to remove this link: "%s"?' => 'Voulez-vous vraiment supprimer ce lien : « %s » ?', + 'Do you really want to remove this link with task #%d?' => 'Voulez-vous vraiment supprimer ce lien avec la tâche n°%d ?', + 'Field required' => 'Champ obligatoire', + 'Link added successfully.' => 'Lien créé avec succès.', + 'Link updated successfully.' => 'Lien mis à jour avec succès.', + 'Link removed successfully.' => 'Lien supprimé avec succès.', + 'Link labels' => 'Libellé des liens', + 'Link modification' => 'Modification d\'un lien', + 'Links' => 'Liens', + 'Link settings' => 'Paramètres des liens', + 'Opposite label' => 'Nom du libellé opposé', + 'Remove a link' => 'Supprimer un lien', + 'Task\'s links' => 'Liens des tâches', + 'The labels must be different' => 'Les libellés doivent être différents', + 'There is no link.' => 'Il n\'y a aucun lien.', + 'This label must be unique' => 'Ce libellé doit être unique', + 'Unable to create your link.' => 'Impossible d\'ajouter ce lien.', + 'Unable to update your link.' => 'Impossible de mettre à jour ce lien.', + 'Unable to remove this link.' => 'Impossible de supprimer ce lien.', + 'relates to' => 'est liée à', + 'blocks' => 'bloque', + 'is blocked by' => 'est bloquée par', + 'duplicates' => 'duplique', + 'is duplicated by' => 'est dupliquée par', + 'is a child of' => 'est un enfant de', + 'is a parent of' => 'est un parent de', + 'targets milestone' => 'vise l\'étape importante', + 'is a milestone of' => 'est une étape importante incluant', + 'fixes' => 'corrige', + 'is fixed by' => 'est corrigée par', + 'This task' => 'Cette tâche', + '<1h' => '<1h', + '%dh' => '%dh', + '%b %e' => '%e %b', + 'Expand tasks' => 'Déplier les tâches', + 'Collapse tasks' => 'Replier les tâches', + 'Expand/collapse tasks' => 'Plier/déplier les tâches', + 'Close dialog box' => 'Fermer une boite de dialogue', + 'Submit a form' => 'Enregistrer un formulaire', + 'Board view' => 'Page du tableau', + 'Keyboard shortcuts' => 'Raccourcis clavier', + 'Open board switcher' => 'Ouvrir le sélecteur de tableau', + 'Application' => 'Application', + 'Filter recently updated' => 'Récemment modifié', + 'since %B %e, %Y at %k:%M %p' => 'depuis le %d/%m/%Y à %H:%M', + 'More filters' => 'Plus de filtres', + 'Compact view' => 'Vue compacte', + 'Horizontal scrolling' => 'Défilement horizontal', + 'Compact/wide view' => 'Basculer entre la vue compacte et étendue', + 'No results match:' => 'Aucun résultat :', + 'Remove hourly rate' => 'Supprimer un taux horaire', + 'Do you really want to remove this hourly rate?' => 'Voulez-vous vraiment supprimer ce taux horaire ?', + 'Hourly rates' => 'Taux horaires', + 'Hourly rate' => 'Taux horaire', + 'Currency' => 'Devise', + 'Effective date' => 'Date d\'effet', + 'Add new rate' => 'Ajouter un nouveau taux horaire', + 'Rate removed successfully.' => 'Taux horaire supprimé avec succès.', + 'Unable to remove this rate.' => 'Impossible de supprimer ce taux horaire.', + 'Unable to save the hourly rate.' => 'Impossible de sauvegarder ce taux horaire.', + 'Hourly rate created successfully.' => 'Taux horaire créé avec succès.', + 'Start time' => 'Date de début', + 'End time' => 'Date de fin', + 'Comment' => 'Commentaire', + 'All day' => 'Toute la journée', + 'Day' => 'Jour', + 'Manage timetable' => 'Gérer les horaires', + 'Overtime timetable' => 'Heures supplémentaires', + 'Time off timetable' => 'Heures d\'absences', + 'Timetable' => 'Horaires', + 'Work timetable' => 'Horaires travaillés', + 'Week timetable' => 'Horaires de la semaine', + 'Day timetable' => 'Horaire d\'une journée', + 'From' => 'Depuis', + 'To' => 'À', + 'Time slot created successfully.' => 'Créneau horaire créé avec succès.', + 'Unable to save this time slot.' => 'Impossible de sauvegarder ce créneau horaire.', + 'Time slot removed successfully.' => 'Créneau horaire supprimé avec succès.', + 'Unable to remove this time slot.' => 'Impossible de supprimer ce créneau horaire.', + 'Do you really want to remove this time slot?' => 'Voulez-vous vraiment supprimer ce créneau horaire ?', + 'Remove time slot' => 'Supprimer un créneau horaire', + 'Add new time slot' => 'Ajouter un créneau horaire', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ces horaires sont utilisés lorsque la case « Toute la journée » est cochée pour les heures d\'absences ou supplémentaires programmées.', + 'Files' => 'Fichiers', + 'Images' => 'Images', + 'Private project' => 'Projet privé', + 'Amount' => 'Montant', + 'AUD - Australian Dollar' => 'AUD - Dollar australien', + 'Budget' => 'Budget', + 'Budget line' => 'Ligne budgétaire', + 'Budget line removed successfully.' => 'Ligne budgétaire supprimée avec succès.', + 'Budget lines' => 'Lignes budgétaire', + 'CAD - Canadian Dollar' => 'CAD - Dollar canadien', + 'CHF - Swiss Francs' => 'CHF - Franc suisse', + 'Cost' => 'Coût', + 'Cost breakdown' => 'Détail des coûts', + 'Custom Stylesheet' => 'Feuille de style personalisée', + 'download' => 'télécharger', + 'Do you really want to remove this budget line?' => 'Voulez-vous vraiment supprimer cette ligne budgétaire ?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Dépenses', + 'GBP - British Pound' => 'GBP - Livre sterling', + 'INR - Indian Rupee' => 'INR - Roupie indienne', + 'JPY - Japanese Yen' => 'JPY - Yen', + 'New budget line' => 'Nouvelle ligne budgétaire', + 'NZD - New Zealand Dollar' => 'NZD - Dollar néo-zélandais', + 'Remove a budget line' => 'Supprimer une ligne budgétaire', + 'Remove budget line' => 'Supprimer une ligne budgétaire', + 'RSD - Serbian dinar' => 'RSD - Dinar serbe', + 'The budget line have been created successfully.' => 'La ligne de budgétaire a été créée avec succès.', + 'Unable to create the budget line.' => 'Impossible de créer cette ligne budgétaire.', + 'Unable to remove this budget line.' => 'Impossible de supprimer cette ligne budgétaire.', + 'USD - US Dollar' => 'USD - Dollar américain', + 'Remaining' => 'Restant', + 'Destination column' => 'Colonne de destination', + 'Move the task to another column when assigned to a user' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci est assignée à quelqu\'un', + 'Move the task to another column when assignee is cleared' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci n\'est plus assignée', + 'Source column' => 'Colonne d\'origine', + 'Show subtask estimates (forecast of future work)' => 'Afficher l\'estimation des sous-tâches (prévision du travail à venir)', + 'Transitions' => 'Transitions', + 'Executer' => 'Exécutant', + 'Time spent in the column' => 'Temps passé dans la colonne', + 'Task transitions' => 'Transitions des tâches', + 'Task transitions export' => 'Export des transitions des tâches', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Ce rapport contient tous les mouvements de colonne pour chaque tâche avec la date, l\'utilisateur et le temps passé pour chaque transition.', + 'Currency rates' => 'Taux de change des devises', + 'Rate' => 'Taux', + 'Change reference currency' => 'Changer la monnaie de référence', + 'Add a new currency rate' => 'Ajouter un nouveau taux pour une devise', + 'Currency rates are used to calculate project budget.' => 'Le cours des devises est utilisé pour calculer le budget des projets.', + 'Reference currency' => 'Devise de référence', + 'The currency rate have been added successfully.' => 'Le taux de change a été ajouté avec succès.', + 'Unable to add this currency rate.' => 'Impossible d\'ajouter ce taux de change', + 'Send notifications to a Slack channel' => 'Envoyer les notifications sur un salon de discussion Slack', + 'Webhook URL' => 'URL du webhook', + 'Help on Slack integration' => 'Aide sur l\'intégration avec Slack', + '%s remove the assignee of the task %s' => '%s a enlevé la personne assignée à la tâche %s', + 'Send notifications to Hipchat' => 'Envoyer les notifications vers Hipchat', + 'API URL' => 'URL de l\'api', + 'Room API ID or name' => 'Nom ou identifiant du salon de discussion', + 'Room notification token' => 'Jeton de sécurité du salon de discussion', + 'Help on Hipchat integration' => 'Aide sur l\'intégration avec Hipchat', + 'Enable Gravatar images' => 'Activer les images Gravatar', + 'Information' => 'Informations', + 'Check two factor authentication code' => 'Vérification du code pour l\'authentification à deux-facteurs', + 'The two factor authentication code is not valid.' => 'Le code pour l\'authentification à deux-facteurs n\'est pas valide.', + 'The two factor authentication code is valid.' => 'Le code pour l\'authentification à deux-facteurs est valide.', + 'Code' => 'Code', + 'Two factor authentication' => 'Authentification à deux-facteurs', + 'Enable/disable two factor authentication' => 'Activer/désactiver l\'authentification à deux-facteurs', + 'This QR code contains the key URI: ' => 'Ce code QR contient l\'url de la clé : ', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Sauvegardez cette clé secrete dans votre logiciel TOTP (par exemple Google Authenticator ou FreeOTP).', + 'Check my code' => 'Vérifier mon code', + 'Secret key: ' => 'Clé secrète : ', + 'Test your device' => 'Testez votre appareil', + 'Assign a color when the task is moved to a specific column' => 'Assigner une couleur lorsque la tâche est déplacée dans une colonne spécifique', + '%s via Kanboard' => '%s via Kanboard', + 'uploaded by: %s' => 'Téléchargé par %s', + 'uploaded on: %s' => 'Téléchargé le %s', + 'size: %s' => 'Taille : %s', + 'Burndown chart for "%s"' => 'Graphique d\'avancement pour « %s »', + 'Burndown chart' => 'Graphique d\'avancement', + 'This chart show the task complexity over the time (Work Remaining).' => 'Ce graphique représente la complexité des tâches en fonction du temps (travail restant).', + 'Screenshot taken %s' => 'Capture d\'écran prise le %s', + 'Add a screenshot' => 'Ajouter une capture d\'écran', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Prenez une capture d\'écran et appuyez sur CTRL+V ou ⌘+V pour coller ici.', + 'Screenshot uploaded successfully.' => 'Capture d\'écran téléchargée avec succès.', + 'SEK - Swedish Krona' => 'SEK - Couronne suédoise', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'L\'identificateur du projet est un code alpha-numérique optionnel pour identifier votre projet.', + 'Identifier' => 'Identificateur', + 'Postmark (incoming emails)' => 'Postmark (emails entrants)', + 'Help on Postmark integration' => 'Aide sur l\'intégration avec Postmark', + 'Mailgun (incoming emails)' => 'Mailgun (emails entrants)', + 'Help on Mailgun integration' => 'Aide sur l\'intégration avec Mailgun', + 'Sendgrid (incoming emails)' => 'Sendgrid (emails entrants)', + 'Help on Sendgrid integration' => 'Aide sur l\'intégration avec Sendgrid', + 'Disable two factor authentication' => 'Désactiver l\'authentification à deux facteurs', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Voulez-vous vraiment désactiver l\'authentification à deux facteurs pour cet utilisateur : « %s » ?', + 'Edit link' => 'Modifier un lien', + 'Start to type task title...' => 'Entrez le titre de la tâche...', + 'A task cannot be linked to itself' => 'Une tâche ne peut être liée à elle-même', + 'The exact same link already exists' => 'Un lien identique existe déjà', + 'Recurrent task is scheduled to be generated' => 'La tâche récurrente est programmée pour être créée', + 'Recurring information' => 'Information sur la récurrence', + 'Score' => 'Complexité', + 'The identifier must be unique' => 'L\'identifiant doit être unique', + 'This linked task id doesn\'t exists' => 'L\'identifiant de la task liée n\'existe pas', + 'This value must be alphanumeric' => 'Cette valeur doit être alpha-numérique', + 'Edit recurrence' => 'Modifier la récurrence', + 'Generate recurrent task' => 'Générer une tâche récurrente', + 'Trigger to generate recurrent task' => 'Déclencheur pour générer la tâche récurrente', + 'Factor to calculate new due date' => 'Facteur pour calculer la nouvelle date d\'échéance', + 'Timeframe to calculate new due date' => 'Échelle de temps pour calculer la nouvelle date d\'échéance', + 'Base date to calculate new due date' => 'Date à utiliser pour calculer la nouvelle date d\'échéance', + 'Action date' => 'Date de l\'action', + 'Base date to calculate new due date: ' => 'Date utilisée pour calculer la nouvelle date d\'échéance : ', + 'This task has created this child task: ' => 'Cette tâche a créée la tâche enfant : ', + 'Day(s)' => 'Jour(s)', + 'Existing due date' => 'Date d\'échéance existante', + 'Factor to calculate new due date: ' => 'Facteur pour calculer la nouvelle date d\'échéance : ', + 'Month(s)' => 'Mois', + 'Recurrence' => 'Récurrence', + 'This task has been created by: ' => 'Cette tâche a été créée par :', + 'Recurrent task has been generated:' => 'Une tâche récurrente a été générée :', + 'Timeframe to calculate new due date: ' => 'Échelle de temps pour calculer la nouvelle date d\'échéance : ', + 'Trigger to generate recurrent task: ' => 'Déclencheur pour générer la tâche récurrente : ', + 'When task is closed' => 'Lorsque la tâche est fermée', + 'When task is moved from first column' => 'Lorsque la tâche est déplacée en dehors de la première colonne', + 'When task is moved to last column' => 'Lorsque la tâche est déplacée dans la dernière colonne', + 'Year(s)' => 'Année(s)', + 'Jabber (XMPP)' => 'Jabber (XMPP)', + 'Send notifications to Jabber' => 'Envoyer les notifications vers Jabber', + 'XMPP server address' => 'Adresse du serveur XMPP', + 'Jabber domain' => 'Nom de domaine Jabber', + 'Jabber nickname' => 'Pseudonyme Jabber', + 'Multi-user chat room' => 'Salon de discussion multi-utilisateurs', + 'Help on Jabber integration' => 'Aide sur l\'intégration avec Jabber', + 'The server address must use this format: "tcp://hostname:5222"' => 'L\'adresse du serveur doit utiliser le format suivant : « tcp://hostname:5222 »', + 'Calendar settings' => 'Paramètres du calendrier', + 'Project calendar view' => 'Vue en mode projet du calendrier', + 'Project settings' => 'Paramètres des projets', + 'Show subtasks based on the time tracking' => 'Afficher les sous-tâches basé sur le suivi du temps', + 'Show tasks based on the creation date' => 'Afficher les tâches en fonction de la date de création', + 'Show tasks based on the start date' => 'Afficher les tâches en fonction de la date de début', + 'Subtasks time tracking' => 'Suivi du temps par rapport aux sous-tâches', + 'User calendar view' => 'Vue en mode utilisateur du calendrier', + 'Automatically update the start date' => 'Mettre à jour automatiquement la date de début', + 'iCal feed' => 'Abonnement iCal', + 'Preferences' => 'Préférences', + 'Security' => 'Sécurité', + 'Two factor authentication disabled' => 'Authentification à deux facteurs désactivé', + 'Two factor authentication enabled' => 'Authentification à deux facteurs activée', + 'Unable to update this user.' => 'Impossible de mettre à jour cet utilisateur.', + 'There is no user management for private projects.' => 'Il n\'y a pas de gestion d\'utilisateurs pour les projets privés.', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php new file mode 100644 index 00000000..3d8302c5 --- /dev/null +++ b/app/Locale/hu_HU/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + 'number.decimals_separator' => ',', + 'number.thousands_separator' => ' ', + 'None' => 'Nincs', + 'edit' => 'szerkesztés', + 'Edit' => 'Szerkesztés', + 'remove' => 'törlés', + 'Remove' => 'Törlés', + 'Update' => 'Frissítés', + 'Yes' => 'Igen', + 'No' => 'Nem', + 'cancel' => 'Mégsem', + 'or' => 'vagy', + 'Yellow' => 'Sárga', + 'Blue' => 'Kék', + 'Green' => 'Zöld', + 'Purple' => 'Lila', + 'Red' => 'Piros', + 'Orange' => 'Narancs', + 'Grey' => 'Szürke', + 'Save' => 'Mentés', + 'Login' => 'Bejelentkezés', + 'Official website:' => 'Hivatalos honlap:', + 'Unassigned' => 'Nincs felelős', + 'View this task' => 'Feladat megtekintése', + 'Remove user' => 'Felhasználó törlése', + 'Do you really want to remove this user: "%s"?' => 'Tényleg törli ezt a felhasználót: "%s"?', + 'New user' => 'Új felhasználó', + 'All users' => 'Minden felhasználó', + 'Username' => 'Felhasználónév', + 'Password' => 'Jelszó', + 'Default project' => 'Alapértelmezett projekt', + 'Administrator' => 'Rendszergazda', + 'Sign in' => 'Jelentkezzen be', + 'Users' => 'Felhasználók', + 'No user' => 'Nincs felhasználó', + 'Forbidden' => 'tiltott', + 'Access Forbidden' => 'Hozzáférés megtagadva', + 'Only administrators can access to this page.' => 'Csak a rendszergazdák férhetnek hozzá az oldalhoz.', + 'Edit user' => 'Felhasználó módosítása', + 'Logout' => 'Kilépés', + 'Bad username or password' => 'Rossz felhasználónév vagy jelszó', + 'users' => 'felhasználók', + 'projects' => 'projektek', + 'Edit project' => 'Projekt szerkesztése', + 'Name' => 'Név', + 'Activated' => 'Aktiválva', + 'Projects' => 'Projektek', + 'No project' => 'Nincs projekt', + 'Project' => 'Projekt', + 'Status' => 'Állapot', + 'Tasks' => 'Feladat', + 'Board' => 'Tábla', + 'Actions' => 'Műveletek', + 'Inactive' => 'Inaktív', + 'Active' => 'Aktív', + 'Column %d' => 'Oszlop %d', + 'Add this column' => 'Oszlop hozzáadása', + '%d tasks on the board' => '%d feladat a táblán', + '%d tasks in total' => 'Összesen %d feladat', + 'Unable to update this board.' => 'Nem lehet frissíteni a táblát.', + 'Edit board' => 'Tábla szerkesztése', + 'Disable' => 'Letiltás', + 'Enable' => 'Engedélyezés', + 'New project' => 'Új projekt', + 'Do you really want to remove this project: "%s"?' => 'Valóban törölni akarja ezt a projektet: "%s"?', + 'Remove project' => 'Projekt törlése', + 'Boards' => 'Táblák', + 'Edit the board for "%s"' => 'Tábla szerkesztése: "%s"', + 'All projects' => 'Minden projekt', + 'Change columns' => 'Oszlop módosítása', + 'Add a new column' => 'Új oszlop', + 'Title' => 'Cím', + 'Add Column' => 'Oszlopot hozzáad', + 'Project "%s"' => 'Projekt "%s"', + 'Nobody assigned' => 'Nincs felelős', + 'Assigned to %s' => 'Felelős: %s', + 'Remove a column' => 'Oszlop törlése', + 'Remove a column from a board' => 'Oszlop törlése a tábláról', + 'Unable to remove this column.' => 'Az oszlop törlése nem lehetséges.', + 'Do you really want to remove this column: "%s"?' => 'Valóban törölni akarja ezt az oszlopot: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Az oszlophoz rendelt ÖSSZES FELADAT TÖRLŐDNI FOG!', + 'Settings' => 'Beállítások', + 'Application settings' => 'Alkalmazás beállítások', + 'Language' => 'Nyelv', + 'Webhook token:' => 'Webhook token:', + 'API token:' => 'API token:', + 'More information' => 'További információ', + 'Database size:' => 'Adatbázis méret:', + 'Download the database' => 'Adatbázis letöltése', + 'Optimize the database' => 'Adatbázis optimalizálása', + '(VACUUM command)' => '(VACUUM parancs)', + '(Gzip compressed Sqlite file)' => '(Gzip tömörített SQLite fájl)', + 'User settings' => 'Felhasználói beállítások', + 'My default project:' => 'Alapértelmezett projekt: ', + 'Close a task' => 'Feladat lezárása', + 'Do you really want to close this task: "%s"?' => 'Tényleg le akarja zárni ezt a feladatot: "%s"?', + 'Edit a task' => 'Feladat módosítása', + 'Column' => 'Oszlop', + 'Color' => 'Szín', + 'Assignee' => 'Felelős', + 'Create another task' => 'Új feladat létrehozása', + 'New task' => 'Új feladat', + 'Open a task' => 'Feladat felnyitás', + 'Do you really want to open this task: "%s"?' => 'Tényleg meg akarja nyitni ezt a feladatot: "%s"?', + 'Back to the board' => 'Vissza a táblához', + 'Created on %B %e, %Y at %k:%M %p' => 'Létrehozva: %Y. %m. %d. %H:%M', + 'There is nobody assigned' => 'Nincs felelős', + 'Column on the board:' => 'Tábla oszlopa: ', + 'Status is open' => 'Nyitott állapot', + 'Status is closed' => 'Zárt állapot', + 'Close this task' => 'Feladat lezárása', + 'Open this task' => 'Feladat felnyitása', + 'There is no description.' => 'Nincs elérhető leírás.', + 'Add a new task' => 'Új feladat hozzáadása', + 'The username is required' => 'Felhasználói név szükséges', + 'The maximum length is %d characters' => 'A maximális hossz %d karakter', + 'The minimum length is %d characters' => 'A minimális hossza %d karakter', + 'The password is required' => 'Jelszó szükséges', + 'This value must be an integer' => 'Ez az érték csak egész szám lehet', + 'The username must be unique' => 'A felhasználó nevének egyedinek kell lennie', + 'The username must be alphanumeric' => 'A felhasználói név csak alfanumerikus lehet (betűk és számok)', + 'The user id is required' => 'A felhasználói azonosítót meg kell adni', + 'Passwords don\'t match' => 'A jelszavak nem egyeznek', + 'The confirmation is required' => 'Megerősítés szükséges', + 'The column is required' => 'Az oszlopot meg kell adni', + 'The project is required' => 'A projektet meg kell adni', + 'The color is required' => 'A színt meg kell adni', + 'The id is required' => 'Az ID-t (azonosítót) meg kell adni', + 'The project id is required' => 'A projekt ID-t (azonosítót) meg kell adni', + 'The project name is required' => 'A projekt nevét meg kell adni', + 'This project must be unique' => 'A projekt nevének egyedinek kell lennie', + 'The title is required' => 'A címet meg kell adni', + 'The language is required' => 'A nyelvet meg kell adni', + 'There is no active project, the first step is to create a new project.' => 'Nincs aktív projekt. Először létre kell hozni egy projektet.', + 'Settings saved successfully.' => 'A beállítások sikeresen mentve.', + 'Unable to save your settings.' => 'A beállítások mentése sikertelen.', + 'Database optimization done.' => 'Adatbázis optimalizálás kész.', + 'Your project have been created successfully.' => 'Projekt sikeresen létrehozva', + 'Unable to create your project.' => 'Projekt létrehozása sikertelen.', + 'Project updated successfully.' => 'Projekt sikeresen frissítve.', + 'Unable to update this project.' => 'Projekt frissítése sikertelen.', + 'Unable to remove this project.' => 'Projekt törlése sikertelen.', + 'Project removed successfully.' => 'Projekt sikeresen törölve.', + 'Project activated successfully.' => 'Projekt sikeresen aktiválva.', + 'Unable to activate this project.' => 'Projekt aktiválása sikertelen.', + 'Project disabled successfully.' => 'Projekt sikeresen letiltva.', + 'Unable to disable this project.' => 'Projekt letiltása sikertelen.', + 'Unable to open this task.' => 'A feladat felnyitása sikertelen.', + 'Task opened successfully.' => 'Feladat sikeresen megnyitva .', + 'Unable to close this task.' => 'A feladat lezárása sikertelen.', + 'Task closed successfully.' => 'Feladat sikeresen lezárva.', + 'Unable to update your task.' => 'Feladat frissítése sikertelen.', + 'Task updated successfully.' => 'Feladat sikeresen frissítve.', + 'Unable to create your task.' => 'Feladat létrehozása sikertelen.', + 'Task created successfully.' => 'Feladat sikeresen létrehozva.', + 'User created successfully.' => 'Felhasználó létrehozva.', + 'Unable to create your user.' => 'Felhasználó létrehozása sikertelen.', + 'User updated successfully.' => 'Felhasználó sikeresen frissítve.', + 'Unable to update your user.' => 'Felhasználó frissítése sikertelen.', + 'User removed successfully.' => 'Felhasználó sikeresen törölve.', + 'Unable to remove this user.' => 'Felhasználó törlése sikertelen.', + 'Board updated successfully.' => 'Tábla sikeresen frissítve.', + 'Ready' => 'Előkészítés', + 'Backlog' => 'Napló', + 'Work in progress' => 'Folyamatban', + 'Done' => 'Kész', + 'Application version:' => 'Alkalmazás verzió:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Elkészült: %Y. %m. %d. %H:%M', + '%B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M', + 'Date created' => 'Létrehozás időpontja', + 'Date completed' => 'Befejezés időpontja', + 'Id' => 'ID', + 'No task' => 'Nincs feladat', + 'Completed tasks' => 'Elvégzett feladatok', + 'List of projects' => 'Projektek listája', + 'Completed tasks for "%s"' => 'Elvégzett feladatok: %s', + '%d closed tasks' => '%d lezárt feladat', + 'No task for this project' => 'Nincs feladat ebben a projektben', + 'Public link' => 'Nyilvános link', + 'There is no column in your project!' => 'Nincs oszlop a projektben!', + 'Change assignee' => 'Felelős módosítása', + 'Change assignee for the task "%s"' => 'Feladat felelősének módosítása: "%s"', + 'Timezone' => 'Időzóna', + 'Sorry, I didn\'t find this information in my database!' => 'Ez az információ nem található az adatbázisban!', + 'Page not found' => 'Az oldal nem található', + 'Complexity' => 'Bonyolultság', + 'limit' => 'határ', + 'Task limit' => 'Maximális számú feladat', + 'Task count' => 'Feladatok száma', + 'This value must be greater than %d' => 'Az értéknek nagyobbnak kell lennie, mint %d', + 'Edit project access list' => 'Projekt hozzáférés módosítása', + 'Edit users access' => 'Felhasználók hozzáférésének módosítása', + 'Allow this user' => 'Engedélyezi ezt a felhasználót', + 'Only those users have access to this project:' => 'Csak ezek a felhasználók férhetnek hozzá a projekthez:', + 'Don\'t forget that administrators have access to everything.' => 'Ne felejtsük el: a rendszergazdák mindenhez hozzáférnek.', + 'Revoke' => 'Visszavon', + 'List of authorized users' => 'Az engedélyezett felhasználók', + 'User' => 'Felhasználó', + 'Nobody have access to this project.' => 'Senkinek sincs hozzáférése a projekthez.', + 'You are not allowed to access to this project.' => 'Nincs hozzáférési joga a projekthez.', + 'Comments' => 'Hozzászólások', + 'Post comment' => 'Hozzászólás elküldése', + 'Write your text in Markdown' => 'Írja be a szöveget Markdown szintaxissal', + 'Leave a comment' => 'Írjon hozzászólást ...', + 'Comment is required' => 'A hozzászólás mező kötelező', + 'Leave a description' => 'Írjon leírást ...', + 'Comment added successfully.' => 'Hozzászólás sikeresen elküldve.', + 'Unable to create your comment.' => 'Hozzászólás létrehozása nem lehetséges.', + 'The description is required' => 'A leírás szükséges', + 'Edit this task' => 'Feladat módosítása', + 'Due Date' => 'Határidő', + 'Invalid date' => 'Érvénytelen dátum', + 'Must be done before %B %e, %Y' => 'Kész kell lennie %Y. %m. %d. előtt', + '%B %e, %Y' => '%Y. %m. %d.', + '%b %e, %Y' => '%Y. %m. %d.', + 'Automatic actions' => 'Automatikus intézkedések', + 'Your automatic action have been created successfully.' => 'Az automatikus intézkedés sikeresen elkészült.', + 'Unable to create your automatic action.' => 'Automatikus intézkedés létrehozása nem lehetséges.', + 'Remove an action' => 'Intézkedés törlése', + 'Unable to remove this action.' => 'Intézkedés törlése nem lehetséges.', + 'Action removed successfully.' => 'Intézkedés sikeresen törölve.', + 'Automatic actions for the project "%s"' => 'Automatikus intézkedések a projektben: "%s"', + 'Defined actions' => 'Intézkedések', + 'Add an action' => 'Intézkedés létrehozása', + 'Event name' => 'Esemény neve', + 'Action name' => 'Intézkedés neve', + 'Action parameters' => 'Intézkedés paraméterei', + 'Action' => 'Intézkedés', + 'Event' => 'Esemény', + 'When the selected event occurs execute the corresponding action.' => 'Ha a kiválasztott esemény bekövetkezik, hajtsa végre a megfelelő intézkedéseket.', + 'Next step' => 'Következő lépés', + 'Define action parameters' => 'Határozza meg az intézkedés paramétereit', + 'Save this action' => 'Intézkedés mentése', + 'Do you really want to remove this action: "%s"?' => 'Valóban törölni akarja ezt az intézkedést: "%s"?', + 'Remove an automatic action' => 'Automatikus intézkedés törlése', + 'Close the task' => 'Feladat lezárása', + 'Assign the task to a specific user' => 'Feladat kiosztása megadott felhasználónak', + 'Assign the task to the person who does the action' => 'Feladat kiosztása az intézkedő személynek', + 'Duplicate the task to another project' => 'Feladat másolása másik projektbe', + 'Move a task to another column' => 'Feladat mozgatása másik oszlopba', + 'Move a task to another position in the same column' => 'Feladat mozgatása oszlopon belül', + 'Task modification' => 'Feladat módosítása', + 'Task creation' => 'Feladat létrehozása', + 'Open a closed task' => 'Lezárt feladat felnyitása', + 'Closing a task' => 'Feladat lezárása', + 'Assign a color to a specific user' => 'Szín hozzárendelése a felhasználóhoz', + 'Column title' => 'Oszlopfejléc', + 'Position' => 'Pozíció', + 'Move Up' => 'Fel', + 'Move Down' => 'Le', + 'Duplicate to another project' => 'Másolás másik projektbe', + 'Duplicate' => 'Másolás', + 'link' => 'link', + 'Update this comment' => 'Hozzászólás frissítése', + 'Comment updated successfully.' => 'Megjegyzés sikeresen frissítve.', + 'Unable to update your comment.' => 'Megjegyzés frissítése sikertelen.', + 'Remove a comment' => 'Megjegyzés törlése', + 'Comment removed successfully.' => 'Megjegyzés sikeresen törölve.', + 'Unable to remove this comment.' => 'Megjegyzés törölése nem lehetséges.', + 'Do you really want to remove this comment?' => 'Valóban törölni szeretné ezt a megjegyzést?', + 'Only administrators or the creator of the comment can access to this page.' => 'Csak a rendszergazdák és a megjegyzés létrehozója férhet hozzá az oldalhoz.', + 'Details' => 'Részletek', + 'Current password for the user "%s"' => 'Felhasználó jelenlegi jelszava: "%s"', + 'The current password is required' => 'A jelenlegi jelszót meg kell adni', + 'Wrong password' => 'Hibás jelszó', + 'Reset all tokens' => 'Reseteld az összes tokent', + 'All tokens have been regenerated.' => 'Minden token újra lett generálva.', + 'Unknown' => 'Ismeretlen', + 'Last logins' => 'Legutóbbi bejelentkezések', + 'Login date' => 'Bejelentkezés dátuma', + 'Authentication method' => 'Azonosítási módszer', + 'IP address' => 'IP-cím', + 'User agent' => 'User Agent', + 'Persistent connections' => 'Tartós (perzisztens) kapcsolatok', + 'No session.' => 'Nincs session.', + 'Expiration date' => 'Lejárati dátum', + 'Remember Me' => 'Emlékezz rám', + 'Creation date' => 'Létrehozás dátuma', + 'Filter by user' => 'Szűrés felhasználó szerint', + 'Filter by due date' => 'Szűrés határidő szerint', + 'Everybody' => 'Minden felhasználó', + 'Open' => 'Nyitott', + 'Closed' => 'Lezárt', + 'Search' => 'Keresés', + 'Nothing found.' => 'Nincs találat.', + 'Search in the project "%s"' => 'Keresés a projektben: "%s"', + 'Due date' => 'Határidő', + 'Others formats accepted: %s and %s' => 'Egyéb érvényes formátumok: "%s" és "%s"', + 'Description' => 'Leírás', + '%d comments' => '%d megjegyzés', + '%d comment' => '%d megjegyzés', + 'Email address invalid' => 'Érvénytelen e-mail cím', + 'Your Google Account is not linked anymore to your profile.' => 'Google Fiók már nincs a profilhoz kapcsolva.', + 'Unable to unlink your Google Account.' => 'Leválasztás a Google fiókról nem lehetséges.', + 'Google authentication failed' => 'Google azonosítás sikertelen', + 'Unable to link your Google Account.' => 'A Google profilhoz kapcsolás sikertelen.', + 'Your Google Account is linked to your profile successfully.' => 'Google fiókkal sikeresen összekapcsolva.', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Kapcsold össze a Google fiókkal', + 'Unlink my Google Account' => 'Válaszd le a Google fiókomat', + 'Login with my Google Account' => 'Jelentkezzen be Google fiókkal', + 'Project not found.' => 'A projekt nem található.', + 'Task #%d' => 'Feladat #%d.', + 'Task removed successfully.' => 'Feladat sikeresen törölve.', + 'Unable to remove this task.' => 'A feladatot nem lehet törölni.', + 'Remove a task' => 'Feladat törlése', + 'Do you really want to remove this task: "%s"?' => 'Valóban törölni akarja ezt a feladatot: "%s"?', + 'Assign automatically a color based on a category' => 'Szín hozzárendelése automatikusan kategória alapján', + 'Assign automatically a category based on a color' => 'Kategória hozzárendelése automatikusan szín alapján', + 'Task creation or modification' => 'Feladat létrehozása vagy módosítása', + 'Category' => 'Kategória', + 'Category:' => 'Kategória:', + 'Categories' => 'Kategóriák', + 'Category not found.' => 'Kategória nem található.', + 'Your category have been created successfully.' => 'Kategória sikeresen létrehozva.', + 'Unable to create your category.' => 'A kategória létrehozása nem lehetséges.', + 'Your category have been updated successfully.' => 'Kategória sikeresen frissítve.', + 'Unable to update your category.' => 'Kategória frissítése nem lehetséges.', + 'Remove a category' => 'Kategória törlése', + 'Category removed successfully.' => 'Kategória törlése megtörtént.', + 'Unable to remove this category.' => 'A kategória törlése nem lehetséges.', + 'Category modification for the project "%s"' => 'Kategória módosítása a projektben "%s"', + 'Category Name' => 'Kategória neve', + 'Categories for the project "%s"' => 'Projekt kategóriák "%s"', + 'Add a new category' => 'Új kategória', + 'Do you really want to remove this category: "%s"?' => 'Valóban törölni akarja ezt a kategóriát: "%s"?', + 'Filter by category' => 'Szűrés kategória szerint', + 'All categories' => 'Minden kategória', + 'No category' => 'Nincs kategória', + 'The name is required' => 'A név megadása kötelező', + 'Remove a file' => 'Fájl törlése', + 'Unable to remove this file.' => 'Fájl törlése nem lehetséges.', + 'File removed successfully.' => 'Fájl sikeresen törölve.', + 'Attach a document' => 'Fájl csatolása', + 'Do you really want to remove this file: "%s"?' => 'Valóban törölni akarja a fájlt: "%s"?', + 'open' => 'nyitott', + 'Attachments' => 'Mellékletek', + 'Edit the task' => 'Feladat módosítása', + 'Edit the description' => 'Leírás szerkesztése', + 'Add a comment' => 'Új megjegyzés', + 'Edit a comment' => 'Megjegyzés szerkesztése', + 'Summary' => 'Összegzés', + 'Time tracking' => 'Idő követés', + 'Estimate:' => 'Becsült:', + 'Spent:' => 'Eltöltött:', + 'Do you really want to remove this sub-task?' => 'Valóban törölni akarja ezt a részfeladatot?', + 'Remaining:' => 'Hátralévő:', + 'hours' => 'óra', + 'spent' => 'eltöltött', + 'estimated' => 'becsült', + 'Sub-Tasks' => 'Részfeladatok', + 'Add a sub-task' => 'Részfeladat létrehozása', + 'Original estimate' => 'Eredeti időbecslés', + 'Create another sub-task' => 'További részfeladat létrehozása', + 'Time spent' => 'Eltöltött idő', + 'Edit a sub-task' => 'Részfeladat szerkesztése', + 'Remove a sub-task' => 'Részfeladat törlése', + 'The time must be a numeric value' => 'Idő csak számérték lehet', + 'Todo' => 'Teendő', + 'In progress' => 'Folyamatban', + 'Sub-task removed successfully.' => 'Részfeladat sikeresen törölve.', + 'Unable to remove this sub-task.' => 'Részfeladat törlése nem lehetséges.', + 'Sub-task updated successfully.' => 'Részfeladat sikeresen frissítve.', + 'Unable to update your sub-task.' => 'Részfeladat frissítése nem lehetséges.', + 'Unable to create your sub-task.' => 'Részfeladat létrehozása nem lehetséges.', + 'Sub-task added successfully.' => 'Részfeladat sikeresen létrehozva.', + 'Maximum size: ' => 'Maximális méret: ', + 'Unable to upload the file.' => 'Fájl feltöltése nem lehetséges.', + 'Display another project' => 'Másik projekt megjelenítése', + 'Your GitHub account was successfully linked to your profile.' => 'GitHub fiók sikeresen csatolva a profilhoz.', + 'Unable to link your GitHub Account.' => 'Nem lehet csatolni a GitHub fiókot.', + 'GitHub authentication failed' => 'GitHub azonosítás sikertelen', + 'Your GitHub account is no longer linked to your profile.' => 'GitHub fiók már nincs profilhoz kapcsolva.', + 'Unable to unlink your GitHub Account.' => 'GitHub fiók leválasztása nem lehetséges.', + 'Login with my GitHub Account' => 'Jelentkezzen be GitHub fiókkal', + 'Link my GitHub Account' => 'GitHub fiók csatolása', + 'Unlink my GitHub Account' => 'GitHub fiók leválasztása', + 'Created by %s' => 'Készítette: %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Utolsó módosítás: %Y. %m. %d. %H:%M', + 'Tasks Export' => 'Feladatok exportálása', + 'Tasks exportation for "%s"' => 'Feladatok exportálása: "%s"', + 'Start Date' => 'Kezdés dátuma', + 'End Date' => 'Befejezés dátuma', + 'Execute' => 'Végrehajt', + 'Task Id' => 'Feladat ID', + 'Creator' => 'Készítette', + 'Modification date' => 'Módosítás dátuma', + 'Completion date' => 'Befejezés határideje', + 'Clone' => 'Másolat', + 'Clone Project' => 'Projekt másolása', + 'Project cloned successfully.' => 'A projekt sikeresen másolva.', + 'Unable to clone this project.' => 'A projekt másolása sikertelen.', + 'Email notifications' => 'E-mail értesítések', + 'Enable email notifications' => 'E-mail értesítések engedélyezése', + 'Task position:' => 'Feladat helye:', + 'The task #%d have been opened.' => 'Feladat #%d megnyitva.', + 'The task #%d have been closed.' => 'Feladat #%d lezárva.', + 'Sub-task updated' => 'Részfeladat frissítve', + 'Title:' => 'Cím', + 'Status:' => 'Állapot:', + 'Assignee:' => 'Felelős:', + 'Time tracking:' => 'Idő követés:', + 'New sub-task' => 'Új részfeladat', + 'New attachment added "%s"' => 'Új melléklet "%s" hozzáadva.', + 'Comment updated' => 'Megjegyzés frissítve', + 'New comment posted by %s' => 'Új megjegyzés %s', + 'List of due tasks for the project "%s"' => 'Projekt esedékes feladatai: "%s"', + 'New attachment' => 'Új melléklet', + 'New comment' => 'Új megjegyzés', + 'New subtask' => 'Új részfeladat', + 'Subtask updated' => 'Részfeladat frissítve', + 'Task updated' => 'Feladat frissítve', + 'Task closed' => 'Feladat lezárva', + 'Task opened' => 'Feladat megnyitva', + '[%s][Due tasks]' => '[%s] [Esedékes feladatok]', + '[Kanboard] Notification' => '[Kanboard] értesítés', + 'I want to receive notifications only for those projects:' => 'Csak ezekről a projektekről kérek értesítést:', + 'view the task on Kanboard' => 'feladat megtekintése a Kanboardon', + 'Public access' => 'Nyilvános hozzáférés', + 'Category management' => 'Kategóriák kezelése', + 'User management' => 'Felhasználók kezelése', + 'Active tasks' => 'Aktív feladatok', + 'Disable public access' => 'Nyilvános hozzáférés letiltása', + 'Enable public access' => 'Nyilvános hozzáférés engedélyezése', + 'Active projects' => 'Aktív projektek', + 'Inactive projects' => 'Inaktív projektek', + 'Public access disabled' => 'Nyilvános hozzáférés letiltva', + 'Do you really want to disable this project: "%s"?' => 'Tényleg szeretné letiltani ezt a projektet: "%s"', + 'Do you really want to duplicate this project: "%s"?' => 'Tényleg szeretné megkettőzni ezt a projektet: "%s"', + 'Do you really want to enable this project: "%s"?' => 'Tényleg szeretné engedélyezni ezt a projektet: "%s"', + 'Project activation' => 'Projekt aktiválás', + 'Move the task to another project' => 'Feladat áthelyezése másik projektbe', + 'Move to another project' => 'Áthelyezés másik projektbe', + 'Do you really want to duplicate this task?' => 'Tényleg szeretné megkettőzni ezt a feladatot?', + 'Duplicate a task' => 'Feladat másolása', + 'External accounts' => 'Külső fiókok', + 'Account type' => 'Fiók típusa', + 'Local' => 'Helyi', + 'Remote' => 'Távoli', + 'Enabled' => 'Engedélyezve', + 'Disabled' => 'Letiltva', + 'Google account linked' => 'Google fiók összekapcsolva', + 'Github account linked' => 'GitHub fiók összekapcsolva', + 'Username:' => 'Felhasználónév:', + 'Name:' => 'Név:', + 'Email:' => 'E-mail:', + 'Default project:' => 'Alapértelmezett projekt:', + 'Notifications:' => 'Értesítések:', + 'Notifications' => 'Értesítések', + 'Group:' => 'Csoport:', + 'Regular user' => 'Default User', + 'Account type:' => 'Fiók típusa:', + 'Edit profile' => 'Profil szerkesztése', + 'Change password' => 'Jelszó módosítása', + 'Password modification' => 'Jelszó módosítása', + 'External authentications' => 'Külső azonosítás', + 'Google Account' => 'Google fiók', + 'Github Account' => 'Github fiók', + 'Never connected.' => 'Sosem csatlakozva.', + 'No account linked.' => 'Nincs csatlakoztatott fiók.', + 'Account linked.' => 'Fiók csatlakoztatva.', + 'No external authentication enabled.' => 'A külső azonosítás nincs engedélyezve.', + 'Password modified successfully.' => 'A jelszó sikeresen módosítva.', + 'Unable to change the password.' => 'A jelszó módosítása sikertelen.', + 'Change category for the task "%s"' => 'Feladat kategória módosítása "%s"', + 'Change category' => 'Kategória módosítása', + '%s updated the task %s' => '%s frissítette a feladatot %s', + '%s opened the task %s' => '%s megnyitott a feladatot %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s átmozgatta a feladatot %s #%d pozícióba a "%s" oszlopban', + '%s moved the task %s to the column "%s"' => '%s átmozgatta a feladatot %s "%s" oszlopba', + '%s created the task %s' => '%s létrehozta a feladatot %s', + '%s closed the task %s' => '%s lezárta a feladatot %s', + '%s created a subtask for the task %s' => '%s létrehozott egy részfeladat a feladathoz %s', + '%s updated a subtask for the task %s' => '%s frissített egy részfeladatot a feladathoz %s', + 'Assigned to %s with an estimate of %s/%sh' => '%s-nek kiosztva %s/%s óra becsült idő mellett', + 'Not assigned, estimate of %sh' => 'Nincs kiosztva, becsült idő: %s óra', + '%s updated a comment on the task %s' => '%s frissítette a megjegyzését a feladatban %s', + '%s commented the task %s' => '%s megjegyzést fűzött a feladathoz %s', + '%s\'s activity' => 'Tevékenységek: %s', + 'No activity.' => 'Nincs tevékenység.', + 'RSS feed' => 'RSS feed', + '%s updated a comment on the task #%d' => '%s frissített egy megjegyzést a feladatban #%d', + '%s commented on the task #%d' => '%s megjegyzést tett a feladathoz #%d', + '%s updated a subtask for the task #%d' => '%s frissített egy részfeladatot a feladatban #%d', + '%s created a subtask for the task #%d' => '%s létrehozott egy részfeladatot a feladatban #%d', + '%s updated the task #%d' => '%s frissítette a feladatot #%d', + '%s created the task #%d' => '%s létrehozta a feladatot #%d', + '%s closed the task #%d' => '%s lezárta a feladatot #%d', + '%s open the task #%d' => '%s megnyitotta a feladatot #%d', + '%s moved the task #%d to the column "%s"' => '%s átmozgatta a feladatot #%d a "%s" oszlopba', + '%s moved the task #%d to the position %d in the column "%s"' => '%s átmozgatta a feladatot #%d a %d pozícióba a "%s" oszlopban', + 'Activity' => 'Tevékenységek', + 'Default values are "%s"' => 'Az alapértelmezett értékek: %s', + 'Default columns for new projects (Comma-separated)' => 'Alapértelmezett oszlopok az új projektekben (vesszővel elválasztva)', + 'Task assignee change' => 'Felelős módosítása', + '%s change the assignee of the task #%d to %s' => '%s a felelőst módosította #%d %s', + '%s changed the assignee of the task %s to %s' => '%s a felelőst %s módosította: %s', + 'Column Change' => 'Oszlop változtatás', + 'Position Change' => 'Pozíció változtatás', + 'Assignee Change' => 'Felelős változtatás', + 'New password for the user "%s"' => 'Felhasználó új jelszava: %s', + 'Choose an event' => 'Válasszon eseményt', + 'Github commit received' => 'GitHub commit érkezett', + 'Github issue opened' => 'GitHub issue nyitás', + 'Github issue closed' => 'GitHub issue zárás', + 'Github issue reopened' => 'GitHub issue újranyitva', + 'Github issue assignee change' => 'GitHub issue felelős változás', + 'Github issue label change' => 'GitHub issue címke változás', + 'Create a task from an external provider' => 'Feladat létrehozása külsős számára', + 'Change the assignee based on an external username' => 'Felelős módosítása külső felhasználónév alapján', + 'Change the category based on an external label' => 'Kategória módosítása külső címke alapján', + 'Reference' => 'Hivatkozás', + 'Reference: %s' => 'Hivatkozás: %s', + 'Label' => 'Címke', + 'Database' => 'Adatbázis', + 'About' => 'Kanboard információ', + 'Database driver:' => 'Adatbázis motor:', + 'Board settings' => 'Tábla beállítások', + 'URL and token' => 'URL és tokenek', + 'Webhook settings' => 'Webhook beállítások', + 'URL for task creation:' => 'Feladat létrehozás URL:', + 'Reset token' => 'Token újragenerálása', + 'API endpoint:' => 'API végpont:', + 'Refresh interval for private board' => 'Privát táblák frissítési intervalluma', + 'Refresh interval for public board' => 'Nyilvános táblák frissítési intervalluma', + 'Task highlight period' => 'Feladat kiemelés időtartama', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Mennyi ideig tekintendő egy feladat "mostanában" módosítottnak (másodpercben) (0: funkció letiltva, alapértelmezés szerint 2 nap)', + 'Frequency in second (60 seconds by default)' => 'Gyakoriság másodpercben (alapértelmezetten 60 másodperc)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Gyakoriság másodpercben (0 funkció letiltva, alapértelmezetten 10 másodperc)', + 'Application URL' => 'Alkalmazás URL', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Példa: http://example.kanboard.net/ (e-mail értesítőben használt)', + 'Token regenerated.' => 'Token újragenerálva.', + 'Date format' => 'Dátum formátum', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formátum mindig elfogadott, pl: "%s" és "%s"', + 'New private project' => 'Új privát projekt', + 'This project is private' => 'Ez egy privát projekt', + 'Type here to create a new sub-task' => 'Ide írva létrehozhat egy új részfeladatot', + 'Add' => 'Hozzáadás', + 'Estimated time: %s hours' => 'Becsült idő: %s óra', + 'Time spent: %s hours' => 'Eltöltött idő: %s óra', + 'Started on %B %e, %Y' => 'Elkezdve: %Y. %m. %d.', + 'Start date' => 'Kezdés dátuma', + 'Time estimated' => 'Becsült időtartam', + 'There is nothing assigned to you.' => 'Nincs kiosztott feladat.', + 'My tasks' => 'Feladataim', + 'Activity stream' => 'Legutóbbi tevékenységek', + 'Dashboard' => 'Vezérlőpult', + 'Confirmation' => 'Megerősítés', + 'Allow everybody to access to this project' => 'A projekt elérése mindenkinek engedélyezett', + 'Everybody have access to this project.' => 'Mindenki elérheti a projektet', + 'Webhooks' => 'Webhook', + 'API' => 'API', + 'Integration' => 'Integráció', + 'Github webhooks' => 'Github webhooks', + 'Help on Github webhooks' => 'Github Webhook súgó', + 'Create a comment from an external provider' => 'Megjegyzés létrehozása külső felhasználótól', + 'Github issue comment created' => 'Github issue megjegyzés létrehozva', + 'Configure' => 'Beállítások', + 'Project management' => 'Projekt menedzsment', + 'My projects' => 'Projektjeim', + 'Columns' => 'Oszlopok', + 'Task' => 'Feladat', + 'Your are not member of any project.' => 'Ön nem tagja projektnek.', + 'Percentage' => 'Százalék', + 'Number of tasks' => 'A feladatok száma', + 'Task distribution' => 'Feladatelosztás', + 'Reportings' => 'Jelentések', + 'Task repartition for "%s"' => 'Feladat újraosztása: %s', + 'Analytics' => 'Analitika', + 'Subtask' => 'Részfeladat', + 'My subtasks' => 'Részfeladataim', + 'User repartition' => 'Felhasználó újrafelosztás', + 'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s', + 'Clone this project' => 'Projekt másolása', + 'Column removed successfully.' => 'Oszlop sikeresen törölve.', + 'Edit Project' => 'Projekt szerkesztése', + 'Github Issue' => 'Github issue', + 'Not enough data to show the graph.' => 'Nincs elég adat a grafikonhoz.', + 'Previous' => 'Előző', + 'The id must be an integer' => 'Az ID csak egész szám lehet', + 'The project id must be an integer' => 'A projekt ID csak egész szám lehet', + 'The status must be an integer' => 'Az állapot csak egész szám lehet', + 'The subtask id is required' => 'A részfeladat ID-t meg kell adni', + 'The subtask id must be an integer' => 'A részfeladat ID csak egész szám lehet', + 'The task id is required' => 'A feladat ID-t meg kell adni', + 'The task id must be an integer' => 'A feladat ID csak egész szám lehet', + 'The user id must be an integer' => 'A felhasználói ID csak egész szám lehet', + 'This value is required' => 'Ez a mező kötelező', + 'This value must be numeric' => 'Ez a mező csak szám lehet', + 'Unable to create this task.' => 'A feladat nem hozható létre,', + 'Cumulative flow diagram' => 'Kumulatív Flow Diagram', + 'Cumulative flow diagram for "%s"' => 'Kumulatív Flow Diagram: %s', + 'Daily project summary' => 'Napi projektösszefoglaló', + 'Daily project summary export' => 'Napi projektösszefoglaló exportálása', + 'Daily project summary export for "%s"' => 'Napi projektösszefoglaló exportálása: %s', + 'Exports' => 'Exportálások', + 'This export contains the number of tasks per column grouped per day.' => 'Ez az export tartalmazza a feladatok számát oszloponként összesítve, napokra lebontva.', + 'Nothing to preview...' => 'Nincs semmi az előnézetben ...', + 'Preview' => 'Előnézet', + 'Write' => 'Szerkesztés', + 'Active swimlanes' => 'Aktív folyamatok', + 'Add a new swimlane' => 'Új folyamat', + 'Change default swimlane' => 'Alapértelmezett folyamat változtatás', + 'Default swimlane' => 'Alapértelmezett folyamat', + 'Do you really want to remove this swimlane: "%s"?' => 'Valóban törli a folyamatot:%s ?', + 'Inactive swimlanes' => 'Inaktív folyamatok', + 'Set project manager' => 'Beállítás projekt kezelőnek', + 'Set project member' => 'Beállítás projekt felhasználónak', + 'Remove a swimlane' => 'Folyamat törlés', + 'Rename' => 'Átnevezés', + 'Show default swimlane' => 'Alapértelmezett folyamat megjelenítése', + 'Swimlane modification for the project "%s"' => '%s projekt folyamatainak módosítása', + 'Swimlane not found.' => 'Folyamat nem található', + 'Swimlane removed successfully.' => 'Folyamat sikeresen törölve.', + 'Swimlanes' => 'Folyamatok', + 'Swimlane updated successfully.' => 'Folyamat sikeresn frissítve', + 'The default swimlane have been updated successfully.' => 'Az alapértelmezett folyamat sikeresen frissítve.', + 'Unable to create your swimlane.' => 'A folyamat létrehozása sikertelen.', + 'Unable to remove this swimlane.' => 'A folyamat törlése sikertelen.', + 'Unable to update this swimlane.' => 'A folyamat frissítése sikertelen.', + 'Your swimlane have been created successfully.' => 'A folyamat sikeresen létrehozva.', + 'Example: "Bug, Feature Request, Improvement"' => 'Például: Hiba, Új funkció, Fejlesztés', + 'Default categories for new projects (Comma-separated)' => 'Alapértelmezett kategóriák az új projektekben (Vesszővel elválasztva)', + 'Gitlab commit received' => 'Gitlab commit érkezett', + 'Gitlab issue opened' => 'Gitlab issue nyitás', + 'Gitlab issue closed' => 'Gitlab issue zárás', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Gitlab webhooks súgó', + 'Integrations' => 'Integráció', + 'Integration with third-party services' => 'Integráció harmadik féllel', + 'Role for this project' => 'Projekt szerepkör', + 'Project manager' => 'Projekt kezelő', + 'Project member' => 'Projekt felhasználó', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'A projekt kezelő képes megváltoztatni a projekt beállításait és több joggal rendelkezik mint az alap felhasználók.', + 'Gitlab Issue' => 'Gitlab issue', + 'Subtask Id' => 'Részfeladat id', + 'Subtasks' => 'Részfeladatok', + 'Subtasks Export' => 'Részfeladat exportálás', + 'Subtasks exportation for "%s"' => 'Részfeladatok exportálása: %s', + 'Task Title' => 'Feladat címe', + 'Untitled' => 'Névtelen', + 'Application default' => 'Alkalmazás alapértelmezett', + 'Language:' => 'Nyelv:', + 'Timezone:' => 'Időzóna:', + 'All columns' => 'Minden oszlop', + 'Calendar for "%s"' => 'Naptár: %s', + 'Filter by column' => 'Szűrés oszlop szerint', + 'Filter by status' => 'Szűrés állapot szerint', + 'Calendar' => 'Naptár', + 'Next' => 'Következő', + '#%d' => '#%d', + 'Filter by color' => 'Szűrés szín szerint', + 'Filter by swimlane' => 'Szűrés folyamat szerint', + 'All swimlanes' => 'Minden folyamat', + 'All colors' => 'Minden szín', + 'All status' => 'Minden állapot', + 'Add a comment logging moving the task between columns' => 'Feladat oszlopok közötti mozgatását megjegyzésben feltüntetni', + 'Moved to column %s' => '%s oszlopba áthelyezve', + 'Change description' => 'Leírás szerkesztés', + 'User dashboard' => 'Felhasználói vezérlőpult', + 'Allow only one subtask in progress at the same time for a user' => 'Egyszerre csak egy folyamatban levő részfeladat engedélyezése a felhasználóknak', + 'Edit column "%s"' => 'Oszlop szerkesztés: %s', + 'Enable time tracking for subtasks' => 'Idő követés engedélyezése a részfeladatokhoz', + 'Select the new status of the subtask: "%s"' => 'Részfeladat állapot változtatás: %s', + 'Subtask timesheet' => 'Részfeladat idővonal', + 'There is nothing to show.' => 'Nincs megjelenítendő adat.', + 'Time Tracking' => 'Idő követés', + 'You already have one subtask in progress' => 'Már van egy folyamatban levő részfeladata', + 'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné másolni?', + 'Change dashboard view' => 'Vezérlőpult megjelenés változtatás', + 'Show/hide activities' => 'Tevékenységek megjelenítése/elrejtése', + 'Show/hide projects' => 'Projektek megjelenítése/elrejtése', + 'Show/hide subtasks' => 'Részfeladatok megjelenítése/elrejtése', + 'Show/hide tasks' => 'Feladatok megjelenítése/elrejtése', + 'Disable login form' => 'Bejelentkező képernyő tiltása', + 'Show/hide calendar' => 'Naptár megjelenítés/elrejtés', + 'User calendar' => 'Naptár', + 'Bitbucket commit received' => 'Bitbucket commit érkezett', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Bitbucket webhooks súgó', + 'Start' => 'Kezdet', + 'End' => 'Vég', + 'Task age in days' => 'Feladat életkora napokban', + 'Days in this column' => 'Napok ebben az oszlopban', + '%dd' => '%dd', + 'Add a link' => 'Hivatkozás hozzáadása', + 'Add a new link' => 'Új hivatkozás hozzáadása', + 'Do you really want to remove this link: "%s"?' => 'Biztos törölni akarja a hivatkozást: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Biztos törölni akarja a(z) #%s. feladatra mutató hivatkozást?', + 'Field required' => 'Kötelező mező', + 'Link added successfully.' => 'Hivatkozás sikeresen létrehozva.', + 'Link updated successfully.' => 'Hivatkozás sikeresen frissítve.', + 'Link removed successfully.' => 'Hivatkozás sikeresen törölve.', + 'Link labels' => 'Hivatkozás címkék', + 'Link modification' => 'Hivatkozás módosítás', + 'Links' => 'Hivatkozások', + 'Link settings' => 'Hivatkozás beállítasok', + 'Opposite label' => 'Ellenekező címke', + 'Remove a link' => 'Hivatkozás törlése', + 'Task\'s links' => 'Feladat hivatkozások', + 'The labels must be different' => 'A címkék nem lehetnek azonosak', + 'There is no link.' => 'Nincs hivatkozás.', + 'This label must be unique' => 'A címkének egyedinek kell lennie.', + 'Unable to create your link.' => 'Hivatkozás létrehozása sikertelen.', + 'Unable to update your link.' => 'Hivatkozás frissítése sikertelen.', + 'Unable to remove this link.' => 'Hivatkozás törlése sikertelen.', + 'relates to' => 'hozzá tartozik:', + 'blocks' => 'letiltva:', + 'is blocked by' => 'letitoltta:', + 'duplicates' => 'eredeti:', + 'is duplicated by' => 'másolat:', + 'is a child of' => 'szülője:', + 'is a parent of' => 'gyermeke:', + 'targets milestone' => 'megcélzott mérföldkő:', + 'is a milestone of' => 'ehhez a mérföldkőhöz tartozik:', + 'fixes' => 'javítás:', + 'is fixed by' => 'javította:', + 'This task' => 'Ez a feladat', + '<1h' => '<1ó', + '%dh' => '%dó', + '%b %e' => '%b %e', + 'Expand tasks' => 'Feladatok lenyitása', + 'Collapse tasks' => 'Feladatok összecsukása', + 'Expand/collapse tasks' => 'Feladatok lenyitása/összecsukása', + 'Close dialog box' => 'Ablak bezárása', + 'Submit a form' => 'Űrlap beküldése', + 'Board view' => 'Tábla nézet', + 'Keyboard shortcuts' => 'Billentyű kombinációk', + 'Open board switcher' => 'Tábla választó lenyitása', + 'Application' => 'Alkalmazás', + 'Filter recently updated' => 'Szűrés az utolsó módosítás ideje szerint', + 'since %B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M óta', + 'More filters' => 'További szűrők', + 'Compact view' => 'Kompakt nézet', + 'Horizontal scrolling' => 'Vízszintes görgetés', + 'Compact/wide view' => 'Kompakt/széles nézet', + 'No results match:' => 'Nincs találat:', + 'Remove hourly rate' => 'Órabér törlése', + 'Do you really want to remove this hourly rate?' => 'Valóban törölni kívánja az órabért?', + 'Hourly rates' => 'Órabérek', + 'Hourly rate' => 'Órabér', + 'Currency' => 'Pénznem', + 'Effective date' => 'Hatálybalépés ideje', + 'Add new rate' => 'Új bér', + 'Rate removed successfully.' => 'Bér sikeresen törölve.', + 'Unable to remove this rate.' => 'Bér törlése sikertelen.', + 'Unable to save the hourly rate.' => 'Órabér mentése sikertelen.', + 'Hourly rate created successfully.' => 'Órabér sikeresen mentve.', + 'Start time' => 'Kezdés ideje', + 'End time' => 'Végzés ideje', + 'Comment' => 'Megjegyzés', + 'All day' => 'Egész nap', + 'Day' => 'Nap', + 'Manage timetable' => 'Időbeosztás kezelése', + 'Overtime timetable' => 'Túlóra időbeosztás', + 'Time off timetable' => 'Szabadság időbeosztás', + 'Timetable' => 'Időbeosztás', + 'Work timetable' => 'Munka időbeosztás', + 'Week timetable' => 'Heti időbeosztás', + 'Day timetable' => 'Napi időbeosztás', + 'From' => 'Feladó:', + 'To' => 'Címzett:', + 'Time slot created successfully.' => 'Időszelet sikeresen létrehozva.', + 'Unable to save this time slot.' => 'Időszelet mentése sikertelen.', + 'Time slot removed successfully.' => 'Időszelet sikeresen törölve.', + 'Unable to remove this time slot.' => 'Időszelet törlése sikertelen.', + 'Do you really want to remove this time slot?' => 'Biztos törli ezt az időszeletet?', + 'Remove time slot' => 'Időszelet törlése', + 'Add new time slot' => 'Új Időszelet', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ez az időbeosztás van használatban ha az "egész nap" jelölőnégyzet be van jelölve a tervezett szabadságnál és túlóránál.', + 'Files' => 'Fájlok', + 'Images' => 'Képek', + 'Private project' => 'Privát projekt', + 'Amount' => 'Összeg', + 'AUD - Australian Dollar' => 'AUD - Ausztrál dollár', + 'Budget' => 'Költségvetés', + 'Budget line' => 'Költségvetési tétel', + 'Budget line removed successfully.' => 'Költségvetési tétel sikeresen törölve.', + 'Budget lines' => 'Költségvetési tételek', + 'CAD - Canadian Dollar' => 'CAD - Kanadai dollár', + 'CHF - Swiss Francs' => 'CHF - Svájci frank', + 'Cost' => 'Költség', + 'Cost breakdown' => 'Költség visszaszámlálás', + 'Custom Stylesheet' => 'Egyéni sítluslap', + 'download' => 'letöltés', + 'Do you really want to remove this budget line?' => 'Biztos törölni akarja ezt a költségvetési tételt?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Kiadások', + 'GBP - British Pound' => 'GBP - Angol font', + 'INR - Indian Rupee' => 'INR - Indiai rúpia', + 'JPY - Japanese Yen' => 'JPY - Japán Yen', + 'New budget line' => 'Új költségvetési tétel', + 'NZD - New Zealand Dollar' => 'NZD - Új-Zélandi dollár', + 'Remove a budget line' => 'Költségvetési tétel törlése', + 'Remove budget line' => 'Költségvetési tétel törlése', + 'RSD - Serbian dinar' => 'RSD - Szerb dínár', + 'The budget line have been created successfully.' => 'Költségvetési tétel sikeresen létrehozva.', + 'Unable to create the budget line.' => 'Költségvetési tétel létrehozása sikertelen.', + 'Unable to remove this budget line.' => 'Költségvetési tétel törlése sikertelen.', + 'USD - US Dollar' => 'USD - Amerikai ollár', + 'Remaining' => 'Maradék', + 'Destination column' => 'Cél oszlop', + 'Move the task to another column when assigned to a user' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés után', + 'Move the task to another column when assignee is cleared' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés törlésekor', + 'Source column' => 'Forrás oszlop', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php new file mode 100644 index 00000000..09ae351d --- /dev/null +++ b/app/Locale/it_IT/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + 'number.decimals_separator' => ',', + 'number.thousands_separator' => '.', + 'None' => 'Nessuno', + 'edit' => 'modificare', + 'Edit' => 'Modificare', + 'remove' => 'cancellare', + 'Remove' => 'Cancellare', + 'Update' => 'Aggiornare', + 'Yes' => 'Si', + 'No' => 'No', + 'cancel' => 'annullare', + 'or' => 'o', + 'Yellow' => 'Giallo', + 'Blue' => 'Blu', + 'Green' => 'Verde', + 'Purple' => 'Viola', + 'Red' => 'Rosso', + 'Orange' => 'Arancione', + 'Grey' => 'Grigio', + 'Save' => 'Salvare', + 'Login' => 'Entra', + 'Official website:' => 'Sito web ufficiale:', + 'Unassigned' => 'Non assegnato', + 'View this task' => 'Vedere questo compito', + 'Remove user' => 'Cancellare un utente', + 'Do you really want to remove this user: "%s"?' => 'Veramente vuoi cancellare questo utente: « %s » ?', + 'New user' => 'Aggiungere un utente', + 'All users' => 'Tutti gli utenti', + 'Username' => 'Nome utente', + 'Password' => 'Password', + 'Default project' => 'Progetto predefinito', + 'Administrator' => 'Amministratore', + 'Sign in' => 'Iscriversi', + 'Users' => 'Utenti', + 'No user' => 'Nessun utente', + 'Forbidden' => 'Vietato', + 'Access Forbidden' => 'Accesso vietato', + 'Only administrators can access to this page.' => 'Solo gli amministratori possono accedere a questa pagina.', + 'Edit user' => 'Modificare un utente', + 'Logout' => 'Uscire', + 'Bad username or password' => 'Utente o password errati', + 'users' => 'utenti', + 'projects' => 'progetti', + 'Edit project' => 'Modificare progetto', + 'Name' => 'Nome', + 'Activated' => 'Attivo', + 'Projects' => 'Progetti', + 'No project' => 'Nessun progetto', + 'Project' => 'Progetto', + 'Status' => 'Stato', + 'Tasks' => 'Compiti', + 'Board' => 'Bacheca', + 'Actions' => 'Azioni', + 'Inactive' => 'Inattivo', + 'Active' => 'Attivo', + 'Column %d' => 'Colonna %d', + 'Add this column' => 'Aggiungere questa colonna', + '%d tasks on the board' => '%d compiti sulla bacheca', + '%d tasks in total' => '%d compiti in totale', + 'Unable to update this board.' => 'Non si può aggiornare questa bacheca.', + 'Edit board' => 'Modificare questa bacheca', + 'Disable' => 'Disattivare', + 'Enable' => 'Attivare', + 'New project' => 'Nuovo progetto', + 'Do you really want to remove this project: "%s"?' => 'Veramente vuoi eliminare questo progetto: « %s » ?', + 'Remove project' => 'Cancellare il progetto', + 'Boards' => 'Bacheche', + 'Edit the board for "%s"' => 'Modificare la bacheca per « %s »', + 'All projects' => 'Tutti i progetti', + 'Change columns' => 'Cambiare le colonne', + 'Add a new column' => 'Aggiungere una nuova colonna', + 'Title' => 'Titolo', + 'Add Column' => 'Aggiungere colonna', + 'Project "%s"' => 'progetto « %s »', + 'Nobody assigned' => 'Nessuno assegnato', + 'Assigned to %s' => 'Assegnato a %s', + 'Remove a column' => 'Cancellare questa colonna', + 'Remove a column from a board' => 'Cancellare una colonna da una bacheca', + 'Unable to remove this column.' => 'Non si può cancellare questa colonna.', + 'Do you really want to remove this column: "%s"?' => 'Veramente desideri cancellare questa colonna : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Questa azione cancellerà TUTTI I COMPITI legati a questa colonna!', + 'Settings' => 'Impostazioni', + 'Application settings' => 'Impostazioni dell\'applicazione', + 'Language' => 'Lingua', + 'Webhook token:' => 'Identificatore (token) per i webhooks :', + 'API token:' => 'Token dell\'API:', + 'More information' => 'Più informazioni', + 'Database size:' => 'Dimensioni della base dati:', + 'Download the database' => 'Scaricare la base dati', + 'Optimize the database' => 'Ottimizare la base dati', + '(VACUUM command)' => '(Comando VACUUM)', + '(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)', + 'User settings' => 'Impostazioni di utente', + 'My default project:' => 'Il mio progetto predefinito:', + 'Close a task' => 'Chiudere un compito', + 'Do you really want to close this task: "%s"?' => 'Veramente desideri chiudere questo compito: « %s » ?', + 'Edit a task' => 'Modificare un compito', + 'Column' => 'colonna', + 'Color' => 'Colore', + 'Assignee' => 'Persona assegnata', + 'Create another task' => 'Creare un nuovo compito', + 'New task' => 'Nuovo compito', + 'Open a task' => 'Aprire un compito', + 'Do you really want to open this task: "%s"?' => 'Veramente desideri aprire questo compito: « %s » ?', + 'Back to the board' => 'Tornare alla bacheca', + 'Created on %B %e, %Y at %k:%M %p' => 'Creato il %B %e, %Y alle %k:%M %p', + 'There is nobody assigned' => 'Non c\'è nessuno assegnato a questo compito', + 'Column on the board:' => 'Colonna sulla bacheca: ', + 'Status is open' => 'Stato aperto', + 'Status is closed' => 'stato chiuso', + 'Close this task' => 'Chiudere questo compito', + 'Open this task' => 'Aprire questo compito', + 'There is no description.' => 'Non c\'è descrizione.', + 'Add a new task' => 'Aggiungere un nuovo compito', + 'The username is required' => 'Si richiede un nome di utente', + 'The maximum length is %d characters' => 'La lunghezza massima è di %d caratteri', + 'The minimum length is %d characters' => 'La lunghezza minima è di %d caratteri', + 'The password is required' => 'Si richiede una password', + 'This value must be an integer' => 'questo valore deve essere un intero', + 'The username must be unique' => 'Il nome di utente deve essere unico', + 'The username must be alphanumeric' => 'Il nome di utente deve essere alfanumerico', + 'The user id is required' => 'Si richiede l\'identificatore dell\'utente', + 'Passwords don\'t match' => 'Le password non corrispondono', + 'The confirmation is required' => 'Si richiede una conferma', + 'The column is required' => 'Si richiede una colonna', + 'The project is required' => 'Si richiede il progetto', + 'The color is required' => 'Si richiede il colore', + 'The id is required' => 'Si richiede l\'identificatore', + 'The project id is required' => 'Si richiede l\'identificatore del progetto', + 'The project name is required' => 'Si richiede il nome del progetto', + 'This project must be unique' => 'Il nome del progetto deve essere unico', + 'The title is required' => 'Si richiede un titolo', + 'The language is required' => 'Si richiede una lingua', + 'There is no active project, the first step is to create a new project.' => 'Non ci sono progetti attivi, il primo passo consiste in creare un nuovo progetto.', + 'Settings saved successfully.' => 'Impostazioni salvate correttamente.', + 'Unable to save your settings.' => 'Non si possono salvare le impostazioni.', + 'Database optimization done.' => 'Ottimizzazione della base dati conclusa.', + 'Your project have been created successfully.' => 'Il tuo progetto è stato creato correttamente.', + 'Unable to create your project.' => 'Non si può creare il progetto.', + 'Project updated successfully.' => 'Progetto aggiornato correttamente.', + 'Unable to update this project.' => 'Non si può aggiornare il progetto.', + 'Unable to remove this project.' => 'Non si può cancellare questo progetto.', + 'Project removed successfully.' => 'Progetto cancellato correttamente.', + 'Project activated successfully.' => 'Progetto attivato correttamente.', + 'Unable to activate this project.' => 'Non si può attivare il progetto.', + 'Project disabled successfully.' => 'Progetto disattivato correttamente.', + 'Unable to disable this project.' => 'Non si può disattivare il progetto.', + 'Unable to open this task.' => 'Non si può aprire questo compito.', + 'Task opened successfully.' => 'Il compito è stato aperto correttamente.', + 'Unable to close this task.' => 'Non si può chiudere questo compito.', + 'Task closed successfully.' => 'Compito chiuso correttamente.', + 'Unable to update your task.' => 'Non si può modificare questo compito.', + 'Task updated successfully.' => 'Compito modificato correttamente.', + 'Unable to create your task.' => 'Non si può creare questo compito.', + 'Task created successfully.' => 'Compito creato correttamente.', + 'User created successfully.' => 'Utente creato correttamente.', + 'Unable to create your user.' => 'Non si può creare l\'utente.', + 'User updated successfully.' => 'Utente aggiornato correttamente.', + 'Unable to update your user.' => 'Non si può aggiornare questo utente.', + 'User removed successfully.' => 'Utente cancellato correttamente.', + 'Unable to remove this user.' => 'Non si può cancellare questo utente.', + 'Board updated successfully.' => 'Bacheca aggiornata correttamente.', + 'Ready' => 'Pronto', + 'Backlog' => 'In attesa', + 'Work in progress' => 'In corso', + 'Done' => 'Fatto', + 'Application version:' => 'Versione dell\'applicazione:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Completato il %B %e, %Y alle %k:%M %p', + // '%B %e, %Y at %k:%M %p' => '', + 'Date created' => 'Data di creazione', + 'Date completed' => 'Data di termine', + 'Id' => 'Identificatore', + 'No task' => 'Nessun compito', + 'Completed tasks' => 'Compiti fatti', + 'List of projects' => 'Lista di progetti', + 'Completed tasks for "%s"' => 'Compiti fatti da « %s »', + '%d closed tasks' => '%d compiti chiusi', + 'No task for this project' => 'Nessun compito per questo progetto', + 'Public link' => 'Link pubblico', + 'There is no column in your project!' => 'Non c\'è nessuna colonna per questo progetto!', + 'Change assignee' => 'Cambiare la persona assegnata', + 'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »', + 'Timezone' => 'Fuso orario', + 'Sorry, I didn\'t find this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!', + 'Page not found' => 'Pagina non trovata', + 'Complexity' => 'Complessità', + 'limit' => 'limite', + 'Task limit' => 'Numero massimo di compiti', + 'Task count' => 'Numero di compiti', + 'This value must be greater than %d' => 'questo valore deve essere maggiore di %d', + 'Edit project access list' => 'Modificare i permessi del progetto', + 'Edit users access' => 'Modificare i permessi degli utenti', + 'Allow this user' => 'Permettere a questo utente', + 'Only those users have access to this project:' => 'Solo questi utenti hanno accesso a questo progetto:', + 'Don\'t forget that administrators have access to everything.' => 'Non dimenticare che gli amministratori hanno accesso a tutto.', + 'Revoke' => 'Revocare', + 'List of authorized users' => 'Lista di utenti autorizzati', + 'User' => 'Utente', + 'Nobody have access to this project.' => 'Nessuno ha accesso a questo progetto.', + 'You are not allowed to access to this project.' => 'Non hai l\'accesso a questo progetto.', + 'Comments' => 'Commenti', + 'Post comment' => 'Mandare commento', + 'Write your text in Markdown' => 'Scrivi il testo in Markdown', + 'Leave a comment' => 'Lasciare un commento', + 'Comment is required' => 'Si richiede un commento', + 'Leave a description' => 'Lasciare una descrizione', + 'Comment added successfully.' => 'Commenti aggiunti correttamente.', + 'Unable to create your comment.' => 'Non si può creare questo commento.', + 'The description is required' => 'Si richiede una descrizione', + 'Edit this task' => 'Modificare questo compito', + 'Due Date' => 'Data di scadenza', + 'Invalid date' => 'Data sbagliata', + 'Must be done before %B %e, %Y' => 'Deve essere completato prima del %B %e, %Y', + // '%B %e, %Y' => '', + // '%b %e, %Y' => '', + 'Automatic actions' => 'Azioni automatiche', + 'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata correttamente.', + 'Unable to create your automatic action.' => 'Non si può creare quest\'azione automatica.', + 'Remove an action' => 'Cancellare un\'azione', + 'Unable to remove this action.' => 'Non si può cancellare questa azione.', + 'Action removed successfully.' => 'Azione cancellata correttamente.', + 'Automatic actions for the project "%s"' => 'Azioni automatiche per questo progetto « %s »', + 'Defined actions' => 'Azioni definite', + 'Add an action' => 'Aggiungi un\'azione', + 'Event name' => 'Nome dell\'evento', + 'Action name' => 'Nome dell\'azione', + 'Action parameters' => 'Parametri d\'azione', + 'Action' => 'Azione', + 'Event' => 'Evento', + 'When the selected event occurs execute the corresponding action.' => 'Quando accade l\'evento selezionato, eseguire l\'azione corrispondente.', + 'Next step' => 'Passo seguente', + 'Define action parameters' => 'Definire i parametri dell\'azione', + 'Save this action' => 'Salvare questa azione', + 'Do you really want to remove this action: "%s"?' => 'Veramente vuole cancellare questa azione « %s » ?', + 'Remove an automatic action' => 'Cancellare un\'azione automatica', + 'Close the task' => 'Chiudere questo compito', + 'Assign the task to a specific user' => 'Assegnare questo compito a un utente specifico', + 'Assign the task to the person who does the action' => 'Assegnare il compito all\'utente che svolge l\'azione', + 'Duplicate the task to another project' => 'Duplicare il compito in altro progetto', + 'Move a task to another column' => 'Muovere un compito in un\'altra colonna', + 'Move a task to another position in the same column' => 'Muovere un compito in un\'altra posizione sulla stessa colonna', + 'Task modification' => 'Modifica di un compito', + 'Task creation' => 'Creazione di un compito', + 'Open a closed task' => 'Riaprire un compito', + 'Closing a task' => 'Chiudere un compito', + 'Assign a color to a specific user' => 'Assegna un colore ad un utente specifico', + 'Column title' => 'Titolo della colonna', + 'Position' => 'Posizione', + 'Move Up' => 'Alzare', + 'Move Down' => 'Abassare', + 'Duplicate to another project' => 'Duplicare in un altro progetto', + 'Duplicate' => 'Duplicare', + 'link' => 'link', + 'Update this comment' => 'Aggiornare questo commento', + 'Comment updated successfully.' => 'Commento aggiornato correttamente.', + 'Unable to update your comment.' => 'Non si può aggiornare questo commento.', + 'Remove a comment' => 'Cancellare un commento', + 'Comment removed successfully.' => 'Commento cancellato correttamente.', + 'Unable to remove this comment.' => 'Non si può cancellare questo commento.', + 'Do you really want to remove this comment?' => 'Desidera cancellare questo commento?', + 'Only administrators or the creator of the comment can access to this page.' => 'Solo gli amministratori o l\'autore del commento hanno accesso a questa pagina.', + 'Details' => 'Dettagli', + 'Current password for the user "%s"' => 'Password attuale per l\'utente: « %s »', + 'The current password is required' => 'Si richiede la password attuale', + 'Wrong password' => 'password sbagliata', + 'Reset all tokens' => 'Azzerare gli identificatori (tokens) di sicurezza ', + 'All tokens have been regenerated.' => 'Tutti gli identificatori (tokens) sono stati rigenerati.', + 'Unknown' => 'Sconociuto', + 'Last logins' => 'Ultimi ingressi', + 'Login date' => 'Data di ingresso', + 'Authentication method' => 'Metodo di autenticazzione', + 'IP address' => 'Indirizzo IP', + 'User agent' => 'Navigatore', + 'Persistent connections' => 'Connessioni persistenti', + 'No session.' => 'Non esiste sessione.', + 'Expiration date' => 'Data di scadenza', + 'Remember Me' => 'Ricordami', + 'Creation date' => 'Data di creazione', + 'Filter by user' => 'Filtrato mediante utente', + 'Filter by due date' => 'Filtrare attraverso data di scadenza', + 'Everybody' => 'Tutti', + 'Open' => 'Aperto', + 'Closed' => 'Chiuso', + 'Search' => 'Cercare', + 'Nothing found.' => 'Non si è trovato nulla.', + 'Search in the project "%s"' => 'Cercare nel progetto "%s"', + 'Due date' => 'Data di scadenza', + 'Others formats accepted: %s and %s' => 'Altri formati accettati: %s y %s', + 'Description' => 'Descrizione', + '%d comments' => '%d commenti', + '%d comment' => '%d commento', + 'Email address invalid' => 'Indirizzo e-mail sbagliato', + 'Your Google Account is not linked anymore to your profile.' => 'Il suo account Google non è più collegato al suo profilo', + 'Unable to unlink your Google Account.' => 'Non si può svincolare l\'account di Google.', + 'Google authentication failed' => 'Autenticazione con Google non riuscita', + 'Unable to link your Google Account.' => 'Non si può collegare il tuo account di Google.', + 'Your Google Account is linked to your profile successfully.' => 'Il tuo account di Google è stato collegato correttamente al tuo profilo.', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Collegare il mio Account di Google', + 'Unlink my Google Account' => 'Scollegare il mio account di Google', + 'Login with my Google Account' => 'Entra con il mio Account di Google', + 'Project not found.' => 'progetto non trovato.', + 'Task #%d' => 'Compito numero %d', + 'Task removed successfully.' => 'Compito cancellato correttamente.', + 'Unable to remove this task.' => 'Non si può cancellare questo compito.', + 'Remove a task' => 'Cancellare un compito', + 'Do you really want to remove this task: "%s"?' => 'Veramente vuoi cancellare questo compito: "%s"?', + 'Assign automatically a color based on a category' => 'Assegnare un colore in modo automatico basandosi sulla categoria', + 'Assign automatically a category based on a color' => 'Assegnare una categoria in modo automatico basandosi sul colore', + 'Task creation or modification' => 'Creazione o modifica di compito', + 'Category' => 'Categoria', + 'Category:' => 'Categoria:', + 'Categories' => 'Categorie', + 'Category not found.' => 'Categoria non trovata.', + 'Your category have been created successfully.' => 'La tua categoria è stata creata correttamente.', + 'Unable to create your category.' => 'Non si può creare la tua categoria.', + 'Your category have been updated successfully.' => 'La tua categoria è stata aggiornata correttamente.', + 'Unable to update your category.' => 'Non si può aggiornare la tua categoria.', + 'Remove a category' => 'Cancellare una categoria', + 'Category removed successfully.' => 'Categoria cancellata correttamente.', + 'Unable to remove this category.' => 'Non si può cancellare questa categoria.', + 'Category modification for the project "%s"' => 'Modifica di categoria per il progetto "%s"', + 'Category Name' => 'Nome di categoria', + 'Categories for the project "%s"' => 'Categorie per il progetto', + 'Add a new category' => 'Aggiungere una nuova categoria', + 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare questa categoria: "%s"?', + 'Filter by category' => 'Filtrare attraverso categoria', + 'All categories' => 'Tutte le categorie', + 'No category' => 'Senza categoria', + 'The name is required' => 'Si richiede un nome', + 'Remove a file' => 'Cancellare un file', + 'Unable to remove this file.' => 'Non si può cancellare questo file.', + 'File removed successfully.' => 'File cancellato correttamente.', + 'Attach a document' => 'Allegare un documento', + 'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?', + 'open' => 'aprire', + 'Attachments' => 'Allegati', + 'Edit the task' => 'Modificare il compito', + 'Edit the description' => 'Modificare la descrizione', + 'Add a comment' => 'Aggiungere un commento', + 'Edit a comment' => 'Modificare un commento', + 'Summary' => 'Sommario', + 'Time tracking' => 'Time tracking', + 'Estimate:' => 'Stimato:', + 'Spent:' => 'Trascorso:', + 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sotto-compito?', + 'Remaining:' => 'Rimangono', + 'hours' => 'ore', + 'spent' => 'trascorse', + 'estimated' => 'stimate', + 'Sub-Tasks' => 'Sotto-compiti', + 'Add a sub-task' => 'Aggiungere un sotto-compito', + 'Original estimate' => 'Stima originale', + 'Create another sub-task' => 'Creare un altro sotto-compito', + 'Time spent' => 'Tempo Trascorso', + 'Edit a sub-task' => 'Modificare un sotto-compito', + 'Remove a sub-task' => 'Cancellare un sotto-compito', + 'The time must be a numeric value' => 'Il tempo deve essere un valore numerico', + 'Todo' => 'Da fare', + 'In progress' => 'In corso', + 'Sub-task removed successfully.' => 'Sotto-compito cancellato correttamente.', + 'Unable to remove this sub-task.' => 'Non si può cancellare questo sotto-compito.', + 'Sub-task updated successfully.' => 'Sotto-compito aggiornato correttamente.', + 'Unable to update your sub-task.' => 'Non si può aggiornare il tuo sotto-compito.', + 'Unable to create your sub-task.' => 'Non si può creare il tuo sotto-compito.', + 'Sub-task added successfully.' => 'Sotto-compito aggiunto correttamente.', + 'Maximum size: ' => 'Dimensioni massime', + 'Unable to upload the file.' => 'Non si può caricare il file.', + 'Display another project' => 'Mostrare un altro progetto', + 'Your GitHub account was successfully linked to your profile.' => 'Il suo account di Github è stato collegato correttamente col tuo profilo.', + 'Unable to link your GitHub Account.' => 'Non si può collegarre il tuo account di Github.', + 'GitHub authentication failed' => 'Autenticazione con GitHub non riuscita', + 'Your GitHub account is no longer linked to your profile.' => 'Il tuo account di Github non è più collegato al tuo profilo.', + 'Unable to unlink your GitHub Account.' => 'Non si può collegare il tuo account di Github.', + 'Login with my GitHub Account' => 'Entrare col tuo account di Github', + 'Link my GitHub Account' => 'Collegare il mio account Github', + 'Unlink my GitHub Account' => 'Scollegare il mio account di Github', + 'Created by %s' => 'Creato da %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Ultima modifica il %d/%m/%Y alle %H:%M', + 'Tasks Export' => 'Esportazione di compiti', + 'Tasks exportation for "%s"' => 'Esportazione di compiti per « %s »', + 'Start Date' => 'Data d\'inizio', + 'End Date' => 'Data di fine', + 'Execute' => 'Eseguire', + 'Task Id' => 'Identificatore del compito', + 'Creator' => 'Creatore', + 'Modification date' => 'Data di modifica', + 'Completion date' => 'Data di termine', + 'Clone' => 'Clona', + 'Clone Project' => 'Clona il progetto', + 'Project cloned successfully.' => 'Progetto clonato con successo.', + 'Unable to clone this project.' => 'Impossibile clonare questo progetto', + 'Email notifications' => 'Notifiche email', + 'Enable email notifications' => 'Abilita le notifiche via email', + 'Task position:' => 'Posizione del compito:', + 'The task #%d have been opened.' => 'Il compito #%d è stato aperto.', + 'The task #%d have been closed.' => 'Il compito #%d è stato chiuso.', + 'Sub-task updated' => 'Sotto-compito aggiornato', + 'Title:' => 'Titolo', + 'Status:' => 'Stato', + 'Assignee:' => 'Assegnatario', + 'Time tracking:' => 'Gestione del tempo:', + 'New sub-task' => 'Nuovo sotto-compito', + 'New attachment added "%s"' => 'Nuovo allegato aggiunto « %s »', + 'Comment updated' => 'Commento aggiornato', + 'New comment posted by %s' => 'Nuovo commento aggiunto da « %s »', + 'List of due tasks for the project "%s"' => 'Lista dei compiti scaduti per il progetto « %s »', + 'New attachment' => 'Nuovo allegato', + 'New comment' => 'Nuovo commento', + 'New subtask' => 'Nuovo sotto-compito', + 'Subtask updated' => 'Sotto-compito aggiornato', + 'Task updated' => 'Compito aggiornato', + 'Task closed' => 'Compito chiuso', + 'Task opened' => 'Compito aperto', + '[%s][Due tasks]' => '[%s][Compiti scaduti]', + '[Kanboard] Notification' => '[Kanboard] Notifica', + 'I want to receive notifications only for those projects:' => 'Vorrei ricevere le notifiche solo da questi progetti:', + 'view the task on Kanboard' => 'vedi il compito su Kanboard', + 'Public access' => 'Accesso pubblico', + 'Category management' => 'Gestione delle categorie', + 'User management' => 'Gestione utenti', + 'Active tasks' => 'Compiti attivi', + 'Disable public access' => 'Disabilita l\'accesso pubblico', + 'Enable public access' => 'Abilita l\'accesso pubblico', + 'Active projects' => 'Progetti attivi', + 'Inactive projects' => 'Progetti inattivi', + 'Public access disabled' => 'Accesso pubblico disattivato', + 'Do you really want to disable this project: "%s"?' => 'Vuoi davvero disabilitare questo progetto: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Vuoi davvero duplicare questo progetto: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Vuoi davvero abilitare questo progetto: "%s"?', + 'Project activation' => 'Attivazione progetto', + 'Move the task to another project' => 'Muovi il compito in un altro progetto', + 'Move to another project' => 'Muovi in un altro progetto', + 'Do you really want to duplicate this task?' => 'Vuoi davvero duplicare questo compito?', + 'Duplicate a task' => 'Duplica il compito', + 'External accounts' => 'Account esterni', + 'Account type' => 'Tipo di account', + 'Local' => 'Locale', + 'Remote' => 'Remoto', + 'Enabled' => 'Abilitato', + 'Disabled' => 'Disabilitato', + 'Google account linked' => 'Account Google collegato', + 'Github account linked' => 'Account Github collegato', + // 'Username:' => '', + 'Name:' => 'Nome:', + // 'Email:' => '', + 'Default project:' => 'Progetto di default', + 'Notifications:' => 'Notifiche:', + 'Notifications' => 'Notifiche', + 'Group:' => 'Gruppo', + 'Regular user' => 'Utente regolare', + 'Account type:' => 'Tipo di account', + 'Edit profile' => 'Modifica il profilo', + 'Change password' => 'Cambia password', + 'Password modification' => 'Modifica della password', + 'External authentications' => 'Autenticazione esterna', + 'Google Account' => 'Account Google', + 'Github Account' => 'Account Github', + 'Never connected.' => 'Mai connesso.', + 'No account linked.' => 'Nessun account collegato.', + 'Account linked.' => 'Account collegato.', + 'No external authentication enabled.' => 'Nessuna autenticazione esterna abilitata.', + 'Password modified successfully.' => 'Password modificata con successo.', + 'Unable to change the password.' => 'Impossibile cambiare la password.', + 'Change category for the task "%s"' => 'Cambia categoria per il compito "%s"', + 'Change category' => 'Cambia categoria', + '%s updated the task %s' => '%s ha aggiornato il compito %s', + '%s opened the task %s' => '%s ha aperto il compito %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s ha spostato il compito %s nella posizione #%d della colonna "%s"', + '%s moved the task %s to the column "%s"' => '%s ha spostato il compito %s nella colonna "%s"', + '%s created the task %s' => '%s ha creato il compito %s', + '%s closed the task %s' => '%s ha chiuso il compito %s', + '%s created a subtask for the task %s' => '%s ha creato un sotto-compito per il compito %s', + '%s updated a subtask for the task %s' => '%s ha aggiornato un sotto-compito per il compito %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Assegnato a %s con una stima di %s/%sh', + 'Not assigned, estimate of %sh' => 'Non assegnato, stima %sh', + '%s updated a comment on the task %s' => '%s ha aggiornato un commento del compito %s', + '%s commented the task %s' => '%s ha commentato il compito %s', + '%s\'s activity' => 'Attività di %s', + 'No activity.' => 'Nessuna attività.', + 'RSS feed' => 'Feed RSS', + '%s updated a comment on the task #%d' => '%s ha aggiornato un commento del compito #%d', + '%s commented on the task #%d' => '%s ha commentato il compito #%d', + '%s updated a subtask for the task #%d' => '%s ha aggiornato un sotto-compito del compito #%d', + '%s created a subtask for the task #%d' => '%s ha creato un sotto-compito del compito #%d', + '%s updated the task #%d' => '%s ha aggiornato il compito #%d', + '%s created the task #%d' => '%s ha creato il compito #%d', + '%s closed the task #%d' => '%s ha chiuso il compito #%d', + '%s open the task #%d' => '%s ha aperto il compito #%d', + '%s moved the task #%d to the column "%s"' => '%s ha spostato il compito #%d nella colonna "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s ha spostato il compito #%d nella posizione %d della colonna "%s"', + 'Activity' => 'Attività', + 'Default values are "%s"' => 'Valori di default "%s"', + 'Default columns for new projects (Comma-separated)' => 'Colonne di default per i nuovi progetti (Separati da virgola)', + 'Task assignee change' => 'Cambiare l\'assegnatario del compito', + '%s change the assignee of the task #%d to %s' => '% dai l\'assegnazione del compito #%d a %s', + '%s changed the assignee of the task %s to %s' => '%s ha cambiato l\'assegnatario del compito %', + 'Column Change' => 'Cambio di colonna', + 'Position Change' => 'Posizione cambiata', + 'Assignee Change' => 'Assegnatario cambiato', + 'New password for the user "%s"' => 'Nuova password per l\'utente "%s"', + 'Choose an event' => 'Scegli un evento', + 'Github commit received' => 'Commit di Github ricevuto', + 'Github issue opened' => 'Issue di Github ricevuto', + 'Github issue closed' => 'Issue di Github chiusa', + 'Github issue reopened' => 'Issue di Github riaperta', + 'Github issue assignee change' => 'Assegnatario dell\'issue di Github cambiato', + 'Github issue label change' => 'Etichetta dell\'issue di Github cambiata', + 'Create a task from an external provider' => 'Crea un compito da un provider esterno', + 'Change the assignee based on an external username' => 'Cambia l\'assegnatario basandosi su un username esterno', + 'Change the category based on an external label' => 'Cambia la categoria basandosi su un\'etichetta esterna', + 'Reference' => 'Referenza', + 'Reference: %s' => 'Referenza :%s', + 'Label' => 'Etichetta', + // 'Database' => '', + 'About' => 'Info', + 'Database driver:' => 'Driver per Database', + 'Board settings' => 'Impostazioni bacheca', + 'URL and token' => 'URL e token', + 'Webhook settings' => 'Impostazione Webhook', + 'URL for task creation:' => 'URL per la creazione dei compiti:', + 'Reset token' => 'Rigenera il token', + 'API endpoint:' => 'Endpoint dell\'API:', + 'Refresh interval for private board' => 'Intervallo di refresh per le bacheche private', + 'Refresh interval for public board' => 'Intervallo di refresh per le bacheche pubbliche', + 'Task highlight period' => 'Periodo di evidenza per il compito', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periodo (in secondi) per considerare un compito come modificato recentemente (0 per disabilitare, 2 giorni di default)', + 'Frequency in second (60 seconds by default)' => 'Frequenza in secondi (60 secondi di default)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequenza in secondi (0 secondi di default)', + 'Application URL' => 'URL dell\'applicazione', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Esempio: http://example.kanboard.net/ (usato dalle notifiche email)', + 'Token regenerated.' => 'Token rigenerato', + 'Date format' => 'Formato data', + 'ISO format is always accepted, example: "%s" and "%s"' => 'Il formato ISO è sempre accettato, esempio: "%s" e "%s"', + 'New private project' => 'Nuovo progetto privato', + 'This project is private' => 'Questo progetto è privato', + 'Type here to create a new sub-task' => 'Scrivi qui per creare un sotto-compito', + 'Add' => 'Aggiungi', + 'Estimated time: %s hours' => 'Tempo stimato: %s ore', + 'Time spent: %s hours' => 'Tempo trascorso: %s ore', + 'Started on %B %e, %Y' => 'Avviato il %B %e, %Y', + 'Start date' => 'Data di inizio', + 'Time estimated' => 'Tempo stimato', + 'There is nothing assigned to you.' => 'Non c\'è nulla assegnato a te.', + 'My tasks' => 'I miei compiti', + 'Activity stream' => 'Flusso di attività', + 'Dashboard' => 'Bacheca', + 'Confirmation' => 'Conferma', + 'Allow everybody to access to this project' => 'Abilita tutti ad accedere a questo progetto', + 'Everybody have access to this project.' => 'Tutti hanno accesso a questo progetto', + // 'Webhooks' => '', + // 'API' => '', + 'Integration' => 'Integrazione', + 'Github webhooks' => 'Webhooks di Github', + 'Help on Github webhooks' => 'Guida ai Webhooks di Github', + 'Create a comment from an external provider' => 'Crea un commit da un provider esterno', + 'Github issue comment created' => 'Commento ad un Issue di Github creato', + 'Configure' => 'Configura', + 'Project management' => 'Gestione del progetto', + 'My projects' => 'I miei progetti', + 'Columns' => 'Colonne', + 'Task' => 'Compito', + 'Your are not member of any project.' => 'Non sei membro di alcun progetto', + 'Percentage' => 'Percentuale', + 'Number of tasks' => 'Numero di compiti', + 'Task distribution' => 'Distribuzione dei compiti', + 'Reportings' => 'Rapporti', + 'Task repartition for "%s"' => 'Ripartizione compiti per "%s"', + // 'Analytics' => '', + 'Subtask' => 'Sotto-compiti', + 'My subtasks' => 'I miei sotto-compiti', + 'User repartition' => 'Ripartizione per utente', + 'User repartition for "%s"' => 'Ripartizione utente per "%s"', + 'Clone this project' => 'Clona questo progetto', + 'Column removed successfully.' => 'Colonna rimossa con successo', + 'Edit Project' => 'Modifica progetto', + 'Github Issue' => 'Issue di Github', + 'Not enough data to show the graph.' => 'Non ci sono abbastanza dati per visualizzare il grafico.', + 'Previous' => 'Precendete', + 'The id must be an integer' => 'L\'id deve essere un intero', + 'The project id must be an integer' => 'L\'id del progetto deve essere un intero', + 'The status must be an integer' => 'Lo status deve essere un intero', + 'The subtask id is required' => 'L\'id del sotto-compito è necessario', + 'The subtask id must be an integer' => 'L\'id del sotto-compito deve essere un intero', + 'The task id is required' => 'Richiesto l\'id del compito', + 'The task id must be an integer' => 'L\'id del compito deve essere un intero', + 'The user id must be an integer' => 'L\'id dell\'utente deve essere un intero', + 'This value is required' => 'Questo valore è necessario', + 'This value must be numeric' => 'Questo valore deve essere numerico', + 'Unable to create this task.' => 'Impossibile creare questo compito', + 'Cumulative flow diagram' => 'Diagramma di flusso cumulativo', + 'Cumulative flow diagram for "%s"' => 'Diagramma di flusso comulativo per "%s"', + 'Daily project summary' => 'Sommario giornaliero del progetto', + 'Daily project summary export' => 'Esportazione del sommario giornaliero del progetto', + 'Daily project summary export for "%s"' => 'Esportazione del sommario giornaliero del progetto per "%s"', + 'Exports' => 'Esporta', + 'This export contains the number of tasks per column grouped per day.' => 'Questo export contiene il numero di compiti per colonna raggruppati per giorno', + 'Nothing to preview...' => 'Nessuna anteprima...', + 'Preview' => 'Anteprima', + 'Write' => 'Scrivi', + 'Active swimlanes' => 'Corsie attive', + 'Add a new swimlane' => 'Aggiungi una corsia', + 'Change default swimlane' => 'Cambia la corsia di default', + 'Default swimlane' => 'Corsia di default', + 'Do you really want to remove this swimlane: "%s"?' => 'Vuoi davvero rimuovere questa corsia: "%s"?', + 'Inactive swimlanes' => 'Corsie inattive', + 'Set project manager' => 'Imposta un manager del progetto', + 'Set project member' => 'Imposta un membro del progetto', + 'Remove a swimlane' => 'Rimuovi una corsia', + 'Rename' => 'Rinomina', + 'Show default swimlane' => 'Mostra le corsie di default', + 'Swimlane modification for the project "%s"' => 'Modifica corsia per il progetto "%s"', + 'Swimlane not found.' => 'Corsia non trovata', + 'Swimlane removed successfully.' => 'Corsia rimossa con successo', + 'Swimlanes' => 'Corsie', + 'Swimlane updated successfully.' => 'Corsia aggiornata con successo', + 'The default swimlane have been updated successfully.' => 'La corsia di default è stata aggiornata con successo.', + 'Unable to create your swimlane.' => 'Impossibile creare la sua corsia.', + 'Unable to remove this swimlane.' => 'Impossibile rimuovere questa corsia.', + 'Unable to update this swimlane.' => 'Impossibile aggiornare questa corsia.', + 'Your swimlane have been created successfully.' => 'La sua corsia è stata creata con successo', + 'Example: "Bug, Feature Request, Improvement"' => 'Esempio: "Bug, Richiesta di Funzioni, Migliorie"', + 'Default categories for new projects (Comma-separated)' => 'Categorie di default per i progetti (Separati da virgola)', + 'Gitlab commit received' => 'Commit ricevuto da Gitlab', + 'Gitlab issue opened' => 'Issue di Gitlab aperta', + 'Gitlab issue closed' => 'Issue di Gitlab chiusa', + 'Gitlab webhooks' => 'Webhooks di Gitlab', + 'Help on Gitlab webhooks' => 'Guida ai Webhooks di Gitlab', + 'Integrations' => 'Integrazioni', + 'Integration with third-party services' => 'Integrazione con servizi di terze parti', + 'Role for this project' => 'Ruolo per questo progetto', + 'Project manager' => 'Manager del progetto', + 'Project member' => 'Membro del progetto', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Un manager del progetto può cambiare le impostazioni del progetto ed avere più privilegi di un utente standard.', + 'Gitlab Issue' => 'Issue di Gitlab', + 'Subtask Id' => 'Id del sotto-compito', + 'Subtasks' => 'Sotto-compiti', + 'Subtasks Export' => 'Esporta sotto-compiti', + 'Subtasks exportation for "%s"' => 'Esportazione dei sotto-compiti per "%s"', + 'Task Title' => 'Titolo del compito', + 'Untitled' => 'Senza titolo', + 'Application default' => 'Default dell\'applicazione', + 'Language:' => 'Lingua', + 'Timezone:' => 'Fuso Orario', + 'All columns' => 'Tutte le colonne', + 'Calendar for "%s"' => 'Calendario per "%s"', + 'Filter by column' => 'Filtra per colonna', + 'Filter by status' => 'Filtra per status', + 'Calendar' => 'Calendario', + 'Next' => 'Prossimo', + // '#%d' => '', + 'Filter by color' => 'Filtra per colore', + 'Filter by swimlane' => 'Filtra per corsia', + 'All swimlanes' => 'Tutte le corsie', + 'All colors' => 'Tutti i colori', + 'All status' => 'Tutti gli stati', + 'Add a comment logging moving the task between columns' => 'Aggiungi un commento per tracciare lo spostamento del compito tra colonne', + 'Moved to column %s' => 'Spostato sulla colonna "%s"', + 'Change description' => 'Cambia descrizione', + 'User dashboard' => 'Bacheca utente', + 'Allow only one subtask in progress at the same time for a user' => 'Permetti un solo sotto-compito in progresso per utente nello stesso tempo', + 'Edit column "%s"' => 'Modifica la colonna "%s"', + 'Enable time tracking for subtasks' => 'Abilita la gestione del tempo per i sotto-compiti', + 'Select the new status of the subtask: "%s"' => 'Selziona il nuovo status per il sotto-compito: "%s"', + 'Subtask timesheet' => 'Timesheet del sotto-compito', + 'There is nothing to show.' => 'Nulla da mostrare.', + 'Time Tracking' => 'Gestione del tempo', + 'You already have one subtask in progress' => 'Hai già un sotto-compito in progresso', + 'Which parts of the project do you want to duplicate?' => 'Quali parti del progetto vuoi duplicare?', + 'Change dashboard view' => 'Cambia la vista della bacheca', + 'Show/hide activities' => 'Mostra/nascondi attività', + 'Show/hide projects' => 'Mostra/nascondi progetti', + 'Show/hide subtasks' => 'Mostra/nascondi sotto-compiti', + 'Show/hide tasks' => 'Mostra/nascondi compiti', + 'Disable login form' => 'Disabilita form di login', + 'Show/hide calendar' => 'Mostra/nascondi calendario', + 'User calendar' => 'Calendario utente', + 'Bitbucket commit received' => 'Commit ricevuto da Bitbucket', + 'Bitbucket webhooks' => 'Webhooks di Bitbucket', + 'Help on Bitbucket webhooks' => 'Guida ai Webhooks di Bitbucket', + 'Start' => 'Inizio', + 'End' => 'Fine', + 'Task age in days' => 'Anzianità del compito in giorni', + 'Days in this column' => 'Giorni in questa colonna', + // '%dd' => '', + 'Add a link' => 'Aggiungi un link', + 'Add a new link' => 'Aggiungi un nuovo link', + 'Do you really want to remove this link: "%s"?' => 'Vuoi davvero rimuovere questo link: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Vuoi davvero rimuovere questo link dal compito #%d?', + 'Field required' => 'Campo necessario', + 'Link added successfully.' => 'Link aggiunto con successo.', + 'Link updated successfully.' => 'Linka aggiornato con successo.', + 'Link removed successfully.' => 'Link rimosso con successo.', + 'Link labels' => 'Etichette dei link', + 'Link modification' => 'Modifica link', + 'Links' => 'Link', + 'Link settings' => 'Impostazioni link', + // 'Opposite label' => '', + 'Remove a link' => 'Rimuovi un link', + 'Task\'s links' => 'Link del compito', + 'The labels must be different' => 'Le etichette devono essere diverse', + 'There is no link.' => 'Non c\'è alcun link', + 'This label must be unique' => 'Questa etichetta deve essere unica', + 'Unable to create your link.' => 'Impossibile creare il suo link.', + 'Unable to update your link.' => 'Impossibile aggiornare il suo link.', + 'Unable to remove this link.' => 'Impossibile rimuovere il suo link.', + 'relates to' => 'si riferisce a', + 'blocks' => 'blocca', + 'is blocked by' => 'è bloccato da', + 'duplicates' => 'duplica', + 'is duplicated by' => 'è duplicato da', + 'is a child of' => 'è un figlio di', + 'is a parent of' => 'è un genitore di', + 'targets milestone' => 'punta alla milestone', + 'is a milestone of' => 'è una milestone di', + 'fixes' => 'sistema', + 'is fixed by' => 'è sistemato da', + 'This task' => 'Questo compito', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + 'Expand tasks' => 'Espandi i compiti', + 'Collapse tasks' => 'Minimizza i compiti', + 'Expand/collapse tasks' => 'Espandi/Minimizza compiti', + 'Close dialog box' => 'Chiudi dialog box', + 'Submit a form' => 'Invia i dati', + 'Board view' => 'Vista bacheca', + 'Keyboard shortcuts' => 'Scorciatoie da tastiera', + 'Open board switcher' => 'Apri il selezionatore di bacheche', + 'Application' => 'Applicazione', + 'Filter recently updated' => 'Filtri recentemente aggiornati', + 'since %B %e, %Y at %k:%M %p' => 'dal %B %e, %Y alle %k:%M %p', + 'More filters' => 'Più filtri', + 'Compact view' => 'Vista compatta', + 'Horizontal scrolling' => 'Scrolling orizzontale', + 'Compact/wide view' => 'Vista compatta/estesa', + 'No results match:' => 'Nessun risultato trovato:', + 'Remove hourly rate' => 'Rimuovi tariffa oraria', + 'Do you really want to remove this hourly rate?' => 'Vuoi davvero rimuovere questa tariffa oraria?', + 'Hourly rates' => 'Tariffe orarie', + 'Hourly rate' => 'Tariffa oraria', + 'Currency' => 'Valuta', + 'Effective date' => 'Data effettiva', + 'Add new rate' => 'Aggiungi una nuova tariffa', + 'Rate removed successfully.' => 'Tariffa rimossa con successo.', + 'Unable to remove this rate.' => 'Impossibile rimuovere questa tariffa.', + 'Unable to save the hourly rate.' => 'Impossibile salvare la tariffa oraria.', + 'Hourly rate created successfully.' => 'Tariffa oraria creata con successo.', + 'Start time' => 'Data di inizio', + 'End time' => 'Data di completamento', + 'Comment' => 'Commento', + 'All day' => 'Tutto il giorno', + 'Day' => 'Giorno', + 'Manage timetable' => 'Gestisci orario', + 'Overtime timetable' => 'Straordinari', + 'Time off timetable' => 'Fuori orario', + 'Timetable' => 'Orario', + 'Work timetable' => 'Orario di lavoro', + 'Week timetable' => 'Orario settimanale', + 'Day timetable' => 'Orario giornaliero', + 'From' => 'Da', + 'To' => 'A', + 'Time slot created successfully.' => 'Fascia oraria creata con successo.', + 'Unable to save this time slot.' => 'Impossibile creare questa fascia oraria.', + 'Time slot removed successfully.' => 'Fascia oraria rimossa con successo.', + 'Unable to remove this time slot.' => 'Impossibile rimuovere questa fascia oraria.', + 'Do you really want to remove this time slot?' => 'Vuoi davvero rimuovere questa fascia oraria?', + 'Remove time slot' => 'Rimuovi fascia oraria', + 'Add new time slot' => 'Aggiungi nuova fascia oraria', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Questo orario è utilizzato quando la casella "tutto il giorno" è selezionata per i fuori orari e per gli straordinari', + // 'Files' => '', + 'Images' => 'Immagini', + 'Private project' => 'Progetto privato', + 'Amount' => 'Totale', + 'AUD - Australian Dollar' => 'AUD - Dollari Australiani', + 'Budget' => 'Bilancio', + 'Budget line' => 'Limite di bilancio', + 'Budget line removed successfully.' => 'Limite al bilancio rimosso con successo.', + 'Budget lines' => 'Limiti al bilancio', + 'CAD - Canadian Dollar' => 'CAD - Dollari Canadesi', + 'CHF - Swiss Francs' => 'CHF - Franchi Svizzeri', + 'Cost' => 'Costi', + 'Cost breakdown' => 'Abbattimento dei costi', + 'Custom Stylesheet' => 'CSS personalizzato', + // 'download' => '', + 'Do you really want to remove this budget line?' => 'Vuoi davvero rimuovere questo limite al bilancio?', + // 'EUR - Euro' => '', + 'Expenses' => 'Spese', + 'GBP - British Pound' => 'GBP - Pound Inglesi', + 'INR - Indian Rupee' => 'INR - Rupie Indiani', + 'JPY - Japanese Yen' => 'JPY - Yen Giapponesi', + 'New budget line' => 'Nuovo limite al bilancio', + 'NZD - New Zealand Dollar' => 'NZD - Dollari della Nuova Zelanda', + 'Remove a budget line' => 'Rimuovi un limite al bilancio', + 'Remove budget line' => 'Rimuovi limite di bilancio', + 'RSD - Serbian dinar' => 'RSD - Dinar Serbi', + 'The budget line have been created successfully.' => 'Il limite al bilancio è stato creato correttamente', + 'Unable to create the budget line.' => 'Impossibile creare il limite al bilancio', + 'Unable to remove this budget line.' => 'Impossibile rimuovere questo limite al bilancio.', + 'USD - US Dollar' => 'USD - Dollari Americani', + 'Remaining' => 'Restanti', + 'Destination column' => 'Colonna destinazione', + 'Move the task to another column when assigned to a user' => 'Sposta il compito in un\'altra colonna quando viene assegnato ad un utente', + 'Move the task to another column when assignee is cleared' => 'Sposta il compito in un\'altra colonna quando l\'assegnatario cancellato', + 'Source column' => 'Colonna sorgente', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => 'Transizioni', + 'Executer' => 'Esecutore', + 'Time spent in the column' => 'Tempo trascorso nella colonna', + 'Task transitions' => 'Transizioni del compito', + 'Task transitions export' => 'Esporta le transizioni del compito', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Questo report contiene tutti i movimenti di colonna per ogni compito con le date, l\'utente ed il tempo trascorso per ogni transizione', + 'Currency rates' => 'Tassi di cambio', + 'Rate' => 'Cambio', + 'Change reference currency' => 'Cambia la valuta di riferimento', + 'Add a new currency rate' => 'Aggiungi un nuovo tasso di cambio', + 'Currency rates are used to calculate project budget.' => 'I tassi di cambio sono utilizzati per calcolare i bilanci dei progetti', + 'Reference currency' => 'Valuta di riferimento', + 'The currency rate have been added successfully.' => 'Il tasso di cambio è stato aggiunto con successo.', + 'Unable to add this currency rate.' => 'Impossibile aggiungere questo tasso di cambio.', + 'Send notifications to a Slack channel' => 'Invia notifiche al canale Slack', + 'Webhook URL' => 'URL Webhook', + 'Help on Slack integration' => 'Guida all\'integrazione con Slack', + '%s remove the assignee of the task %s' => '%s rimuove l\'assegnatario del compito %s', + 'Send notifications to Hipchat' => 'Invia notifiche a Hipchat', + 'API URL' => 'URL API', + 'Room API ID or name' => 'Nome o ID API della Room', + 'Room notification token' => 'Token per le notifiche della Room', + 'Help on Hipchat integration' => 'Guida all\'integrazione di Hipchat', + 'Enable Gravatar images' => 'Abilita immagini Gravatar', + 'Information' => 'Informazioni', + 'Check two factor authentication code' => 'Controlla il codice di autenticazione a due fattori', + 'The two factor authentication code is not valid.' => 'Il codice di autenticazione a due fattori non è valido', + 'The two factor authentication code is valid.' => 'Il codice di autenticazione a due fattori è valido', + 'Code' => 'Codice', + 'Two factor authentication' => 'Autenticazione a due fattori', + 'Enable/disable two factor authentication' => 'Abilita/disabilita autenticazione a due fattori', + 'This QR code contains the key URI: ' => 'Questo QR code contiene l\'URI: ', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salva la chiave privata nel tuo software TOTP (per esempio Google Authenticator oppure FreeOTP).', + 'Check my code' => 'Controlla il mio codice', + 'Secret key: ' => 'Chiave privata:', + 'Test your device' => 'Testa il tuo dispositivo', + 'Assign a color when the task is moved to a specific column' => 'Assegna un colore quando il compito viene spostato in una colonna specifica', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locales/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 6b6c795d..22dd98ff 100644 --- a/app/Locales/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -1,6 +1,8 @@ <?php return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', 'None' => 'なし', 'edit' => '変更', 'Edit' => '変更', @@ -182,18 +184,19 @@ return array( 'Change assignee' => '担当を変更する', 'Change assignee for the task "%s"' => 'タスク「%s」の担当を変更する', 'Timezone' => 'タイムゾーン', - 'Sorry, I didn\'t found this information in my database!' => 'データベース上で情報が見つかりませんでした!', + 'Sorry, I didn\'t find this information in my database!' => 'データベース上で情報が見つかりませんでした!', 'Page not found' => 'ページが見つかりません', 'Complexity' => '複雑さ', 'limit' => '制限', 'Task limit' => 'タスク数制限', + 'Task count' => 'タスク数', 'This value must be greater than %d' => '%d より大きな値を入力してください', 'Edit project access list' => 'プロジェクトのアクセス許可を変更', 'Edit users access' => 'ユーザのアクセス許可を変更', 'Allow this user' => 'このユーザを許可する', 'Only those users have access to this project:' => 'これらのユーザのみがプロジェクトにアクセスできます:', 'Don\'t forget that administrators have access to everything.' => '管理者には全ての権限が与えられます。', - 'revoke' => '許可を取り下げる', + 'Revoke' => '許可を取り下げる', 'List of authorized users' => '許可されたユーザ', 'User' => 'ユーザ', 'Nobody have access to this project.' => 'だれもプロジェクトにアクセスできません。', @@ -211,7 +214,8 @@ return array( 'Due Date' => '期限', 'Invalid date' => '日付が無効です', 'Must be done before %B %e, %Y' => '%Y/%m/%d までに完了', - '%B %e, %Y' => '%d %B %Y', + '%B %e, %Y' => '%Y %B %e', + '%b %e, %Y' => '%Y %b %e', 'Automatic actions' => '自動アクションを管理する', 'Your automatic action have been created successfully.' => '自動アクションを作成しました。', 'Unable to create your automatic action.' => '自動アクションの作成に失敗しました。', @@ -385,8 +389,6 @@ return array( 'Creator' => '作成者', 'Modification date' => '変更日', 'Completion date' => '完了日', - 'Webhook URL for task creation' => 'タスク作成の Webhook URL', - 'Webhook URL for task modification' => 'タスク変更の Webhook URL', 'Clone' => '複製', 'Clone Project' => 'プロジェクトの複製', 'Project cloned successfully.' => 'プロジェクトを複製しました。', @@ -406,15 +408,13 @@ return array( 'Comment updated' => 'コメントが更新されました', 'New comment posted by %s' => '「%s」の新しいコメントが追加されました', 'List of due tasks for the project "%s"' => 'プロジェクト「%s」の期限切れのタスク', - '[%s][New attachment] %s (#%d)' => '[%s][新規添付ファイル] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][新規コメント] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][コメント更新] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][新規サブタスク] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][サブタスク更新] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][新規タスク] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][タスク更新] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][タスククローズ] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][タスクオープン] %s (#%d)', + 'New attachment' => '新しい添付ファイル', + 'New comment' => '新しいコメント', + 'New subtask' => '新しいサブタスク', + 'Subtask updated' => 'サブタスクの更新', + 'Task updated' => 'タスクの更新', + 'Task closed' => 'タスクのクローズ', + 'Task opened' => 'タスクのオープン', '[%s][Due tasks]' => '[%s][タスク期限切れ]', '[Kanboard] Notification' => '[Kanboard] 通知', 'I want to receive notifications only for those projects:' => '以下のプロジェクトにのみ通知を受け取る:', @@ -449,7 +449,7 @@ return array( 'Email:' => 'Email:', 'Default project:' => 'デフォルトプロジェクト:', 'Notifications:' => '通知:', - // 'Notifications' => '', + 'Notifications' => '通知', 'Group:' => 'グループ:', 'Regular user' => '通常のユーザ', 'Account type:' => 'アカウントの種類:', @@ -467,18 +467,18 @@ return array( 'Unable to change the password.' => 'パスワードが変更できませんでした。', 'Change category for the task "%s"' => 'タスク「%s」のカテゴリの変更', 'Change category' => 'カテゴリの変更', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> をアップデートしました', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> をオープンしました', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> をポジション #%d カラム %s に移動しました', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> をカラム「%s」に移動しました', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> を作成しました', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> をクローズしました', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> のサブタスクを追加しました', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> のサブタスクを更新しました', + '%s updated the task %s' => '%s がタスク %s をアップデートしました', + '%s opened the task %s' => '%s がタスク %s をオープンしました', + '%s moved the task %s to the position #%d in the column "%s"' => '%s がタスク %s をポジション #%d カラム %s に移動しました', + '%s moved the task %s to the column "%s"' => '%s がタスク %s をカラム「%s」に移動しました', + '%s created the task %s' => '%s がタスク %s を作成しました', + '%s closed the task %s' => '%s がタスク %s をクローズしました', + '%s created a subtask for the task %s' => '%s がタスク %s のサブタスクを追加しました', + '%s updated a subtask for the task %s' => '%s がタスク %s のサブタスクを更新しました', 'Assigned to %s with an estimate of %s/%sh' => '担当者 %s に予想 %s/%sh に変更されました', 'Not assigned, estimate of %sh' => '担当者無しで予想 %sh に変更されました', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> のコメントを更新しました', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> にコメントしました', + '%s updated a comment on the task %s' => '%s がタスク %s のコメントを更新しました', + '%s commented the task %s' => '%s がタスク %s にコメントしました', '%s\'s activity' => '%s のアクティビティ', 'No activity.' => 'アクティビティなし。', 'RSS feed' => 'RSS フィード', @@ -497,10 +497,10 @@ return array( 'Default columns for new projects (Comma-separated)' => '新規プロジェクトのデフォルトカラム (コンマで区切って入力)', 'Task assignee change' => '担当者の変更', '%s change the assignee of the task #%d to %s' => '%s がタスク #%d の担当を %s に変更しました', - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '%s がタスク <a href="?controller=task&action=show&task_id=%d">#%d</a> の担当を %s に変更しました', - '[%s][Column Change] %s (#%d)' => '[%s][カラムの変更] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][位置の変更] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][担当者変更] %s (#%d)', + '%s changed the assignee of the task %s to %s' => '%s がタスク %s の担当を %s に変更しました', + 'Column Change' => 'カラムの変更', + 'Position Change' => '位置の変更', + 'Assignee Change' => '担当の変更', 'New password for the user "%s"' => 'ユーザ「%s」の新しいパスワード', 'Choose an event' => 'イベントの選択', 'Github commit received' => 'Github のコミットを受け取った', @@ -544,11 +544,382 @@ return array( 'Started on %B %e, %Y' => '開始 %Y/%m/%d', 'Start date' => '開始時間', 'Time estimated' => '予想時間', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', + 'There is nothing assigned to you.' => '何もアサインされていません。', + 'My tasks' => '自分のタスク', + 'Activity stream' => 'アクティビティストリーム', + 'Dashboard' => 'ダッシュボード', + 'Confirmation' => '確認', + 'Allow everybody to access to this project' => '全員にプロジェクトへのアクセスを許す', + 'Everybody have access to this project.' => '誰でもこのプロジェクトにアクセスできます。', + 'Webhooks' => 'Webhook', + 'API' => 'API', + 'Integration' => '連携', + 'Github webhooks' => 'Github Webhook', + 'Help on Github webhooks' => 'Github webhook のヘルプ', + 'Create a comment from an external provider' => '外部サービスからコメントを作成する', + 'Github issue comment created' => 'Github Issue コメントが作られました', + 'Configure' => '設定', + 'Project management' => 'プロジェクト・マネジメント', + 'My projects' => '自分のプロジェクト', + 'Columns' => 'カラム', + 'Task' => 'タスク', + 'Your are not member of any project.' => 'どのプロジェクトにも属していません。', + 'Percentage' => '割合', + 'Number of tasks' => 'タスク数', + 'Task distribution' => 'タスク分布', + 'Reportings' => 'レポート', + 'Task repartition for "%s"' => '「%s」のタスク分布', + 'Analytics' => '分析', + 'Subtask' => 'サブタスク', + 'My subtasks' => '自分のサブタスク', + 'User repartition' => '担当者分布', + 'User repartition for "%s"' => '「%s」の担当者分布', + 'Clone this project' => 'このプロジェクトを複製する', + 'Column removed successfully.' => 'カラムを削除しました', + 'Edit Project' => 'プロジェクトを編集する', + 'Github Issue' => 'Github Issue', + 'Not enough data to show the graph.' => 'グラフを描画するには出たが足りません', + 'Previous' => '戻る', + 'The id must be an integer' => 'id は数字でなければなりません', + 'The project id must be an integer' => 'project id は数字でなければなりません', + 'The status must be an integer' => 'status は数字でなければなりません', + 'The subtask id is required' => 'subtask id が必要です', + 'The subtask id must be an integer' => 'subtask id は数字でなければなりません', + 'The task id is required' => 'task id が必要です', + 'The task id must be an integer' => 'task id は数字でなければなりません', + 'The user id must be an integer' => 'user id は数字でなければなりません', + 'This value is required' => 'この値が必要です', + 'This value must be numeric' => 'この値は数字でなければなりません', + 'Unable to create this task.' => 'このタスクを作成できませんでした', + 'Cumulative flow diagram' => '蓄積フロー図', + 'Cumulative flow diagram for "%s"' => '「%s」の蓄積フロー図', + 'Daily project summary' => '日時プロジェクトサマリー', + 'Daily project summary export' => '日時プロジェクトサマリーの出力', + 'Daily project summary export for "%s"' => '「%s」の日時プロジェクトサマリーの出力', + 'Exports' => '出力', + 'This export contains the number of tasks per column grouped per day.' => 'この出力は日時のカラムごとのタスク数を集計したものです', + 'Nothing to preview...' => 'プレビューがありません', + 'Preview' => 'プレビュー', + 'Write' => '書く', + 'Active swimlanes' => 'アクティブなスイムレーン', + 'Add a new swimlane' => '新しいスイムレーン', + 'Change default swimlane' => 'デフォルトスイムレーンの変更', + 'Default swimlane' => 'デフォルトスイムレーン', + 'Do you really want to remove this swimlane: "%s"?' => 'このスイムレーン「%s」を本当に削除しますか?', + 'Inactive swimlanes' => 'インタラクティブなスイムレーン', + 'Set project manager' => 'プロジェクトマネジャーをセット', + 'Set project member' => 'プロジェクトメンバーをセット', + 'Remove a swimlane' => 'スイムレーンの削除', + 'Rename' => '名前の変更', + 'Show default swimlane' => 'デフォルトスイムレーンの表示', + 'Swimlane modification for the project "%s"' => '「%s」に対するスイムレーン変更', + 'Swimlane not found.' => 'スイムレーンが見つかりません。', + 'Swimlane removed successfully.' => 'スイムレーンを削除しました。', + 'Swimlanes' => 'スイムレーン', + 'Swimlane updated successfully.' => 'スイムレーンを更新しました。', + 'The default swimlane have been updated successfully.' => 'デフォルトスイムレーンを更新しました。', + 'Unable to create your swimlane.' => 'スイムレーンを追加できませんでした。', + 'Unable to remove this swimlane.' => 'スイムレーンを削除できませんでした。', + 'Unable to update this swimlane.' => 'スイムレーンを更新できませんでした。', + 'Your swimlane have been created successfully.' => 'スイムレーンが作成されました。', + 'Example: "Bug, Feature Request, Improvement"' => '例: バグ, 機能, 改善', + 'Default categories for new projects (Comma-separated)' => '新しいプロジェクトのデフォルトカテゴリー (コンマ区切り)', + 'Gitlab commit received' => 'Gitlab コミットを受診しました', + 'Gitlab issue opened' => 'Gitlab Issue がオープンされました', + 'Gitlab issue closed' => 'Gitlab Issue がクローズされました', + 'Gitlab webhooks' => 'Gitlab Webhooks', + 'Help on Gitlab webhooks' => 'Gitlab Webhooks のヘルプ', + 'Integrations' => '連携', + 'Integration with third-party services' => 'サードパーティサービスとの連携', + 'Role for this project' => 'このプロジェクトの役割', + 'Project manager' => 'プロジェクトマネジャー', + 'Project member' => 'プロジェクトメンバー', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'プロジェクトマネジャーはプロジェクトの設定を変更するなどの通常ユーザにはない権限があります。', + 'Gitlab Issue' => 'Gitlab Issue', + 'Subtask Id' => 'サブタスク Id', + 'Subtasks' => 'サブタスク', + 'Subtasks Export' => 'サブタスクの出力', + 'Subtasks exportation for "%s"' => '「%s」のサブタスク出力', + 'Task Title' => 'タスクタイトル', + 'Untitled' => 'タイトル無し', + 'Application default' => 'アプリケーションデフォルト', + 'Language:' => '言語:', + 'Timezone:' => 'タイムゾーン:', + 'All columns' => '全てのカラム', + 'Calendar for "%s"' => '「%s」のカレンダー', + 'Filter by column' => 'カラムでフィルタ', + 'Filter by status' => 'ステータスでフィルタ', + 'Calendar' => 'カレンダー', + 'Next' => '次へ', + '#%d' => '#%d', + 'Filter by color' => '色でフィルタ', + 'Filter by swimlane' => 'スイムレーンでフィルタ', + 'All swimlanes' => '全てのスイムレーン', + 'All colors' => '全ての色', + 'All status' => '全てのステータス', + 'Add a comment logging moving the task between columns' => 'カラム間のタスク移動をコメントに記録', + 'Moved to column %s' => 'カラム %s へ移動しました', + 'Change description' => '説明を変更', + 'User dashboard' => 'ユーザダッシュボード', + 'Allow only one subtask in progress at the same time for a user' => '一人のユーザにつき一つのタスクのみ進行中にできます', + 'Edit column "%s"' => 'カラム「%s」の編集', + 'Enable time tracking for subtasks' => 'サブタスクのタイムトラッキングを有効', + 'Select the new status of the subtask: "%s"' => 'サブタスク「%s」のステータスを選択', + 'Subtask timesheet' => 'サブタスクタイムシート', + 'There is nothing to show.' => '何も表示するものがありません。', + 'Time Tracking' => 'タイムトラッキング', + 'You already have one subtask in progress' => 'すでに進行中のサブタスクがあります。', + 'Which parts of the project do you want to duplicate?' => 'プロジェクトの何を複製しますか?', + 'Change dashboard view' => 'ダッシュボードビューを変更', + 'Show/hide activities' => 'アクティビティの表示・非表示', + 'Show/hide projects' => 'プロジェクトの表示・非表示', + 'Show/hide subtasks' => 'サブタスクの表示・非表示', + 'Show/hide tasks' => 'タスクの表示・非表示', + 'Disable login form' => 'ログインフォームの無効化', + 'Show/hide calendar' => 'カレンダーの表示・非表示', + 'User calendar' => 'ユーザカレンダー', + 'Bitbucket commit received' => 'Bitbucket コミットを受信しました', + 'Bitbucket webhooks' => 'Bitbucket Webhooks', + 'Help on Bitbucket webhooks' => 'Bitbucket Webhooks のヘルプ', + 'Start' => '開始', + 'End' => '終了', + 'Task age in days' => 'タスクの経過日数', + 'Days in this column' => 'カラムでの経過日数', + '%dd' => '%d 日', + 'Add a link' => 'リンクの追加', + 'Add a new link' => '新しいリンクの追加', + 'Do you really want to remove this link: "%s"?' => 'リンク「%s」を本当に削除しますか?', + 'Do you really want to remove this link with task #%d?' => 'このリンクとタスク#%dを削除しますか?', + 'Field required' => 'フィールドが必要です', + 'Link added successfully.' => 'リンクを追加しました。', + 'Link updated successfully.' => 'リンクを更新しました。', + 'Link removed successfully.' => 'リンクを削除しました。', + 'Link labels' => 'リンクラベル', + 'Link modification' => 'リンクの変更', + 'Links' => 'リンク', + 'Link settings' => 'リンク設定', + 'Opposite label' => '反対のラベル', + 'Remove a link' => 'ラベルの削除', + 'Task\'s links' => 'タスクのラベル', + 'The labels must be different' => '異なるラベルを指定してください', + 'There is no link.' => 'リンクがありません', + 'This label must be unique' => 'ラベルはユニークである必要があります', + 'Unable to create your link.' => 'リンクを作成できませんでした。', + 'Unable to update your link.' => 'リンクを更新できませんでした。', + 'Unable to remove this link.' => 'リンクを削除できませんでした。', + 'relates to' => '次に関連します', + 'blocks' => '次をブロックしています', + 'is blocked by' => '次にブロックされています', + 'duplicates' => '次に重複しています', + 'is duplicated by' => '次に重複しています', + 'is a child of' => '次の子タスクです ', + 'is a parent of' => '次の親タスクです', + 'targets milestone' => '次のマイルストーンを目標とします', + 'is a milestone of' => '次のタスクのマイルストーンです', + 'fixes' => '次を修正します', + 'is fixed by' => '次に修正されます', + 'This task' => 'このタスクは', + '<1h' => '<1時間', + '%dh' => '%d 時間', + '%b %e' => '%b/%e', + 'Expand tasks' => 'タスクを展開する', + 'Collapse tasks' => 'タスクを閉じる', + 'Expand/collapse tasks' => 'タスクの展開/閉じる', + 'Close dialog box' => 'ダイアログボックスを閉じる', + 'Submit a form' => 'フォームを送信する', + 'Board view' => 'ボードビュー', + 'Keyboard shortcuts' => 'キーボードショートカット', + 'Open board switcher' => 'ボード切り替えを開く', + 'Application' => 'アプリケーション', + 'Filter recently updated' => 'フィルタがアップデートされました', + 'since %B %e, %Y at %k:%M %p' => '%Y/%m/%d %k:%M から', + 'More filters' => '他のフィルタ', + 'Compact view' => 'コンパクトビュー', + 'Horizontal scrolling' => '縦スクロール', + 'Compact/wide view' => 'コンパクト/ワイドビュー', + 'No results match:' => '結果が一致しませんでした', + 'Remove hourly rate' => '毎時レートを削除', + 'Do you really want to remove this hourly rate?' => '毎時レートを削除しますか?', + 'Hourly rates' => '毎時レート', + 'Hourly rate' => '毎時レート', + 'Currency' => '通貨', + 'Effective date' => '有効期限', + 'Add new rate' => '新しいレート', + 'Rate removed successfully.' => 'レートの削除に成功しました。', + 'Unable to remove this rate.' => 'レートを削除できませんでした。', + 'Unable to save the hourly rate.' => '時間毎のレートを保存できませんでした。', + 'Hourly rate created successfully.' => '時間毎のレートを作成しました。', + 'Start time' => '開始時間', + 'End time' => '終了時間', + 'Comment' => 'コメント', + 'All day' => '終日', + 'Day' => '日', + 'Manage timetable' => 'タイムテーブルの管理', + 'Overtime timetable' => '残業タイムテーブル', + 'Time off timetable' => '休暇タイムテーブル', + 'Timetable' => 'タイムテーブル', + 'Work timetable' => 'ワークタイムテーブル', + 'Week timetable' => '週次タイムテーブル', + 'Day timetable' => '日時タイムテーブル', + 'From' => 'ここから', + 'To' => 'ここまで', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + 'Remove time slot' => 'タイムスロットの削除', + 'Add new time slot' => 'タイムラインの追加', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'このタイムテーブルは、残業や休暇で全日がチェックされた場合に用いられます。', + 'Files' => 'ファイル', + 'Images' => '画像', + 'Private project' => 'プライベートプロジェクト', + 'Amount' => '数量', + 'AUD - Australian Dollar' => 'AUD - 豪ドル', + 'Budget' => '予算', + 'Budget line' => '予算ライン', + 'Budget line removed successfully.' => '予算ラインを削除しました.', + 'Budget lines' => '予算ライン', + 'CAD - Canadian Dollar' => 'CAD - 加ドル', + 'CHF - Swiss Francs' => 'CHF - スイスフラン', + 'Cost' => 'コスト', + 'Cost breakdown' => 'コストブレークダウン', + 'Custom Stylesheet' => 'カスタムスタイルシート', + 'download' => 'ダウンロード', + 'Do you really want to remove this budget line?' => 'この予算ラインを本当に削除しますか?', + 'EUR - Euro' => 'EUR - ユーロ', + 'Expenses' => '支出', + 'GBP - British Pound' => 'GBP - 独ポンド', + 'INR - Indian Rupee' => 'INR - 伊ルピー', + 'JPY - Japanese Yen' => 'JPY - 日本円', + 'New budget line' => '新しい予算ライン', + 'NZD - New Zealand Dollar' => 'NZD - NZ ドル', + 'Remove a budget line' => '予算ラインの削除', + 'Remove budget line' => '予算ラインの削除', + 'RSD - Serbian dinar' => 'RSD - セルビアデナール', + 'The budget line have been created successfully.' => '予算ラインを作成しました', + 'Unable to create the budget line.' => '予算ラインを作成できませんでした。', + 'Unable to remove this budget line.' => '予算ラインを削除できませんでした。', + 'USD - US Dollar' => 'USD - 米ドル', + 'Remaining' => '残り', + 'Destination column' => '移動先のカラム', + 'Move the task to another column when assigned to a user' => 'ユーザの割り当てをしたらタスクを他のカラムに移動', + 'Move the task to another column when assignee is cleared' => 'ユーザの割り当てがなくなったらタスクを他のカラムに移動', + 'Source column' => '移動元のカラム', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => '履歴', + 'Executer' => '実行者', + 'Time spent in the column' => 'カラムでの時間消費', + 'Task transitions' => 'タスクの遷移', + 'Task transitions export' => 'タスクの遷移を出力', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'このレポートはタスクのカラム間における移動を時間、ユーザー、経過時間と共に記録した物です。', + 'Currency rates' => '為替レート', + 'Rate' => 'レート', + 'Change reference currency' => '現在の基軸通貨', + 'Add a new currency rate' => '新しい通貨レートを追加', + 'Currency rates are used to calculate project budget.' => '通貨レートはプロジェクト予算の算出に利用されます。', + 'Reference currency' => '基軸通貨', + // 'The currency rate have been added successfully.' => '', + 'Unable to add this currency rate.' => 'この通貨レートを追加できません。', + 'Send notifications to a Slack channel' => 'Slack チャンネルに通知を送信', + 'Webhook URL' => 'Webhook URL', + 'Help on Slack integration' => 'Slack 連携のヘルプ', + '%s remove the assignee of the task %s' => '%s がタスク「%s」の担当を解除しました。', + 'Send notifications to Hipchat' => 'Hipchat に通知を送信', + 'API URL' => 'API URL', + 'Room API ID or name' => 'Room API ID または名前', + 'Room notification token' => 'Room 通知トークン', + 'Help on Hipchat integration' => 'Hipchat 連携のヘルプ', + 'Enable Gravatar images' => 'Gravatar イメージを有効化', + 'Information' => '情報 ', + 'Check two factor authentication code' => '2 段認証をチェックする', + 'The two factor authentication code is not valid.' => '2 段認証コードは無効です。', + 'The two factor authentication code is valid.' => '2 段認証コードは有効です。', + 'Code' => 'コード', + 'Two factor authentication' => '2 段認証', + 'Enable/disable two factor authentication' => '2 段認証の有効/無効', + 'This QR code contains the key URI: ' => 'この QR コードが URI キーを含んでいます: ', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '秘密鍵を TOTP ソフトに保存 (Google Authenticator や FreeOTP など)', + 'Check my code' => '自分のコードをチェック', + 'Secret key: ' => '秘密鍵: ', + 'Test your device' => 'デバイスをテストする', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php new file mode 100644 index 00000000..43ed7e35 --- /dev/null +++ b/app/Locale/nl_NL/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => 'Geen', + 'edit' => 'bewerken', + 'Edit' => 'Bewerken', + 'remove' => 'verwijderen', + 'Remove' => 'Verwijderen', + 'Update' => 'Update', + 'Yes' => 'Ja', + 'No' => 'Nee', + 'cancel' => 'annuleren', + 'or' => 'of', + 'Yellow' => 'Geel', + 'Blue' => 'Blauw', + 'Green' => 'Groen', + 'Purple' => 'Paars', + 'Red' => 'Rood', + 'Orange' => 'Oranje', + 'Grey' => 'Grijs', + 'Save' => 'Opslaan', + 'Login' => 'Inloggen', + 'Official website:' => 'Officiële website :', + 'Unassigned' => 'Niet toegewezen', + 'View this task' => 'Deze taak bekijken', + 'Remove user' => 'Gebruiker verwijderen', + 'Do you really want to remove this user: "%s"?' => 'Weet u zeker dat u deze gebruiker wil verwijderen : « %s » ?', + 'New user' => 'Nieuwe gebruiker', + 'All users' => 'Alle gebruikers', + 'Username' => 'Gebruikersnaam', + 'Password' => 'Wachtwoord', + 'Default project' => 'Standaard wachtwoord', + 'Administrator' => 'Administrator', + 'Sign in' => 'Inloggen', + 'Users' => 'Gebruikers', + 'No user' => 'Geen gebruiker', + 'Forbidden' => 'Geweigerd', + 'Access Forbidden' => 'Toegang geweigerd', + 'Only administrators can access to this page.' => 'Alleen administrators hebben toegang tot deze pagina.', + 'Edit user' => 'Gebruiker bewerken', + 'Logout' => 'Uitloggen', + 'Bad username or password' => 'Verkeerde gebruikersnaam of wachtwoord', + 'users' => 'gebruikers', + 'projects' => 'projecten', + 'Edit project' => 'Project bewerken', + 'Name' => 'Naam', + 'Activated' => 'Geactiveerd', + 'Projects' => 'Projecten', + 'No project' => 'Geen project', + 'Project' => 'Project', + 'Status' => 'Status', + 'Tasks' => 'Taken', + 'Board' => 'Bord', + 'Actions' => 'Acties', + 'Inactive' => 'Inactief', + 'Active' => 'Actief', + 'Column %d' => 'Kolom %d', + 'Add this column' => 'Deze kolom toevoegen', + '%d tasks on the board' => '%d taken op het bord', + '%d tasks in total' => '%d taken in totaal', + 'Unable to update this board.' => 'Update van dit bord niet mogelijk.', + 'Edit board' => 'Bord bewerken', + 'Disable' => 'Deactiveren', + 'Enable' => 'Activeren', + 'New project' => 'Nieuw project', + 'Do you really want to remove this project: "%s"?' => 'Weet u zeker dat u dit project wil verwijderen : « %s » ?', + 'Remove project' => 'Project verwijderen', + 'Boards' => 'Borden', + 'Edit the board for "%s"' => 'Bord bewerken voor « %s »', + 'All projects' => 'Alle projecten', + 'Change columns' => 'Kolommen veranderen', + 'Add a new column' => 'Kolom toevoegen', + 'Title' => 'Titel', + 'Add Column' => 'Kolom toevoegen', + 'Project "%s"' => 'Project « %s »', + 'Nobody assigned' => 'Niemand toegewezen', + 'Assigned to %s' => 'Toegewezen aan %s', + 'Remove a column' => 'Kolom verwijderen', + 'Remove a column from a board' => 'Kolom verwijderen van het bord', + 'Unable to remove this column.' => 'Verwijderen van deze kolom niet mogelijk.', + 'Do you really want to remove this column: "%s"?' => 'Weet u zeker dat u deze kolom wil verwijderen : « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Deze actie zal ALLE TAKEN VERWIJDEREN die zijn geassocieerd met deze kolom!', + 'Settings' => 'Instellingen', + 'Application settings' => 'Applicatie instellingen', + 'Language' => 'Taal', + 'Webhook token:' => 'Webhook token :', + 'API token:' => 'API token :', + 'More information' => 'Meer informatie', + 'Database size:' => 'Database grootte :', + 'Download the database' => 'Download de database', + 'Optimize the database' => 'Optimaliseer de database', + '(VACUUM command)' => '(VACUUM commando)', + '(Gzip compressed Sqlite file)' => '(Gzip ingepakt Sqlite bestand)', + 'User settings' => 'Gebruikers instellingen', + 'My default project:' => 'Mijn standaard project : ', + 'Close a task' => 'Taak sluiten', + 'Do you really want to close this task: "%s"?' => 'Weet u zeker dat u deze taak wil sluiten : « %s » ?', + 'Edit a task' => 'Taak bewerken', + 'Column' => 'Kolom', + 'Color' => 'Kleur', + 'Assignee' => 'Toegewezene', + 'Create another task' => 'Nog een taak aanmaken', + 'New task' => 'Nieuwe taak', + 'Open a task' => 'Een taak openen', + 'Do you really want to open this task: "%s"?' => 'Weet u zeker dat u deze taak wil openen : « %s » ?', + 'Back to the board' => 'Terug naar het bord', + 'Created on %B %e, %Y at %k:%M %p' => 'Aangemaakt op %d/%m/%Y à %H:%M', + 'There is nobody assigned' => 'Er is niemand toegewezen', + 'Column on the board:' => 'Kolom op het bord : ', + 'Status is open' => 'Status is open', + 'Status is closed' => 'Status is gesloten', + 'Close this task' => 'Deze taak sluiten', + 'Open this task' => 'Deze taak openen', + 'There is no description.' => 'Er is geen omschrijving.', + 'Add a new task' => 'Een nieuwe taak toevoegen', + 'The username is required' => 'De gebruikersnaam is verplicht', + 'The maximum length is %d characters' => 'De maximale lengte is %d karakters', + 'The minimum length is %d characters' => 'De minimale lengte is %d karakters', + 'The password is required' => 'Het wachtwoord is verplicht', + 'This value must be an integer' => 'Deze waarde dient een integer te zijn', + 'The username must be unique' => 'De gebruikersnaam moet uniek zijn', + 'The username must be alphanumeric' => 'De gebruikersnaam moet alfanumeriek zijn', + 'The user id is required' => 'Het gebruikers id is verplicht', + 'Passwords don\'t match' => 'De wachtwoorden komen niet overeen', + 'The confirmation is required' => 'De bevestiging is verplicht', + 'The column is required' => 'De kolom is verplicht', + 'The project is required' => 'Het project is verplicht', + 'The color is required' => 'De kleur is verplicht', + 'The id is required' => 'Het id is verplicht', + 'The project id is required' => 'Het project id is verplicht', + 'The project name is required' => 'De projectnaam is verplicht', + 'This project must be unique' => 'Dit project moet uniek zijn', + 'The title is required' => 'De titel is verplicht', + 'The language is required' => 'De taal is verplicht', + 'There is no active project, the first step is to create a new project.' => 'Er is geen actief project, de eerste stap is een nieuw project aanmaken.', + 'Settings saved successfully.' => 'Instellingen succesvol opgeslagen.', + 'Unable to save your settings.' => 'Instellingen opslaan niet gelukt.', + 'Database optimization done.' => 'Database optimaliseren voltooid.', + 'Your project have been created successfully.' => 'Uw project is succesvol aangemaakt.', + 'Unable to create your project.' => 'Het aanmaken van het project is niet gelukt.', + 'Project updated successfully.' => 'Project succesvol geupdate.', + 'Unable to update this project.' => 'Updaten van project niet gelukt.', + 'Unable to remove this project.' => 'Verwijderen van project niet gelukt.', + 'Project removed successfully.' => 'Project succesvol verwijderd.', + 'Project activated successfully.' => 'Project succesvol geactiveerd.', + 'Unable to activate this project.' => 'Project activeren niet gelukt.', + 'Project disabled successfully.' => 'Project uitschakelen succesvol.', + 'Unable to disable this project.' => 'Project uitschakelen niet gelukt.', + 'Unable to open this task.' => 'Openen van deze taak niet gelukt.', + 'Task opened successfully.' => 'Taak succesvol geopend.', + 'Unable to close this task.' => 'Sluiten van deze taak niet gelukt.', + 'Task closed successfully.' => 'Taak succesvol gesloten.', + 'Unable to update your task.' => 'Updaten van uw taak mislukt.', + 'Task updated successfully.' => 'Taak succesvol geupdate.', + 'Unable to create your task.' => 'Taak aanmaken niet gelukt.', + 'Task created successfully.' => 'Taak succesvol aangemaakt.', + 'User created successfully.' => 'Gebruiker succesvol aangemaakt.', + 'Unable to create your user.' => 'Aanmaken van gebruiker niet gelukt.', + 'User updated successfully.' => 'Gebruiker succesvol geupdate', + 'Unable to update your user.' => 'Updaten van gebruiker niet gelukt.', + 'User removed successfully.' => 'Gebruiker succesvol verwijderd.', + 'Unable to remove this user.' => 'Verwijderen van gebruikers niet gelukt.', + 'Board updated successfully.' => 'Board succesvol geupdate.', + 'Ready' => 'Klaar', + 'Backlog' => 'Het wachten', + 'Work in progress' => 'In behandeling', + 'Done' => 'Afgewerkt', + 'Application version:' => 'Applicatie versie :', + 'Completed on %B %e, %Y at %k:%M %p' => 'Voltooid op %d/%m/%Y à %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y op %H:%M', + 'Date created' => 'Datum aangemaakt', + 'Date completed' => 'Datum voltooid', + 'Id' => 'Id', + 'No task' => 'Geen taak', + 'Completed tasks' => 'Voltooide taken', + 'List of projects' => 'Lijst van projecten', + 'Completed tasks for "%s"' => 'Vooltooide taken voor « %s »', + '%d closed tasks' => '%d gesloten taken', + 'No task for this project' => 'Geen taken voor dit project', + 'Public link' => 'Publieke link', + 'There is no column in your project!' => 'Er is geen kolom in uw project !', + 'Change assignee' => 'Toegewezene aanpassen', + 'Change assignee for the task "%s"' => 'Toegewezene aanpassen voor taak « %s »', + 'Timezone' => 'Tijdzone', + 'Sorry, I didn\'t find this information in my database!' => 'Sorry deze informatie kon niet worden gevonden in de database !', + 'Page not found' => 'Pagina niet gevonden', + 'Complexity' => 'Complexiteit', + 'limit' => 'Limiet', + 'Task limit' => 'Taak limiet.', + 'Task count' => 'Aantal taken', + 'This value must be greater than %d' => 'Deze waarde moet groter zijn dan %d', + 'Edit project access list' => 'Aanpassen toegangsrechten project', + 'Edit users access' => 'Gebruikerstoegang aanpassen', + 'Allow this user' => 'Deze gebruiker toestaan', + 'Only those users have access to this project:' => 'Alleen deze gebruikers hebben toegang tot dit project :', + 'Don\'t forget that administrators have access to everything.' => 'Vergeet niet dat administrators overal toegang hebben.', + 'Revoke' => 'Intrekken', + 'List of authorized users' => 'Lijst met geautoriseerde gebruikers', + 'User' => 'Gebruiker', + 'Nobody have access to this project.' => 'Niemand heeft toegang tot dit project', + 'You are not allowed to access to this project.' => 'U heeft geen toegang tot dit project.', + 'Comments' => 'Commentaar', + 'Post comment' => 'Commentaar toevoegen', + 'Write your text in Markdown' => 'Schrijf uw tekst in Markdown', + 'Leave a comment' => 'Schrijf een commentaar', + 'Comment is required' => 'Commentaar is verplicht', + 'Leave a description' => 'Schrijf een omschrijving', + 'Comment added successfully.' => 'Commentaar succesvol toegevoegd.', + 'Unable to create your comment.' => 'Commentaar toevoegen niet gelukt.', + 'The description is required' => 'Omschrijving is verplicht', + 'Edit this task' => 'Deze taak aanpassen', + 'Due Date' => 'Vervaldag', + 'Invalid date' => 'Ongeldige datum', + 'Must be done before %B %e, %Y' => 'Moet voltooid zijn voor %d/%m/%Y', + '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d/%m/%Y', + 'Automatic actions' => 'Geautomatiseerd acties', + 'Your automatic action have been created successfully.' => 'Geautomatiseerde actie succesvol aangemaakt.', + 'Unable to create your automatic action.' => 'Geautomatiseerde actie aanmaken niet gelukt.', + 'Remove an action' => 'Actie verwijderen', + 'Unable to remove this action.' => 'Actie verwijderen niet gelukt', + 'Action removed successfully.' => 'Actie succesvol verwijder.', + 'Automatic actions for the project "%s"' => 'Automatiseer acties voor project « %s »', + 'Defined actions' => 'Gedefinieerde acties', + 'Add an action' => 'Actie toevoegen', + 'Event name' => 'Naam gebeurtenis', + 'Action name' => 'Actie naam', + 'Action parameters' => 'Actie paramaters', + 'Action' => 'Actie', + 'Event' => 'Evenement', + 'When the selected event occurs execute the corresponding action.' => 'Als de geselecteerde gebeurtenis optreedt de volgende actie uitvoeren', + 'Next step' => 'Volgende stap', + 'Define action parameters' => 'Bepaal actie parameters', + 'Save this action' => 'Actie opslaan', + 'Do you really want to remove this action: "%s"?' => 'Weet u zeker dat u de volgende actie wil verwijderen : « %s » ?', + 'Remove an automatic action' => 'Automatische actie verwijderen', + 'Close the task' => 'Taak sluiten', + 'Assign the task to a specific user' => 'Taak toewijzen aan een gebruiker', + 'Assign the task to the person who does the action' => 'Taak toewijzen aan een gebruiker die de actie uitvoert', + 'Duplicate the task to another project' => 'Taak dupliceren in een ander project', + 'Move a task to another column' => 'Taak verplaatsen naar een andere kolom', + 'Move a task to another position in the same column' => 'Taak verplaatsen naar een andere positie in dezelfde kolom', + 'Task modification' => 'Taak aanpassen', + 'Task creation' => 'Taak aanmaken', + 'Open a closed task' => 'Gesloten taak openen', + 'Closing a task' => 'Taak sluiten', + 'Assign a color to a specific user' => 'Wijs een kleur toe aan een gebruiker', + 'Column title' => 'Kolom titel', + 'Position' => 'Positie', + 'Move Up' => 'Omhoog verplaatsen', + 'Move Down' => 'Omlaag verplaatsen', + 'Duplicate to another project' => 'Dupliceren in een ander project', + 'Duplicate' => 'Dupliceren', + 'link' => 'koppelen', + 'Update this comment' => 'Commentaar aanpassen', + 'Comment updated successfully.' => 'Commentaar succesvol aangepast.', + 'Unable to update your comment.' => 'Commentaar aanpassen niet gelukt.', + 'Remove a comment' => 'Commentaar verwijderen', + 'Comment removed successfully.' => 'Commentaar succesvol verwijder.', + 'Unable to remove this comment.' => 'Commentaar verwijderen niet gelukt.', + 'Do you really want to remove this comment?' => 'Weet u zeker dat u dit commentaar wil verwijderen ?', + 'Only administrators or the creator of the comment can access to this page.' => 'Alleen administrators of de aanmaker van het commentaar hebben toegang tot deze pagina.', + 'Details' => 'Details', + 'Current password for the user "%s"' => 'Huidig wachtwoord voor gebruiker « %s »', + 'The current password is required' => 'Huidig wachtwoord is verplicht', + 'Wrong password' => 'Onjuist wachtwoord', + 'Reset all tokens' => 'Alle tokens resetten', + 'All tokens have been regenerated.' => 'Alle tokens zijn opnieuw gegenereerd.', + 'Unknown' => 'Onbekend', + 'Last logins' => 'Laatste logins', + 'Login date' => 'Login datum', + 'Authentication method' => 'Authenticatie methode', + 'IP address' => 'IP adres', + 'User agent' => 'User agent', + 'Persistent connections' => 'Persistente connectie', + 'No session.' => 'Geen sessie.', + 'Expiration date' => 'Verloopdatum', + 'Remember Me' => 'Onthoud mij', + 'Creation date' => 'Aanmaakdatum', + 'Filter by user' => 'Filter op gebruiker', + 'Filter by due date' => 'Filter op vervaldatum', + 'Everybody' => 'Iedereen', + 'Open' => 'Open', + 'Closed' => 'Gesloten', + 'Search' => 'Zoek', + 'Nothing found.' => 'Niets gevonden.', + 'Search in the project "%s"' => 'Zoek in project « %s »', + 'Due date' => 'Vervaldatum', + 'Others formats accepted: %s and %s' => 'Andere toegestane formaten : %s en %s', + 'Description' => 'Omschrijving', + '%d comments' => '%d commentaren', + '%d comment' => '%d commentaar', + 'Email address invalid' => 'Ongeldig emailadres', + 'Your Google Account is not linked anymore to your profile.' => 'Uw Google Account is niet meer aan uw profiel gelinkt.', + 'Unable to unlink your Google Account.' => 'Verwijderen link met Google Account niet gelukt.', + 'Google authentication failed' => 'Google authenticatie niet gelukt', + 'Unable to link your Google Account.' => 'Linken met Google Account niet gelukt', + 'Your Google Account is linked to your profile successfully.' => 'Linken met Google Account succesvol.', + 'Email' => 'Email', + 'Link my Google Account' => 'Link mijn Google Account', + 'Unlink my Google Account' => 'Link met Google Account verwijderen', + 'Login with my Google Account' => 'Inloggen met mijn Google Account', + 'Project not found.' => 'Project niet gevonden.', + 'Task #%d' => 'Taak %d', + 'Task removed successfully.' => 'Taak succesvol verwijderd.', + 'Unable to remove this task.' => 'Taak verwijderen niet gelukt.', + 'Remove a task' => 'Taak verwijderen', + 'Do you really want to remove this task: "%s"?' => 'Weet u zeker dat u deze taak wil verwijderen « %s » ?', + 'Assign automatically a color based on a category' => 'Automatisch een kleur toewijzen aan de hand van een categorie', + 'Assign automatically a category based on a color' => 'Automatisch een categorie toewijzen aan de hand van een kleur', + 'Task creation or modification' => 'Taak aanmaken of wijzigen', + 'Category' => 'Categorie', + 'Category:' => 'Categorie :', + 'Categories' => 'Categorieën', + 'Category not found.' => 'Categorie niet gevonden', + 'Your category have been created successfully.' => 'Categorie succesvol aangemaakt.', + 'Unable to create your category.' => 'Categorie aanmaken niet gelukt.', + 'Your category have been updated successfully.' => 'Categorie succesvol aangepast.', + 'Unable to update your category.' => 'Aanpassen van categorie niet gelukt.', + 'Remove a category' => 'Categorie verwijderen', + 'Category removed successfully.' => 'Categorie succesvol verwijderd.', + 'Unable to remove this category.' => 'Categorie verwijderen niet gelukt.', + 'Category modification for the project "%s"' => 'Categorie aanpassen voor project « %s »', + 'Category Name' => 'Categorie naam', + 'Categories for the project "%s"' => 'Categorieën voor project « %s »', + 'Add a new category' => 'Categorie toevoegen', + 'Do you really want to remove this category: "%s"?' => 'Weet u zeker dat u deze categorie wil verwijderen: « %s » ?', + 'Filter by category' => 'Filter op categorie', + 'All categories' => 'Alle categorieën', + 'No category' => 'Geen categorie', + 'The name is required' => 'De naam is verplicht', + 'Remove a file' => 'Bestand verwijderen', + 'Unable to remove this file.' => 'Bestand verwijderen niet gelukt.', + 'File removed successfully.' => 'Bestand succesvol verwijdered.', + 'Attach a document' => 'Document toevoegen', + 'Do you really want to remove this file: "%s"?' => 'Weet u zeker dat u dit bestand wil verwijderen: « %s » ?', + 'open' => 'openen', + 'Attachments' => 'Bijlages', + 'Edit the task' => 'Taak aanpassen', + 'Edit the description' => 'Omschrijving aanpassen', + 'Add a comment' => 'Commentaar toevoegen', + 'Edit a comment' => 'Commentaar aanpassen', + 'Summary' => 'Samenvatting', + 'Time tracking' => 'Tijdschrijven', + 'Estimate:' => 'Schatting :', + 'Spent:' => 'Besteed :', + 'Do you really want to remove this sub-task?' => 'Weet u zeker dat u deze subtaak wil verwijderen ?', + 'Remaining:' => 'Restant :', + 'hours' => 'uren', + 'spent' => 'besteed', + 'estimated' => 'geschat', + 'Sub-Tasks' => 'Subtaken', + 'Add a sub-task' => 'Subtaak toevoegen', + 'Original estimate' => 'Orginele schatting', + 'Create another sub-task' => 'Nog een subtaak toevoegen', + 'Time spent' => 'Tijd besteed', + 'Edit a sub-task' => 'Subtaak aanpassen', + 'Remove a sub-task' => 'Subtaak verwijderen', + 'The time must be a numeric value' => 'De tijd moet een numerieke waarde zijn', + 'Todo' => 'Nog te doen', + 'In progress' => 'In behandeling', + 'Sub-task removed successfully.' => 'Subtaak succesvol verwijderd.', + 'Unable to remove this sub-task.' => 'Subtaak verwijderen niet gelukt.', + 'Sub-task updated successfully.' => 'Subtaak succesvol aangepast.', + 'Unable to update your sub-task.' => 'Subtaak aanpassen niet gelukt.', + 'Unable to create your sub-task.' => 'Subtaak aanmaken niet gelukt.', + 'Sub-task added successfully.' => 'Subtaak succesvol aangemaakt.', + 'Maximum size: ' => 'Maximale grootte : ', + 'Unable to upload the file.' => 'Uploaden van bestand niet gelukt.', + 'Display another project' => 'Een ander project weergeven', + 'Your GitHub account was successfully linked to your profile.' => 'Uw Github Account is succesvol gelinkt aan uw profiel.', + 'Unable to link your GitHub Account.' => 'Linken van uw Github Account niet gelukt.', + 'GitHub authentication failed' => 'Github Authenticatie niet gelukt', + 'Your GitHub account is no longer linked to your profile.' => 'Uw Github Account is niet langer gelinkt aan uw profiel.', + 'Unable to unlink your GitHub Account.' => 'Verwijdern van de link met uw Github Account niet gelukt.', + 'Login with my GitHub Account' => 'Login met mijn Github Account', + 'Link my GitHub Account' => 'Link met mijn Github', + 'Unlink my GitHub Account' => 'Link met mijn Github verwijderen', + 'Created by %s' => 'Aangemaakt door %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Laatst gewijzigd op %d/%m/%Y à %H:%M', + 'Tasks Export' => 'Taken exporteren', + 'Tasks exportation for "%s"' => 'Taken exporteren voor « %s »', + 'Start Date' => 'Startdatum', + 'End Date' => 'Einddatum', + 'Execute' => 'Uitvoeren', + 'Task Id' => 'Taak Id', + 'Creator' => 'Aangemaakt door', + 'Modification date' => 'Wijzigingsdatum', + 'Completion date' => 'Afgerond op', + 'Clone' => 'Kloon', + 'Clone Project' => 'Project klonen', + 'Project cloned successfully.' => 'Project succesvol gekloond.', + 'Unable to clone this project.' => 'Klonen van project niet gelukt.', + 'Email notifications' => 'Email notificatie', + 'Enable email notifications' => 'Email notificatie aanzetten', + 'Task position:' => 'Taak positie :', + 'The task #%d have been opened.' => 'Taak #%d is geopend.', + 'The task #%d have been closed.' => 'Taak #%d is gesloten.', + 'Sub-task updated' => 'Subtaak aangepast', + 'Title:' => 'Titel :', + 'Status:' => 'Status :', + 'Assignee:' => 'Toegewezene :', + 'Time tracking:' => 'Tijdschrijven :', + 'New sub-task' => 'Nieuwe subtaak', + 'New attachment added "%s"' => 'Nieuwe bijlage toegevoegd « %s »', + 'Comment updated' => 'Commentaar aangepast', + 'New comment posted by %s' => 'Nieuw commentaar geplaatst door « %s »', + 'List of due tasks for the project "%s"' => 'Lijst van taken die binnenkort voltooid moeten worden voor project « %s »', + 'New attachment' => 'Nieuwe bijlage', + 'New comment' => 'Nieuw commentaar', + 'New subtask' => 'Nieuwe subtaak', + 'Subtask updated' => 'Subtaak aangepast', + 'Task updated' => 'Taak aangepast', + 'Task closed' => 'Taak gesloten', + 'Task opened' => 'Taak geopend', + '[%s][Due tasks]' => '[%s][binnekort te voltooien taken]', + '[Kanboard] Notification' => '[Kanboard] Notificatie', + 'I want to receive notifications only for those projects:' => 'Ik wil notificaties ontvangen van de volgende projecten :', + 'view the task on Kanboard' => 'taak bekijken op Kanboard', + 'Public access' => 'Publieke toegang', + 'Category management' => 'Categorie management', + 'User management' => 'Gebruikers management', + 'Active tasks' => 'Actieve taken', + 'Disable public access' => 'Publieke toegang uitschakelen', + 'Enable public access' => 'Publieke toegang inschakelen', + 'Active projects' => 'Actieve projecten', + 'Inactive projects' => 'Inactieve projecten', + 'Public access disabled' => 'Publieke toegang uitgeschakeld', + 'Do you really want to disable this project: "%s"?' => 'Weet u zeker dat u dit project wil uitschakelen : « %s » ?', + 'Do you really want to duplicate this project: "%s"?' => 'Weet u zeker dat u dit project wil dupliceren : « %s » ?', + 'Do you really want to enable this project: "%s"?' => 'Weet u zeker dat u dit project wil activeren : « %s » ?', + 'Project activation' => 'Project activatie', + 'Move the task to another project' => 'Taak verplaatsen naar een ander project', + 'Move to another project' => 'Verplaats naar een ander project', + 'Do you really want to duplicate this task?' => 'Weet u zeker dat u deze taak wil dupliceren ?', + 'Duplicate a task' => 'Taak dupliceren', + 'External accounts' => 'Externe accounts', + 'Account type' => 'Account type', + 'Local' => 'Lokaal', + 'Remote' => 'Remote', + 'Enabled' => 'Actief', + 'Disabled' => 'Inactief', + 'Google account linked' => 'Gelinkt Google Account', + 'Github account linked' => 'Gelinkt Github Account', + 'Username:' => 'Gebruikersnaam :', + 'Name:' => 'Naam :', + 'Email:' => 'Email :', + 'Default project:' => 'Standaard project :', + 'Notifications:' => 'Notificaties :', + 'Notifications' => 'Notificaties', + 'Group:' => 'Groep :', + 'Regular user' => 'Normale gebruiker', + 'Account type:' => 'Account type:', + 'Edit profile' => 'Profiel aanpassen', + 'Change password' => 'Wachtwoord aanpassen', + 'Password modification' => 'Wachtwoord aanpassen', + 'External authentications' => 'Externe authenticatie', + 'Google Account' => 'Google Account', + 'Github Account' => 'Github Account', + 'Never connected.' => 'Nooit verbonden.', + 'No account linked.' => 'Geen account gelinkt.', + 'Account linked.' => 'Account gelinkt.', + 'No external authentication enabled.' => 'Geen externe authenticatie aangezet.', + 'Password modified successfully.' => 'Wachtwoord succesvol aangepast.', + 'Unable to change the password.' => 'Aanpassen van wachtwoord niet gelukt.', + 'Change category for the task "%s"' => 'Pas categorie aan voor taak « %s »', + 'Change category' => 'Categorie aanpassen', + '%s updated the task %s' => '%s heeft taak %s aangepast', + '%s opened the task %s' => '%s heeft taak %s geopend', + '%s moved the task %s to the position #%d in the column "%s"' => '%s heeft taak %s naar positie %d in de kolom « %s » verplaatst', + '%s moved the task %s to the column "%s"' => '%s heeft taak %s verplaatst naar kolom « %s »', + '%s created the task %s' => '%s heeft taak %s aangemaakt', + '%s closed the task %s' => '%s heeft taak %s gesloten', + '%s created a subtask for the task %s' => '%s heeft een subtaak aangemaakt voor taak %s', + '%s updated a subtask for the task %s' => '%s heeft een subtaak aangepast voor taak %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Toegewezen aan %s met een schatting van %s/%sh', + 'Not assigned, estimate of %sh' => 'Niet toegewezen, schatting: %sh', + '%s updated a comment on the task %s' => '%s heeft een commentaar aangepast voor taak %s', + '%s commented the task %s' => '%s heeft een commentaar geplaatst voor taak %s', + '%s\'s activity' => 'Activiteiten van %s', + 'No activity.' => 'Geen activiteiten.', + 'RSS feed' => 'RSS feed', + '%s updated a comment on the task #%d' => '%s heeft een commentaar aangepast voor taak %d', + '%s commented on the task #%d' => '%s heeft commentaar geplaatst voor taak %d', + '%s updated a subtask for the task #%d' => '%s heeft een commentaar aangepast voor subtaak %d', + '%s created a subtask for the task #%d' => '%s heeft een subtaak aangemaakt voor taak %d', + '%s updated the task #%d' => '%s heeft taak %d aangepast', + '%s created the task #%d' => '%s heeft taak %d aangemaakt', + '%s closed the task #%d' => '%s heeft taak %d gesloten', + '%s open the task #%d' => '%s a heeft taak %d geopend', + '%s moved the task #%d to the column "%s"' => '%s heeft taak %d verplaatst naar kolom « %s »', + '%s moved the task #%d to the position %d in the column "%s"' => '%s heeft taak %d verplaatst naar positie %d in kolom « %s »', + 'Activity' => 'Activiteit', + 'Default values are "%s"' => 'Standaardwaarden zijn « %s »', + 'Default columns for new projects (Comma-separated)' => 'Standaard kolommen voor nieuw projecten (komma gescheiden)', + 'Task assignee change' => 'Taak toegewezene verandering', + '%s change the assignee of the task #%d to %s' => '%s heeft de toegewezene voor taak %d veranderd in %s', + '%s changed the assignee of the task %s to %s' => '%s heeft de toegewezene voor taak %d veranderd in %s', + 'Column Change' => 'Kolom verandering', + 'Position Change' => 'Positie verandering', + 'Assignee Change' => 'Toegewezene verandering', + 'New password for the user "%s"' => 'Nieuw wachtwoord voor gebruiker « %s »', + 'Choose an event' => 'Kies een gebeurtenis', + 'Github commit received' => 'Github commentaar ontvangen', + 'Github issue opened' => 'Github issue geopend', + 'Github issue closed' => 'Github issue gesloten', + 'Github issue reopened' => 'Github issue heropend', + 'Github issue assignee change' => 'Github toegewezen veranderd', + 'Github issue label change' => 'Github issue label verander', + 'Create a task from an external provider' => 'Maak een taak aan vanuit een externe provider', + 'Change the assignee based on an external username' => 'Verander de toegewezene aan de hand van de externe gebruikersnaam', + 'Change the category based on an external label' => 'Verander de categorie aan de hand van een extern label', + 'Reference' => 'Referentie', + 'Reference: %s' => 'Referentie : %s', + 'Label' => 'Label', + 'Database' => 'Database', + 'About' => 'Over', + 'Database driver:' => 'Database driver :', + 'Board settings' => 'Bord instellingen', + 'URL and token' => 'URL en token', + 'Webhook settings' => 'Webhook instellingen', + 'URL for task creation:' => 'URL voor aanmaken taken :', + 'Reset token' => 'Token resetten', + 'API endpoint:' => 'API endpoint :', + 'Refresh interval for private board' => 'Verversingsinterval voor private borden', + 'Refresh interval for public board' => 'Verversingsinterval voor publieke borden', + 'Task highlight period' => 'Taak highlight periode', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periode (in seconden) om aan te geven of een taak recent is aangepast (0 om uit te schakelen, standaard 2 dagen)', + 'Frequency in second (60 seconds by default)' => 'Frequentie in seconden (stadaard 60)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequentie in seconden (0 om uit te schakelen, standaard 10)', + 'Application URL' => 'Applicatie URL', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Voorbeeld: http://example.kanboard.net/ (gebruikt voor email notificaties)', + 'Token regenerated.' => 'Token opnieuw gegenereerd.', + 'Date format' => 'Datum formaat', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formaat is altijd geaccepteerd, bijvoorbeeld : « %s » et « %s »', + 'New private project' => 'Nieuw privé project', + 'This project is private' => 'Dit project is privé', + 'Type here to create a new sub-task' => 'Typ hier om een nieuwe subtaak aan te maken', + 'Add' => 'Toevoegen', + 'Estimated time: %s hours' => 'Geschatte tijd: %s hours', + 'Time spent: %s hours' => 'Tijd besteed : %s heures', + 'Started on %B %e, %Y' => 'Gestart op %d/%m/%Y', + 'Start date' => 'Startdatum', + 'Time estimated' => 'Geschatte tijd', + 'There is nothing assigned to you.' => 'Er is niets aan u toegewezen.', + 'My tasks' => 'Mijn taken', + 'Activity stream' => 'Activiteiten', + 'Dashboard' => 'Dashboard', + // 'Confirmation' => '', + 'Allow everybody to access to this project' => 'Geef iedereen toegang tot dit project', + 'Everybody have access to this project.' => 'Iedereen heeft toegang tot dit project.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Integratue', + 'Github webhooks' => 'Github webhooks', + 'Help on Github webhooks' => 'Hulp bij Github webhooks', + 'Create a comment from an external provider' => 'Voeg een commentaar toe van een externe provider', + 'Github issue comment created' => 'Github issue commentaar aangemaakt', + 'Configure' => 'Configureren', + 'Project management' => 'Project management', + 'My projects' => 'Mijn projecten', + 'Columns' => 'Kolommen', + 'Task' => 'Taak', + 'Your are not member of any project.' => 'U bent van geen enkel project lid.', + 'Percentage' => 'Percentage', + 'Number of tasks' => 'Aantal taken', + 'Task distribution' => 'Distributie van taken', + 'Reportings' => 'Rapporten', + 'Task repartition for "%s"' => 'Taakverdeling voor « %s »', + 'Analytics' => 'Analytics', + 'Subtask' => 'Subtaak', + 'My subtasks' => 'Mijn subtaken', + 'User repartition' => 'Gebruikerverdeling', + 'User repartition for "%s"' => 'Gebruikerverdeling voor « %s »', + 'Clone this project' => 'Kloon dit project', + 'Column removed successfully.' => 'Kolom succesvol verwijderd.', + 'Edit Project' => 'Project aanpassen', + 'Github Issue' => 'Github issue', + 'Not enough data to show the graph.' => 'Niet genoeg data om de grafiek te laten zien.', + // 'Previous' => '', + 'The id must be an integer' => 'Het id moet een integer zijn', + 'The project id must be an integer' => 'Het project id moet een integer zijn', + 'The status must be an integer' => 'De status moet een integer zijn', + 'The subtask id is required' => 'Het id van de subtaak is verplicht', + 'The subtask id must be an integer' => 'Het id van de subtaak moet een integer zijn', + 'The task id is required' => 'Het id van de taak is verplicht', + 'The task id must be an integer' => 'Het id van de taak moet een integer zijn', + 'The user id must be an integer' => 'Het id van de gebruiker moet een integer zijn', + 'This value is required' => 'Deze waarde is verplicht', + 'This value must be numeric' => 'Deze waarde moet numeriek zijn', + 'Unable to create this task.' => 'Aanmaken van de taak mislukt', + 'Cumulative flow diagram' => 'Cummulatief stroomdiagram', + 'Cumulative flow diagram for "%s"' => 'Cummulatief stroomdiagram voor « %s »', + 'Daily project summary' => 'Dagelijkse project samenvatting', + 'Daily project summary export' => 'Dagelijkse project samenvatting export', + 'Daily project summary export for "%s"' => 'Dagelijkse project samenvatting voor « %s »', + 'Exports' => 'Exports', + 'This export contains the number of tasks per column grouped per day.' => 'Dit rapport bevat het aantal taken per kolom gegroupeerd per dag.', + 'Nothing to preview...' => 'Niets om te previewen...', + 'Preview' => 'Preview', + 'Write' => 'Schrijf', + 'Active swimlanes' => 'Actieve swinlanes', + 'Add a new swimlane' => 'Nieuwe swimlane toevoegen', + 'Change default swimlane' => 'Standaard swimlane aapassen', + 'Default swimlane' => 'Standaard swinlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Weet u zeker dat u deze swimlane wil verwijderen : « %s » ?', + 'Inactive swimlanes' => 'Inactieve swinlanes', + 'Set project manager' => 'Project manager instellen', + 'Set project member' => 'Project lid instellen', + 'Remove a swimlane' => 'Verwijder swinlane', + 'Rename' => 'Hernoemen', + 'Show default swimlane' => 'Standaard swimlane tonen', + 'Swimlane modification for the project "%s"' => 'Swinlane aanpassing voor project « %s »', + 'Swimlane not found.' => 'Swimlane niet gevonden.', + 'Swimlane removed successfully.' => 'Swimlane succesvol verwijderd.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane succesvol aangepast.', + 'The default swimlane have been updated successfully.' => 'De standaard swimlane is succesvol aangepast.', + 'Unable to create your swimlane.' => 'Swimlane aanmaken niet gelukt.', + 'Unable to remove this swimlane.' => 'Swimlane verwijderen niet gelukt.', + 'Unable to update this swimlane.' => 'Swimlane aanpassen niet gelukt.', + 'Your swimlane have been created successfully.' => 'Swimlane succesvol aangemaakt.', + 'Example: "Bug, Feature Request, Improvement"' => 'Voorbeeld: « Bug, Feature Request, Improvement »', + 'Default categories for new projects (Comma-separated)' => 'Standaard categorieën voor nieuwe projecten (komma gescheiden)', + 'Gitlab commit received' => 'Gitlab commir ontvangen', + 'Gitlab issue opened' => 'Gitlab issue geopend', + 'Gitlab issue closed' => 'Gitlab issue gesloten', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Hulp bij Gitlab webhooks', + 'Integrations' => 'Integraties', + 'Integration with third-party services' => 'Integratie met derde-partij-services', + 'Role for this project' => 'Rol voor dit project', + 'Project manager' => 'Project manager', + 'Project member' => 'Project lid', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Een project manager kan de instellingen van het project wijzigen en heeft meer rechten dan een normale gebruiker.', + 'Gitlab Issue' => 'Gitlab issue', + 'Subtask Id' => 'Subtaak id', + 'Subtasks' => 'Subtaken', + 'Subtasks Export' => 'Subtaken exporteren', + 'Subtasks exportation for "%s"' => 'Subtaken exporteren voor project « %s »', + 'Task Title' => 'Taak title', + 'Untitled' => 'Geen titel', + 'Application default' => 'Standaard taal voor applicatie', + 'Language:' => 'Taal :', + 'Timezone:' => 'Tijdzone :', + 'All columns' => 'Alle kolommen', + 'Calendar for "%s"' => 'Agenda voor « %s »', + 'Filter by column' => 'Filter op kolom', + 'Filter by status' => 'Filter op status', + 'Calendar' => 'Agenda', + 'Next' => 'Volgende', + '#%d' => '%d', + 'Filter by color' => 'Filter op kleur', + 'Filter by swimlane' => 'Filter op swimlane', + 'All swimlanes' => 'Alle swimlanes', + 'All colors' => 'Alle kleuren', + 'All status' => 'Alle statussen', + 'Add a comment logging moving the task between columns' => 'Voeg een commentaar toe bij het verplaatsen van een taak tussen kolommen', + 'Moved to column %s' => 'Verplaatst naar kolom', + 'Change description' => 'Verandering omschrijving', + 'User dashboard' => 'Gebruiker dashboard', + 'Allow only one subtask in progress at the same time for a user' => 'Sta maximaal één subtaak in behandeling toe per gebruiker', + 'Edit column "%s"' => 'Kolom « %s » aanpassen', + 'Enable time tracking for subtasks' => 'Activeer tijdschrijven voor subtaken', + 'Select the new status of the subtask: "%s"' => 'Selecteer nieuwe status voor subtaak : « %s »', + 'Subtask timesheet' => 'Subtaak timesheet', + 'There is nothing to show.' => 'Er is niets om te laten zijn.', + 'Time Tracking' => 'Tijdschrijven', + 'You already have one subtask in progress' => 'U heeft al een subtaak in behandeling', + 'Which parts of the project do you want to duplicate?' => 'Welke onderdelen van het project wilt u dupliceren?', + 'Change dashboard view' => 'Pas dashboard aan', + 'Show/hide activities' => 'Toon/verberg activiteiten', + 'Show/hide projects' => 'Toon/verberg projecten', + 'Show/hide subtasks' => 'Toon/verberg subtaken', + 'Show/hide tasks' => 'Toon/verberg taken', + 'Disable login form' => 'Schakel login scherm uit', + 'Show/hide calendar' => 'Toon/verberg agenda', + 'User calendar' => 'Agenda gebruiker', + 'Bitbucket commit received' => 'Bitbucket commit ontvangen', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Help bij Bitbucket webhooks', + 'Start' => 'Start', + 'End' => 'Eind', + 'Task age in days' => 'Leeftijd taak in dagen', + 'Days in this column' => 'Dagen in deze kolom', + '%dd' => '%dj', + 'Add a link' => 'Link toevoegen', + 'Add a new link' => 'Nieuwe link toevoegen', + 'Do you really want to remove this link: "%s"?' => 'Weet u zeker dat u deze link wil verwijderen : « %s » ?', + 'Do you really want to remove this link with task #%d?' => 'Weet u zeker dat u deze link met taak %d wil verwijderen?', + 'Field required' => 'Veld verplicht', + 'Link added successfully.' => 'Link succesvol toegevoegd.', + 'Link updated successfully.' => 'Link succesvol aangepast.', + 'Link removed successfully.' => 'Link succesvol verwijderd.', + 'Link labels' => 'Link labels', + 'Link modification' => 'Link aanpassing', + 'Links' => 'Links', + 'Link settings' => 'Link instellingen', + 'Opposite label' => 'Tegenovergesteld label', + 'Remove a link' => 'Link verwijderen', + 'Task\'s links' => 'Links van taak', + 'The labels must be different' => 'De labels moeten verschillend zijn', + 'There is no link.' => 'Er is geen link.', + 'This label must be unique' => 'Dit label moet uniek zijn', + 'Unable to create your link.' => 'Link aanmaken niet gelukt.', + 'Unable to update your link.' => 'Link aanpassen niet gelukt.', + 'Unable to remove this link.' => 'Link verwijderen niet gelukt.', + 'relates to' => 'is gerelateerd aan', + 'blocks' => 'blokkeert', + 'is blocked by' => 'is geblokkeerd door', + 'duplicates' => 'dupliceert', + 'is duplicated by' => 'is gedupliceerd', + 'is a child of' => 'is een kind van', + 'is a parent of' => 'is een ouder van', + 'targets milestone' => 'is nodig voor milestone', + 'is a milestone of' => 'is een milestone voor', + 'fixes' => 'corrigeert', + 'is fixed by' => 'word gecorrigeerd door', + 'This task' => 'Deze taal', + '<1h' => '<1h', + '%dh' => '%dh', + '%b %e' => '%e %b', + 'Expand tasks' => 'Taken uitklappen', + 'Collapse tasks' => 'Taken inklappen', + 'Expand/collapse tasks' => 'Taken in/uiklappen', + 'Close dialog box' => 'Venster sluiten', + 'Submit a form' => 'Formulier insturen', + 'Board view' => 'Bord weergave', + 'Keyboard shortcuts' => 'Keyboard snelkoppelingen', + 'Open board switcher' => 'Open bord switcher', + 'Application' => 'Applicatie', + 'Filter recently updated' => 'Filter recent aangepast', + 'since %B %e, %Y at %k:%M %p' => 'sinds %d/%m/%Y à %H:%M', + 'More filters' => 'Meer filters', + // 'Compact view' => '', + // 'Horizontal scrolling' => '', + // 'Compact/wide view' => '', + // 'No results match:' => '', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + // 'Hourly rates' => '', + // 'Hourly rate' => '', + // 'Currency' => '', + // 'Effective date' => '', + // 'Add new rate' => '', + // 'Rate removed successfully.' => '', + // 'Unable to remove this rate.' => '', + // 'Unable to save the hourly rate.' => '', + // 'Hourly rate created successfully.' => '', + // 'Start time' => '', + // 'End time' => '', + // 'Comment' => '', + // 'All day' => '', + // 'Day' => '', + // 'Manage timetable' => '', + // 'Overtime timetable' => '', + // 'Time off timetable' => '', + // 'Timetable' => '', + // 'Work timetable' => '', + // 'Week timetable' => '', + // 'Day timetable' => '', + // 'From' => '', + // 'To' => '', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + // 'Remove time slot' => '', + // 'Add new time slot' => '', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + // 'Files' => '', + // 'Images' => '', + // 'Private project' => '', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + // 'Budget' => '', + // 'Budget line' => '', + // 'Budget line removed successfully.' => '', + // 'Budget lines' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + // 'Cost' => '', + // 'Cost breakdown' => '', + // 'Custom Stylesheet' => '', + // 'download' => '', + // 'Do you really want to remove this budget line?' => '', + // 'EUR - Euro' => '', + // 'Expenses' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'New budget line' => '', + // 'NZD - New Zealand Dollar' => '', + // 'Remove a budget line' => '', + // 'Remove budget line' => '', + // 'RSD - Serbian dinar' => '', + // 'The budget line have been created successfully.' => '', + // 'Unable to create the budget line.' => '', + // 'Unable to remove this budget line.' => '', + // 'USD - US Dollar' => '', + // 'Remaining' => '', + // 'Destination column' => '', + // 'Move the task to another column when assigned to a user' => '', + // 'Move the task to another column when assignee is cleared' => '', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php new file mode 100644 index 00000000..9e3fe96c --- /dev/null +++ b/app/Locale/pl_PL/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => 'Brak', + 'edit' => 'edytuj', + 'Edit' => 'Edytuj', + 'remove' => 'usuń', + 'Remove' => 'Usuń', + 'Update' => 'Aktualizuj', + 'Yes' => 'Tak', + 'No' => 'Nie', + 'cancel' => 'anuluj', + 'or' => 'lub', + 'Yellow' => 'Żółty', + 'Blue' => 'Niebieski', + 'Green' => 'Zielony', + 'Purple' => 'Fioletowy', + 'Red' => 'Czerwony', + 'Orange' => 'Pomarańczowy', + 'Grey' => 'Szary', + 'Save' => 'Zapisz', + 'Login' => 'Login', + 'Official website:' => 'Oficjalna strona:', + 'Unassigned' => 'Nieprzypisany', + 'View this task' => 'Zobacz zadanie', + 'Remove user' => 'Usuń użytkownika', + 'Do you really want to remove this user: "%s"?' => 'Na pewno chcesz usunąć użytkownika: "%s"?', + 'New user' => 'Nowy użytkownik', + 'All users' => 'Wszyscy użytkownicy', + 'Username' => 'Nazwa użytkownika', + 'Password' => 'Hasło', + 'Default project' => 'Domyślny projekt', + 'Administrator' => 'Administrator', + 'Sign in' => 'Zaloguj', + 'Users' => 'Użytkownicy', + 'No user' => 'Brak użytkowników', + 'Forbidden' => 'Zabroniony', + 'Access Forbidden' => 'Dostęp zabroniony', + 'Only administrators can access to this page.' => 'Tylko administrator może wejść na tą stronę.', + 'Edit user' => 'Edytuj użytkownika', + 'Logout' => 'Wyloguj', + 'Bad username or password' => 'Zła nazwa uyżytkownika lub hasło', + 'users' => 'użytkownicy', + 'projects' => 'projekty', + 'Edit project' => 'Edytuj projekt', + 'Name' => 'Nazwa', + 'Activated' => 'Aktywny', + 'Projects' => 'Projekty', + 'No project' => 'Brak projektów', + 'Project' => 'Projekt', + 'Status' => 'Status', + 'Tasks' => 'Zadania', + 'Board' => 'Tablica', + 'Actions' => 'Akcje', + 'Inactive' => 'Nieaktywny', + 'Active' => 'Aktywny', + 'Column %d' => 'Kolumna %d', + 'Add this column' => 'Dodaj kolumnę', + '%d tasks on the board' => '%d zadań na tablicy', + '%d tasks in total' => '%d wszystkich zadań', + 'Unable to update this board.' => 'Nie można zaktualizować tablicy.', + 'Edit board' => 'Edytuj tablicę', + 'Disable' => 'Wyłącz', + 'Enable' => 'Włącz', + 'New project' => 'Nowy projekt', + 'Do you really want to remove this project: "%s"?' => 'Na pewno chcesz usunąć projekt: "%s"?', + 'Remove project' => 'Usuń projekt', + 'Boards' => 'Tablice', + 'Edit the board for "%s"' => 'Edytuj tablię dla "%s"', + 'All projects' => 'Wszystkie projekty', + 'Change columns' => 'Zmień kolumny', + 'Add a new column' => 'Dodaj nową kolumnę', + 'Title' => 'Tytuł', + 'Add Column' => 'Dodaj kolumnę', + 'Project "%s"' => 'Projekt "%s"', + 'Nobody assigned' => 'Nikt nie przypisany', + 'Assigned to %s' => 'Przypisane do %s', + 'Remove a column' => 'Usuń kolumnę', + 'Remove a column from a board' => 'Usuń kolumnę z tablicy', + 'Unable to remove this column.' => 'Nie udało się usunąć kolumny.', + 'Do you really want to remove this column: "%s"?' => 'Na pewno chcesz usunąć kolumnę: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Wszystkie zadania w kolumnie zostaną usunięte!', + 'Settings' => 'Ustawienia', + 'Application settings' => 'Ustawienia aplikacji', + 'Language' => 'Język', + 'Webhook token:' => 'Token :', + 'API token:' => 'Token dla API', + 'More information' => 'Więcej informacji', + 'Database size:' => 'Rozmiar bazy danych :', + 'Download the database' => 'Pobierz bazę danych', + 'Optimize the database' => 'Optymalizuj bazę danych', + '(VACUUM command)' => '(komenda VACUUM)', + '(Gzip compressed Sqlite file)' => '(baza danych spakowana Gzip)', + 'User settings' => 'Ustawienia użytkownika', + 'My default project:' => 'Mój domyślny projekt:', + 'Close a task' => 'Zakończ zadanie', + 'Do you really want to close this task: "%s"?' => 'Na pewno chcesz zakończyć to zadanie: "%s"?', + 'Edit a task' => 'Edytuj zadanie', + 'Column' => 'Kolumna', + 'Color' => 'Kolor', + 'Assignee' => 'Odpowiedzialny', + 'Create another task' => 'Dodaj kolejne zadanie', + 'New task' => 'Nowe zadanie', + 'Open a task' => 'Otwórz zadanie', + 'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?', + 'Back to the board' => 'Powrót do tablicy', + 'Created on %B %e, %Y at %k:%M %p' => 'Utworzono dnia %e %B %Y o %k:%M', + 'There is nobody assigned' => 'Nikt nie jest przypisany', + 'Column on the board:' => 'Kolumna na tablicy:', + 'Status is open' => 'Status otwarty', + 'Status is closed' => 'Status zamknięty', + 'Close this task' => 'Zamknij zadanie', + 'Open this task' => 'Otwórz zadanie', + 'There is no description.' => 'Brak opisu.', + 'Add a new task' => 'Dodaj zadanie', + 'The username is required' => 'Nazwa użytkownika jest wymagana', + 'The maximum length is %d characters' => 'Maksymalna długość wynosi %d znaków', + 'The minimum length is %d characters' => 'Minimalna długość wynosi %d znaków', + 'The password is required' => 'Hasło jest wymagane', + 'This value must be an integer' => 'Wartość musi być liczbą całkowitą', + 'The username must be unique' => 'Nazwa użytkownika musi być unikalna', + 'The username must be alphanumeric' => 'Nazwa użytkownika musi być alfanumeryczna', + 'The user id is required' => 'ID użytkownika jest wymagane', + 'Passwords don\'t match' => 'Hasła nie pasują do siebie', + 'The confirmation is required' => 'Wymagane jest potwierdzenie', + 'The column is required' => 'Kolumna jest wymagana', + 'The project is required' => 'Projekt jest wymagany', + 'The color is required' => 'Kolor jest wymagany', + 'The id is required' => 'ID jest wymagane', + 'The project id is required' => 'ID projektu jest wymagane', + 'The project name is required' => 'Nazwa projektu jest wymagana', + 'This project must be unique' => 'Projekt musi być unikalny', + 'The title is required' => 'Tutył jest wymagany', + 'The language is required' => 'Język jest wymagany', + 'There is no active project, the first step is to create a new project.' => 'Brak aktywnych projektów. Pierwszym krokiem jest utworzenie nowego projektu.', + 'Settings saved successfully.' => 'Ustawienia zapisane.', + 'Unable to save your settings.' => 'Nie udało się zapisać ustawień.', + 'Database optimization done.' => 'Optymalizacja bazy danych zakończona.', + 'Your project have been created successfully.' => 'Projekt został pomyślnie utworzony.', + 'Unable to create your project.' => 'Nie udało się stworzyć projektu.', + 'Project updated successfully.' => 'Projekt zaktualizowany.', + 'Unable to update this project.' => 'Nie można zaktualizować projektu.', + 'Unable to remove this project.' => 'Nie można usunąć projektu.', + 'Project removed successfully.' => 'Projekt usunięty.', + 'Project activated successfully.' => 'Projekt aktywowany.', + 'Unable to activate this project.' => 'Nie można aktywować projektu.', + 'Project disabled successfully.' => 'Projekt wyłączony.', + 'Unable to disable this project.' => 'Nie można wyłączyć projektu.', + 'Unable to open this task.' => 'Nie można otworzyć tego zadania.', + 'Task opened successfully.' => 'Zadanie otwarte.', + 'Unable to close this task.' => 'Nie można zamknąć tego zadania.', + 'Task closed successfully.' => 'Zadanie zamknięte.', + 'Unable to update your task.' => 'Nie można zaktualizować tego zadania.', + 'Task updated successfully.' => 'Zadanie zaktualizowane.', + 'Unable to create your task.' => 'Nie można dodać zadania.', + 'Task created successfully.' => 'Zadanie zostało utworzone.', + 'User created successfully.' => 'Użytkownik dodany', + 'Unable to create your user.' => 'Nie udało się dodać użytkownika.', + 'User updated successfully.' => 'Użytkownik zaktualizowany.', + 'Unable to update your user.' => 'Nie udało się zaktualizować użytkownika.', + 'User removed successfully.' => 'Użytkownik usunięty.', + 'Unable to remove this user.' => 'Nie udało się usunąć użytkownika.', + 'Board updated successfully.' => 'Tablica została zaktualizowana.', + 'Ready' => 'Gotowe', + 'Backlog' => 'Log', + 'Work in progress' => 'W trakcie', + 'Done' => 'Zakończone', + 'Application version:' => 'Wersja aplikacji:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Zakończono dnia %e %B %Y o %k:%M', + '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', + 'Date created' => 'Data utworzenia', + 'Date completed' => 'Data zakończenia', + 'Id' => 'Id', + 'No task' => 'Brak zadań', + 'Completed tasks' => 'Ukończone zadania', + 'List of projects' => 'Lista projektów', + 'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"', + '%d closed tasks' => '%d zamkniętych zadań', + 'No task for this project' => 'Brak zadań dla tego projektu', + 'Public link' => 'Link publiczny', + 'There is no column in your project!' => 'Brak kolumny w Twoim projekcie', + 'Change assignee' => 'Zmień odpowiedzialną osobę', + 'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"', + 'Timezone' => 'Strefa czasowa', + 'Sorry, I didn\'t find this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', + 'Page not found' => 'Strona nie istnieje', + 'Complexity' => 'Poziom trudności', + 'limit' => 'limit', + 'Task limit' => 'Limit zadań', + 'Task count' => 'Liczba zadań', + 'This value must be greater than %d' => 'Wartość musi być większa niż %d', + 'Edit project access list' => 'Edycja list dostępu dla projektu', + 'Edit users access' => 'Edytuj dostęp', + 'Allow this user' => 'Dodaj użytkownika', + 'Only those users have access to this project:' => 'Użytkownicy mający dostęp:', + 'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!', + 'Revoke' => 'Odbierz dostęp', + 'List of authorized users' => 'Lista użytkowników mających dostęp', + 'User' => 'Użytkownik', + 'Nobody have access to this project.' => 'Żaden użytkownik nie ma dostępu do tego projektu', + 'You are not allowed to access to this project.' => 'Nie masz dostępu do tego projektu.', + 'Comments' => 'Komentarze', + 'Post comment' => 'Dodaj komentarz', + 'Write your text in Markdown' => 'Możesz użyć Markdown', + 'Leave a comment' => 'Zostaw komentarz', + 'Comment is required' => 'Komentarz jest wymagany', + 'Leave a description' => 'Dodaj opis', + 'Comment added successfully.' => 'Komentarz dodany', + 'Unable to create your comment.' => 'Nie udało się dodać komentarza', + 'The description is required' => 'Opis jest wymagany', + 'Edit this task' => 'Edytuj zadanie', + 'Due Date' => 'Termin', + 'Invalid date' => 'Błędna data', + 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y', + '%B %e, %Y' => '%e %B %Y', + // '%b %e, %Y' => '', + 'Automatic actions' => 'Akcje automatyczne', + 'Your automatic action have been created successfully.' => 'Twoja akcja została dodana', + 'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji', + 'Remove an action' => 'Usuń akcję', + 'Unable to remove this action.' => 'Nie można usunąć akcji', + 'Action removed successfully.' => 'Akcja usunięta', + 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"', + 'Defined actions' => 'Zdefiniowane akcje', + 'Add an action' => 'Nowa akcja', + 'Event name' => 'Nazwa zdarzenia', + 'Action name' => 'Nazwa akcji', + 'Action parameters' => 'Parametry akcji', + 'Action' => 'Akcja', + 'Event' => 'Zdarzenie', + 'When the selected event occurs execute the corresponding action.' => 'Gdy następuje wybrane zdarzenie, uruchom odpowiednią akcję', + 'Next step' => 'Następny krok', + 'Define action parameters' => 'Zdefiniuj parametry akcji', + 'Save this action' => 'Zapisz akcję', + 'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcję "%s"?', + 'Remove an automatic action' => 'Usuń akcję automatyczną', + 'Close the task' => 'Zamknij zadanie', + 'Assign the task to a specific user' => 'Przypisz zadanie do wybranego użytkownika', + 'Assign the task to the person who does the action' => 'Przypisz zadanie to osoby wykonującej akcję', + 'Duplicate the task to another project' => 'Kopiuj zadanie do innego projektu', + 'Move a task to another column' => 'Przeniesienie zadania do innej kolumny', + 'Move a task to another position in the same column' => 'Zmiania pozycji zadania w kolumnie', + 'Task modification' => 'Modyfikacja zadania', + 'Task creation' => 'Tworzenie zadania', + 'Open a closed task' => 'Otwarcie zamkniętego zadania', + 'Closing a task' => 'Zamknięcie zadania', + 'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika', + 'Column title' => 'Tytuł kolumny', + 'Position' => 'Pozycja', + 'Move Up' => 'Przenieś wyżej', + 'Move Down' => 'Przenieś niżej', + 'Duplicate to another project' => 'Skopiuj do innego projektu', + 'Duplicate' => 'Utwórz kopię', + 'link' => 'link', + 'Update this comment' => 'Zapisz komentarz', + 'Comment updated successfully.' => 'Komentarz został zapisany.', + 'Unable to update your comment.' => 'Nie udało się zapisanie komentarza.', + 'Remove a comment' => 'Usuń komentarz', + 'Comment removed successfully.' => 'Komentarz został usunięty.', + 'Unable to remove this comment.' => 'Nie udało się usunąć komentarza.', + 'Do you really want to remove this comment?' => 'Czy na pewno usunąć ten komentarz?', + 'Only administrators or the creator of the comment can access to this page.' => 'Tylko administratorzy oraz autor komentarza ma dostęp do tej strony.', + 'Details' => 'Szczegóły', + 'Current password for the user "%s"' => 'Aktualne hasło dla użytkownika "%s"', + 'The current password is required' => 'Wymanage jest aktualne hasło', + 'Wrong password' => 'Błędne hasło', + 'Reset all tokens' => 'Zresetuj wszystkie tokeny', + 'All tokens have been regenerated.' => 'Wszystkie tokeny zostały zresetowane.', + 'Unknown' => 'Nieznany', + 'Last logins' => 'Ostatnie logowania', + 'Login date' => 'Data logowania', + 'Authentication method' => 'Sposób autentykacji', + 'IP address' => 'Adres IP', + 'User agent' => 'Przeglądarka', + 'Persistent connections' => 'Stałe połączenia', + 'No session.' => 'Brak sesji.', + 'Expiration date' => 'Data zakończenia', + 'Remember Me' => 'Pamiętaj mnie', + 'Creation date' => 'Data utworzenia', + 'Filter by user' => 'Filtruj według użytkowników', + 'Filter by due date' => 'Filtruj według terminów', + 'Everybody' => 'Wszyscy', + 'Open' => 'Otwarto', + 'Closed' => 'Zamknięto', + 'Search' => 'Szukaj', + 'Nothing found.' => 'Nic nie znaleziono', + 'Search in the project "%s"' => 'Szukaj w projekcie "%s"', + 'Due date' => 'Termin', + 'Others formats accepted: %s and %s' => 'Inne akceptowane formaty: %s and %s', + 'Description' => 'Opis', + '%d comments' => '%d Komentarzy', + '%d comment' => '%d Komentarz', + 'Email address invalid' => 'Błędny adres email', + 'Your Google Account is not linked anymore to your profile.' => 'Twoje konto Google nie jest już połączone', + 'Unable to unlink your Google Account.' => 'Nie można odłączyć konta Google', + 'Google authentication failed' => 'Autentykacja Google nieudana', + 'Unable to link your Google Account.' => 'Nie można podłączyć konta Google', + 'Your Google Account is linked to your profile successfully.' => 'Podłączanie konta Google ukończone pomyślnie', + 'Email' => 'Email', + 'Link my Google Account' => 'Połącz z kontem Google', + 'Unlink my Google Account' => 'Rozłącz z kontem Google', + 'Login with my Google Account' => 'Zaloguj przy pomocy konta Google', + 'Project not found.' => 'Projek nieznaleziony.', + 'Task #%d' => 'Zadanie #%d', + 'Task removed successfully.' => 'Zadanie usunięto pomyślnie.', + 'Unable to remove this task.' => 'Nie można usunąć tego zadania.', + 'Remove a task' => 'Usuń zadanie', + 'Do you really want to remove this task: "%s"?' => 'Czy na pewno chcesz usunąć zadanie "%s"?', + 'Assign automatically a color based on a category' => 'Przypisz kolor automatycznie na podstawie kategori', + 'Assign automatically a category based on a color' => 'Przypisz kategorię automatycznie na podstawie koloru', + 'Task creation or modification' => 'Tworzenie lub usuwanie zadania', + 'Category' => 'Kategoria', + 'Category:' => 'Kategoria:', + 'Categories' => 'Kategorie', + 'Category not found.' => 'Kategoria nie istnieje', + 'Your category have been created successfully.' => 'Pomyślnie utworzono kategorię.', + 'Unable to create your category.' => 'Nie można tworzyć kategorii.', + 'Your category have been updated successfully.' => 'Pomyślnie zaktualizowano kategorię', + 'Unable to update your category.' => 'Nie można zaktualizować kategorii', + 'Remove a category' => 'Usuń kategorię', + 'Category removed successfully.' => 'Pomyślnie usunięto kategorię.', + 'Unable to remove this category.' => 'Nie można usunąć tej kategorii.', + 'Category modification for the project "%s"' => 'Zmiania kategorii projektu "%s"', + 'Category Name' => 'Nazwa kategorii', + 'Categories for the project "%s"' => 'Kategorie projektu', + 'Add a new category' => 'Utwórz nową kategorię', + 'Do you really want to remove this category: "%s"?' => 'Czy na pewno chcesz usunąć kategorię: "%s"?', + 'Filter by category' => 'Filtruj według kategorii', + 'All categories' => 'Wszystkie kategorie', + 'No category' => 'Brak kategorii', + 'The name is required' => 'Nazwa jest wymagana', + 'Remove a file' => 'Usuń plik', + 'Unable to remove this file.' => 'Nie można usunąć tego pliku.', + 'File removed successfully.' => 'Plik Usunięty pomyślnie.', + 'Attach a document' => 'Dołącz plik', + 'Do you really want to remove this file: "%s"?' => 'Czy na pewno chcesz usunąć plik: "%s"?', + 'open' => 'otwórz', + 'Attachments' => 'Załączniki', + 'Edit the task' => 'Edytuj Zadanie', + 'Edit the description' => 'Edytuj opis', + 'Add a comment' => 'Dodaj komentarz', + 'Edit a comment' => 'Edytuj komentarz', + 'Summary' => 'Podsumowanie', + 'Time tracking' => 'Śledzenie czasu', + 'Estimate:' => 'Szacowany:', + 'Spent:' => 'Przeznaczony:', + 'Do you really want to remove this sub-task?' => 'Czy na pewno chcesz usunąć to pod-zadanie?', + 'Remaining:' => 'Pozostało:', + 'hours' => 'godzin', + 'spent' => 'przeznaczono', + 'estimated' => 'szacowany', + 'Sub-Tasks' => 'Pod-zadanie', + 'Add a sub-task' => 'Dodaj pod-zadanie', + 'Original estimate' => 'Szacowanie początkowe', + 'Create another sub-task' => 'Dodaj kolejne pod-zadanie', + 'Time spent' => 'Przeznaczony czas', + 'Edit a sub-task' => 'Edytuj pod-zadanie', + 'Remove a sub-task' => 'Usuń pod-zadanie', + 'The time must be a numeric value' => 'Czas musi być wartością liczbową', + 'Todo' => 'Do zrobienia', + 'In progress' => 'W trakcie', + 'Sub-task removed successfully.' => 'Pod-zadanie usunięte pomyślnie.', + 'Unable to remove this sub-task.' => 'Nie można usunąć tego pod-zadania.', + 'Sub-task updated successfully.' => 'Pod-zadanie zaktualizowane pomyślnie.', + 'Unable to update your sub-task.' => 'Nie można zaktalizować tego pod-zadania.', + 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.', + 'Sub-task added successfully.' => 'Pod-zadanie utworzone pomyślnie', + 'Maximum size: ' => 'Maksymalny rozmiar: ', + 'Unable to upload the file.' => 'Nie można wczytać pliku.', + 'Display another project' => 'Wyświetl inny projekt', + 'Your GitHub account was successfully linked to your profile.' => 'Konto Github podłączone pomyślnie.', + 'Unable to link your GitHub Account.' => 'Nie można połączyć z kontem Github.', + 'GitHub authentication failed' => 'Autentykacja Github nieudana', + 'Your GitHub account is no longer linked to your profile.' => 'Konto Github nie jest już podłączone do twojego profilu.', + 'Unable to unlink your GitHub Account.' => 'Nie można odłączyć konta Github.', + 'Login with my GitHub Account' => 'Zaloguj przy użyciu konta Github', + 'Link my GitHub Account' => 'Podłącz konto Github', + 'Unlink my GitHub Account' => 'Odłącz konto Github', + 'Created by %s' => 'Utworzone przez %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Ostatnio zmienione %e %B %Y o %k:%M', + 'Tasks Export' => 'Eksport zadań', + 'Tasks exportation for "%s"' => 'Eksport zadań dla "%s"', + 'Start Date' => 'Data początkowa', + 'End Date' => 'Data Końcowa', + 'Execute' => 'Wykonaj', + 'Task Id' => 'Identyfikator Zadania', + 'Creator' => 'Autor', + 'Modification date' => 'Data modyfyfikacji', + 'Completion date' => 'Data ukończenia', + 'Clone' => 'Sklonuj', + 'Clone Project' => 'Sklonuj projekt', + 'Project cloned successfully.' => 'Projekt sklonowany pomyślnie.', + 'Unable to clone this project.' => 'Nie można sklonować projektu.', + 'Email notifications' => 'Powiadomienia email', + 'Enable email notifications' => 'Włącz powiadomienia email', + 'Task position:' => 'Pozycja zadania:', + 'The task #%d have been opened.' => 'Zadania #%d zostały otwarte.', + 'The task #%d have been closed.' => 'Zadania #$d zostały zamknięte.', + 'Sub-task updated' => 'Pod-zadanie zaktualizowane', + 'Title:' => 'Tytuł:', + // 'Status:' => '', + 'Assignee:' => 'Przypisano do:', + 'Time tracking:' => 'Śledzenie czasu: ', + 'New sub-task' => 'Nowe Pod-zadanie', + 'New attachment added "%s"' => 'Nowy załącznik dodany "%s"', + 'Comment updated' => 'Komentarz zaktualizowany', + 'New comment posted by %s' => 'Nowy komentarz dodany przez %s', + 'List of due tasks for the project "%s"' => 'Lista zadań oczekujących projektu "%s"', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', + '[%s][Due tasks]' => '[%s][Zadania oczekujące]', + '[Kanboard] Notification' => '[Kanboard] Powiadomienie', + 'I want to receive notifications only for those projects:' => 'Chcę otrzymywaćpowiadiomienia tylko dla tych projektów:', + 'view the task on Kanboard' => 'Zobacz zadanie', + 'Public access' => 'Dostęp publiczny', + 'Category management' => 'Zarządzanie kategoriami', + 'User management' => 'Zarządzanie użytkownikami', + 'Active tasks' => 'Aktywne zadania', + 'Disable public access' => 'Zablokuj dostęp publiczny', + 'Enable public access' => 'Odblokuj dostęp publiczny', + 'Active projects' => 'Aktywne projety', + 'Inactive projects' => 'Nieaktywne projekty', + 'Public access disabled' => 'Dostęp publiczny zablokowany', + 'Do you really want to disable this project: "%s"?' => 'Czy napewno chcesz zablokować projekt: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Czy napewno chcesz zduplikować projekt: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Czy napewno chcesz odblokować projekt: "%s"?', + 'Project activation' => 'Aktywacja projekt', + 'Move the task to another project' => 'Przenieś zadanie do innego projektu', + 'Move to another project' => 'Przenieś do innego projektu', + 'Do you really want to duplicate this task?' => 'Czy napewno chcesz zduplikować to zadanie: "%s"?', + 'Duplicate a task' => 'Zduplikuj zadanie', + 'External accounts' => 'Konta zewnętrzne', + 'Account type' => 'Typ konta', + 'Local' => 'Lokalne', + 'Remote' => 'Zdalne', + 'Enabled' => 'Odblokowane', + 'Disabled' => 'Zablokowane', + 'Google account linked' => 'Połączone konto Google', + 'Github account linked' => 'Połączone konto Github', + 'Username:' => 'Nazwa Użytkownika:', + 'Name:' => 'Imię i Nazwisko', + 'Email:' => 'Email: ', + 'Default project:' => 'Projekt domyślny:', + 'Notifications:' => 'Powiadomienia: ', + 'Notifications' => 'Powiadomienia', + 'Group:' => 'Grupa:', + 'Regular user' => 'Zwykły użytkownik', + 'Account type:' => 'Typ konta:', + 'Edit profile' => 'Edytuj profil', + 'Change password' => 'Zmień hasło', + 'Password modification' => 'Zmiania hasła', + 'External authentications' => 'Autentykacja zewnętrzna', + 'Google Account' => 'Konto Google', + 'Github Account' => 'Konto Github', + 'Never connected.' => 'Nigdy nie połączone.', + 'No account linked.' => 'Brak połączonych kont.', + 'Account linked.' => 'Konto połączone.', + 'No external authentication enabled.' => 'Brak autentykacji zewnętrznych.', + 'Password modified successfully.' => 'Hasło zmienione pomyślne.', + 'Unable to change the password.' => 'Nie można zmienić hasła.', + 'Change category for the task "%s"' => 'Zmień kategorię dla zadania "%s"', + 'Change category' => 'Zmień kategorię', + '%s updated the task %s' => '%s zaktualizował zadanie %s', + '%s opened the task %s' => '%s otworzył zadanie %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s przeniósł zadanie %s na pozycję #%d w kolumnie "%s"', + '%s moved the task %s to the column "%s"' => '%s przeniósł zadanie %s do kolumny "%s"', + '%s created the task %s' => '%s utworzył zadanie %s', + '%s closed the task %s' => '%s zamknął zadanie %s', + '%s created a subtask for the task %s' => '%s utworzył pod-zadanie dla zadania %s', + '%s updated a subtask for the task %s' => '%s zaktualizował pod-zadanie dla zadania %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Przypisano do %s z szacowanym czasem wykonania %s/%sh', + 'Not assigned, estimate of %sh' => 'Nie przypisane, szacowany czas wykonania %sh', + '%s updated a comment on the task %s' => '%s zaktualizował komentarz do zadania %s', + '%s commented the task %s' => '%s skomentował zadanie %s', + '%s\'s activity' => 'Aktywność %s', + 'No activity.' => 'Brak aktywności.', + 'RSS feed' => 'Kanał RSS', + '%s updated a comment on the task #%d' => '%s zaktualizował komentarz do zadania #%d', + '%s commented on the task #%d' => '%s skomentował zadanie #%d', + '%s updated a subtask for the task #%d' => '%s zaktualizował pod-zadanie dla zadania #%d', + '%s created a subtask for the task #%d' => '%s utworzył pod-zadanie dla zadania #%d', + '%s updated the task #%d' => '%s zaktualizował zadanie #%d', + '%s created the task #%d' => '%s utworzył zadanie #%d', + '%s closed the task #%d' => '%s zamknął zadanie #%d', + '%s open the task #%d' => '%s otworzył zadanie #%d', + '%s moved the task #%d to the column "%s"' => '%s przeniósł zadanie #%d do kolumny "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s przeniósł zadanie #%d na pozycję %d w kolmnie "%s"', + 'Activity' => 'Aktywność', + 'Default values are "%s"' => 'Domyślne wartości: "%s"', + 'Default columns for new projects (Comma-separated)' => 'Domyślne kolmny dla nowych projektów (oddzielone przecinkiem)', + 'Task assignee change' => 'Zmień osobę odpowiedzialną', + '%s change the assignee of the task #%d to %s' => '%s zmienił osobę odpowiedzialną za zadanie #%d na %s', + '%s changed the assignee of the task %s to %s' => '%s zmienił osobę odpowiedzialną za zadanie %s na %s', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', + 'New password for the user "%s"' => 'Nowe hasło użytkownika "%s"', + 'Choose an event' => 'Wybierz zdarzenie', + // 'Github commit received' => '', + // 'Github issue opened' => '', + // 'Github issue closed' => '', + // 'Github issue reopened' => '', + // 'Github issue assignee change' => '', + // 'Github issue label change' => '', + 'Create a task from an external provider' => 'Utwórz zadanie z dostawcy zewnętrznego', + 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', + 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrzenj etykiety', + // 'Reference' => '', + // 'Reference: %s' => '', + 'Label' => 'Etykieta', + 'Database' => 'Baza danych', + 'About' => 'Informacje', + 'Database driver:' => 'Silnik bazy danych:', + 'Board settings' => 'Ustawienia tablicy', + 'URL and token' => 'URL i token', + // 'Webhook settings' => '', + 'URL for task creation:' => 'URL do tworzenia zadań', + 'Reset token' => 'Resetuj token', + // 'API endpoint:' => '', + 'Refresh interval for private board' => 'Częstotliwość odświerzania dla tablicy prywatnej', + 'Refresh interval for public board' => 'Częstotliwość odświerzania dla tablicy publicznej', + 'Task highlight period' => 'Okres wyróżniania zadań', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Okres (w sekundach) wymagany do uznania projektu za niedawno zmieniony (0 ab zablokować, domyślnie 2 dni)', + 'Frequency in second (60 seconds by default)' => 'Częstotliwosć w sekundach (domyślnie 60 sekund)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Częstotliwość w sekundach (0 aby zablokować, domyślnie 10 sekund)', + 'Application URL' => 'Adres URL aplikacji', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Przykład: http://example.kanboard.net/ (Używane przez powiadomienia email)', + 'Token regenerated.' => 'Token wygenerowany ponownie.', + 'Date format' => 'Format daty', + 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO jest zawsze akceptowany, przykłady: "%s", "%s"', + 'New private project' => 'Nowy projekt prywatny', + 'This project is private' => 'Ten projekt jest prywatny', + 'Type here to create a new sub-task' => 'Wpisz tutaj aby utworzyć pod-zadanie', + 'Add' => 'Dodaj', + 'Estimated time: %s hours' => 'Szacowany czas: %s godzin', + 'Time spent: %s hours' => 'Przeznaczony czas: %s godzin', + 'Started on %B %e, %Y' => 'Rozpoczęto %e %B %Y', + 'Start date' => 'Data rozpoczęcia', + 'Time estimated' => 'Szacowany czas', + 'There is nothing assigned to you.' => 'Nie ma przypisanych zadań', + 'My tasks' => 'Moje zadania', + 'Activity stream' => 'Dziennik aktywności', + 'Dashboard' => 'Panel', + 'Confirmation' => 'Potwierdzenie', + 'Allow everybody to access to this project' => 'Udostepnij ten projekt wszystkim', + 'Everybody have access to this project.' => 'Wszyscy mają dostęp do tego projektu.', + // 'Webhooks' => '', + // 'API' => '', + 'Integration' => 'Integracja', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', + 'Create a comment from an external provider' => 'Utwórz komentarz od zewnętrznego dostawcy', + // 'Github issue comment created' => '', + 'Configure' => 'Konfiguruj', + 'Project management' => 'Menadżer projektu', + 'My projects' => 'Moje projekty', + 'Columns' => 'Kolumny', + 'Task' => 'zadania', + 'Your are not member of any project.' => 'Nie bierzesz udziału w żadnym projekcie', + 'Percentage' => 'Procent', + 'Number of tasks' => 'Liczba zadań', + 'Task distribution' => 'Rozmieszczenie zadań', + 'Reportings' => 'Raporty', + 'Task repartition for "%s"' => 'Przydział zadań dla "%s"', + 'Analytics' => 'Analizy', + 'Subtask' => 'Pod-zadanie', + 'My subtasks' => 'Moje pod-zadania', + 'User repartition' => 'Przydział użytkownika', + 'User repartition for "%s"' => 'Przydział użytkownika dla "%s"', + 'Clone this project' => 'Sklonuj ten projekt', + 'Column removed successfully.' => 'Kolumna usunięta pomyslnie.', + 'Edit Project' => 'Edytuj projekt', + // 'Github Issue' => '', + 'Not enough data to show the graph.' => 'Za mało danych do utworzenia wykresu.', + 'Previous' => 'Poprzedni', + 'The id must be an integer' => 'ID musi być liczbą całkowitą', + 'The project id must be an integer' => 'ID projektu musi być liczbą całkowitą', + 'The status must be an integer' => 'Status musi być liczbą całkowitą', + 'The subtask id is required' => 'ID pod-zadanie jest wymagane', + 'The subtask id must be an integer' => 'ID pod-zadania musi być liczbą całkowitą', + 'The task id is required' => 'ID zadania jest wymagane', + 'The task id must be an integer' => 'ID zadania musi być liczbą całkowitą', + 'The user id must be an integer' => 'ID użytkownika musi być liczbą całkowitą', + 'This value is required' => 'Wymagana wartość', + 'This value must be numeric' => 'Wartość musi być liczbą', + 'Unable to create this task.' => 'Nie można tworzyć zadania.', + 'Cumulative flow diagram' => 'Zbiorowy diagram przepływu', + 'Cumulative flow diagram for "%s"' => 'Zbiorowy diagram przepływu dla "%s"', + 'Daily project summary' => 'Dzienne podsumowanie projektu', + 'Daily project summary export' => 'Eksport dziennego podsumowania projektu', + 'Daily project summary export for "%s"' => 'Eksport dziennego podsumowania projektu dla "%s"', + 'Exports' => 'Eksporty', + 'This export contains the number of tasks per column grouped per day.' => 'Ten eksport zawiera ilość zadań zgrupowanych w kolumnach na dzień', + 'Nothing to preview...' => 'Nic do podejrzenia...', + 'Preview' => 'Podgląd', + 'Write' => 'Edycja', + 'Active swimlanes' => 'Aktywne procesy', + 'Add a new swimlane' => 'Dodaj proces', + 'Change default swimlane' => 'Zmień domyślny proces', + 'Default swimlane' => 'Domyślny proces', + 'Do you really want to remove this swimlane: "%s"?' => 'Czy na pewno chcesz usunąć proces: "%s"?', + 'Inactive swimlanes' => 'Nieaktywne procesy', + 'Set project manager' => 'Ustaw menadżera projektu', + 'Set project member' => 'Ustaw członka projektu', + 'Remove a swimlane' => 'Usuń proces', + 'Rename' => 'Zmień nazwe', + 'Show default swimlane' => 'Pokaż domyślny proces', + 'Swimlane modification for the project "%s"' => 'Edycja procesów dla projektu "%s"', + 'Swimlane not found.' => 'Nie znaleziono procesu.', + 'Swimlane removed successfully.' => 'Proces usunięty pomyslnie.', + 'Swimlanes' => 'Procesy', + 'Swimlane updated successfully.' => 'Proces zaktualizowany pomyślnie.', + 'The default swimlane have been updated successfully.' => 'Domyślny proces zaktualizowany pomyślnie.', + 'Unable to create your swimlane.' => 'Nie można utworzyć procesu.', + 'Unable to remove this swimlane.' => 'Nie można usunąć procesu.', + 'Unable to update this swimlane.' => 'Nie można zaktualizować procesu.', + 'Your swimlane have been created successfully.' => 'Proces tworzony pomyślnie.', + 'Example: "Bug, Feature Request, Improvement"' => 'Przykład: "Błąd, Żądanie Funkcjonalnośći, Udoskonalenia"', + 'Default categories for new projects (Comma-separated)' => 'Domyślne kategorie dla nowych projektów (oddzielone przecinkiem)', + // 'Gitlab commit received' => '', + // 'Gitlab issue opened' => '', + // 'Gitlab issue closed' => '', + // 'Gitlab webhooks' => '', + // 'Help on Gitlab webhooks' => '', + 'Integrations' => 'Integracje', + 'Integration with third-party services' => 'Integracja z usługami firm trzecich', + 'Role for this project' => 'Rola w tym projekcie', + 'Project manager' => 'Manadżer projektu', + 'Project member' => 'Członek projektu', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Menadżer projektu może zmieniać ustawienia projektu i posiada większe uprawnienia od zwykłego użytkownika', + // 'Gitlab Issue' => '', + 'Subtask Id' => 'ID pod-zadania', + 'Subtasks' => 'Pod-zadania', + 'Subtasks Export' => 'Eksport pod-zadań', + 'Subtasks exportation for "%s"' => 'Eksporty pod-zadań dla "%s"', + 'Task Title' => 'Tytuł zadania', + 'Untitled' => 'Bez tytułu', + 'Application default' => 'Domyślne dla aplikacji', + 'Language:' => 'Język:', + 'Timezone:' => 'Strefa czasowa:', + 'All columns' => 'Wszystkie kolumny', + 'Calendar for "%s"' => 'Kalendarz dla "%s"', + 'Filter by column' => 'Filtrj według kolumn', + 'Filter by status' => 'Filtruj według statusu', + 'Calendar' => 'Kalendarz', + 'Next' => 'Następny', + // '#%d' => '', + 'Filter by color' => 'Filtruj według koloru', + 'Filter by swimlane' => 'Filtruj według procesu', + 'All swimlanes' => 'Wszystkie procesy', + 'All colors' => 'Wszystkie kolory', + 'All status' => 'Wszystkie statusy', + 'Add a comment logging moving the task between columns' => 'Dodaj komentarz dokumentujący przeniesienie zadania pomiędzy kolumnami', + 'Moved to column %s' => 'Przeniosiono do kolumny %s', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', + // 'Compact view' => '', + // 'Horizontal scrolling' => '', + // 'Compact/wide view' => '', + // 'No results match:' => '', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + // 'Hourly rates' => '', + // 'Hourly rate' => '', + // 'Currency' => '', + // 'Effective date' => '', + // 'Add new rate' => '', + // 'Rate removed successfully.' => '', + // 'Unable to remove this rate.' => '', + // 'Unable to save the hourly rate.' => '', + // 'Hourly rate created successfully.' => '', + // 'Start time' => '', + // 'End time' => '', + // 'Comment' => '', + // 'All day' => '', + // 'Day' => '', + // 'Manage timetable' => '', + // 'Overtime timetable' => '', + // 'Time off timetable' => '', + // 'Timetable' => '', + // 'Work timetable' => '', + // 'Week timetable' => '', + // 'Day timetable' => '', + // 'From' => '', + // 'To' => '', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + // 'Remove time slot' => '', + // 'Add new time slot' => '', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + // 'Files' => '', + // 'Images' => '', + // 'Private project' => '', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + // 'Budget' => '', + // 'Budget line' => '', + // 'Budget line removed successfully.' => '', + // 'Budget lines' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + // 'Cost' => '', + // 'Cost breakdown' => '', + // 'Custom Stylesheet' => '', + // 'download' => '', + // 'Do you really want to remove this budget line?' => '', + // 'EUR - Euro' => '', + // 'Expenses' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'New budget line' => '', + // 'NZD - New Zealand Dollar' => '', + // 'Remove a budget line' => '', + // 'Remove budget line' => '', + // 'RSD - Serbian dinar' => '', + // 'The budget line have been created successfully.' => '', + // 'Unable to create the budget line.' => '', + // 'Unable to remove this budget line.' => '', + // 'USD - US Dollar' => '', + // 'Remaining' => '', + // 'Destination column' => '', + // 'Move the task to another column when assigned to a user' => '', + // 'Move the task to another column when assignee is cleared' => '', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php new file mode 100644 index 00000000..d16916df --- /dev/null +++ b/app/Locale/pt_BR/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + 'number.decimals_separator' => ',', + 'number.thousands_separator' => ' ', + 'None' => 'Nenhum', + 'edit' => 'editar', + 'Edit' => 'Editar', + 'remove' => 'remover', + 'Remove' => 'Remover', + 'Update' => 'Atualizar', + 'Yes' => 'Sim', + 'No' => 'Não', + 'cancel' => 'cancelar', + 'or' => 'ou', + 'Yellow' => 'Amarelo', + 'Blue' => 'Azul', + 'Green' => 'Verde', + 'Purple' => 'Roxo', + 'Red' => 'Vermelho', + 'Orange' => 'Laranja', + 'Grey' => 'Cinza', + 'Save' => 'Salvar', + 'Login' => 'Login', + 'Official website:' => 'Site oficial:', + 'Unassigned' => 'Não Atribuída', + 'View this task' => 'Ver esta tarefa', + 'Remove user' => 'Remover usuário', + 'Do you really want to remove this user: "%s"?' => 'Você realmente deseja remover este usuário: "%s"?', + 'New user' => 'Novo usuário', + 'All users' => 'Todos os usuários', + 'Username' => 'Nome de usuário', + 'Password' => 'Senha', + 'Default project' => 'Projeto padrão', + 'Administrator' => 'Administrador', + 'Sign in' => 'Entrar', + 'Users' => 'Usuários', + 'No user' => 'Sem usuário', + 'Forbidden' => 'Proibido', + 'Access Forbidden' => 'Acesso negado', + 'Only administrators can access to this page.' => 'Somente administradores têm acesso a esta página.', + 'Edit user' => 'Editar usuário', + 'Logout' => 'Sair', + 'Bad username or password' => 'Usuário ou senha inválidos', + 'users' => 'usuários', + 'projects' => 'projetos', + 'Edit project' => 'Editar projeto', + 'Name' => 'Nome', + 'Activated' => 'Ativado', + 'Projects' => 'Projetos', + 'No project' => 'Nenhum projeto', + 'Project' => 'Projeto', + 'Status' => 'Status', + 'Tasks' => 'Tarefas', + 'Board' => 'Board', + 'Actions' => 'Ações', + 'Inactive' => 'Inativo', + 'Active' => 'Ativo', + 'Column %d' => 'Coluna %d', + 'Add this column' => 'Adicionar esta coluna', + '%d tasks on the board' => '%d tarefas no board', + '%d tasks in total' => '%d tarefas no total', + 'Unable to update this board.' => 'Não foi possível atualizar este board.', + 'Edit board' => 'Editar board', + 'Disable' => 'Desativar', + 'Enable' => 'Ativar', + 'New project' => 'Novo projeto', + 'Do you really want to remove this project: "%s"?' => 'Você realmente deseja remover este projeto: "%s" ?', + 'Remove project' => 'Remover projeto', + 'Boards' => 'Boards', + 'Edit the board for "%s"' => 'Editar o board para "%s"', + 'All projects' => 'Todos os projetos', + 'Change columns' => 'Modificar colunas', + 'Add a new column' => 'Adicionar uma nova coluna', + 'Title' => 'Título', + 'Add Column' => 'Adicionar Coluna', + 'Project "%s"' => 'Projeto "%s"', + 'Nobody assigned' => 'Ninguém designado', + 'Assigned to %s' => 'Designado para %s', + 'Remove a column' => 'Remover uma coluna', + 'Remove a column from a board' => 'Remover uma coluna do board', + 'Unable to remove this column.' => 'Não foi possível remover esta coluna.', + 'Do you really want to remove this column: "%s"?' => 'Você realmente deseja remover esta coluna: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação irá REMOVER TODAS AS TAREFAS associadas a esta coluna!', + 'Settings' => 'Configurações', + 'Application settings' => 'Configurações da aplicação', + 'Language' => 'Idioma', + 'Webhook token:' => 'Token de webhooks:', + 'API token:' => 'API Token:', + 'More information' => 'Mais informações', + 'Database size:' => 'Tamanho do banco de dados:', + 'Download the database' => 'Download do banco de dados', + 'Optimize the database' => 'Otimizar o banco de dados', + '(VACUUM command)' => '(Comando VACUUM)', + '(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)', + 'User settings' => 'Configurações do usuário', + 'My default project:' => 'Meu projeto padrão:', + 'Close a task' => 'Finalizar uma tarefa', + 'Do you really want to close this task: "%s"?' => 'Você realmente deseja finalizar esta tarefa: "%s"?', + 'Edit a task' => 'Editar uma tarefa', + 'Column' => 'Coluna', + 'Color' => 'Cor', + 'Assignee' => 'Designação', + 'Create another task' => 'Criar outra tarefa', + 'New task' => 'Nova tarefa', + 'Open a task' => 'Abrir uma tarefa', + 'Do you really want to open this task: "%s"?' => 'Você realmente deseja abrir esta tarefa: "%s"?', + 'Back to the board' => 'Voltar ao board', + 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M', + 'There is nobody assigned' => 'Não há ninguém designado', + 'Column on the board:' => 'Coluna no board:', + 'Status is open' => 'Status está aberto', + 'Status is closed' => 'Status está finalizado', + 'Close this task' => 'Finalizar esta tarefa', + 'Open this task' => 'Abrir esta tarefa', + 'There is no description.' => 'Não há descrição.', + 'Add a new task' => 'Adicionar uma nova tarefa', + 'The username is required' => 'O nome de usuário é obrigatório', + 'The maximum length is %d characters' => 'O tamanho máximo é %d caracteres', + 'The minimum length is %d characters' => 'O tamanho mínimo é %d caracteres', + 'The password is required' => 'A senha é obrigatória', + 'This value must be an integer' => 'O valor deve ser um número inteiro', + 'The username must be unique' => 'O nome de usuário deve ser único', + 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico', + 'The user id is required' => 'O ID de usuário é obrigatório', + 'Passwords don\'t match' => 'As senhas não coincidem', + 'The confirmation is required' => 'A confirmação é obrigatória', + 'The column is required' => 'A coluna é obrigatória', + 'The project is required' => 'O projeto é obrigatório', + 'The color is required' => 'A cor é obrigatória', + 'The id is required' => 'O ID é obrigatório', + 'The project id is required' => 'O ID do projeto é obrigatório', + 'The project name is required' => 'O nome do projeto é obrigatório', + 'This project must be unique' => 'Este projeto deve ser único', + 'The title is required' => 'O título é obrigatório', + 'The language is required' => 'O idioma é obrigatório', + 'There is no active project, the first step is to create a new project.' => 'Não há projeto ativo. O primeiro passo é criar um novo projeto.', + 'Settings saved successfully.' => 'Configurações salvas com sucesso.', + 'Unable to save your settings.' => 'Não é possível salvar suas configurações.', + 'Database optimization done.' => 'Otimização do banco de dados finalizada.', + 'Your project have been created successfully.' => 'Seu projeto foi criado com sucesso.', + 'Unable to create your project.' => 'Não é possível criar o seu projeto.', + 'Project updated successfully.' => 'Projeto atualizado com sucesso.', + 'Unable to update this project.' => 'Não é possível atualizar este projeto.', + 'Unable to remove this project.' => 'Não é possível remover este projeto.', + 'Project removed successfully.' => 'Projeto removido com sucesso.', + 'Project activated successfully.' => 'Projeto ativado com sucesso.', + 'Unable to activate this project.' => 'Não é possível ativar este projeto.', + 'Project disabled successfully.' => 'Projeto desativado com sucesso.', + 'Unable to disable this project.' => 'Não é possível desativar este projeto.', + 'Unable to open this task.' => 'Não é possível abrir esta tarefa.', + 'Task opened successfully.' => 'Tarefa aberta com sucesso.', + 'Unable to close this task.' => 'Não é possível finalizar esta tarefa.', + 'Task closed successfully.' => 'Tarefa finalizada com sucesso.', + 'Unable to update your task.' => 'Não é possível atualizar a sua tarefa.', + 'Task updated successfully.' => 'Tarefa atualizada com sucesso.', + 'Unable to create your task.' => 'Não é possível criar a sua tarefa.', + 'Task created successfully.' => 'Tarefa criada com sucesso.', + 'User created successfully.' => 'Usuário criado com sucesso.', + 'Unable to create your user.' => 'Não é possível criar o seu usuário.', + 'User updated successfully.' => 'Usuário atualizado com sucesso.', + 'Unable to update your user.' => 'Não é possível atualizar o seu usuário.', + 'User removed successfully.' => 'Usuário removido com sucesso.', + 'Unable to remove this user.' => 'Não é possível remover este usuário.', + 'Board updated successfully.' => 'Board atualizado com sucesso.', + 'Ready' => 'Pronto', + 'Backlog' => 'Backlog', + 'Work in progress' => 'Em andamento', + 'Done' => 'Finalizado', + 'Application version:' => 'Versão da aplicação:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Finalizado em %d %B %Y às %H:%M', + '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M', + 'Date created' => 'Data de criação', + 'Date completed' => 'Data da finalização', + 'Id' => 'Id', + 'No task' => 'Nenhuma tarefa', + 'Completed tasks' => 'Tarefas completadas', + 'List of projects' => 'Lista de projetos', + 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"', + '%d closed tasks' => '%d tarefas finalizadas', + 'No task for this project' => 'Não há tarefa para este projeto', + 'Public link' => 'Link público', + 'There is no column in your project!' => 'Não há colunas no seu projeto!', + 'Change assignee' => 'Mudar a designação', + 'Change assignee for the task "%s"' => 'Modificar designação para a tarefa "%s"', + 'Timezone' => 'Fuso horário', + 'Sorry, I didn\'t find this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', + 'Page not found' => 'Página não encontrada', + 'Complexity' => 'Complexidade', + 'limit' => 'limite', + 'Task limit' => 'Limite da tarefa', + 'Task count' => 'Número de tarefas', + 'This value must be greater than %d' => 'Este valor deve ser maior que %d', + 'Edit project access list' => 'Editar lista de acesso ao projeto', + 'Edit users access' => 'Editar acesso de usuários', + 'Allow this user' => 'Permitir este usuário', + 'Only those users have access to this project:' => 'Somente esses usuários têm acesso a este projeto:', + 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.', + 'Revoke' => 'Revogar', + 'List of authorized users' => 'Lista de usuários autorizados', + 'User' => 'Usuário', + 'Nobody have access to this project.' => 'Ninguém tem acesso a este projeto.', + 'You are not allowed to access to this project.' => 'Você não está autorizado a acessar este projeto.', + 'Comments' => 'Comentários', + 'Post comment' => 'Postar comentário', + 'Write your text in Markdown' => 'Escreva seu texto em Markdown', + 'Leave a comment' => 'Deixe um comentário', + 'Comment is required' => 'Comentário é obrigatório', + 'Leave a description' => 'Deixe uma descrição', + 'Comment added successfully.' => 'Comentário adicionado com sucesso.', + 'Unable to create your comment.' => 'Não é possível criar o seu comentário.', + 'The description is required' => 'A descrição é obrigatória', + 'Edit this task' => 'Editar esta tarefa', + 'Due Date' => 'Data de vencimento', + 'Invalid date' => 'Data inválida', + 'Must be done before %B %e, %Y' => 'Deve ser finalizado antes de %d %B %Y', + '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d %B %Y', + 'Automatic actions' => 'Ações automáticas', + 'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.', + 'Unable to create your automatic action.' => 'Não é possível criar sua ação automática.', + 'Remove an action' => 'Remover uma ação', + 'Unable to remove this action.' => 'Não é possível remover esta ação.', + 'Action removed successfully.' => 'Ação removida com sucesso.', + 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"', + 'Defined actions' => 'Ações definidas', + 'Add an action' => 'Adicionar Ação', + 'Event name' => 'Nome do evento', + 'Action name' => 'Nome da ação', + 'Action parameters' => 'Parâmetros da ação', + 'Action' => 'Ação', + 'Event' => 'Evento', + 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer execute a ação correspondente.', + 'Next step' => 'Próximo passo', + 'Define action parameters' => 'Definir parêmetros da ação', + 'Save this action' => 'Salvar esta ação', + 'Do you really want to remove this action: "%s"?' => 'Você realmente deseja remover esta ação: "%s"?', + 'Remove an automatic action' => 'Remover uma ação automática', + 'Close the task' => 'Finalizar tarefa', + 'Assign the task to a specific user' => 'Designar a tarefa para um usuário específico', + 'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a ação', + 'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projeto', + 'Move a task to another column' => 'Mover a tarefa para outra coluna', + 'Move a task to another position in the same column' => 'Mover a tarefa para outra posição na mesma coluna', + 'Task modification' => 'Modificação de tarefa', + 'Task creation' => 'Criação de tarefa', + 'Open a closed task' => 'Reabrir uma tarefa finalizada', + 'Closing a task' => 'Finalizando uma tarefa', + 'Assign a color to a specific user' => 'Designar uma cor para um usuário específico', + 'Column title' => 'Título da coluna', + 'Position' => 'Posição', + 'Move Up' => 'Mover para cima', + 'Move Down' => 'Mover para baixo', + 'Duplicate to another project' => 'Duplicar para outro projeto', + 'Duplicate' => 'Duplicar', + 'link' => 'link', + 'Update this comment' => 'Atualizar este comentário', + 'Comment updated successfully.' => 'Comentário atualizado com sucesso.', + 'Unable to update your comment.' => 'Não é possível atualizar o seu comentário.', + 'Remove a comment' => 'Remover um comentário', + 'Comment removed successfully.' => 'Comentário removido com sucesso.', + 'Unable to remove this comment.' => 'Não é possível remover este comentário.', + 'Do you really want to remove this comment?' => 'Você realmente deseja remover este comentário?', + 'Only administrators or the creator of the comment can access to this page.' => 'Somente os administradores ou o criator deste comentário possuem acesso a esta página.', + 'Details' => 'Detalhes', + 'Current password for the user "%s"' => 'Senha atual para o usuário "%s"', + 'The current password is required' => 'A senha atual é obrigatória', + 'Wrong password' => 'Senha incorreta', + 'Reset all tokens' => 'Resetar todos os tokens', + 'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente.', + 'Unknown' => 'Desconhecido', + 'Last logins' => 'Últimos logins', + 'Login date' => 'Data de login', + 'Authentication method' => 'Método de autenticação', + 'IP address' => 'Endereço IP', + 'User agent' => 'User Agent', + 'Persistent connections' => 'Conexões persistentes', + 'No session.' => 'Nenhuma sessão.', + 'Expiration date' => 'Data de expiração', + 'Remember Me' => 'Lembre-se de mim', + 'Creation date' => 'Data de criação', + 'Filter by user' => 'Filtrar por usuário', + 'Filter by due date' => 'Filtrar por data de vencimento', + 'Everybody' => 'Todos', + 'Open' => 'Abrir', + 'Closed' => 'Finalizado', + 'Search' => 'Pesquisar', + 'Nothing found.' => 'Nada foi encontrado.', + 'Search in the project "%s"' => 'Pesquisar no projeto "%s"', + 'Due date' => 'Data de vencimento', + 'Others formats accepted: %s and %s' => 'Outros formatos permitidos: %s e %s', + 'Description' => 'Descrição', + '%d comments' => '%d comentários', + '%d comment' => '%d comentário', + 'Email address invalid' => 'Endereço de e-mail inválido', + 'Your Google Account is not linked anymore to your profile.' => 'Sua conta do Google não está mais associada ao seu perfil.', + 'Unable to unlink your Google Account.' => 'Não foi possível desassociar a sua Conta do Google.', + 'Google authentication failed' => 'Autenticação do Google falhou.', + 'Unable to link your Google Account.' => 'Não foi possível associar a sua Conta do Google.', + 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google foi associada ao seu perfil com sucesso.', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Vincular minha Conta do Google', + 'Unlink my Google Account' => 'Desvincular minha Conta do Google', + 'Login with my Google Account' => 'Entrar com minha Conta do Google', + 'Project not found.' => 'Projeto não encontrado.', + 'Task #%d' => 'Tarefa #%d', + 'Task removed successfully.' => 'Tarefa removida com sucesso.', + 'Unable to remove this task.' => 'Não foi possível remover esta tarefa.', + 'Remove a task' => 'Remover uma tarefa', + 'Do you really want to remove this task: "%s"?' => 'Você realmente deseja remover esta tarefa: "%s"', + 'Assign automatically a color based on a category' => 'Atribuir automaticamente uma cor com base em uma categoria', + 'Assign automatically a category based on a color' => 'Atribuir automaticamente uma categoria com base em uma cor', + 'Task creation or modification' => 'Criação ou modificação de tarefa', + 'Category' => 'Categoria', + 'Category:' => 'Categoria:', + 'Categories' => 'Categorias', + 'Category not found.' => 'Categoria não encontrada.', + 'Your category have been created successfully.' => 'Sua categoria foi criada com sucesso.', + 'Unable to create your category.' => 'Não foi possível criar a sua categoria.', + 'Your category have been updated successfully.' => 'A sua categoria foi atualizada com sucesso.', + 'Unable to update your category.' => 'Não foi possível atualizar a sua categoria.', + 'Remove a category' => 'Remover uma categoria', + 'Category removed successfully.' => 'Categoria removido com sucesso.', + 'Unable to remove this category.' => 'Não foi possível remover esta categoria.', + 'Category modification for the project "%s"' => 'Modificação de categoria para o projeto "%s"', + 'Category Name' => 'Nome da Categoria', + 'Categories for the project "%s"' => 'Categorias para o projeto "%s"', + 'Add a new category' => 'Adicionar uma nova categoria', + 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"', + 'Filter by category' => 'Filtrar por categoria', + 'All categories' => 'Todas as categorias', + 'No category' => 'Nenhum categoria', + 'The name is required' => 'O nome é obrigatório', + 'Remove a file' => 'Remover um arquivo', + 'Unable to remove this file.' => 'Não foi possível remover este arquivo.', + 'File removed successfully.' => 'Arquivo removido com sucesso.', + 'Attach a document' => 'Anexar um documento', + 'Do you really want to remove this file: "%s"?' => 'Você realmente deseja remover este arquivo: "%s"', + 'open' => 'Aberto', + 'Attachments' => 'Anexos', + 'Edit the task' => 'Editar a tarefa', + 'Edit the description' => 'Editar a descrição', + 'Add a comment' => 'Adicionar um comentário', + 'Edit a comment' => 'Editar um comentário', + 'Summary' => 'Resumo', + 'Time tracking' => 'Rastreamento de tempo', + 'Estimate:' => 'Estimado:', + 'Spent:' => 'Gasto:', + 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta subtarefa?', + 'Remaining:' => 'Restante:', + 'hours' => 'horas', + 'spent' => 'gasto', + 'estimated' => 'estimado', + 'Sub-Tasks' => 'Subtarefas', + 'Add a sub-task' => 'Adicionar uma subtarefa', + 'Original estimate' => 'Estimativa original', + 'Create another sub-task' => 'Criar uma outra subtarefa', + 'Time spent' => 'Tempo gasto', + 'Edit a sub-task' => 'Editar uma subtarefa', + 'Remove a sub-task' => 'Remover uma subtarefa', + 'The time must be a numeric value' => 'O tempo deve ser um valor numérico', + 'Todo' => 'À fazer', + 'In progress' => 'Em andamento', + 'Sub-task removed successfully.' => 'Subtarefa removida com sucesso.', + 'Unable to remove this sub-task.' => 'Não foi possível remover esta subtarefa.', + 'Sub-task updated successfully.' => 'Subtarefa atualizada com sucesso.', + 'Unable to update your sub-task.' => 'Não foi possível atualizar a sua subtarefa.', + 'Unable to create your sub-task.' => 'Não é possível criar a sua subtarefa.', + 'Sub-task added successfully.' => 'Subtarefa adicionada com sucesso.', + 'Maximum size: ' => 'Tamanho máximo:', + 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.', + 'Display another project' => 'Exibir outro projeto', + 'Your GitHub account was successfully linked to your profile.' => 'A sua Conta do GitHub foi associada com sucesso ao seu perfil.', + 'Unable to link your GitHub Account.' => 'Não foi possível associar sua Conta do GitHub.', + 'GitHub authentication failed' => 'Autenticação do GitHub falhou', + 'Your GitHub account is no longer linked to your profile.' => 'A sua Conta do GitHub não está mais associada ao seu perfil.', + 'Unable to unlink your GitHub Account.' => 'Não foi possível desassociar a sua Conta do GitHub.', + 'Login with my GitHub Account' => 'Entrar com minha Conta do GitHub', + 'Link my GitHub Account' => 'Associar à minha Conta do GitHub', + 'Unlink my GitHub Account' => 'Desassociar a minha Conta do GitHub', + 'Created by %s' => 'Criado por %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p', + 'Tasks Export' => 'Exportar Tarefas', + 'Tasks exportation for "%s"' => 'As tarefas foram exportadas para "%s"', + 'Start Date' => 'Data inicial', + 'End Date' => 'Data final', + 'Execute' => 'Executar', + 'Task Id' => 'ID da Tarefa', + 'Creator' => 'Criado por', + 'Modification date' => 'Data da modificação', + 'Completion date' => 'Data da finalização', + 'Clone' => 'Clonar', + 'Clone Project' => 'Clonar Projeto', + 'Project cloned successfully.' => 'Projeto clonado com sucesso.', + 'Unable to clone this project.' => 'Não foi possível clonar este projeto.', + 'Email notifications' => 'Notificações por email', + 'Enable email notifications' => 'Habilitar notificações por email', + 'Task position:' => 'Posição da tarefa:', + 'The task #%d have been opened.' => 'A tarefa #%d foi aberta.', + 'The task #%d have been closed.' => 'A tarefa #%d foi finalizada.', + 'Sub-task updated' => 'Subtarefa atualizada', + 'Title:' => 'Título:', + 'Status:' => 'Status:', + 'Assignee:' => 'Designado:', + 'Time tracking:' => 'Controle de tempo:', + 'New sub-task' => 'Nova subtarefa', + 'New attachment added "%s"' => 'Novo anexo adicionado "%s"', + 'Comment updated' => 'Comentário atualizado', + 'New comment posted by %s' => 'Novo comentário postado por %s', + 'List of due tasks for the project "%s"' => 'Lista de tarefas pendentes para o projeto "%s"', + 'New attachment' => 'Novo anexo', + 'New comment' => 'Novo comentário', + 'New subtask' => 'Nova subtarefa', + 'Subtask updated' => 'Subtarefa alterada', + 'Task updated' => 'Tarefa alterada', + 'Task closed' => 'Tarefa finalizada', + 'Task opened' => 'Tarefa aberta', + '[%s][Due tasks]' => '[%s][Tarefas pendentes]', + '[Kanboard] Notification' => '[Kanboard] Notificação', + 'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projetos:', + 'view the task on Kanboard' => 'ver a tarefa no Kanboard', + 'Public access' => 'Acesso público', + 'Category management' => 'Gerenciamento de categorias', + 'User management' => 'Gerenciamento de usuários', + 'Active tasks' => 'Tarefas ativas', + 'Disable public access' => 'Desabilitar o acesso público', + 'Enable public access' => 'Habilitar o acesso público', + 'Active projects' => 'Projetos ativos', + 'Inactive projects' => 'Projetos inativos', + 'Public access disabled' => 'Acesso público desabilitado', + 'Do you really want to disable this project: "%s"?' => 'Você realmente deseja desabilitar este projeto: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Você realmente deseja duplicar este projeto: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Você realmente deseja habilitar este projeto: "%s"?', + 'Project activation' => 'Ativação do projeto', + 'Move the task to another project' => 'Mover a tarefa para outro projeto', + 'Move to another project' => 'Mover para outro projeto', + 'Do you really want to duplicate this task?' => 'Você realmente deseja duplicar esta tarefa?', + 'Duplicate a task' => 'Duplicar uma tarefa', + 'External accounts' => 'Contas externas', + 'Account type' => 'Tipo de conta', + 'Local' => 'Local', + 'Remote' => 'Remoto', + 'Enabled' => 'Habilitado', + 'Disabled' => 'Desabilitado', + 'Google account linked' => 'Conta do Google associada', + 'Github account linked' => 'Conta do Github associada', + 'Username:' => 'Usuário:', + 'Name:' => 'Nome:', + 'Email:' => 'E-mail:', + 'Default project:' => 'Projeto padrão:', + 'Notifications:' => 'Notificações:', + 'Notifications' => 'Notificações', + 'Group:' => 'Grupo:', + 'Regular user' => 'Usuário comum', + 'Account type:' => 'Tipo de conta:', + 'Edit profile' => 'Editar perfil', + 'Change password' => 'Alterar senha', + 'Password modification' => 'Alteração de senha', + 'External authentications' => 'Autenticação externa', + 'Google Account' => 'Conta do Google', + 'Github Account' => 'Conta do Github', + 'Never connected.' => 'Nunca conectado.', + 'No account linked.' => 'Nenhuma conta associada.', + 'Account linked.' => 'Conta associada.', + 'No external authentication enabled.' => 'Nenhuma autenticação externa habilitada.', + 'Password modified successfully.' => 'Senha alterada com sucesso.', + 'Unable to change the password.' => 'Não foi possível alterar a senha.', + 'Change category for the task "%s"' => 'Mudar categoria da tarefa "%s"', + 'Change category' => 'Mudar categoria', + '%s updated the task %s' => '%s atualizou a tarefa %s', + '%s opened the task %s' => '%s abriu a tarefa %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s moveu a tarefa %s para a posição #%d na coluna "%s"', + '%s moved the task %s to the column "%s"' => '%s moveu a tarefa %s para a coluna "%s"', + '%s created the task %s' => '%s criou a tarefa %s', + '%s closed the task %s' => '%s finalizou a tarefa %s', + '%s created a subtask for the task %s' => '%s criou uma subtarefa para a tarefa %s', + '%s updated a subtask for the task %s' => '%s atualizou uma subtarefa da tarefa %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Designado para %s com tempo estimado de %s/%sh', + 'Not assigned, estimate of %sh' => 'Não designado, estimado em %sh', + '%s updated a comment on the task %s' => '%s atualizou o comentário na tarefa %s', + '%s commented the task %s' => '%s comentou a tarefa %s', + '%s\'s activity' => 'Atividades de%s', + 'No activity.' => 'Sem atividade.', + 'RSS feed' => 'Feed RSS', + '%s updated a comment on the task #%d' => '%s atualizou um comentário sobre a tarefa #%d', + '%s commented on the task #%d' => '%s comentou sobre a tarefa #%d', + '%s updated a subtask for the task #%d' => '%s atualizou uma subtarefa para a tarefa #%d', + '%s created a subtask for the task #%d' => '%s criou uma subtarefa para a tarefa #%d', + '%s updated the task #%d' => '%s atualizou a tarefa #%d', + '%s created the task #%d' => '%s criou a tarefa #%d', + '%s closed the task #%d' => '%s finalizou a tarefa #%d', + '%s open the task #%d' => '%s abriu a tarefa #%d', + '%s moved the task #%d to the column "%s"' => '%s moveu a tarefa #%d para a coluna "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s moveu a tarefa #%d para a posição %d na coluna "%s"', + 'Activity' => 'Atividade', + 'Default values are "%s"' => 'Os valores padrão são "%s"', + 'Default columns for new projects (Comma-separated)' => 'Colunas padrão para novos projetos (Separado por vírgula)', + 'Task assignee change' => 'Mudar designação da tarefa', + '%s change the assignee of the task #%d to %s' => '%s mudou a designação da tarefa #%d para %s', + '%s changed the assignee of the task %s to %s' => '%s mudou a designação da tarefa %s para %s', + 'Column Change' => 'Mudança de coluna', + 'Position Change' => 'Mudança de posição', + 'Assignee Change' => 'Mudança de designado', + 'New password for the user "%s"' => 'Nova senha para o usuário "%s"', + 'Choose an event' => 'Escolher um evento', + 'Github commit received' => 'Github commit received', + 'Github issue opened' => 'Github issue opened', + 'Github issue closed' => 'Github issue closed', + 'Github issue reopened' => 'Github issue reopened', + 'Github issue assignee change' => 'Github issue assignee change', + 'Github issue label change' => 'Github issue label change', + 'Create a task from an external provider' => 'Criar uma tarefa por meio de um serviço externo', + 'Change the assignee based on an external username' => 'Alterar designação com base em um usuário externo', + 'Change the category based on an external label' => 'Alterar categoria com base em um rótulo externo', + 'Reference' => 'Referência', + 'Reference: %s' => 'Referência: %s', + 'Label' => 'Rótulo', + 'Database' => 'Banco de dados', + 'About' => 'Sobre', + 'Database driver:' => 'Driver do banco de dados:', + 'Board settings' => 'Configurações do Board', + 'URL and token' => 'URL e token', + 'Webhook settings' => 'Configurações do Webhook', + 'URL for task creation:' => 'URL para a criação da tarefa:', + 'Reset token' => 'Resetar token', + 'API endpoint:' => 'API endpoint:', + 'Refresh interval for private board' => 'Intervalo de atualização para um board privado', + 'Refresh interval for public board' => 'Intervalo de atualização para um board público', + 'Task highlight period' => 'Período de Tarefa em destaque', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Período (em segundos) para considerar que uma tarefa foi modificada recentemente (0 para desativar, 2 dias por padrão)', + 'Frequency in second (60 seconds by default)' => 'Frequência em segundos (60 segundos por padrão)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequência em segundos (0 para desativar este recurso, 10 segundos por padrão)', + 'Application URL' => 'URL da Aplicação', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Exemplo: http://example.kanboard.net/ (utilizado nas notificações por e-mail)', + 'Token regenerated.' => 'Token ', + 'Date format' => 'Formato de data', + 'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceito, exemplo: "%s" e "%s"', + 'New private project' => 'Novo projeto privado', + 'This project is private' => 'Este projeto é privado', + 'Type here to create a new sub-task' => 'Digite aqui para criar uma nova subtarefa', + 'Add' => 'Adicionar', + 'Estimated time: %s hours' => 'Tempo estimado: %s horas', + 'Time spent: %s hours' => 'Tempo gasto: %s horas', + 'Started on %B %e, %Y' => 'Iniciado em %B %e, %Y', + 'Start date' => 'Data de início', + 'Time estimated' => 'Tempo estimado', + 'There is nothing assigned to you.' => 'Não há nada designado à você.', + 'My tasks' => 'Minhas tarefas', + 'Activity stream' => 'Atividades Recentes', + 'Dashboard' => 'Painel de Controle', + 'Confirmation' => 'Confirmação', + 'Allow everybody to access to this project' => 'Permitir que todos acessem este projeto', + 'Everybody have access to this project.' => 'Todos possuem acesso a este projeto.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Integração', + 'Github webhooks' => 'Github webhooks', + 'Help on Github webhooks' => 'Ajuda para o Github webhooks', + 'Create a comment from an external provider' => 'Criar um comentário por meio de um serviço externo', + 'Github issue comment created' => 'Github issue comment created', + 'Configure' => 'Configurar', + 'Project management' => 'Gerenciamento de projetos', + 'My projects' => 'Meus projetos', + 'Columns' => 'Colunas', + 'Task' => 'Tarefas', + 'Your are not member of any project.' => 'Você não é membro de nenhum projeto.', + 'Percentage' => 'Porcentagem', + 'Number of tasks' => 'Número de tarefas', + 'Task distribution' => 'Distribuição de tarefas', + 'Reportings' => 'Relatórios', + 'Task repartition for "%s"' => 'Redistribuição da tarefa para "%s"', + 'Analytics' => 'Estatísticas', + 'Subtask' => 'Subtarefa', + 'My subtasks' => 'Minhas subtarefas', + 'User repartition' => 'Redistribuição de usuário', + 'User repartition for "%s"' => 'Redistribuição de usuário para "%s"', + 'Clone this project' => 'Clonar este projeto', + 'Column removed successfully.' => 'Coluna removida com sucesso.', + 'Edit Project' => 'Editar projeto', + 'Github Issue' => 'Github Issue', + 'Not enough data to show the graph.' => 'Não há dados suficientes para mostrar o gráfico.', + 'Previous' => 'Anterior', + 'The id must be an integer' => 'O ID deve ser um número inteiro', + 'The project id must be an integer' => 'O ID do projeto deve ser um inteiro', + 'The status must be an integer' => 'O status deve ser um número inteiro', + 'The subtask id is required' => 'O ID da subtarefa é obrigatório', + 'The subtask id must be an integer' => 'O ID da subtarefa deve ser um número inteiro', + 'The task id is required' => 'O ID da tarefa é obrigatório', + 'The task id must be an integer' => 'O ID da tarefa deve ser um número inteiro', + 'The user id must be an integer' => 'O ID do usuário deve ser um número inteiro', + 'This value is required' => 'Este valor é obrigatório', + 'This value must be numeric' => 'Este valor deve ser numérico', + 'Unable to create this task.' => 'Não foi possível criar esta tarefa.', + 'Cumulative flow diagram' => 'Fluxograma cumulativo', + 'Cumulative flow diagram for "%s"' => 'Fluxograma cumulativo para "%s"', + 'Daily project summary' => 'Resumo diário do projeto', + 'Daily project summary export' => 'Exportação diária do resumo do projeto', + 'Daily project summary export for "%s"' => 'Exportação diária do resumo do projeto para "%s"', + 'Exports' => 'Exportar', + 'This export contains the number of tasks per column grouped per day.' => 'Esta exportação contém o número de tarefas por coluna agrupada por dia.', + 'Nothing to preview...' => 'Nada para pré-visualizar...', + 'Preview' => 'Pré-visualizar', + 'Write' => 'Escrever', + 'Active swimlanes' => 'Ativar swimlanes', + 'Add a new swimlane' => 'Adicionar novo swimlane', + 'Change default swimlane' => 'Alterar swimlane padrão', + 'Default swimlane' => 'Swimlane padrão', + 'Do you really want to remove this swimlane: "%s"?' => 'Você realmente deseja remover este swimlane: "%s"?', + 'Inactive swimlanes' => 'Desativar swimlanes', + 'Set project manager' => 'Definir gerente do projeto', + 'Set project member' => 'Definir membro do projeto', + 'Remove a swimlane' => 'Remover um swimlane', + 'Rename' => 'Renomear', + 'Show default swimlane' => 'Exibir swimlane padrão', + 'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projeto "%s"', + 'Swimlane not found.' => 'Swimlane não encontrado.', + 'Swimlane removed successfully.' => 'Swimlane removido com sucesso.', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane atualizado com sucesso.', + 'The default swimlane have been updated successfully.' => 'O swimlane padrão foi atualizado com sucesso.', + 'Unable to create your swimlane.' => 'Não foi possível criar o seu swimlane.', + 'Unable to remove this swimlane.' => 'Não foi possível remover este swimlane.', + 'Unable to update this swimlane.' => 'Não foi possível atualizar este swimlane.', + 'Your swimlane have been created successfully.' => 'Seu swimlane foi criado com sucesso.', + 'Example: "Bug, Feature Request, Improvement"' => 'Exemplo: "Bug, Feature Request, Improvement"', + 'Default categories for new projects (Comma-separated)' => 'Categorias padrão para novos projetos (Separadas por vírgula)', + 'Gitlab commit received' => 'Gitlab commit received', + 'Gitlab issue opened' => 'Gitlab issue opened', + 'Gitlab issue closed' => 'Gitlab issue closed', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Ajuda sobre Gitlab webhooks', + 'Integrations' => 'Integrações', + 'Integration with third-party services' => 'Integração com serviços de terceiros', + 'Role for this project' => 'Função para este projeto', + 'Project manager' => 'Gerente do projeto', + 'Project member' => 'Membro do projeto', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Um gerente do projeto pode alterar as configurações do projeto e ter mais privilégios que um usuário padrão.', + 'Gitlab Issue' => 'Gitlab Issue', + 'Subtask Id' => 'ID da subtarefa', + 'Subtasks' => 'Subtarefas', + 'Subtasks Export' => 'Exportar subtarefas', + 'Subtasks exportation for "%s"' => 'Subtarefas exportadas para "%s"', + 'Task Title' => 'Título da Tarefa', + 'Untitled' => 'Sem título', + 'Application default' => 'Aplicação padrão', + 'Language:' => 'Idioma', + 'Timezone:' => 'Fuso horário', + 'All columns' => 'Todas as colunas', + 'Calendar for "%s"' => 'Calendário para "%s"', + 'Filter by column' => 'Filtrar por coluna', + 'Filter by status' => 'Filtrar por status', + 'Calendar' => 'Calendário', + 'Next' => 'Próximo', + // '#%d' => '', + 'Filter by color' => 'Filtrar por cor', + 'Filter by swimlane' => 'Filtrar por swimlane', + 'All swimlanes' => 'Todos os swimlane', + 'All colors' => 'Todas as cores', + 'All status' => 'Todos os status', + 'Add a comment logging moving the task between columns' => 'Adicionar un comentário de log ao mover uma tarefa em outra coluna', + 'Moved to column %s' => 'Mover para a coluna %s', + 'Change description' => 'Modificar a descrição', + 'User dashboard' => 'Painel de Controle do usuário', + 'Allow only one subtask in progress at the same time for a user' => 'Permitir apenas uma subtarefa em andamento ao mesmo tempo para um usuário', + 'Edit column "%s"' => 'Editar a coluna "%s"', + 'Enable time tracking for subtasks' => 'Ativar a gestão de tempo par a subtarefa', + 'Select the new status of the subtask: "%s"' => 'Selecionar um novo status para a subtarefa', + 'Subtask timesheet' => 'Gestão de tempo das subtarefas', + 'There is nothing to show.' => 'Não há nada para mostrar', + 'Time Tracking' => 'Gestão de tempo', + 'You already have one subtask in progress' => 'Você já tem um subtarefa em andamento', + 'Which parts of the project do you want to duplicate?' => 'Quais as partes do projeto você deseja duplicar?', + 'Change dashboard view' => 'Alterar a vista do Painel de Controle', + 'Show/hide activities' => 'Mostrar / ocultar as atividades', + 'Show/hide projects' => 'Mostrar / ocultar os projetos', + 'Show/hide subtasks' => 'Mostrar / ocultar as subtarefas', + 'Show/hide tasks' => 'Mostrar / ocultar as tarefas', + 'Disable login form' => 'Desativar o formulário de login', + 'Show/hide calendar' => 'Mostrar / ocultar calendário', + 'User calendar' => 'Calendário do usuário', + 'Bitbucket commit received' => '"Commit" recebido via Bitbucket', + 'Bitbucket webhooks' => 'Webhook Bitbucket', + 'Help on Bitbucket webhooks' => 'Ajuda sobre os webhooks Bitbucket', + 'Start' => 'Inicio', + 'End' => 'Fim', + 'Task age in days' => 'Idade da tarefa em dias', + 'Days in this column' => 'Dias nesta coluna', + // '%dd' => '', + 'Add a link' => 'Adicionar uma associação', + 'Add a new link' => 'Adicionar uma nova associação', + 'Do you really want to remove this link: "%s"?' => 'Você realmente deseja remover esta associação: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Você realmente deseja remover esta associação com a tarefa n°%d?', + 'Field required' => 'Campo requerido', + 'Link added successfully.' => 'Associação criada com sucesso.', + 'Link updated successfully.' => 'Associação atualizada com sucesso.', + 'Link removed successfully.' => 'Associação removida com sucesso.', + 'Link labels' => 'Etiquetas das associações', + 'Link modification' => 'Modificação de uma associação', + 'Links' => 'Associações', + 'Link settings' => 'Configuração das associações', + 'Opposite label' => 'Nome da etiqueta oposta', + 'Remove a link' => 'Remover uma associação', + 'Task\'s links' => 'Associações das tarefas', + 'The labels must be different' => 'As etiquetas devem ser diferentes', + 'There is no link.' => 'Não há nenhuma associação.', + 'This label must be unique' => 'Esta etiqueta deve ser unica', + 'Unable to create your link.' => 'Impossível de adicionar sua associação.', + 'Unable to update your link.' => 'Impossível de atualizar sua associação.', + 'Unable to remove this link.' => 'Impossível de remover sua associação.', + 'relates to' => 'é associado com', + 'blocks' => 'blocos', + 'is blocked by' => 'esta bloqueado por', + 'duplicates' => 'duplica', + 'is duplicated by' => 'é duplicado por', + 'is a child of' => 'é um filho de', + 'is a parent of' => 'é um parente do', + 'targets milestone' => 'visa um milestone', + 'is a milestone of' => 'é um milestone de', + 'fixes' => 'corrige', + 'is fixed by' => 'foi corrigido por', + 'This task' => 'Esta tarefa', + '<1h' => '<1h', + '%dh' => '%dh', + '%b %e' => '%e %b', + 'Expand tasks' => 'Expandir tarefas', + 'Collapse tasks' => 'Contrair tarefas', + 'Expand/collapse tasks' => 'Expandir/Contrair tarefas', + 'Close dialog box' => 'Fechar a caixa de diálogo', + 'Submit a form' => 'Envia o formulário', + 'Board view' => 'Página do painel', + 'Keyboard shortcuts' => 'Atalhos de teclado', + 'Open board switcher' => 'Abrir o comutador de painel', + 'Application' => 'Aplicação', + 'Filter recently updated' => 'Filtro recentemente atualizado', + 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M', + 'More filters' => 'Mais filtros', + 'Compact view' => 'Vista reduzida', + 'Horizontal scrolling' => 'Rolagem horizontal', + 'Compact/wide view' => 'Alternar entre a vista compacta e ampliada', + 'No results match:' => 'Nenhum resultado:', + 'Remove hourly rate' => 'Retirar taxa horária', + 'Do you really want to remove this hourly rate?' => 'Você deseja realmente remover esta taxa horária?', + 'Hourly rates' => 'Taxas horárias', + 'Hourly rate' => 'Taxa horária', + 'Currency' => 'Moeda', + 'Effective date' => 'Data efetiva', + 'Add new rate' => 'Adicionar nova taxa', + 'Rate removed successfully.' => 'Taxa removido com sucesso.', + 'Unable to remove this rate.' => 'Impossível de remover esta taxa.', + 'Unable to save the hourly rate.' => 'Impossível salvar a taxa horária.', + 'Hourly rate created successfully.' => 'Taxa horária criada com sucesso.', + 'Start time' => 'Horário de início', + 'End time' => 'Horário de término', + 'Comment' => 'comentário', + 'All day' => 'Dia inteiro', + 'Day' => 'Dia', + 'Manage timetable' => 'Gestão dos horários', + 'Overtime timetable' => 'Horas extras', + 'Time off timetable' => 'Horas de ausência', + 'Timetable' => 'Horários', + 'Work timetable' => 'Horas trabalhadas', + 'Week timetable' => 'Horário da semana', + 'Day timetable' => 'Horário de un dia', + 'From' => 'Desde', + 'To' => 'A', + 'Time slot created successfully.' => 'Intervalo de tempo criado com sucesso.', + 'Unable to save this time slot.' => 'Impossível de guardar este intervalo de tempo.', + 'Time slot removed successfully.' => 'Intervalo de tempo removido com sucesso.', + 'Unable to remove this time slot.' => 'Impossível de remover esse intervalo de tempo.', + 'Do you really want to remove this time slot?' => 'Você deseja realmente remover este intervalo de tempo?', + 'Remove time slot' => 'Remover um intervalo de tempo', + 'Add new time slot' => 'Adicionar um intervalo de tempo', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Esses horários são usados quando a caixa de seleção "Dia inteiro" está marcada para Horas de ausência ou Extras', + 'Files' => 'Arquivos', + 'Images' => 'Imagens', + 'Private project' => 'Projeto privado', + 'Amount' => 'Quantia', + 'AUD - Australian Dollar' => 'AUD - Dólar australiano', + 'Budget' => 'Orçamento', + 'Budget line' => 'Rubrica orçamental', + 'Budget line removed successfully.' => 'Rubrica orçamental removida com sucesso', + 'Budget lines' => 'Rubricas orçamentais', + 'CAD - Canadian Dollar' => 'CAD - Dólar canadense', + 'CHF - Swiss Francs' => 'CHF - Francos Suíços', + 'Cost' => 'Custo', + 'Cost breakdown' => 'Repartição dos custos', + 'Custom Stylesheet' => 'Folha de estilo personalizado', + 'download' => 'baixar', + 'Do you really want to remove this budget line?' => 'Você deseja realmente remover esta rubrica orçamental?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Despesas', + 'GBP - British Pound' => 'GBP - Libra Esterlina', + 'INR - Indian Rupee' => 'INR - Rúpia indiana', + 'JPY - Japanese Yen' => 'JPY - Iene japonês', + 'New budget line' => 'Nova rubrica orçamental', + 'NZD - New Zealand Dollar' => 'NZD - Dólar Neozelandês', + 'Remove a budget line' => 'Remover uma rubrica orçamental', + 'Remove budget line' => 'Remover uma rubrica orçamental', + 'RSD - Serbian dinar' => 'RSD - Dinar sérvio', + 'The budget line have been created successfully.' => 'A rubrica orçamental foi criada com sucesso.', + 'Unable to create the budget line.' => 'Impossível de adicionar esta rubrica orçamental.', + 'Unable to remove this budget line.' => 'Impossível de remover esta rubrica orçamental.', + 'USD - US Dollar' => 'USD - Dólar norte-americano', + 'Remaining' => 'Restante', + 'Destination column' => 'Coluna de destino', + 'Move the task to another column when assigned to a user' => 'Mover a tarefa para uma outra coluna quando esta está atribuída a um usuário', + 'Move the task to another column when assignee is cleared' => 'Mover a tarefa para uma outra coluna quando esta não está atribuída', + 'Source column' => 'Coluna de origem', + 'Show subtask estimates (forecast of future work)' => 'Mostrar a estimativa das subtarefas (previsão para o trabalho futuro)', + 'Transitions' => 'Transições', + 'Executer' => 'Executor(a)', + 'Time spent in the column' => 'Tempo gasto na coluna', + 'Task transitions' => 'Transições das tarefas', + 'Task transitions export' => 'Exportação das transições das tarefas', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Este relatório contém todos os movimentos de coluna para cada tarefa com a data, o usuário e o tempo gasto para cada transição.', + 'Currency rates' => 'Taxas de câmbio das moedas estrangeiras', + 'Rate' => 'Taxa', + 'Change reference currency' => 'Mudar a moeda de referência', + 'Add a new currency rate' => 'Adicionar uma nova taxa para uma moeda', + 'Currency rates are used to calculate project budget.' => 'As taxas de câmbio são utilizadas para calcular o orçamento do projeto.', + 'Reference currency' => 'Moeda de Referência', + 'The currency rate have been added successfully.' => 'A taxa de câmbio foi adicionada com sucesso.', + 'Unable to add this currency rate.' => 'Impossível de adicionar essa taxa de câmbio.', + 'Send notifications to a Slack channel' => 'Enviar as notificações em um canal Slack', + 'Webhook URL' => 'URL do webhook', + 'Help on Slack integration' => 'Ajuda na integração com o Slack', + '%s remove the assignee of the task %s' => '%s removeu a pessoa designada para a tarefa %s', + 'Send notifications to Hipchat' => 'Enviar as notificações para o Hipchat', + 'API URL' => 'URL da API', + 'Room API ID or name' => 'Nome ou ID da sala de discussão', + 'Room notification token' => 'Código de segurança da sala de discussão', + 'Help on Hipchat integration' => 'Ajuda na integração com o Hipchat', + 'Enable Gravatar images' => 'Ativar imagem Gravatar', + 'Information' => 'Informações', + 'Check two factor authentication code' => 'Verificação do código de autenticação à fator duplo', + 'The two factor authentication code is not valid.' => 'O código de autenticação à fator duplo não é válido', + 'The two factor authentication code is valid.' => 'O código de autenticação à fator duplo é válido', + 'Code' => 'Código', + 'Two factor authentication' => 'Autenticação à fator duplo', + 'Enable/disable two factor authentication' => 'Ativar/Desativar autenticação à fator duplo', + 'This QR code contains the key URI: ' => 'Este Código QR contém a chave URI:', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Salve esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).', + 'Check my code' => 'Verifique o meu código', + 'Secret key: ' => 'Chave secreta:', + 'Test your device' => 'Teste o seu dispositivo', + 'Assign a color when the task is moved to a specific column' => 'Atribuir uma cor quando a tarefa é movida em uma coluna específica', + '%s via Kanboard' => '%s via Kanboard', + 'uploaded by: %s' => 'carregado por: %s', + 'uploaded on: %s' => 'carregado em: %s', + 'size: %s' => 'tamanho: %s', + 'Burndown chart for "%s"' => 'Gráfico de Burndown para', + 'Burndown chart' => 'Gráfico de Burndown', + 'This chart show the task complexity over the time (Work Remaining).' => 'Este gráfico mostra a complexidade da tarefa ao longo do tempo (Trabalho Restante).', + 'Screenshot taken %s' => 'Screenshot tomada em %s', + 'Add a screenshot' => 'Adicionar uma Screenshot', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Tomar um screenshot e pressione CTRL + V ou ⌘ + V para colar aqui.', + 'Screenshot uploaded successfully.' => 'Screenshot enviada com sucesso.', + 'SEK - Swedish Krona' => 'SEK - Coroa sueca', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'O identificador de projeto é um código alfanumérico opcional utilizado para identificar o seu projeto.', + 'Identifier' => 'Identificador', + 'Postmark (incoming emails)' => 'Postmark (e-mails recebidos)', + 'Help on Postmark integration' => 'Ajuda na integração do Postmark', + 'Mailgun (incoming emails)' => 'Mailgun (e-mails recebidos)', + 'Help on Mailgun integration' => 'Ajuda na integração do Mailgun', + 'Sendgrid (incoming emails)' => 'Sendgrid (e-mails recebidos)', + 'Help on Sendgrid integration' => 'Ajuda na integração do Sendgrid', + 'Disable two factor authentication' => 'Desativar autenticação à dois fatores', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Você deseja realmente desativar a autenticação à dois fatores para esse usuário: "%s"?', + 'Edit link' => 'Editar um link', + 'Start to type task title...' => 'Digite o título do trabalho...', + 'A task cannot be linked to itself' => 'Uma tarefa não pode ser ligada a si própria', + 'The exact same link already exists' => 'Um link idêntico jà existe', + 'Recurrent task is scheduled to be generated' => 'A tarefa recorrente está programada para ser criada', + 'Recurring information' => 'Informação sobre a recorrência', + 'Score' => 'Complexidade', + 'The identifier must be unique' => 'O identificador deve ser único', + 'This linked task id doesn\'t exists' => 'O identificador da tarefa associada não existe', + 'This value must be alphanumeric' => 'Este valor deve ser alfanumérico', + 'Edit recurrence' => 'Modificar a recorrência', + 'Generate recurrent task' => 'Gerar uma tarefa recorrente', + 'Trigger to generate recurrent task' => 'Trigger para gerar tarefa recorrente', + 'Factor to calculate new due date' => 'Fator para o cálculo do nova data limite', + 'Timeframe to calculate new due date' => 'Escala de tempo para o cálculo da nova data limite', + 'Base date to calculate new due date' => 'Data a ser utilizada para calcular a nova data limite', + 'Action date' => 'Data da ação', + 'Base date to calculate new due date: ' => 'Data a ser utilizada para calcular a nova data limite: ', + 'This task has created this child task: ' => 'Esta tarefa criou a tarefa filha: ', + 'Day(s)' => 'Dia(s)', + 'Existing due date' => 'Data limite existente', + 'Factor to calculate new due date: ' => 'Fator para calcular a nova data limite: ', + 'Month(s)' => 'Mês(es)', + 'Recurrence' => 'Recorrência', + 'This task has been created by: ' => 'Esta tarefa foi criada por: ', + 'Recurrent task has been generated:' => 'A tarefa recorrente foi gerada:', + 'Timeframe to calculate new due date: ' => 'Escala de tempo para o cálculo da nova data limite: ', + 'Trigger to generate recurrent task: ' => 'Trigger para gerar tarefa recorrente: ', + 'When task is closed' => 'Quando a tarefa é fechada', + 'When task is moved from first column' => 'Quando a tarefa é movida fora da primeira coluna', + 'When task is moved to last column' => 'Quando a tarefa é movida para a última coluna', + 'Year(s)' => 'Ano(s)', + 'Jabber (XMPP)' => 'Jabber (XMPP)', + 'Send notifications to Jabber' => 'Enviar notificações para o Jabber', + 'XMPP server address' => 'Endereço do servidor XMPP', + 'Jabber domain' => 'Nome de domínio Jabber', + 'Jabber nickname' => 'Apelido Jabber', + 'Multi-user chat room' => 'Sala de chat multi-usuário', + 'Help on Jabber integration' => 'Ajuda na integração com Jabber', + 'The server address must use this format: "tcp://hostname:5222"' => 'O endereço do servidor deve usar o seguinte formato: "tcp://hostname:5222"', + 'Calendar settings' => 'Configurações do calendário', + 'Project calendar view' => 'Vista em modo projeto do calendário', + 'Project settings' => 'Configurações dos projetos', + 'Show subtasks based on the time tracking' => 'Mostrar as subtarefas com base no controle de tempo', + 'Show tasks based on the creation date' => 'Mostrar as tarefas em função da data de criação', + 'Show tasks based on the start date' => 'Mostrar as tarefas em função da data de início', + 'Subtasks time tracking' => 'Monitoramento do tempo comparado as subtarefas', + 'User calendar view' => 'Vista em modo utilizador do calendário', + 'Automatically update the start date' => 'Atualizar automaticamente a data de início', + 'iCal feed' => 'Subscrição iCal', + 'Preferences' => 'Preferências', + 'Security' => 'Segurança', + 'Two factor authentication disabled' => 'Autenticação à fator duplo desativado', + 'Two factor authentication enabled' => 'Autenticação à fator duplo activado', + 'Unable to update this user.' => 'Impossível de atualizar esse usuário.', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php new file mode 100644 index 00000000..ee71ea95 --- /dev/null +++ b/app/Locale/ru_RU/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => 'Отсутствует', + 'edit' => 'изменить', + 'Edit' => 'Изменить', + 'remove' => 'удалить', + 'Remove' => 'Удалить', + 'Update' => 'Обновить', + 'Yes' => 'Да', + 'No' => 'Нет', + 'cancel' => 'Отменить', + 'or' => 'или', + 'Yellow' => 'Желтый', + 'Blue' => 'Синий', + 'Green' => 'Зеленый', + 'Purple' => 'Фиолетовый', + 'Red' => 'Красный', + 'Orange' => 'Оранжевый', + 'Grey' => 'Серый', + 'Save' => 'Сохранить', + 'Login' => 'Вход', + 'Official website:' => 'Официальный сайт:', + 'Unassigned' => 'Не назначена', + 'View this task' => 'Посмотреть задачу', + 'Remove user' => 'Удалить пользователя', + 'Do you really want to remove this user: "%s"?' => 'Вы точно хотите удалить пользователя: « %s » ?', + 'New user' => 'Новый пользователь', + 'All users' => 'Все пользователи', + 'Username' => 'Имя пользователя', + 'Password' => 'Пароль', + 'Default project' => 'Проект по умолчанию', + 'Administrator' => 'Администратор', + 'Sign in' => 'Войти', + 'Users' => 'Пользователи', + 'No user' => 'Нет пользователя', + 'Forbidden' => 'Запрещено', + 'Access Forbidden' => 'Доступ запрещен', + 'Only administrators can access to this page.' => 'Только администраторы могут войти на эту страницу.', + 'Edit user' => 'Изменить пользователя', + 'Logout' => 'Выйти', + 'Bad username or password' => 'Неверное имя пользователя или пароль', + 'users' => 'пользователи', + 'projects' => 'проекты', + 'Edit project' => 'Изменить проект', + 'Name' => 'Имя', + 'Activated' => 'Активен', + 'Projects' => 'Проекты', + 'No project' => 'Нет проекта', + 'Project' => 'Проект', + 'Status' => 'Статус', + 'Tasks' => 'Задачи', + 'Board' => 'Доска', + 'Actions' => 'Действия', + 'Inactive' => 'Неактивен', + 'Active' => 'Активен', + 'Column %d' => 'Колонка %d', + 'Add this column' => 'Добавить колонку', + '%d tasks on the board' => 'Задач на доске - %d', + '%d tasks in total' => 'Задач всего - %d', + 'Unable to update this board.' => 'Не удалось обновить доску.', + 'Edit board' => 'Изменить доски', + 'Disable' => 'Деактивировать', + 'Enable' => 'Активировать', + 'New project' => 'Новый проект', + 'Do you really want to remove this project: "%s"?' => 'Вы точно хотите удалить проект: « %s » ?', + 'Remove project' => 'Удалить проект', + 'Boards' => 'Доски', + 'Edit the board for "%s"' => 'Изменить доску для « %s »', + 'All projects' => 'Все проекты', + 'Change columns' => 'Изменить колонки', + 'Add a new column' => 'Добавить новую колонку', + 'Title' => 'Название', + 'Add Column' => 'Добавить колонку', + 'Project "%s"' => 'Проект « %s »', + 'Nobody assigned' => 'Никто не назначен', + 'Assigned to %s' => 'Исполнитель: %s', + 'Remove a column' => 'Удалить колонку', + 'Remove a column from a board' => 'Удалить колонку с доски', + 'Unable to remove this column.' => 'Не удалось удалить колонку.', + 'Do you really want to remove this column: "%s"?' => 'Вы точно хотите удалить эту колонку: « %s » ?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Вы УДАЛИТЕ ВСЕ ЗАДАЧИ находящиеся в этой колонке !', + 'Settings' => 'Настройки', + 'Application settings' => 'Настройки приложения', + 'Language' => 'Язык', + 'Webhook token:' => 'Webhooks токен :', + 'API token:' => 'API токен :', + 'More information' => 'Подробнее', + 'Database size:' => 'Размер базы данных :', + 'Download the database' => 'Скачать базу данных', + 'Optimize the database' => 'Оптимизировать базу данных', + '(VACUUM command)' => '(Команда VACUUM)', + '(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)', + 'User settings' => 'Настройки пользователя', + 'My default project:' => 'Мой проект по умолчанию:', + 'Close a task' => 'Закрыть задачу', + 'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу: « %s » ?', + 'Edit a task' => 'Изменить задачу', + 'Column' => 'Колонка', + 'Color' => 'Цвет', + 'Assignee' => 'Назначена', + 'Create another task' => 'Создать другую задачу', + 'New task' => 'Новая задача', + 'Open a task' => 'Открыть задачу', + 'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу: « %s » ?', + 'Back to the board' => 'Вернуться на доску', + 'Created on %B %e, %Y at %k:%M %p' => 'Создано %d/%m/%Y в %H:%M', + 'There is nobody assigned' => 'Никто не назначен', + 'Column on the board:' => 'Колонка на доске : ', + 'Status is open' => 'Статус - открыт', + 'Status is closed' => 'Статус - закрыт', + 'Close this task' => 'Закрыть эту задачу', + 'Open this task' => 'Открыть эту задачу', + 'There is no description.' => 'Нет описания.', + 'Add a new task' => 'Добавить новую задачу', + 'The username is required' => 'Требуется имя пользователя', + 'The maximum length is %d characters' => 'Максимальная длина - %d знаков', + 'The minimum length is %d characters' => 'Минимальная длина - %d знаков', + 'The password is required' => 'Требуется пароль', + 'This value must be an integer' => 'Это значение должно быть целым', + 'The username must be unique' => 'Требуется уникальное имя пользователя', + 'The username must be alphanumeric' => 'Имя пользователя должно быть буквенно-цифровым', + 'The user id is required' => 'Требуется ID пользователя', + 'Passwords don\'t match' => 'Пароли не совпадают', + 'The confirmation is required' => 'Требуется подтверждение', + 'The column is required' => 'Требуется колонка', + 'The project is required' => 'Требуется проект', + 'The color is required' => 'Требуется цвет', + 'The id is required' => 'Требуется ID', + 'The project id is required' => 'Требуется ID проекта', + 'The project name is required' => 'Требуется имя проекта', + 'This project must be unique' => 'Проект должен быть уникальным', + 'The title is required' => 'Требуется заголовок', + 'The language is required' => 'Требуется язык', + 'There is no active project, the first step is to create a new project.' => 'Нет активного проекта, сначала создайте новый проект.', + 'Settings saved successfully.' => 'Параметры успешно сохранены.', + 'Unable to save your settings.' => 'Невозможно сохранить параметры.', + 'Database optimization done.' => 'База данных оптимизирована.', + 'Your project have been created successfully.' => 'Ваш проект успешно создан.', + 'Unable to create your project.' => 'Не удалось создать проект.', + 'Project updated successfully.' => 'Проект успешно обновлен.', + 'Unable to update this project.' => 'Не удалось обновить проект.', + 'Unable to remove this project.' => 'Не удалось удалить проект.', + 'Project removed successfully.' => 'Проект удален.', + 'Project activated successfully.' => 'Проект активирован.', + 'Unable to activate this project.' => 'Невозможно активировать проект.', + 'Project disabled successfully.' => 'Проект успешно деактивирован.', + 'Unable to disable this project.' => 'Не удалось деактивировать проект.', + 'Unable to open this task.' => 'Не удалось открыть задачу.', + 'Task opened successfully.' => 'Задача открыта.', + 'Unable to close this task.' => 'Не удалось закрыть задачу.', + 'Task closed successfully.' => 'Задача закрыта.', + 'Unable to update your task.' => 'Не удалось обновить задачу.', + 'Task updated successfully.' => 'Задача обновлена.', + 'Unable to create your task.' => 'Не удалось создать задачу.', + 'Task created successfully.' => 'Задача создана.', + 'User created successfully.' => 'Пользователь создан.', + 'Unable to create your user.' => 'Не удалось создать пользователя.', + 'User updated successfully.' => 'Пользователь обновлен.', + 'Unable to update your user.' => 'Не удалось обновить пользователя.', + 'User removed successfully.' => 'Пользователь удален.', + 'Unable to remove this user.' => 'Не удалось удалить пользователя.', + 'Board updated successfully.' => 'Доска успешно обновлена.', + 'Ready' => 'Готовые', + 'Backlog' => 'Ожидающие', + 'Work in progress' => 'В процессе', + 'Done' => 'Выполнена', + 'Application version:' => 'Версия приложения:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Завершен %d/%m/%Y в %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y в %H:%M', + 'Date created' => 'Дата создания', + 'Date completed' => 'Дата завершения', + 'Id' => 'ID', + 'No task' => 'Нет задачи', + 'Completed tasks' => 'Завершенные задачи', + 'List of projects' => 'Список проектов', + 'Completed tasks for "%s"' => 'Завершенные задачи для « %s »', + '%d closed tasks' => '%d завершенных задач', + 'No task for this project' => 'Нет задач для этого проекта', + 'Public link' => 'Ссылка для просмотра', + 'There is no column in your project!' => 'Нет колонки в вашем проекте!', + 'Change assignee' => 'Сменить назначенного', + 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »', + 'Timezone' => 'Часовой пояс', + 'Sorry, I didn\'t find this information in my database!' => 'К сожалению, информация в базе данных не найдена !', + 'Page not found' => 'Страница не найдена', + 'Complexity' => 'Сложность', + 'limit' => 'лимит', + 'Task limit' => 'Лимит задач', + 'Task count' => 'Количество задач', + 'This value must be greater than %d' => 'Это значение должно быть больше %d', + 'Edit project access list' => 'Изменить доступ к проекту', + 'Edit users access' => 'Изменить доступ пользователей', + 'Allow this user' => 'Разрешить этого пользователя', + 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту:', + 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет неограниченные права.', + 'Revoke' => 'Отозвать', + 'List of authorized users' => 'Список авторизованных пользователей', + 'User' => 'Пользователь', + 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту', + 'You are not allowed to access to this project.' => 'Вам запрещен доступ к этому проекту.', + 'Comments' => 'Комментарии', + 'Post comment' => 'Оставить комментарий', + 'Write your text in Markdown' => 'Справка по синтаксису Markdown', + 'Leave a comment' => 'Оставить комментарий 2', + 'Comment is required' => 'Нужен комментарий', + 'Leave a description' => 'Напишите описание', + 'Comment added successfully.' => 'Комментарий успешно добавлен.', + 'Unable to create your comment.' => 'Невозможно создать комментарий.', + 'The description is required' => 'Требуется описание', + 'Edit this task' => 'Изменить задачу', + 'Due Date' => 'Сделать до', + 'Invalid date' => 'Неверная дата', + 'Must be done before %B %e, %Y' => 'Должно быть сделано до %d/%m/%Y', + '%B %e, %Y' => '%d/%m/%Y', + // '%b %e, %Y' => '', + 'Automatic actions' => 'Автоматические действия', + 'Your automatic action have been created successfully.' => 'Автоматика успешно настроена.', + 'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.', + 'Remove an action' => 'Удалить действие', + 'Unable to remove this action.' => 'Не удалось удалить действие', + 'Action removed successfully.' => 'Действие удалено.', + 'Automatic actions for the project "%s"' => 'Автоматические действия для проекта « %s »', + 'Defined actions' => 'Заданные действия', + 'Add an action' => 'Добавить действие', + 'Event name' => 'Имя события', + 'Action name' => 'Имя действия', + 'Action parameters' => 'Параметры действия', + 'Action' => 'Действие', + 'Event' => 'Событие', + 'When the selected event occurs execute the corresponding action.' => 'Когда случится ВЫБРАННОЕ событие выполняется СООТВЕТСТВУЮЩЕЕ действие.', + 'Next step' => 'Следующий шаг', + 'Define action parameters' => 'Задать параметры действия', + 'Save this action' => 'Сохранить это действие', + 'Do you really want to remove this action: "%s"?' => 'Вы точно хотите удалить это действие: « %s » ?', + 'Remove an automatic action' => 'Удалить автоматическое действие', + 'Close the task' => 'Закрыть задачу', + 'Assign the task to a specific user' => 'Назначить задачу определенному пользователю', + 'Assign the task to the person who does the action' => 'Назначить задачу тому кто выполнит действие', + 'Duplicate the task to another project' => 'Создать дубликат задачи в другом проекте', + 'Move a task to another column' => 'Переместить задачу в другую колонку', + 'Move a task to another position in the same column' => 'Переместить задачу в другое место этой же колонки', + 'Task modification' => 'Изменение задачи', + 'Task creation' => 'Создание задачи', + 'Open a closed task' => 'Открыть завершенную задачу', + 'Closing a task' => 'Завершение задачи', + 'Assign a color to a specific user' => 'Назначить определенный цвет пользователю', + 'Column title' => 'Название колонки', + 'Position' => 'Расположение', + 'Move Up' => 'Сдвинуть вверх', + 'Move Down' => 'Сдвинуть вниз', + 'Duplicate to another project' => 'Клонировать в другой проект', + 'Duplicate' => 'Клонировать', + 'link' => 'ссылка', + 'Update this comment' => 'Обновить комментарий', + 'Comment updated successfully.' => 'Комментарий обновлен.', + 'Unable to update your comment.' => 'Не удалось обновить ваш комментарий.', + 'Remove a comment' => 'Удалить комментарий', + 'Comment removed successfully.' => 'Комментарий удален.', + 'Unable to remove this comment.' => 'Не удалось удалить этот комментарий.', + 'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий?', + 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор и автор комментария имеют доступ к этой странице.', + 'Details' => 'Подробности', + 'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »', + 'The current password is required' => 'Требуется текущий пароль', + 'Wrong password' => 'Неверный пароль', + 'Reset all tokens' => 'Сброс всех токенов', + 'All tokens have been regenerated.' => 'Все токены пересозданы.', + 'Unknown' => 'Неизвестно', + 'Last logins' => 'Последние посещения', + 'Login date' => 'Дата входа', + 'Authentication method' => 'Способ аутентификации', + 'IP address' => 'IP адрес', + 'User agent' => 'User agent', + 'Persistent connections' => 'Постоянные соединения', + 'No session.' => 'Нет сеанса', + 'Expiration date' => 'Дата окончания', + 'Remember Me' => 'Запомнить меня', + 'Creation date' => 'Дата создания', + 'Filter by user' => 'Фильтр по пользователям', + 'Filter by due date' => 'Фильтр по дате', + 'Everybody' => 'Все', + 'Open' => 'Открытый', + 'Closed' => 'Закрытый', + 'Search' => 'Поиск', + 'Nothing found.' => 'Ничего не найдено.', + 'Search in the project "%s"' => 'Искать в проекте « %s »', + 'Due date' => 'Срок', + 'Others formats accepted: %s and %s' => 'Другой формат приемлем: %s и %s', + 'Description' => 'Описание', + '%d comments' => '%d комментариев', + '%d comment' => '%d комментарий', + 'Email address invalid' => 'Некорректный e-mail адрес', + 'Your Google Account is not linked anymore to your profile.' => 'Ваш аккаунт в Google больше не привязан к вашему профилю.', + 'Unable to unlink your Google Account.' => 'Не удалось отвязать ваш профиль от Google.', + 'Google authentication failed' => 'Аутентификация Google не удалась', + 'Unable to link your Google Account.' => 'Не удалось привязать ваш профиль к Google.', + 'Your Google Account is linked to your profile successfully.' => 'Ваш профиль успешно привязан к Google.', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Привязать мой профиль к Google', + 'Unlink my Google Account' => 'Отвязать мой профиль от Google', + 'Login with my Google Account' => 'Аутентификация через Google', + 'Project not found.' => 'Проект не найден.', + 'Task #%d' => 'Задача n°%d', + 'Task removed successfully.' => 'Задача удалена.', + 'Unable to remove this task.' => 'Не удалось удалить эту задачу.', + 'Remove a task' => 'Удалить задачу', + 'Do you really want to remove this task: "%s"?' => 'Вы точно хотите удалить эту задачу « %s » ?', + 'Assign automatically a color based on a category' => 'Автоматически назначать цвет по категории', + 'Assign automatically a category based on a color' => 'Автоматически назначать категорию по цвету', + 'Task creation or modification' => 'Создание или изменение задачи', + 'Category' => 'Категория', + 'Category:' => 'Категория:', + 'Categories' => 'Категории', + 'Category not found.' => 'Категория не найдена', + 'Your category have been created successfully.' => 'Категория создана.', + 'Unable to create your category.' => 'Не удалось создать категорию.', + 'Your category have been updated successfully.' => 'Категория обновлена.', + 'Unable to update your category.' => 'Не удалось обновить категорию.', + 'Remove a category' => 'Удалить категорию', + 'Category removed successfully.' => 'Категория удалена.', + 'Unable to remove this category.' => 'Не удалось удалить категорию.', + 'Category modification for the project "%s"' => 'Изменение категории для проекта « %s »', + 'Category Name' => 'Название категории', + 'Categories for the project "%s"' => 'Категории для проекта « %s »', + 'Add a new category' => 'Добавить новую категорию', + 'Do you really want to remove this category: "%s"?' => 'Вы точно хотите удалить категорию « %s » ?', + 'Filter by category' => 'Фильтр по категориям', + 'All categories' => 'Все категории', + 'No category' => 'Нет категории', + 'The name is required' => 'Требуется название', + 'Remove a file' => 'Удалить файл', + 'Unable to remove this file.' => 'Не удалось удалить файл.', + 'File removed successfully.' => 'Файл удален.', + 'Attach a document' => 'Прикрепить файл', + 'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?', + 'open' => 'открыть', + 'Attachments' => 'Приложение', + 'Edit the task' => 'Изменить задачу', + 'Edit the description' => 'Изменить описание', + 'Add a comment' => 'Добавить комментарий', + 'Edit a comment' => 'Изменить комментарий', + 'Summary' => 'Сводка', + 'Time tracking' => 'Отслеживание времени', + 'Estimate:' => 'Приблизительно:', + 'Spent:' => 'Затрачено:', + 'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу?', + 'Remaining:' => 'Осталось:', + 'hours' => 'часов', + 'spent' => 'затрачено', + 'estimated' => 'расчетное', + 'Sub-Tasks' => 'Подзадачи', + 'Add a sub-task' => 'Добавить подзадачу', + 'Original estimate' => 'Первичная оценка', + 'Create another sub-task' => 'Создать другую подзадачу', + 'Time spent' => 'Времени затрачено', + 'Edit a sub-task' => 'Изменить подзадачу', + 'Remove a sub-task' => 'Удалить подзадачу', + 'The time must be a numeric value' => 'Время должно быть числом!', + 'Todo' => 'К исполнению', + 'In progress' => 'В процессе', + 'Sub-task removed successfully.' => 'Подзадача удалена.', + 'Unable to remove this sub-task.' => 'Не удалось удалить подзадачу.', + 'Sub-task updated successfully.' => 'Подзадача обновлена.', + 'Unable to update your sub-task.' => 'Не удалось обновить подзадачу.', + 'Unable to create your sub-task.' => 'Не удалось создать подзадачу.', + 'Sub-task added successfully.' => 'Подзадача добавлена.', + 'Maximum size: ' => 'Максимальный размер: ', + 'Unable to upload the file.' => 'Не удалось загрузить файл.', + 'Display another project' => 'Показать другой проект', + 'Your GitHub account was successfully linked to your profile.' => 'Ваш GitHub привязан к вашему профилю.', + 'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к GitHub.', + 'GitHub authentication failed' => 'Аутентификация в GitHub не удалась', + 'Your GitHub account is no longer linked to your profile.' => 'Ваш GitHub отвязан от вашего профиля.', + 'Unable to unlink your GitHub Account.' => 'Не удалось отвязать ваш профиль от GitHub.', + 'Login with my GitHub Account' => 'Аутентификация через GitHub', + 'Link my GitHub Account' => 'Привязать мой профиль к GitHub', + 'Unlink my GitHub Account' => 'Отвязать мой профиль от GitHub', + 'Created by %s' => 'Создано %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Последнее изменение %d/%m/%Y в %H:%M', + 'Tasks Export' => 'Экспорт задач', + 'Tasks exportation for "%s"' => 'Задача экспортирована для « %s »', + 'Start Date' => 'Дата начала', + 'End Date' => 'Дата завершения', + 'Execute' => 'Выполнить', + 'Task Id' => 'ID задачи', + 'Creator' => 'Автор', + 'Modification date' => 'Дата изменения', + 'Completion date' => 'Дата завершения', + 'Clone' => 'Клонировать', + 'Clone Project' => 'Клонировать проект', + 'Project cloned successfully.' => 'Проект клонирован.', + 'Unable to clone this project.' => 'Не удалось клонировать проект.', + 'Email notifications' => 'Уведомления по e-mail', + 'Enable email notifications' => 'Включить уведомления по e-mail', + 'Task position:' => 'Позиция задачи:', + 'The task #%d have been opened.' => 'Задача #%d была открыта.', + 'The task #%d have been closed.' => 'Задача #%d была закрыта.', + 'Sub-task updated' => 'Подзадача обновлена', + 'Title:' => 'Название:', + 'Status:' => 'Статус:', + 'Assignee:' => 'Назначена:', + 'Time tracking:' => 'Отслеживание времени:', + 'New sub-task' => 'Новая подзадача', + 'New attachment added "%s"' => 'Добавлено вложение « %s »', + 'Comment updated' => 'Комментарий обновлен', + 'New comment posted by %s' => 'Новый комментарий написан « %s »', + 'List of due tasks for the project "%s"' => 'Список сроков к проекту « %s »', + 'New attachment' => 'Новое вложение', + 'New comment' => 'Новый комментарий', + 'New subtask' => 'Новая подзадача', + 'Subtask updated' => 'Подзадача обновлена', + 'Task updated' => 'Задача обновлена', + 'Task closed' => 'Задача закрыта', + 'Task opened' => 'Задача открыта', + '[%s][Due tasks]' => '[%s][Текущие задачи]', + '[Kanboard] Notification' => '[Kanboard] Оповещение', + 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам:', + 'view the task on Kanboard' => 'посмотреть задачу на Kanboard', + 'Public access' => 'Общий доступ', + 'Category management' => 'Управление категориями', + 'User management' => 'Управление пользователями', + 'Active tasks' => 'Активные задачи', + 'Disable public access' => 'Отключить общий доступ', + 'Enable public access' => 'Включить общий доступ', + 'Active projects' => 'Активные проекты', + 'Inactive projects' => 'Неактивные проекты', + 'Public access disabled' => 'Общий доступ отключен', + 'Do you really want to disable this project: "%s"?' => 'Вы точно хотите деактивировать проект: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Вы точно хотите клонировать проект: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Вы точно хотите активировать проект: "%s"?', + 'Project activation' => 'Активация проекта', + 'Move the task to another project' => 'Переместить задачу в другой проект', + 'Move to another project' => 'Переместить в другой проект', + 'Do you really want to duplicate this task?' => 'Вы точно хотите клонировать задачу?', + 'Duplicate a task' => 'Клонировать задачу', + 'External accounts' => 'Внешняя аутентификация', + 'Account type' => 'Тип профиля', + 'Local' => 'Локальный', + 'Remote' => 'Удаленный', + 'Enabled' => 'Включен', + 'Disabled' => 'Выключены', + 'Google account linked' => 'Профиль Google связан', + 'Github account linked' => 'Профиль GitHub связан', + 'Username:' => 'Имя пользователя:', + 'Name:' => 'Имя:', + 'Email:' => 'E-mail:', + 'Default project:' => 'Проект по умолчанию:', + 'Notifications:' => 'Уведомления:', + 'Notifications' => 'Уведомления', + 'Group:' => 'Группа:', + 'Regular user' => 'Обычный пользователь', + 'Account type:' => 'Тип профиля:', + 'Edit profile' => 'Редактировать профиль', + 'Change password' => 'Сменить пароль', + 'Password modification' => 'Изменение пароля', + 'External authentications' => 'Внешняя аутентификация', + 'Google Account' => 'Профиль Google', + 'Github Account' => 'Профиль GitHub', + 'Never connected.' => 'Ранее не соединялось.', + 'No account linked.' => 'Нет связанных профилей.', + 'Account linked.' => 'Профиль связан.', + 'No external authentication enabled.' => 'Нет активной внешней аутентификации.', + 'Password modified successfully.' => 'Пароль изменен.', + 'Unable to change the password.' => 'Не удалось сменить пароль.', + 'Change category for the task "%s"' => 'Сменить категорию для задачи "%s"', + 'Change category' => 'Смена категории', + '%s updated the task %s' => '%s обновил задачу %s', + '%s opened the task %s' => '%s открыл задачу %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s переместил задачу %s на позицию #%d в колонке "%s"', + '%s moved the task %s to the column "%s"' => '%s переместил задачу %s в колонку "%s"', + '%s created the task %s' => '%s создал задачу %s', + '%s closed the task %s' => '%s закрыл задачу %s', + '%s created a subtask for the task %s' => '%s создал подзадачу для задачи %s', + '%s updated a subtask for the task %s' => '%s обновил подзадачу для задачи %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Назначено %s с окончанием %s/%sh', + 'Not assigned, estimate of %sh' => 'Не назначено, окончание %sh', + '%s updated a comment on the task %s' => '%s обновил комментарий к задаче %s', + '%s commented the task %s' => '%s прокомментировал задачу %s', + '%s\'s activity' => '%s активность', + 'No activity.' => 'Нет активности', + 'RSS feed' => 'RSS лента', + '%s updated a comment on the task #%d' => '%s обновил комментарий задачи #%d', + '%s commented on the task #%d' => '%s прокомментировал задачу #%d', + '%s updated a subtask for the task #%d' => '%s обновил подзадачу задачи #%d', + '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d', + '%s updated the task #%d' => '%s обновил задачу #%d', + '%s created the task #%d' => '%s создал задачу #%d', + '%s closed the task #%d' => '%s закрыл задачу #%d', + '%s open the task #%d' => '%s открыл задачу #%d', + '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s переместил задачу #%d на позицию %d в колонке "%s"', + 'Activity' => 'Активность', + 'Default values are "%s"' => 'Колонки по умолчанию: "%s"', + 'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)', + 'Task assignee change' => 'Изменен назначенный', + '%s change the assignee of the task #%d to %s' => '%s сменил назначенного для задачи #%d на %s', + '%s changed the assignee of the task %s to %s' => '%s сменил назначенного для задачи %s на %s', + 'Column Change' => 'Изменение колонки', + 'Position Change' => 'Изменение позиции', + 'Assignee Change' => 'Изменение ответственного', + 'New password for the user "%s"' => 'Новый пароль для пользователя "%s"', + 'Choose an event' => 'Выберите событие', + 'Github commit received' => 'GitHub: коммит получен', + 'Github issue opened' => 'GitHub: новая проблема', + 'Github issue closed' => 'GitHub: проблема закрыта', + 'Github issue reopened' => 'GitHub: проблема переоткрыта', + 'Github issue assignee change' => 'GitHub: сменить ответственного за проблему', + 'Github issue label change' => 'GitHub: ярлык проблемы изменен', + 'Create a task from an external provider' => 'Создать задачу из внешнего источника', + 'Change the assignee based on an external username' => 'Изменить назначенного основываясь на внешнем имени пользователя', + 'Change the category based on an external label' => 'Изменить категорию основываясь на внешнем ярлыке', + 'Reference' => 'Ссылка', + 'Reference: %s' => 'Ссылка: %s', + 'Label' => 'Ярлык', + 'Database' => 'База данных', + 'About' => 'Информация', + 'Database driver:' => 'Драйвер базы данных', + 'Board settings' => 'Настройки доски', + 'URL and token' => 'URL и токен', + 'Webhook settings' => 'Параметры Webhook', + 'URL for task creation:' => 'URL для создания задачи:', + 'Reset token' => 'Перезагрузить токен', + 'API endpoint:' => 'API endpoint:', + 'Refresh interval for private board' => 'Период обновления для частных досок', + 'Refresh interval for public board' => 'Период обновления для публичных досок', + 'Task highlight period' => 'Время подсвечивания задачи', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Период (в секундах) в течении которого задача считается недавно измененной (0 для выключения, 2 дня по умолчанию)', + 'Frequency in second (60 seconds by default)' => 'Частота в секундах (60 секунд по умолчанию)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Частота в секундах (0 для выключения, 10 секунд по умолчанию)', + 'Application URL' => 'URL приложения', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Пример: http://example.kanboard.net (используется в email уведомлениях)', + 'Token regenerated.' => 'Токен пересоздан', + 'Date format' => 'Формат даты', + 'ISO format is always accepted, example: "%s" and "%s"' => 'Время должно быть в ISO-формате, например: "%s" или "%s"', + 'New private project' => 'Новый проект с ограниченным доступом', + 'This project is private' => 'Это проект с ограниченным доступом', + 'Type here to create a new sub-task' => 'Печатайте сюда чтобы создать подзадачу', + 'Add' => 'Добавить', + 'Estimated time: %s hours' => 'Планируемое время: %s часов', + 'Time spent: %s hours' => 'Потрачено времени: %s часов', + 'Started on %B %e, %Y' => 'Начато %B %e, %Y', + 'Start date' => 'Дата начала', + 'Time estimated' => 'Планируемое время', + 'There is nothing assigned to you.' => 'Вам ничего не назначено', + 'My tasks' => 'Мои задачи', + 'Activity stream' => 'Текущая активность', + 'Dashboard' => 'Инфопанель', + 'Confirmation' => 'Подтверждение пароля', + 'Allow everybody to access to this project' => 'Разрешить любому', + 'Everybody have access to this project.' => 'Любой может получить доступ к этому проекту.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Интеграция', + 'Github webhooks' => 'GitHub webhooks', + 'Help on Github webhooks' => 'Помощь по GitHub webhooks', + 'Create a comment from an external provider' => 'Создать комментарий из внешнего источника', + 'Github issue comment created' => 'Github issue комментарий создан', + 'Configure' => 'Настройки', + 'Project management' => 'Управление проектом', + 'My projects' => 'Мои проекты', + 'Columns' => 'Колонки', + 'Task' => 'Задача', + 'Your are not member of any project.' => 'Вы не состоите ни в одном проекте.', + 'Percentage' => 'Процент', + 'Number of tasks' => 'Количество задач', + 'Task distribution' => 'Распределение задач', + 'Reportings' => 'Отчетность', + 'Task repartition for "%s"' => 'Распределение задач для "%s"', + 'Analytics' => 'Аналитика', + 'Subtask' => 'Подзадача', + 'My subtasks' => 'Мои подзадачи', + 'User repartition' => 'Перераспределение пользователей', + 'User repartition for "%s"' => 'Перераспределение пользователей для "%s"', + 'Clone this project' => 'Клонировать проект', + 'Column removed successfully.' => 'Колонка успешно удалена.', + 'Edit Project' => 'Редактировать Проект', + // 'Github Issue' => '', + 'Not enough data to show the graph.' => 'Недостаточно данных, чтобы показать график.', + 'Previous' => 'Предыдущий', + 'The id must be an integer' => 'Этот id должен быть целочисленным', + 'The project id must be an integer' => 'Id проекта должен быть целочисленным', + 'The status must be an integer' => 'Статус должен быть целочисленным', + 'The subtask id is required' => 'Id подзадачи обязателен', + 'The subtask id must be an integer' => 'Id подзадачи должен быть целочисленным', + 'The task id is required' => 'Id задачи обязателен', + 'The task id must be an integer' => 'Id задачи должен быть целочисленным', + 'The user id must be an integer' => 'Id пользователя должен быть целочисленным', + 'This value is required' => 'Это значение обязательно', + 'This value must be numeric' => 'Это значение должно быть цифровым', + 'Unable to create this task.' => 'Невозможно создать задачу.', + 'Cumulative flow diagram' => 'Накопительная диаграма', + 'Cumulative flow diagram for "%s"' => 'Накопительная диаграма для "%s"', + 'Daily project summary' => 'Ежедневное состояние проекта', + 'Daily project summary export' => 'Экспорт ежедневного резюме проекта', + 'Daily project summary export for "%s"' => 'Экспорт ежедневного резюме проекта "%s"', + 'Exports' => 'Экспорт', + 'This export contains the number of tasks per column grouped per day.' => 'Этот экспорт содержит ряд задач в колонках, сгруппированные по дням.', + 'Nothing to preview...' => 'Нет данных для предпросмотра...', + 'Preview' => 'Предпросмотр', + 'Write' => 'Написание', + 'Active swimlanes' => 'Активные ', + 'Add a new swimlane' => 'Добавить новую дорожку', + 'Change default swimlane' => 'Сменить стандартную дорожку', + 'Default swimlane' => 'Стандартная дорожка', + 'Do you really want to remove this swimlane: "%s"?' => 'Вы действительно хотите удалить дорожку "%s"?', + 'Inactive swimlanes' => 'Неактивные дорожки', + 'Set project manager' => 'Установить менеджера проекта', + 'Set project member' => 'Установить участника проекта', + 'Remove a swimlane' => 'Удалить дорожку', + 'Rename' => 'Переименовать', + 'Show default swimlane' => 'Показать стандартную дорожку', + 'Swimlane modification for the project "%s"' => 'Редактирование дорожки для проекта "%s"', + 'Swimlane not found.' => 'Дорожка не найдена.', + 'Swimlane removed successfully.' => 'Дорожка успешно удалена', + 'Swimlanes' => 'Дорожки', + 'Swimlane updated successfully.' => 'Дорожка успешно обновлена.', + 'The default swimlane have been updated successfully.' => 'Стандартная swimlane был успешно обновлен.', + 'Unable to create your swimlane.' => 'Невозможно создать дорожку.', + 'Unable to remove this swimlane.' => 'Невозможно удалить дорожку.', + 'Unable to update this swimlane.' => 'Невозможно обновить дорожку.', + 'Your swimlane have been created successfully.' => 'Ваша дорожка была успешно создан.', + 'Example: "Bug, Feature Request, Improvement"' => 'Например: "Баг, Фича, Улучшение"', + 'Default categories for new projects (Comma-separated)' => 'Стандартные категории для нового проекта (разделяются запятыми)', + // 'Gitlab commit received' => '', + 'Gitlab issue opened' => 'Gitlab вопрос открыт', + 'Gitlab issue closed' => 'Gitlab вопрос закрыт', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Помощь по Gitlab webhooks', + 'Integrations' => 'Интеграции', + 'Integration with third-party services' => 'Интеграция со сторонними сервисами', + 'Role for this project' => 'Роли для этого проекта', + 'Project manager' => 'Менеджер проекта', + 'Project member' => 'Участник проекта', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Менеджер проекта может изменять настройки проекта и имеет больше привелегий чем стандартный пользователь.', + 'Gitlab Issue' => 'Gitlab вопросы', + 'Subtask Id' => 'Id подзадачи', + 'Subtasks' => 'Подзадачи', + 'Subtasks Export' => 'Экспортировать подзадачи', + 'Subtasks exportation for "%s"' => 'Экспорт подзадач для "%s"', + 'Task Title' => 'Загловок задачи', + 'Untitled' => 'Заголовок отсутствует', + 'Application default' => 'Приложение по умолчанию', + 'Language:' => 'Язык:', + 'Timezone:' => 'Временная зона:', + 'All columns' => 'Все колонки', + 'Calendar for "%s"' => 'Календарь для "%s"', + 'Filter by column' => 'Фильтр по колонке', + 'Filter by status' => 'Фильтр по статусу', + 'Calendar' => 'Календарь', + 'Next' => 'Следующий', + // '#%d' => '', + 'Filter by color' => 'Фильтрация по цвету', + 'Filter by swimlane' => 'Фильтрация по дорожкам', + 'All swimlanes' => 'Все дорожки', + 'All colors' => 'Все цвета', + 'All status' => 'Все статусы', + 'Add a comment logging moving the task between columns' => 'Добавлять комментарий при движении задач между колонками', + 'Moved to column %s' => 'Перемещена в колонку %s', + 'Change description' => 'Изменить описание', + 'User dashboard' => 'Пользователь панели мониторинга', + 'Allow only one subtask in progress at the same time for a user' => 'Разрешена только одна подзадача в разработке одновременно для одного пользователя', + 'Edit column "%s"' => 'Редактировать колонку "%s"', + 'Enable time tracking for subtasks' => 'Включить учет времени для подзадач', + 'Select the new status of the subtask: "%s"' => 'Выбрать новый статус для подзадачи: "%s"', + 'Subtask timesheet' => 'Табель времени подзадач', + 'There is nothing to show.' => 'Здесь ничего нет.', + 'Time Tracking' => 'Учет времени', + 'You already have one subtask in progress' => 'У вас уже есть одна задача в разработке', + 'Which parts of the project do you want to duplicate?' => 'Какие части проекта должны быть дублированы?', + 'Change dashboard view' => 'Изменить отображение панели мониторинга', + 'Show/hide activities' => 'Показать/скрыть активность', + 'Show/hide projects' => 'Показать/скрыть проекты', + 'Show/hide subtasks' => 'Показать/скрыть подзадачи', + 'Show/hide tasks' => 'Показать/скрыть задачи', + 'Disable login form' => 'Выключить форму авторизации', + 'Show/hide calendar' => 'Показать/скрыть календарь', + 'User calendar' => 'Пользовательский календарь', + // 'Bitbucket commit received' => '', + 'Bitbucket webhooks' => 'BitBucket webhooks', + 'Help on Bitbucket webhooks' => 'Помощь по BitBucket webhooks', + 'Start' => 'Начало', + 'End' => 'Конец', + 'Task age in days' => 'Возраст задачи в днях', + 'Days in this column' => 'Дней в этой колонке', + // '%dd' => '', + 'Add a link' => 'Добавить ссылку на другие задачи', + 'Add a new link' => 'Добавление новой ссылки', + 'Do you really want to remove this link: "%s"?' => 'Вы уверены что хотите удалить ссылку: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Вы уверены что хотите удалить ссылку вместе с задачей #%d?', + 'Field required' => 'Поле обязательно для заполнения', + 'Link added successfully.' => 'Ссылка успешно добавлена', + 'Link updated successfully.' => 'Ссылка успешно обновлена', + 'Link removed successfully.' => 'Ссылка успешно удалена', + 'Link labels' => 'Метки для ссылки', + 'Link modification' => 'Обновление ссылки', + 'Links' => 'Ссылки', + 'Link settings' => 'Настройки ссылки', + 'Opposite label' => 'Ярлык напротив', + 'Remove a link' => 'Удалить ссылку', + 'Task\'s links' => 'Ссылки задачи', + 'The labels must be different' => 'Ярлыки должны быть разными', + 'There is no link.' => 'Это не ссылка', + 'This label must be unique' => 'Этот ярлык должна быть уникальной ', + 'Unable to create your link.' => 'Не удается создать эту ссылку.', + 'Unable to update your link.' => 'Не удается обновить эту ссылку.', + 'Unable to remove this link.' => 'Не удается удалить эту ссылку.', + 'relates to' => 'связана с', + 'blocks' => 'блокирует', + 'is blocked by' => 'заблокирована в', + 'duplicates' => 'дублирует', + 'is duplicated by' => 'дублирована в', + 'is a child of' => 'наследник', + 'is a parent of' => 'родитель', + 'targets milestone' => 'часть этапа', + 'is a milestone of' => 'является частью этапа', + 'fixes' => 'исправляет', + 'is fixed by' => 'исправлено в', + 'This task' => 'Эта задача', + '<1h' => '<1ч', + // '%dh' => '', + // '%b %e' => '', + 'Expand tasks' => 'Развернуть задачи', + 'Collapse tasks' => 'Свернуть задачи', + 'Expand/collapse tasks' => 'Развернуть/свернуть задачи', + 'Close dialog box' => 'Закрыть диалог', + 'Submit a form' => 'Отправить форму', + 'Board view' => 'Просмотр доски', + 'Keyboard shortcuts' => 'Горячие клавиши', + 'Open board switcher' => 'Открыть переключатель доски', + 'Application' => 'Приложение', + 'Filter recently updated' => 'Сортировать по дате обновления', + // 'since %B %e, %Y at %k:%M %p' => '', + 'More filters' => 'Дополнительные фильтры', + 'Compact view' => 'Компактный вид', + 'Horizontal scrolling' => 'Широкий вид', + 'Compact/wide view' => 'Компактный/широкий вид', + 'No results match:' => 'Отсутствуют результаты:', + 'Remove hourly rate' => 'Удалить почасовую ставку', + 'Do you really want to remove this hourly rate?' => 'Вы действительно хотите удалить эту почасовую ставку?', + 'Hourly rates' => 'Почасовые ставки', + 'Hourly rate' => 'Почасовая ставка', + 'Currency' => 'Валюта', + 'Effective date' => 'Дата вступления в силу', + 'Add new rate' => 'Добавить новый показатель', + 'Rate removed successfully.' => 'Показатель успешно удален.', + 'Unable to remove this rate.' => 'Не удается удалить этот показатель.', + 'Unable to save the hourly rate.' => 'Не удается сохранить почасовую ставку.', + 'Hourly rate created successfully.' => 'Почасовая ставка успешно создана.', + 'Start time' => 'Время начала', + 'End time' => 'Время завершения', + 'Comment' => 'Комментарий', + 'All day' => 'Весь день', + 'Day' => 'День', + 'Manage timetable' => 'Управление графиками', + 'Overtime timetable' => 'График сверхурочных', + 'Time off timetable' => 'Время в графике', + 'Timetable' => 'График', + 'Work timetable' => 'Work timetable', + 'Week timetable' => 'График на неделю', + 'Day timetable' => 'График на день', + 'From' => 'От кого', + 'To' => 'Кому', + 'Time slot created successfully.' => 'Временной интервал успешно создан.', + 'Unable to save this time slot.' => 'Невозможно сохранить этот временной интервал.', + 'Time slot removed successfully.' => 'Временной интервал успешно удален.', + 'Unable to remove this time slot.' => 'Не удается удалить этот временной интервал.', + 'Do you really want to remove this time slot?' => 'Вы действительно хотите удалить этот период времени?', + 'Remove time slot' => 'Удалить новый интервал времени', + 'Add new time slot' => 'Добавить новый интервал времени', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + 'Files' => 'Файлы', + 'Images' => 'Изображения', + 'Private project' => 'Приватный проект', + 'Amount' => 'Количество', + 'AUD - Australian Dollar' => 'AUD - Австралийский доллар', + 'Budget' => 'Бюджет', + 'Budget line' => 'Статья бюджета', + 'Budget line removed successfully.' => 'Бюджетная статья успешно удалена.', + 'Budget lines' => 'Статьи бюджета', + 'CAD - Canadian Dollar' => 'CAD - Канадский доллар', + 'CHF - Swiss Francs' => 'CHF - Швейцарский Франк', + 'Cost' => 'Стоимость', + 'Cost breakdown' => 'Детализация затрат', + 'Custom Stylesheet' => 'Пользовательский стиль', + 'download' => 'загрузить', + 'Do you really want to remove this budget line?' => 'Вы действительно хотите удалить эту статью бюджета?', + 'EUR - Euro' => 'EUR - Евро', + 'Expenses' => 'Расходы', + 'GBP - British Pound' => 'GBP - Британский фунт', + 'INR - Indian Rupee' => 'INR - Индийский рупий', + 'JPY - Japanese Yen' => 'JPY - Японскай йена', + 'New budget line' => 'Новая статья бюджета', + 'NZD - New Zealand Dollar' => 'NZD - Новозеландский доллар', + 'Remove a budget line' => 'Удалить строку в бюджете', + 'Remove budget line' => 'Удалить статью бюджета', + 'RSD - Serbian dinar' => 'RSD - Сербский динар', + 'The budget line have been created successfully.' => 'Статья бюджета успешно создана.', + 'Unable to create the budget line.' => 'Не удается создать эту статью бюджета.', + 'Unable to remove this budget line.' => 'Не удается удалить эту статью бюджета.', + 'USD - US Dollar' => 'USD - доллар США', + 'Remaining' => 'Прочее', + 'Destination column' => 'Колонка назначения', + 'Move the task to another column when assigned to a user' => 'Переместить задачу в другую колонку, когда она назначена пользователю', + 'Move the task to another column when assignee is cleared' => 'Переместить задачу в другую колонку, когда назначение снято ', + 'Source column' => 'Исходная колонка', + 'Show subtask estimates (forecast of future work)' => 'Показать оценку подзадач (прогноз будущей работы)', + 'Transitions' => 'Перемещения', + 'Executer' => 'Исполнитель', + 'Time spent in the column' => 'Время проведенное в колонке', + 'Task transitions' => 'Перемещения задач', + 'Task transitions export' => 'Экспорт перемещений задач', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Этот отчет содержит все перемещения задач в колонках с датой, пользователем и времени, затраченным для каждого перемещения.', + 'Currency rates' => 'Курсы валют', + 'Rate' => 'Курс', + 'Change reference currency' => 'Изменить справочник валют', + 'Add a new currency rate' => 'Add a new currency rate', + 'Currency rates are used to calculate project budget.' => 'Курсы валют используются для расчета бюджета проекта.', + 'Reference currency' => 'Справочник валют', + 'The currency rate have been added successfully.' => 'Курс валюты был успешно добавлен.', + 'Unable to add this currency rate.' => 'Невозможно добавить этот курс валюты.', + 'Send notifications to a Slack channel' => 'Отправлять уведомления в канал Slack', + 'Webhook URL' => 'Webhook URL', + 'Help on Slack integration' => 'Помощь по интеграции Slack', + '%s remove the assignee of the task %s' => '%s удалить назначенную задачу %s', + 'Send notifications to Hipchat' => 'Отправлять уведомления в Hipchat', + 'API URL' => 'API URL', + 'Room API ID or name' => 'API ID комнаты или имя', + 'Room notification token' => 'Ключь комнаты для уведомлений', + 'Help on Hipchat integration' => 'Помощь по интеграции Hipchat', + 'Enable Gravatar images' => 'Включить Gravatar изображения', + 'Information' => 'Информация', + 'Check two factor authentication code' => 'Проверка кода двухфакторной авторизации', + 'The two factor authentication code is not valid.' => 'Код двухфакторной авторизации не валиден', + 'The two factor authentication code is valid.' => 'Код двухфакторной авторизации валиден', + 'Code' => 'Код', + 'Two factor authentication' => 'Двухфакторная авторизация', + 'Enable/disable two factor authentication' => 'Включить/выключить двухфакторную авторизацию', + 'This QR code contains the key URI: ' => 'Это QR-код содержит ключевую URI:', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Сохраните Ваш секретный ключ в TOTP программе (например Google Autentificator или FreeOTP).', + 'Check my code' => 'Проверить мой код', + 'Secret key: ' => 'Секретный ключ: ', + 'Test your device' => 'Проверьте свое устройство', + 'Assign a color when the task is moved to a specific column' => 'Назначить цвет, когда задача перемещается в определенную колонку', + '%s via Kanboard' => '%s через Канборд', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + 'size: %s' => 'размер: %s', + 'Burndown chart for "%s"' => 'Диаграмма сгорания для', + 'Burndown chart' => 'Диаграмма сгорания', + 'This chart show the task complexity over the time (Work Remaining).' => 'Эта диаграмма показывают сложность задачи по времени (оставшейся работы).', + 'Screenshot taken %s' => 'Принято скриншотов', + 'Add a screenshot' => 'Прикрепить картинку', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Сделайте скриншот и нажмите CTRL+V или ⌘+V для вложения', + 'Screenshot uploaded successfully.' => 'Скриншет успешно загружен', + 'SEK - Swedish Krona' => 'SEK - Шведская крона', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Идентификатор проекта - это опциональный буквенно-цифровой код использующийся для идентификации проекта', + 'Identifier' => 'Идентификатор', + 'Postmark (incoming emails)' => 'Postmark (входящие сообщения)', + 'Help on Postmark integration' => 'Справка о Postmark интеграции', + 'Mailgun (incoming emails)' => 'Mailgun (входящие сообщения)', + 'Help on Mailgun integration' => 'Справка о Mailgun интеграции', + 'Sendgrid (incoming emails)' => 'Sendgrid (входящие сообщения)', + 'Help on Sendgrid integration' => 'Справка о Sendgrid интеграции', + 'Disable two factor authentication' => 'Выключить двухфакторную авторизацию', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Вы действительно хотите выключить двухфакторную авторизацию для пользователя "%s"?', + 'Edit link' => 'Редактировать ссылку', + 'Start to type task title...' => 'Начните вводить название задачи...', + 'A task cannot be linked to itself' => 'Задача не может быть связана с собой же', + 'The exact same link already exists' => 'Такая ссылка уже существует', + 'Recurrent task is scheduled to be generated' => 'Периодическая задача запланирована к созданию', + 'Recurring information' => 'Информация о периодичности', + 'Score' => 'Оценка', + 'The identifier must be unique' => 'Идентификатор должен быть уникальным', + 'This linked task id doesn\'t exists' => 'Этот ID звязанной задачи не существует', + 'This value must be alphanumeric' => 'Это значение должно быть буквенно-цифровым', + 'Edit recurrence' => 'Завершить повторение', + 'Generate recurrent task' => 'Создать повторяющуюся задачу', + 'Trigger to generate recurrent task' => 'Триггер для генерации периодической задачи', + 'Factor to calculate new due date' => 'Коэффициент для рассчета новой даты', + 'Timeframe to calculate new due date' => 'Вычисление для рассчета новой даты', + 'Base date to calculate new due date' => 'Базовая дата вычисления новой даты', + 'Action date' => 'Дата действия', + 'Base date to calculate new due date: ' => 'Базовая дата вычисления новой даты: ', + 'This task has created this child task: ' => 'Эта задача создала эту дочернюю задачу:', + 'Day(s)' => 'День(й)', + 'Existing due date' => 'Существующий срок', + 'Factor to calculate new due date: ' => 'Коэффициент для рассчета новой даты: ', + 'Month(s)' => 'Месяц(а)', + 'Recurrence' => 'Повторение', + 'This task has been created by: ' => 'Эта задача была создана: ', + 'Recurrent task has been generated:' => 'Периодическая задача была сформирована:', + 'Timeframe to calculate new due date: ' => 'Вычисление для рассчета новой даты: ', + 'Trigger to generate recurrent task: ' => 'Триггер для генерации периодической задачи: ', + 'When task is closed' => 'Когда задача закрывается', + 'When task is moved from first column' => 'Когда задача перемещается из первой колонки', + 'When task is moved to last column' => 'Когда задача перемещается в последнюю колонку', + 'Year(s)' => 'Год(а)', + 'Jabber (XMPP)' => 'Jabber (XMPP)', + 'Send notifications to Jabber' => 'Отправлять уведомления в Jabber', + 'XMPP server address' => 'Адрес Jabber сервера', + 'Jabber domain' => 'Домен Jabber', + 'Jabber nickname' => 'Имя пользователя Jabber', + 'Multi-user chat room' => 'Многопользовательский чат', + 'Help on Jabber integration' => 'Помощь по интеграции Jabber', + 'The server address must use this format: "tcp://hostname:5222"' => 'Адрес сервера должен быть в формате: tcp://hostname:5222', + 'Calendar settings' => 'Настройки календаря', + 'Project calendar view' => 'Вид календаря проекта', + 'Project settings' => 'Настройки проекта', + 'Show subtasks based on the time tracking' => 'Показать подзадачи, основанные на отслеживании времени', + 'Show tasks based on the creation date' => 'Показать задачи в зависимости от даты создания', + 'Show tasks based on the start date' => 'Показать задачи в зависимости от даты начала', + 'Subtasks time tracking' => 'Отслеживание времени подзадач', + 'User calendar view' => 'Просмотреть календарь пользователя', + 'Automatically update the start date' => 'Автоматическое обновление даты начала', + 'iCal feed' => 'iCal данные', + 'Preferences' => 'Предпочтения', + 'Security' => 'Безопастность', + 'Two factor authentication disabled' => 'Двухфакторная аутентификация отключена', + 'Two factor authentication enabled' => 'Включена двухфакторная аутентификация', + 'Unable to update this user.' => 'Не удается обновить этого пользователя.', + 'There is no user management for private projects.' => 'Там нет управления пользователя для частных проектов', +); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php new file mode 100644 index 00000000..fea7f10e --- /dev/null +++ b/app/Locale/sr_Latn_RS/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => 'None', + 'edit' => 'izmeni', + 'Edit' => 'Izmeni', + 'remove' => 'ukloni', + 'Remove' => 'Ukloni', + 'Update' => 'Ažuriraj', + 'Yes' => 'Da', + 'No' => 'Ne', + 'cancel' => 'odustani', + 'or' => 'ili', + 'Yellow' => 'Žuta', + 'Blue' => 'Plava', + 'Green' => 'Zelena', + 'Purple' => 'Ljubičasta', + 'Red' => 'Crvena', + 'Orange' => 'Narandžasta', + 'Grey' => 'Siva', + 'Save' => 'Snimi', + 'Login' => 'Prijava', + 'Official website:' => 'Zvanična strana:', + 'Unassigned' => 'Nedodeljen', + 'View this task' => 'Pregledaj zadatak', + 'Remove user' => 'Ukloni korisnika', + 'Do you really want to remove this user: "%s"?' => 'Da li zaista želiš da ukloniš korisnika: "%s"?', + 'New user' => 'novi korisnik', + 'All users' => 'Svi korisnici', + 'Username' => 'Korisnik', + 'Password' => 'Lozinka', + 'Default project' => 'Podrazumevani projekat', + 'Administrator' => 'Administrator', + 'Sign in' => 'Odjava', + 'Users' => 'Korisnik', + 'No user' => 'Ne', + 'Forbidden' => 'Zabranjeno', + 'Access Forbidden' => 'Zabranjen prostup', + 'Only administrators can access to this page.' => 'Samo administrator može videti ovu stranu.', + 'Edit user' => 'Izmeni korisnika', + 'Logout' => 'Odjava', + 'Bad username or password' => 'Loše korisničko ime ili lozinka', + 'users' => 'korisnici', + 'projects' => 'projekti', + 'Edit project' => 'Izmeni projekat', + 'Name' => 'Ime', + 'Activated' => 'Aktiviran', + 'Projects' => 'Projekti', + 'No project' => 'Bez projekta', + 'Project' => 'Projekat', + 'Status' => 'Status', + 'Tasks' => 'Zadatak', + 'Board' => 'Tabla', + 'Actions' => 'Akcje', + 'Inactive' => 'Neaktivan', + 'Active' => 'Aktivan', + 'Column %d' => 'Kolona %d', + 'Add this column' => 'Dodaj kolonu', + '%d tasks on the board' => '%d zadataka na tabli', + '%d tasks in total' => '%d zadataka ukupno', + 'Unable to update this board.' => 'Nemogu da ažuriram ovu tablu.', + 'Edit board' => 'Izmeni tablu', + 'Disable' => 'Onemogući', + 'Enable' => 'Omogući', + 'New project' => 'Novi projekat', + 'Do you really want to remove this project: "%s"?' => 'Da li želiš da ukloniš projekat: "%s"?', + 'Remove project' => 'Ukloni projekat', + 'Boards' => 'Table', + 'Edit the board for "%s"' => 'Izmeni tablu za "%s"', + 'All projects' => 'Svi projekti', + 'Change columns' => 'Zameni kolonu', + 'Add a new column' => 'Dodaj novu kolonu', + 'Title' => 'Naslov', + 'Add Column' => 'Dodaj kolunu', + 'Project "%s"' => 'Projekt "%s"', + 'Nobody assigned' => 'Niko nije dodeljen', + 'Assigned to %s' => 'Dodeljen korisniku %s', + 'Remove a column' => 'Ukloni kolonu', + 'Remove a column from a board' => 'Ukloni kolonu sa table', + 'Unable to remove this column.' => 'Nemoguće uklanjanje kolone.', + 'Do you really want to remove this column: "%s"?' => 'Da li zaista želiš da ukoniš ovu kolonu: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Ova akcija BRIŠE SVE ZADATKE vezane za ovu kolonu!', + 'Settings' => 'Podešavanja', + 'Application settings' => 'Podešavanja aplikacije', + 'Language' => 'Jezik', + 'Webhook token:' => 'Token :', + 'API token:' => 'Token za API', + 'More information' => 'Još informacja', + 'Database size:' => 'Veličina baze :', + 'Download the database' => 'Preuzmi bazu', + 'Optimize the database' => 'Optimizuj bazu', + '(VACUUM command)' => '(komanda VACUUM)', + '(Gzip compressed Sqlite file)' => '(Sqlite baza spakovana Gzip-om)', + 'User settings' => 'Korisnička podešavanja', + 'My default project:' => 'Moj podrazumevani projekat:', + 'Close a task' => 'Zatvori zadatak', + 'Do you really want to close this task: "%s"?' => 'Da li zaista želiš da zatvoriš ovaj zadatak: "%s"?', + 'Edit a task' => 'Izmeni zadatak', + 'Column' => 'Kolona', + 'Color' => 'Boja', + 'Assignee' => 'Dodeli', + 'Create another task' => 'Dodaj zadatak', + 'New task' => 'Novi zadatak', + 'Open a task' => 'Otvori zadatak', + 'Do you really want to open this task: "%s"?' => 'Da li zaista želiš da otvoriš zadatak: "%s"?', + 'Back to the board' => 'Nazad na tablu', + 'Created on %B %e, %Y at %k:%M %p' => 'Kreiran %e %B %Y o %k:%M', + 'There is nobody assigned' => 'Niko nije dodeljen!', + 'Column on the board:' => 'Kolona na tabli:', + 'Status is open' => 'Status otvoren', + 'Status is closed' => 'Status zatvoren', + 'Close this task' => 'Zatvori ovaj zadatak', + 'Open this task' => 'Otvori ovaj zadatak', + 'There is no description.' => 'Bez opisa.', + 'Add a new task' => 'Dodaj zadatak', + 'The username is required' => 'Korisničko ime je obavezno', + 'The maximum length is %d characters' => 'Maksimalna dužina je %d znakova', + 'The minimum length is %d characters' => 'Minimalna dužina je %d znakova', + 'The password is required' => 'Lozinka je obavezna', + 'This value must be an integer' => 'Mora biti ceo broj', + 'The username must be unique' => 'Korisničko ime mora biti jedinstveno', + 'The username must be alphanumeric' => 'Korisničko ime sme sadržati samo brojeve i slova', + 'The user id is required' => 'ID korisnika je obavezan', + 'Passwords don\'t match' => 'Lozinke se ne podudaraju', + 'The confirmation is required' => 'Potvrda je obavezna', + 'The column is required' => 'Kolona je obavezna', + 'The project is required' => 'Projekat je obavezan', + 'The color is required' => 'Boja je obavezna', + 'The id is required' => 'ID je obavezan', + 'The project id is required' => 'ID projekta je obavezan', + 'The project name is required' => 'Naziv projekta je obavezan', + 'This project must be unique' => 'Projekat mora biti jedinstven', + 'The title is required' => 'Naslov je obavezan', + 'The language is required' => 'Jezik je obavezan', + 'There is no active project, the first step is to create a new project.' => 'Nema aktivnih projekata. Potrebno je prvo napraviti novi projekat.', + 'Settings saved successfully.' => 'Podešavanja uspešno snimljena.', + 'Unable to save your settings.' => 'Nemoguće snimanje podešavanja.', + 'Database optimization done.' => 'Optimizacija baze je završena.', + 'Your project have been created successfully.' => 'Projekat je uspešno napravljen.', + 'Unable to create your project.' => 'Nemoguće kreiranje projekta.', + 'Project updated successfully.' => 'Projekt je uspešno ažuriran.', + 'Unable to update this project.' => 'Nemoguće ažuriranje projekta.', + 'Unable to remove this project.' => 'Nemoguće uklanjanje projekta.', + 'Project removed successfully.' => 'Projekat uspešno uklonjen.', + 'Project activated successfully.' => 'Projekt uspešno aktiviran.', + 'Unable to activate this project.' => 'Nemoguće aktiviranje projekta.', + 'Project disabled successfully.' => 'Projekat uspešno deaktiviran.', + 'Unable to disable this project.' => 'nemoguće deaktiviranje projekta.', + 'Unable to open this task.' => 'Nemoguće otvaranje zadatka.', + 'Task opened successfully.' => 'Zadatak uspešno otvoren.', + 'Unable to close this task.' => 'Nije moguće zatvaranje ovog zadatka.', + 'Task closed successfully.' => 'Zadatak uspešno zatvoren.', + 'Unable to update your task.' => 'Nije moguće ažuriranje zadatka.', + 'Task updated successfully.' => 'Zadatak uspešno ažuriran.', + 'Unable to create your task.' => 'Nije moguće kreiranje zadatka.', + 'Task created successfully.' => 'Zadatak uspešno kreiran.', + 'User created successfully.' => 'Korisnik uspešno kreiran', + 'Unable to create your user.' => 'Nije uspelo kreiranje korisnika.', + 'User updated successfully.' => 'Korisnik uspešno ažuriran.', + 'Unable to update your user.' => 'Nije moguće ažuriranje korisnika.', + 'User removed successfully.' => 'Korisnik uspešno uklonjen.', + 'Unable to remove this user.' => 'Nije moguće uklanjanje korisnika.', + 'Board updated successfully.' => 'Tabla uspešno ažurirana.', + 'Ready' => 'Spreman', + 'Backlog' => 'Log', + 'Work in progress' => 'U radu', + 'Done' => 'Gotovo', + 'Application version:' => 'Verzija aplikacije:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Završeno u %e %B %Y o %k:%M', + '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', + 'Date created' => 'Kreiran dana', + 'Date completed' => 'Završen dana', + 'Id' => 'Id', + 'No task' => 'bez zadataka', + 'Completed tasks' => 'Zatvoreni zadaci', + 'List of projects' => 'Spisak projekata', + 'Completed tasks for "%s"' => 'zatvoreni zadaci za "%s"', + '%d closed tasks' => '%d zatvorenih zadataka', + 'No task for this project' => 'Nema dodeljenih zadataka ovom projektu', + 'Public link' => 'Javni link', + 'There is no column in your project!' => 'Nema dodeljenih kolona ovom projektu', + 'Change assignee' => 'Izmeni dodelu', + 'Change assignee for the task "%s"' => 'Izmeni dodelu za ovaj zadatak "%s"', + 'Timezone' => 'Vremenska zona', + 'Sorry, I didn\'t find this information in my database!' => 'Na žalost, nije pronađena informacija u bazi', + 'Page not found' => 'Strana nije pronađena', + 'Complexity' => 'Složenost', + 'limit' => 'ograničenje', + 'Task limit' => 'Ograničenje zadatka', + 'Task count' => 'Broj zadataka', + 'This value must be greater than %d' => 'Vrednost mora biti veća od %d', + 'Edit project access list' => 'Izmeni prava pristupa projektu', + 'Edit users access' => 'Izmeni korisnička prava', + 'Allow this user' => 'Dozvoli ovog korisnika', + 'Only those users have access to this project:' => 'Samo ovi korisnici imaju pristup projektu:', + 'Don\'t forget that administrators have access to everything.' => 'Zapamti: Administrator može pristupiti svemu!', + 'Revoke' => 'Povuci', + 'List of authorized users' => 'Spisak odobrenih korisnika', + 'User' => 'Korisnik', + 'Nobody have access to this project.' => 'Niko nema pristup ovom projektu', + 'You are not allowed to access to this project.' => 'Nije ti dozvoljen pristup ovom projektu.', + 'Comments' => 'Komentari', + 'Post comment' => 'Dodaj komentar', + 'Write your text in Markdown' => 'Pisanje teksta pomoću Markdown', + 'Leave a comment' => 'Ostavi komentar', + 'Comment is required' => 'Komentar je obavezan', + 'Leave a description' => 'Dodaj opis', + 'Comment added successfully.' => 'Komentar uspešno ostavljen', + 'Unable to create your comment.' => 'Nemoguće kreiranje komentara', + 'The description is required' => 'Opis je obavezan', + 'Edit this task' => 'Izmeni ovaj zadatak', + 'Due Date' => 'Termin', + 'Invalid date' => 'Loš datum', + 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y', + '%B %e, %Y' => '%e %B %Y', + // '%b %e, %Y' => '', + 'Automatic actions' => 'Automatske akcije', + 'Your automatic action have been created successfully.' => 'Uspešno kreirana automatska akcija', + 'Unable to create your automatic action.' => 'Nemoguće kreiranje automatske akcije', + 'Remove an action' => 'Obriši akciju', + 'Unable to remove this action.' => 'Nije moguće obrisati akciju', + 'Action removed successfully.' => 'Akcija obrisana', + 'Automatic actions for the project "%s"' => 'Akcje za automatizaciju projekta "%s"', + 'Defined actions' => 'Definisane akcje', + 'Add an action' => 'dodaj akcju', + 'Event name' => 'Naziv događaja', + 'Action name' => 'Naziv akcije', + 'Action parameters' => 'Parametri akcije', + 'Action' => 'Akcija', + 'Event' => 'Događaj', + 'When the selected event occurs execute the corresponding action.' => 'Kad se događaj desi izvrši odgovarajuću akciju', + 'Next step' => 'Sledeći korak', + 'Define action parameters' => 'Definiši parametre akcije', + 'Save this action' => 'Snimi akciju', + 'Do you really want to remove this action: "%s"?' => 'Da li da obrišem akciju "%s"?', + 'Remove an automatic action' => 'Obriši automatsku akciju', + 'Close the task' => 'Zatvori zadatak', + 'Assign the task to a specific user' => 'Dodeli zadatak određenom korisniku', + 'Assign the task to the person who does the action' => 'Dodeli zadatak korisniku koji je izvršio akciju', + 'Duplicate the task to another project' => 'Kopiraj akciju u drugi projekat', + 'Move a task to another column' => 'Premesti zadatak u drugu kolonu', + 'Move a task to another position in the same column' => 'Promeni poziciju zadatka u istoj koloni', + 'Task modification' => 'Izman zadatka', + 'Task creation' => 'Kreiranje zadatka', + 'Open a closed task' => 'Otvori zatvoreni zadatak', + 'Closing a task' => 'Zatvaranja zadatka', + 'Assign a color to a specific user' => 'Dodeli boju korisniku', + 'Column title' => 'Naslov kolone', + 'Position' => 'Pozicija', + 'Move Up' => 'Podigni', + 'Move Down' => 'Spusti', + 'Duplicate to another project' => 'Kopiraj u drugi projekat', + 'Duplicate' => 'Napravi kopiju', + 'link' => 'link', + 'Update this comment' => 'Ažuriraj komentar', + 'Comment updated successfully.' => 'Komentar uspešno ažuriran.', + 'Unable to update your comment.' => 'Neuspešno ažuriranje komentara.', + 'Remove a comment' => 'Obriši komentar', + 'Comment removed successfully.' => 'Komentar je uspešno obrisan.', + 'Unable to remove this comment.' => 'Neuspešno brisanje komentara.', + 'Do you really want to remove this comment?' => 'Da li da obrišem ovaj komentar?', + 'Only administrators or the creator of the comment can access to this page.' => 'Samo administrator i kreator komentara mogu ga obrisati.', + 'Details' => 'Detalji', + 'Current password for the user "%s"' => 'Trenutna lozinka za korisnika "%s"', + 'The current password is required' => 'Trenutna lozinka je obavezna', + 'Wrong password' => 'Pogrešna lozinka', + 'Reset all tokens' => 'Resetuj tokene', + 'All tokens have been regenerated.' => 'Svi tokeni su ponovo generisani.', + 'Unknown' => 'Nepoznat', + 'Last logins' => 'Poslednja prijava', + 'Login date' => 'Datum prijave', + 'Authentication method' => 'Metod autentikacije', + 'IP address' => 'IP adresa', + 'User agent' => 'Browser', + 'Persistent connections' => 'Stalna konekcija', + 'No session.' => 'Bez sesjie', + 'Expiration date' => 'Ističe', + 'Remember Me' => 'Zapamti me', + 'Creation date' => 'Datum kreiranja', + 'Filter by user' => 'Po korisniku', + 'Filter by due date' => 'Po terminu', + 'Everybody' => 'Svi', + 'Open' => 'Otvoreni', + 'Closed' => 'Zatvoreni', + 'Search' => 'Traži', + 'Nothing found.' => 'Ništa nije pronađeno', + 'Search in the project "%s"' => 'Traži u prijektu "%s"', + 'Due date' => 'Termin', + 'Others formats accepted: %s and %s' => 'Ostali formati: %s i %s', + 'Description' => 'Opis', + '%d comments' => '%d Komentara', + '%d comment' => '%d Komentar', + 'Email address invalid' => 'Pogrešan e-mail', + 'Your Google Account is not linked anymore to your profile.' => 'Tvoj google nalog više nije povezan sa profilom', + 'Unable to unlink your Google Account.' => 'Neuspešno ukidanje veze od Google naloga', + 'Google authentication failed' => 'Neuspešna Google autentikacija', + 'Unable to link your Google Account.' => 'Neuspešno povezivanje sa Google nalogom', + 'Your Google Account is linked to your profile successfully.' => 'Vaš Google nalog je uspešno povezan sa vašim profilom', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Poveži sa Google nalogom', + 'Unlink my Google Account' => 'Ukini vezu sa Google nalogom', + 'Login with my Google Account' => 'Prijavi se preko Google naloga', + 'Project not found.' => 'Projekat nije pronađen.', + 'Task #%d' => 'Zadatak #%d', + 'Task removed successfully.' => 'Zadatak uspešno uklonjen.', + 'Unable to remove this task.' => 'Nemoguće uklanjanje zadatka.', + 'Remove a task' => 'Ukloni zadatak', + 'Do you really want to remove this task: "%s"?' => 'Da li da obrišem zadatak "%s"?', + 'Assign automatically a color based on a category' => 'Automatski dodeli boju po kategoriji', + 'Assign automatically a category based on a color' => 'Automatski dodeli kategoriju po boji', + 'Task creation or modification' => 'Kreiranje ili izmena zadatka', + 'Category' => 'Kategorija', + 'Category:' => 'Kategorija:', + 'Categories' => 'Kategorije', + 'Category not found.' => 'Kategorija nije pronađena', + 'Your category have been created successfully.' => 'Uspešno kreirana kategorija.', + 'Unable to create your category.' => 'Nije moguće kreirati kategoriju.', + 'Your category have been updated successfully.' => 'Kategorija je uspešno izmenjena', + 'Unable to update your category.' => 'Nemoguće izmeniti kategoriju', + 'Remove a category' => 'Obriši kategoriju', + 'Category removed successfully.' => 'Kategorija uspešno uklonjena.', + 'Unable to remove this category.' => 'Nije moguće ukloniti kategoriju.', + 'Category modification for the project "%s"' => 'Izmena kategorije za projekat "%s"', + 'Category Name' => 'Naziv kategorije', + 'Categories for the project "%s"' => 'Kategorije u projektu', + 'Add a new category' => 'Dodaj novu kategoriju', + 'Do you really want to remove this category: "%s"?' => 'Da li zaista želiš da ukloniš kategoriju: "%s"?', + 'Filter by category' => 'Po kategoriji', + 'All categories' => 'Sve kategorije', + 'No category' => 'Bez kategorije', + 'The name is required' => 'Naziv je obavezan', + 'Remove a file' => 'Ukloni fajl', + 'Unable to remove this file.' => 'Fajl nije moguće ukloniti.', + 'File removed successfully.' => 'Uspešno uklonjen fajl.', + 'Attach a document' => 'Prikači dokument', + 'Do you really want to remove this file: "%s"?' => 'Da li da uklonim fajl: "%s"?', + 'open' => 'otvori', + 'Attachments' => 'Prilozi', + 'Edit the task' => 'Izmena Zadatka', + 'Edit the description' => 'Izmena opisa', + 'Add a comment' => 'Dodaj komentar', + 'Edit a comment' => 'Izmeni komentar', + 'Summary' => 'Pregled', + 'Time tracking' => 'Praćenje vremena', + 'Estimate:' => 'Procena:', + 'Spent:' => 'Potrošeno:', + 'Do you really want to remove this sub-task?' => 'Da li da uklonim pod-zdadatak?', + 'Remaining:' => 'Preostalo:', + 'hours' => 'sati', + 'spent' => 'potrošeno', + 'estimated' => 'procenjeno', + 'Sub-Tasks' => 'Pod-zadaci', + 'Add a sub-task' => 'Dodaj pod-zadatak', + 'Original estimate' => 'Originalna procena', + 'Create another sub-task' => 'Dodaj novi pod-zadatak', + 'Time spent' => 'Utrošeno vreme', + 'Edit a sub-task' => 'Izmeni pod-zadatak', + 'Remove a sub-task' => 'Ukloni pod-zadatak', + 'The time must be a numeric value' => 'Vreme mora biti broj', + 'Todo' => 'Za rad', + 'In progress' => 'U radu', + 'Sub-task removed successfully.' => 'Pod-zadatak uspešno uklonjen.', + 'Unable to remove this sub-task.' => 'Nie można usunąć tego pod-zadania.', + 'Sub-task updated successfully.' => 'Pod-zadatak zaktualizowane pomyślnie.', + 'Unable to update your sub-task.' => 'Nie można zaktalizować tego pod-zadania.', + 'Unable to create your sub-task.' => 'Nie można utworzyć tego pod-zadania.', + 'Sub-task added successfully.' => 'Pod-zadatak utworzone pomyślnie', + 'Maximum size: ' => 'Maksimalna veličina: ', + 'Unable to upload the file.' => 'Nije moguće snimiti fajl.', + 'Display another project' => 'Prikaži drugi projekat', + 'Your GitHub account was successfully linked to your profile.' => 'Konto Github podłączone pomyślnie.', + 'Unable to link your GitHub Account.' => 'Nie można połączyć z kontem Github.', + 'GitHub authentication failed' => 'Autentykacja Github nieudana', + 'Your GitHub account is no longer linked to your profile.' => 'Konto Github nie jest już podłączone do twojego profilu.', + 'Unable to unlink your GitHub Account.' => 'Nie można odłączyć konta Github.', + 'Login with my GitHub Account' => 'Zaloguj przy użyciu konta Github', + 'Link my GitHub Account' => 'Podłącz konto Github', + 'Unlink my GitHub Account' => 'Odłącz konto Github', + 'Created by %s' => 'Kreirao %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Poslednja izmena %e %B %Y o %k:%M', + 'Tasks Export' => 'Izvoz zadataka', + 'Tasks exportation for "%s"' => 'Izvoz zadataka za "%s"', + 'Start Date' => 'Početni datum', + 'End Date' => 'Krajni datum', + 'Execute' => 'Izvrši', + 'Task Id' => 'Identifikator Zadatka', + 'Creator' => 'Autor', + 'Modification date' => 'Datum izmene', + 'Completion date' => 'Datum kompletiranja', + 'Clone' => 'Iskopiraj', + 'Clone Project' => 'Iskopiraj projekat', + 'Project cloned successfully.' => 'Projekat uspešno iskopiran.', + 'Unable to clone this project.' => 'Nije moguće iskopirati projekat.', + 'Email notifications' => 'Obaveštenje e-mailom', + 'Enable email notifications' => 'Omogući obaveštenja e-mailom', + 'Task position:' => 'Pozicija zadatka:', + 'The task #%d have been opened.' => 'Zadatak #%d je otvoren.', + 'The task #%d have been closed.' => 'Zadatak #$d je zatvoren.', + 'Sub-task updated' => 'Pod-zadatak izmenjen', + 'Title:' => 'Naslov:', + // 'Status:' => '', + 'Assignee:' => 'Dodeli:', + 'Time tracking:' => 'Praćenje vremena: ', + 'New sub-task' => 'Novi Pod-zadatak', + 'New attachment added "%s"' => 'Novi prilog ubačen "%s"', + 'Comment updated' => 'Komentar izmenjen', + 'New comment posted by %s' => 'Novi komentar ostavio %s', + 'List of due tasks for the project "%s"' => 'Spisak dospelih zadataka za projekat "%s"', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + 'Task closed' => 'Zadatak je zatvoren', + 'Task opened' => 'Zadatak je otvoren', + '[%s][Due tasks]' => '[%s][Dospeli zadaci]', + '[Kanboard] Notification' => '[Kanboard] Obaveštenja', + 'I want to receive notifications only for those projects:' => 'Želim obaveštenja samo za ovaj projekat:', + 'view the task on Kanboard' => 'Pregledaj zadatke', + 'Public access' => 'Javni pristup', + 'Category management' => 'Uređivanje kategorija', + 'User management' => 'Uređivanje korisnika', + 'Active tasks' => 'Aktivni zadaci', + 'Disable public access' => 'Zabrani javni pristup', + 'Enable public access' => 'Dozvoli javni pristup', + 'Active projects' => 'Aktivni projekti', + 'Inactive projects' => 'Neaktivni projekti', + 'Public access disabled' => 'Javni pristup onemogućen!', + 'Do you really want to disable this project: "%s"?' => 'Da li zaista želiš da deaktiviraš projekat: "%s"?', + 'Do you really want to duplicate this project: "%s"?' => 'Da li da napravim kopiju ovog projekta: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Da li zaista želiš da aktiviraš projekat: "%s"?', + 'Project activation' => 'Aktivacija projekta', + 'Move the task to another project' => 'Premesti zadatak u drugi projekat', + 'Move to another project' => 'Premesti u drugi projekat', + 'Do you really want to duplicate this task?' => 'Da li da napravim kopiju ovog projekta: "%s"?', + 'Duplicate a task' => 'Kopiraj zadatak', + 'External accounts' => 'Spoljni nalozi', + 'Account type' => 'Tip naloga', + 'Local' => 'Lokalno', + 'Remote' => 'Udaljno', + 'Enabled' => 'Omogući', + 'Disabled' => 'Onemogući', + 'Google account linked' => 'Połączone konto Google', + 'Github account linked' => 'Połączone konto Github', + 'Username:' => 'Korisničko ime:', + 'Name:' => 'Ime i Prezime', + 'Email:' => 'Email: ', + 'Default project:' => 'Osnovni projekat:', + 'Notifications:' => 'Obaveštenja: ', + 'Notifications' => 'Obaveštenja', + 'Group:' => 'Grupa:', + 'Regular user' => 'Standardni korisnik', + 'Account type:' => 'Vrsta naloga:', + 'Edit profile' => 'Izmeni profil', + 'Change password' => 'Izmeni lozinku', + 'Password modification' => 'Izmena lozinke', + 'External authentications' => 'Spoljne akcije', + 'Google Account' => 'Google nalog', + 'Github Account' => 'Github nalog', + 'Never connected.' => 'Bez konekcija.', + 'No account linked.' => 'Bez povezanih naloga.', + 'Account linked.' => 'Nalog povezan.', + 'No external authentication enabled.' => 'Bez omogućenih spoljnih autentikacija.', + 'Password modified successfully.' => 'Uspešna izmena lozinke.', + 'Unable to change the password.' => 'Nije moguće izmeniti lozinku.', + 'Change category for the task "%s"' => 'Izmeni kategoriju zadatka "%s"', + 'Change category' => 'Izmeni kategoriju', + '%s updated the task %s' => '%s izmeni zadatak %s', + '%s opened the task %s' => '%s aktivni zadaci %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s premešten zadatak %s na poziciju #%d u koloni "%s"', + '%s moved the task %s to the column "%s"' => '%s premešten zadatak %s u kolonu "%s"', + '%s created the task %s' => '%s kreirao zadatak %s', + '%s closed the task %s' => '%s zatvorio zadatak %s', + '%s created a subtask for the task %s' => '%s kreiran pod-zadatak zadatka %s', + '%s updated a subtask for the task %s' => '%s izmenjen pod-zadatak zadatka %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Dodeljen korisniku %s uz procenu vremena %s/%sh', + 'Not assigned, estimate of %sh' => 'Ne dodeljen, procenjeno vreme %sh', + '%s updated a comment on the task %s' => '%s izmenjen komentar zadatka %s', + '%s commented the task %s' => '%s komentarisao zadatak %s', + '%s\'s activity' => 'Aktivnosti %s', + 'No activity.' => 'Bez aktivnosti.', + 'RSS feed' => 'RSS kanal', + '%s updated a comment on the task #%d' => '%s izmenjen komentar zadatka #%d', + '%s commented on the task #%d' => '%s komentarisao zadatak #%d', + '%s updated a subtask for the task #%d' => '%s izmenjen pod-zadatak zadatka #%d', + '%s created a subtask for the task #%d' => '%s kreirao pod-zadatak zadatka #%d', + '%s updated the task #%d' => '%s izmenjen zadatak #%d', + '%s created the task #%d' => '%s kreirao zadatak #%d', + '%s closed the task #%d' => '%s zatvorio zadatak #%d', + '%s open the task #%d' => '%s otvorio zadatak #%d', + '%s moved the task #%d to the column "%s"' => '%s premestio zadatak #%d u kolonu "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s premestio zadatak #%d na pozycję %d w kolmnie "%s"', + 'Activity' => 'Aktivnosti', + 'Default values are "%s"' => 'Osnovne vrednosti su: "%s"', + 'Default columns for new projects (Comma-separated)' => 'Osnovne kolone za novi projekat (Odvojeni zarezom)', + 'Task assignee change' => 'Zmień osobę odpowiedzialną', + '%s change the assignee of the task #%d to %s' => '%s zamena dodele za zadatak #%d na %s', + '%s changed the assignee of the task %s to %s' => '%s zamena dodele za zadatak %s na %s', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', + 'New password for the user "%s"' => 'Nova lozinka za korisnika "%s"', + 'Choose an event' => 'Izaberi događaj', + // 'Github commit received' => '', + // 'Github issue opened' => '', + // 'Github issue closed' => '', + // 'Github issue reopened' => '', + // 'Github issue assignee change' => '', + // 'Github issue label change' => '', + 'Create a task from an external provider' => 'Kreiraj zadatak preko posrednika', + 'Change the assignee based on an external username' => 'Zmień osobę odpowiedzialną na podstawie zewnętrznej nazwy użytkownika', + 'Change the category based on an external label' => 'Zmień kategorię na podstawie zewnętrzenj etykiety', + // 'Reference' => '', + // 'Reference: %s' => '', + 'Label' => 'Etikieta', + 'Database' => 'Baza', + 'About' => 'Informacje', + 'Database driver:' => 'Database driver:', + 'Board settings' => 'Podešavanje table', + 'URL and token' => 'URL i token', + // 'Webhook settings' => '', + 'URL for task creation:' => 'URL za kreiranje zadataka', + 'Reset token' => 'Resetuj token', + // 'API endpoint:' => '', + 'Refresh interval for private board' => 'Interval osvežavanja privatnih tabli', + 'Refresh interval for public board' => 'Interval osvežavanja javnih tabli', + 'Task highlight period' => 'Task highlight period', + // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', + // 'Frequency in second (60 seconds by default)' => '', + // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', + 'Application URL' => 'Adres URL aplikacji', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Primer: http://example.kanboard.net/ (koristi se u obaveštenjima putem mail-a)', + 'Token regenerated.' => 'Token wygenerowany ponownie.', + 'Date format' => 'Format daty', + 'ISO format is always accepted, example: "%s" and "%s"' => 'Format ISO je uvek prihvatljiv, primer: "%s", "%s"', + 'New private project' => 'Novi privatni projekat', + 'This project is private' => 'Ovaj projekat je privatan', + 'Type here to create a new sub-task' => 'Kucaj ovde za kreiranje novog pod-zadatka', + 'Add' => 'Dodaj', + 'Estimated time: %s hours' => 'Procenjeno vreme: %s godzin', + 'Time spent: %s hours' => 'Utrošeno vreme: %s godzin', + 'Started on %B %e, %Y' => 'Započeto dana %e %B %Y', + 'Start date' => 'Datum početka', + 'Time estimated' => 'Procenjeno vreme', + 'There is nothing assigned to you.' => 'Ništa vam nije dodeljeno', + 'My tasks' => 'Moji zadaci', + 'Activity stream' => 'Spisak aktinosti', + 'Dashboard' => 'Panel', + 'Confirmation' => 'Potvrda', + 'Allow everybody to access to this project' => 'Dozvoli svima pristup projektu', + 'Everybody have access to this project.' => 'Svima je dozvoljen pristup.', + // 'Webhooks' => '', + // 'API' => '', + 'Integration' => 'Integracja', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', + 'Configure' => 'Podesi', + 'Project management' => 'Uređivanje projekata', + 'My projects' => 'Moji projekti', + 'Columns' => 'Kolone', + 'Task' => 'Zadaci', + 'Your are not member of any project.' => 'Nisi član ni jednog projekta', + 'Percentage' => 'Procenat', + 'Number of tasks' => 'Broj zadataka', + 'Task distribution' => 'Podela zadataka', + 'Reportings' => 'Izveštaji', + 'Task repartition for "%s"' => 'Zaduženja zadataka za "%s"', + 'Analytics' => 'Analiza', + 'Subtask' => 'Pod-zadatak', + 'My subtasks' => 'Moji pod-zadaci', + 'User repartition' => 'Zaduženja korisnika', + 'User repartition for "%s"' => 'Zaduženja korisnika za "%s"', + 'Clone this project' => 'Kopiraj projekat', + 'Column removed successfully.' => 'Kolumna usunięta pomyslnie.', + 'Edit Project' => 'Izmeni projekat', + // 'Github Issue' => '', + 'Not enough data to show the graph.' => 'Nedovoljno podataka za grafikon.', + 'Previous' => 'Prethodni', + 'The id must be an integer' => 'ID musi być liczbą całkowitą', + 'The project id must be an integer' => 'ID projektu musi być liczbą całkowitą', + 'The status must be an integer' => 'Status musi być liczbą całkowitą', + 'The subtask id is required' => 'ID pod-zadatak jest wymagane', + 'The subtask id must be an integer' => 'ID pod-zadania musi być liczbą całkowitą', + 'The task id is required' => 'ID zadania jest wymagane', + 'The task id must be an integer' => 'ID zadatka mora biti broj', + 'The user id must be an integer' => 'ID korisnika mora biti broj', + 'This value is required' => 'Vrednost je obavezna', + 'This value must be numeric' => 'Vrednost mora biti broj', + 'Unable to create this task.' => 'Nije moguće kreirati zadatak.', + 'Cumulative flow diagram' => 'Zbirni dijagram toka', + 'Cumulative flow diagram for "%s"' => 'Zbirni dijagram toka za "%s"', + 'Daily project summary' => 'Zbirni pregled po danima', + 'Daily project summary export' => 'Izvoz zbirnog pregleda po danima', + 'Daily project summary export for "%s"' => 'Izvoz zbirnig pregleda po danima za "%s"', + 'Exports' => 'Izvoz', + // 'This export contains the number of tasks per column grouped per day.' => '', + 'Nothing to preview...' => 'Ništa za prikazivanje...', + 'Preview' => 'Pregled', + 'Write' => 'Piši', + 'Active swimlanes' => 'Aktivni razdelnik', + 'Add a new swimlane' => 'Dodaj razdelnik', + 'Change default swimlane' => 'Zameni osnovni razdelnik', + 'Default swimlane' => 'Osnovni razdelnik', + 'Do you really want to remove this swimlane: "%s"?' => 'Da li da uklonim razdelnik: "%s"?', + 'Inactive swimlanes' => 'Neaktivni razdelniki', + 'Set project manager' => 'Podesi menadžera projekta', + 'Set project member' => 'Podesi učesnika projekat', + 'Remove a swimlane' => 'Ukloni razdelnik', + 'Rename' => 'Preimenuj', + 'Show default swimlane' => 'Prikaži osnovni razdelnik', + 'Swimlane modification for the project "%s"' => 'Izmena razdelnika za projekat "%s"', + 'Swimlane not found.' => 'Razdelnik nije pronađen.', + 'Swimlane removed successfully.' => 'Razdelnik uspešno uklonjen.', + 'Swimlanes' => 'Razdelnici', + 'Swimlane updated successfully.' => 'Razdelnik zaktualizowany pomyślnie.', + // 'The default swimlane have been updated successfully.' => '', + // 'Unable to create your swimlane.' => '', + // 'Unable to remove this swimlane.' => '', + // 'Unable to update this swimlane.' => '', + 'Your swimlane have been created successfully.' => 'Razdelnik je uspešno kreiran.', + 'Example: "Bug, Feature Request, Improvement"' => 'Npr: "Greška, Zahtev za izmenama, Poboljšanje"', + 'Default categories for new projects (Comma-separated)' => 'Osnovne kategorije za projekat', + // 'Gitlab commit received' => '', + // 'Gitlab issue opened' => '', + // 'Gitlab issue closed' => '', + // 'Gitlab webhooks' => '', + // 'Help on Gitlab webhooks' => '', + 'Integrations' => 'Integracje', + 'Integration with third-party services' => 'Integracja sa uslugama spoljnih servisa', + 'Role for this project' => 'Uloga u ovom projektu', + 'Project manager' => 'Manadžer projekta', + 'Project member' => 'Učesnik projekta', + // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', + // 'Gitlab Issue' => '', + 'Subtask Id' => 'ID pod-zadania', + 'Subtasks' => 'Pod-zadataka', + 'Subtasks Export' => 'Eksport pod-zadań', + 'Subtasks exportation for "%s"' => 'Izvoz pod-zadań dla "%s"', + 'Task Title' => 'Naslov zadatka', + 'Untitled' => 'Bez naslova', + 'Application default' => 'Postavke aplikacje', + 'Language:' => 'Jezik:', + 'Timezone:' => 'Vremenska zona:', + 'All columns' => 'Sve kolone', + 'Calendar for "%s"' => 'Kalendar za "%s"', + 'Filter by column' => 'Po koloni', + 'Filter by status' => 'Po statusu', + 'Calendar' => 'Kalendar', + 'Next' => 'Sledeći', + // '#%d' => '', + 'Filter by color' => 'Po boji', + 'Filter by swimlane' => 'Po razdelniku', + 'All swimlanes' => 'Svi razdelniki', + 'All colors' => 'Sve boje', + 'All status' => 'Svi statusi', + 'Add a comment logging moving the task between columns' => 'Dodaj logovanje premeštanja zadataka po kolonama', + 'Moved to column %s' => 'Premešten u kolonu %s', + // 'Change description' => '', + 'User dashboard' => 'Korisnički panel', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + 'There is nothing to show.' => 'Nema podataka', + 'Time Tracking' => 'Praćenje vremena', + // 'You already have one subtask in progress' => '', + 'Which parts of the project do you want to duplicate?' => 'Koje delove projekta želite da kopirate', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + 'Add a link' => 'Dodaj link', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', + // 'Compact view' => '', + // 'Horizontal scrolling' => '', + // 'Compact/wide view' => '', + // 'No results match:' => '', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + // 'Hourly rates' => '', + // 'Hourly rate' => '', + // 'Currency' => '', + // 'Effective date' => '', + // 'Add new rate' => '', + // 'Rate removed successfully.' => '', + // 'Unable to remove this rate.' => '', + // 'Unable to save the hourly rate.' => '', + // 'Hourly rate created successfully.' => '', + // 'Start time' => '', + // 'End time' => '', + // 'Comment' => '', + // 'All day' => '', + // 'Day' => '', + // 'Manage timetable' => '', + // 'Overtime timetable' => '', + // 'Time off timetable' => '', + // 'Timetable' => '', + // 'Work timetable' => '', + // 'Week timetable' => '', + // 'Day timetable' => '', + // 'From' => '', + // 'To' => '', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + // 'Remove time slot' => '', + // 'Add new time slot' => '', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + // 'Files' => '', + // 'Images' => '', + // 'Private project' => '', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + // 'Budget' => '', + // 'Budget line' => '', + // 'Budget line removed successfully.' => '', + // 'Budget lines' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + // 'Cost' => '', + // 'Cost breakdown' => '', + // 'Custom Stylesheet' => '', + // 'download' => '', + // 'Do you really want to remove this budget line?' => '', + // 'EUR - Euro' => '', + // 'Expenses' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'New budget line' => '', + // 'NZD - New Zealand Dollar' => '', + // 'Remove a budget line' => '', + // 'Remove budget line' => '', + // 'RSD - Serbian dinar' => '', + // 'The budget line have been created successfully.' => '', + // 'Unable to create the budget line.' => '', + // 'Unable to remove this budget line.' => '', + // 'USD - US Dollar' => '', + // 'Remaining' => '', + // 'Destination column' => '', + // 'Move the task to another column when assigned to a user' => '', + // 'Move the task to another column when assignee is cleared' => '', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locales/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 71e03090..b4056321 100644 --- a/app/Locales/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -1,6 +1,8 @@ <?php return array( + 'number.decimals_separator' => ',', + 'number.thousands_separator' => '.', 'None' => 'Ingen', 'edit' => 'redigera', 'Edit' => 'Redigera', @@ -104,7 +106,7 @@ return array( 'Open a task' => 'Öppna en uppgift', 'Do you really want to open this task: "%s"?' => 'Vill du verkligen öppna denna uppgift: "%s"?', 'Back to the board' => 'Tillbaka till tavlan', - 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %d %B %Y kl %H:%M', + 'Created on %B %e, %Y at %k:%M %p' => 'Skapad %Y-%m-%d kl %H:%M', 'There is nobody assigned' => 'Det finns ingen tilldelad', 'Column on the board:' => 'Kolumn på tavlan:', 'Status is open' => 'Statusen är öppen', @@ -166,8 +168,8 @@ return array( 'Work in progress' => 'Pågående', 'Done' => 'Slutfört', 'Application version:' => 'Version:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %d %B %Y kl %H:%M', - '%B %e, %Y at %k:%M %p' => '%d %B %Y kl %H:%M', + 'Completed on %B %e, %Y at %k:%M %p' => 'Slutfört %Y-%m-%d kl %H:%M', + '%B %e, %Y at %k:%M %p' => '%Y-%m-%d kl %H:%M', 'Date created' => 'Skapat datum', 'Date completed' => 'Slutfört datum', 'Id' => 'ID', @@ -182,18 +184,19 @@ return array( 'Change assignee' => 'Ändra uppdragsinnehavare', 'Change assignee for the task "%s"' => 'Ändra uppdragsinnehavare för uppgiften "%s"', 'Timezone' => 'Tidszon', - 'Sorry, I didn\'t found this information in my database!' => 'Informationen kunde inte hittas i databasen.', + 'Sorry, I didn\'t find this information in my database!' => 'Informationen kunde inte hittas i databasen.', 'Page not found' => 'Sidan hittas inte', - 'Complexity' => 'Ungefärligt antal timmar', + 'Complexity' => 'Komplexitet', 'limit' => 'max', 'Task limit' => 'Uppgiftsbegränsning', + 'Task count' => 'Antal uppgifter', 'This value must be greater than %d' => 'Värdet måste vara större än %d', 'Edit project access list' => 'Ändra projektåtkomst lista', 'Edit users access' => 'Användaråtkomst', 'Allow this user' => 'Tillåt användare', 'Only those users have access to this project:' => 'Bara de användarna har tillgång till detta projekt.', 'Don\'t forget that administrators have access to everything.' => 'Glöm inte att administratörerna har rätt att göra allt.', - 'revoke' => 'Dra tillbaka behörighet', + 'Revoke' => 'Dra tillbaka behörighet', 'List of authorized users' => 'Lista med behöriga användare', 'User' => 'Användare', 'Nobody have access to this project.' => 'Ingen har tillgång till detta projekt.', @@ -210,8 +213,9 @@ return array( 'Edit this task' => 'Ändra denna uppgift', 'Due Date' => 'Måldatum', 'Invalid date' => 'Ej tillåtet datum', - 'Must be done before %B %e, %Y' => 'Måste vara klart innan %B %e, %Y', - '%B %e, %Y' => '%d %B %Y', + 'Must be done before %B %e, %Y' => 'Måste vara klart innan %Y-%m-%d', + '%B %e, %Y' => '%Y-%m-%d', + '%b %e, %Y' => '%Y-%m-%d', 'Automatic actions' => 'Automatiska åtgärder', 'Your automatic action have been created successfully.' => 'Din automatiska åtgärd har skapats.', 'Unable to create your automatic action.' => 'Kunde inte skapa din automatiska åtgärd.', @@ -375,7 +379,7 @@ return array( 'Link my GitHub Account' => 'Anslut mitt GitHub-konto', 'Unlink my GitHub Account' => 'Koppla ifrån mitt GitHub-konto', 'Created by %s' => 'Skapad av %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %B %e, %Y kl %k:%M %p', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Senaste ändring %Y-%m-%d kl %H:%M', 'Tasks Export' => 'Exportera uppgifter', 'Tasks exportation for "%s"' => 'Exportera uppgifter för "%s"', 'Start Date' => 'Startdatum', @@ -385,8 +389,6 @@ return array( 'Creator' => 'Skapare', 'Modification date' => 'Ändringsdatum', 'Completion date' => 'Slutfört datum', - 'Webhook URL for task creation' => 'Webhook URL för att skapa uppgift', - 'Webhook URL for task modification' => 'Webhook URL för att ändra uppgift', 'Clone' => 'Klona', 'Clone Project' => 'Klona projekt', 'Project cloned successfully.' => 'Projektet har klonats.', @@ -406,15 +408,13 @@ return array( 'Comment updated' => 'Kommentaren har uppdaterats', 'New comment posted by %s' => 'Ny kommentar postad av %s', 'List of due tasks for the project "%s"' => 'Lista med uppgifter för projektet "%s"', - '[%s][New attachment] %s (#%d)' => '[%s][Ny bifogning] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Ny kommentar] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Uppdaterad kommentar] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Ny deluppgift] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Deluppgiften uppdaterad] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Ny uppgift] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Uppgiften uppdaterad] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Uppgiften stängd] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Uppgiften öppnad] %s (#%d)', + 'New attachment' => 'Ny bifogning', + 'New comment' => 'Ny kommentar', + 'New subtask' => 'Ny deluppgift', + 'Subtask updated' => 'Deluppgiften har uppdaterats', + 'Task updated' => 'Uppgiften har uppdaterats', + 'Task closed' => 'Uppgiften har stängts', + 'Task opened' => 'Uppgiften har öppnats', '[%s][Due tasks]' => '[%s][Förfallen uppgift]', '[Kanboard] Notification' => '[Kanboard] Notis', 'I want to receive notifications only for those projects:' => 'Jag vill endast få notiser för dessa projekt:', @@ -467,18 +467,18 @@ return array( 'Unable to change the password.' => 'Kunde inte byta lösenord.', 'Change category for the task "%s"' => 'Byt kategori för uppgiften "%s"', 'Change category' => 'Byt kategori', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s uppdaterade uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s öppna uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s flyttade uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a> till positionen #%d i kolumnen "%s"', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s flyttade uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a> till kolumnen "%s"', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s skapade uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s stängde uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s skapade en deluppgift för uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s uppdaterade en deluppgift för uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', + '%s updated the task %s' => '%s uppdaterade uppgiften %s', + '%s opened the task %s' => '%s öppna uppgiften %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s flyttade uppgiften %s till positionen #%d i kolumnen "%s"', + '%s moved the task %s to the column "%s"' => '%s flyttade uppgiften %s till kolumnen "%s"', + '%s created the task %s' => '%s skapade uppgiften %s', + '%s closed the task %s' => '%s stängde uppgiften %s', + '%s created a subtask for the task %s' => '%s skapade en deluppgift för uppgiften %s', + '%s updated a subtask for the task %s' => '%s uppdaterade en deluppgift för uppgiften %s', 'Assigned to %s with an estimate of %s/%sh' => 'Tilldelades %s med en uppskattning på %s/%sh', 'Not assigned, estimate of %sh' => 'Inte tilldelade, uppskattat %sh', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s uppdaterade en kommentar till uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s kommenterade uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a>', + '%s updated a comment on the task %s' => '%s uppdaterade en kommentar till uppgiften %s', + '%s commented the task %s' => '%s kommenterade uppgiften %s', '%s\'s activity' => '%s\'s aktivitet', 'No activity.' => 'Ingen aktivitet.', 'RSS feed' => 'RSS flöde', @@ -497,10 +497,10 @@ return array( 'Default columns for new projects (Comma-separated)' => 'Standardkolumner för nya projekt (kommaseparerade)', 'Task assignee change' => 'Ändra tilldelning av uppgiften', '%s change the assignee of the task #%d to %s' => '%s byt tilldelning av uppgiften #%d till %s', - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '%s byt tilldelning av uppgiften <a href="?controller=task&action=show&task_id=%d">#%d</a> till %s', - '[%s][Column Change] %s (#%d)' => '[%s][Byt kolumn] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Byt position] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Byt tilldelning] %s (#%d)', + '%s changed the assignee of the task %s to %s' => '%s byt tilldelning av uppgiften %s till %s', + 'Column Change' => 'Ändring av kolumn', + 'Position Change' => 'Ändring av position', + 'Assignee Change' => 'Ändring av tilldelning', 'New password for the user "%s"' => 'Nytt lösenord för användaren "%s"', 'Choose an event' => 'Välj en händelse', 'Github commit received' => 'Github-bidrag mottaget', @@ -541,14 +541,385 @@ return array( 'Add' => 'Lägg till', 'Estimated time: %s hours' => 'Uppskattad tid: %s timmar', 'Time spent: %s hours' => 'Nedlaggd tid: %s timmar', - 'Started on %B %e, %Y' => 'Startad den %B %e, %Y', + 'Started on %B %e, %Y' => 'Startad %Y-%m-%d', 'Start date' => 'Startdatum', 'Time estimated' => 'Uppskattad tid', 'There is nothing assigned to you.' => 'Du har inget tilldelat till dig.', - 'My tasks' => 'Mina uppgifte', + 'My tasks' => 'Mina uppgifter', 'Activity stream' => 'Aktivitetsström', 'Dashboard' => 'Instrumentpanel', 'Confirmation' => 'Bekräftelse', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', + 'Allow everybody to access to this project' => 'Ge alla tillgång till projektet', + 'Everybody have access to this project.' => 'Alla har tillgång till projektet', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Integration', + 'Github webhooks' => 'Github webhooks', + 'Help on Github webhooks' => 'Hjälp för Github webhooks', + 'Create a comment from an external provider' => 'Skapa en kommentar från en extern leverantör', + 'Github issue comment created' => 'Github frågekommentar skapad', + 'Configure' => 'Konfigurera', + 'Project management' => 'Projekthantering', + 'My projects' => 'Mina projekt', + 'Columns' => 'Kolumner', + 'Task' => 'Uppgift', + 'Your are not member of any project.' => 'Du är inte medlem i något projekt', + 'Percentage' => 'Procent', + 'Number of tasks' => 'Antal uppgifter', + 'Task distribution' => 'Uppgiftsfördelning', + 'Reportings' => 'Rapportering', + 'Task repartition for "%s"' => 'Uppgiftsdeltagande för "%s"', + 'Analytics' => 'Analyser', + 'Subtask' => 'Deluppgift', + 'My subtasks' => 'Mina deluppgifter', + 'User repartition' => 'Användardeltagande', + 'User repartition for "%s"' => 'Användardeltagande för "%s"', + 'Clone this project' => 'Klona projektet', + 'Column removed successfully.' => 'Kolumnen togs bort', + 'Edit Project' => 'Ändra Projekt', + 'Github Issue' => 'Github fråga', + 'Not enough data to show the graph.' => 'Inte tillräckligt med data för att visa graf', + 'Previous' => 'Föregående', + 'The id must be an integer' => 'ID måste vara ett heltal', + 'The project id must be an integer' => 'Projekt-ID måste vara ett heltal', + 'The status must be an integer' => 'Status måste vara ett heltal', + 'The subtask id is required' => 'Deluppgifts-ID behövs', + 'The subtask id must be an integer' => 'Deluppgifts-ID måste vara ett heltal', + 'The task id is required' => 'Uppgifts-ID behövs', + 'The task id must be an integer' => 'Uppgifts-ID måste vara ett heltal', + 'The user id must be an integer' => 'Användar-ID måste vara ett heltal', + 'This value is required' => 'Värdet behövs', + 'This value must be numeric' => 'Värdet måste vara numeriskt', + 'Unable to create this task.' => 'Kunde inte skapa uppgiften.', + 'Cumulative flow diagram' => 'Diagram med kumulativt flöde', + 'Cumulative flow diagram for "%s"' => 'Diagram med kumulativt flöde för "%s"', + 'Daily project summary' => 'Daglig projektsummering', + 'Daily project summary export' => 'Export av daglig projektsummering', + 'Daily project summary export for "%s"' => 'Export av daglig projektsummering för "%s"', + 'Exports' => 'Exporter', + 'This export contains the number of tasks per column grouped per day.' => 'Denna export innehåller antalet uppgifter per kolumn grupperade per dag.', + 'Nothing to preview...' => 'Inget att förhandsgrandska...', + 'Preview' => 'Förhandsgranska', + 'Write' => 'Skriva', + 'Active swimlanes' => 'Aktiva swimlanes', + 'Add a new swimlane' => 'Lägg till en nytt swimlane', + 'Change default swimlane' => 'Ändra standard swimlane', + 'Default swimlane' => 'Standard swimlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Vill du verkligen ta bort denna swimlane: "%s"?', + 'Inactive swimlanes' => 'Inaktiv swimlane', + 'Set project manager' => 'Sätt Projektadministratör', + 'Set project member' => 'Sätt projektmedlem', + 'Remove a swimlane' => 'Ta bort en swimlane', + 'Rename' => 'Byt namn', + 'Show default swimlane' => 'Visa standard swimlane', + 'Swimlane modification for the project "%s"' => 'Ändra swimlane för projektet "%s"', + 'Swimlane not found.' => 'Swimlane kunde inte hittas', + 'Swimlane removed successfully.' => 'Swimlane togs bort', + 'Swimlanes' => 'Swimlanes', + 'Swimlane updated successfully.' => 'Swimlane uppdaterad', + 'The default swimlane have been updated successfully.' => 'Standardswimlane har uppdaterats', + 'Unable to create your swimlane.' => 'Kunde inte skapa din swimlane', + 'Unable to remove this swimlane.' => 'Kunde inte ta bort swimlane', + 'Unable to update this swimlane.' => 'Kunde inte uppdatera swimlane', + 'Your swimlane have been created successfully.' => 'Din swimlane har skapats', + 'Example: "Bug, Feature Request, Improvement"' => 'Exempel: "Bug, ny funktionalitet, förbättringar"', + 'Default categories for new projects (Comma-separated)' => 'Standardkategorier för nya projekt (komma-separerade)', + 'Gitlab commit received' => 'Gitlab bidrag mottaget', + 'Gitlab issue opened' => 'Gitlab fråga öppnad', + 'Gitlab issue closed' => 'Gitlab fråga stängd', + 'Gitlab webhooks' => 'Gitlab webhooks', + 'Help on Gitlab webhooks' => 'Hjälp för Gitlab webhooks', + 'Integrations' => 'Integrationer', + 'Integration with third-party services' => 'Integration med tjänst från tredjepart', + 'Role for this project' => 'Roll för detta projekt', + 'Project manager' => 'Projektadministratör', + 'Project member' => 'Projektmedlem', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'En projektadministratör kan ändra inställningar för projektet och har mer rättigheter än en standardanvändare.', + 'Gitlab Issue' => 'Gitlab fråga', + 'Subtask Id' => 'Deluppgifts-ID', + 'Subtasks' => 'Deluppgift', + 'Subtasks Export' => 'Export av deluppgifter', + 'Subtasks exportation for "%s"' => 'Export av deluppgifter för "%s"', + 'Task Title' => 'Uppgiftstitel', + 'Untitled' => 'Titel saknas', + 'Application default' => 'Applikationsstandard', + 'Language:' => 'Språk', + 'Timezone:' => 'Tidszon', + 'All columns' => 'Alla kolumner', + 'Calendar for "%s"' => 'Kalender för "%s"', + 'Filter by column' => 'Filtrera på kolumn', + 'Filter by status' => 'Filtrera på status', + 'Calendar' => 'Kalender', + 'Next' => 'Nästa', + '#%d' => '#%d', + 'Filter by color' => 'Filtrera på färg', + 'Filter by swimlane' => 'Filtrera på swimlane', + 'All swimlanes' => 'Alla swimlanes', + 'All colors' => 'Alla färger', + 'All status' => 'Alla status', + 'Add a comment logging moving the task between columns' => 'Lägg till en kommentar för att logga förflyttning av en uppgift mellan kolumner', + 'Moved to column %s' => 'Flyttad till kolumn %s', + 'Change description' => 'Ändra beskrivning', + 'User dashboard' => 'Användardashboard', + 'Allow only one subtask in progress at the same time for a user' => 'Tillåt endast en deluppgift igång samtidigt för en användare', + 'Edit column "%s"' => 'Ändra kolumn "%s"', + 'Enable time tracking for subtasks' => 'Aktivera tidsbevakning för deluppgifter', + 'Select the new status of the subtask: "%s"' => 'Välj ny status för deluppgiften: "%s"', + 'Subtask timesheet' => 'Tidrapport för deluppgiften', + 'There is nothing to show.' => 'Det finns inget att visa', + 'Time Tracking' => 'Tidsbevakning', + 'You already have one subtask in progress' => 'Du har redan en deluppgift igång', + 'Which parts of the project do you want to duplicate?' => 'Vilka delar av projektet vill du duplicera?', + 'Change dashboard view' => 'Ändra dashboard vy', + 'Show/hide activities' => 'Visa/dölj aktiviteter', + 'Show/hide projects' => 'Visa/dölj projekt', + 'Show/hide subtasks' => 'Visa/dölj deluppgifter', + 'Show/hide tasks' => 'Visa/dölj uppgifter', + 'Disable login form' => 'Inaktivera loginformuläret', + 'Show/hide calendar' => 'Visa/dölj kalender', + 'User calendar' => 'Användarkalender', + 'Bitbucket commit received' => 'Bitbucket bidrag mottaget', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Hjälp för Bitbucket webhooks', + 'Start' => 'Start', + 'End' => 'Slut', + 'Task age in days' => 'Uppgiftsålder i dagar', + 'Days in this column' => 'Dagar i denna kolumn', + '%dd' => '%dd', + 'Add a link' => 'Lägg till länk', + 'Add a new link' => 'Lägg till ny länk', + 'Do you really want to remove this link: "%s"?' => 'Vill du verkligen ta bort länken: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Vill du verkligen ta bort länken till uppgiften #%d?', + 'Field required' => 'Fältet krävs', + 'Link added successfully.' => 'Länken har lagts till', + 'Link updated successfully.' => 'Länken har uppdaterats', + 'Link removed successfully.' => 'Länken har tagits bort', + 'Link labels' => 'Länketiketter', + 'Link modification' => 'Länkändring', + 'Links' => 'Länkar', + 'Link settings' => 'Länkinställningar', + 'Opposite label' => 'Motpartslänk', + 'Remove a link' => 'Ta bort en länk', + 'Task\'s links' => 'Uppgiftslänkar', + 'The labels must be different' => 'Etiketterna måste vara olika', + 'There is no link.' => 'Det finns ingen länk', + 'This label must be unique' => 'Länken måste vara unik', + 'Unable to create your link.' => 'Kunde inte skapa din länk', + 'Unable to update your link.' => 'Kunde inte uppdatera din länk', + 'Unable to remove this link.' => 'Kunde inte ta bort din länk', + 'relates to' => 'relaterar till', + 'blocks' => 'blockerar', + 'is blocked by' => 'blockeras av', + 'duplicates' => 'dupplicerar', + 'is duplicated by' => 'är duplicerad av', + 'is a child of' => 'är underliggande till', + 'is a parent of' => 'är överliggande till', + 'targets milestone' => 'milstolpemål', + 'is a milestone of' => 'är en milstolpe för', + 'fixes' => 'åtgärdar', + 'is fixed by' => 'åtgärdas av', + 'This task' => 'Denna uppgift', + '<1h' => '<1h', + '%dh' => '%dh', + '%b %e' => '%b %e', + 'Expand tasks' => 'Expandera uppgifter', + 'Collapse tasks' => 'Minimera uppgifter', + 'Expand/collapse tasks' => 'Expandera/minimera uppgifter', + 'Close dialog box' => 'Stäng dialogruta', + 'Submit a form' => 'Sänd formulär', + 'Board view' => 'Tavelvy', + 'Keyboard shortcuts' => 'Tangentbordsgenvägar', + 'Open board switcher' => 'Växling av öppen tavla', + 'Application' => 'Applikation', + 'Filter recently updated' => 'Filter som uppdaterats nyligen', + 'since %B %e, %Y at %k:%M %p' => 'sedan %B %e, %Y at %k:%M %p', + 'More filters' => 'Fler filter', + 'Compact view' => 'Kompakt vy', + 'Horizontal scrolling' => 'Horisontell scroll', + 'Compact/wide view' => 'Kompakt/bred vy', + 'No results match:' => 'Inga matchande resultat', + 'Remove hourly rate' => 'Ta bort timtaxa', + 'Do you really want to remove this hourly rate?' => 'Vill du verkligen ta bort denna timtaxa?', + 'Hourly rates' => 'Timtaxor', + 'Hourly rate' => 'Timtaxa', + 'Currency' => 'Valuta', + 'Effective date' => 'Giltighetsdatum', + 'Add new rate' => 'Lägg till ny taxa', + 'Rate removed successfully.' => 'Taxan togs bort.', + 'Unable to remove this rate.' => 'Kunde inte ta bort taxan.', + 'Unable to save the hourly rate.' => 'Kunde inte spara timtaxan.', + 'Hourly rate created successfully.' => 'Timtaxan skapades.', + 'Start time' => 'Starttid', + 'End time' => 'Sluttid', + 'Comment' => 'Kommentar', + 'All day' => 'Hela dagen', + 'Day' => 'Dag', + 'Manage timetable' => 'Hantera timplan', + 'Overtime timetable' => 'Övertidstimplan', + 'Time off timetable' => 'Ledighetstimplan', + 'Timetable' => 'Timplan', + 'Work timetable' => 'Arbetstimplan', + 'Week timetable' => 'Veckotidplan', + 'Day timetable' => 'Dagstimplan', + 'From' => 'Från', + 'To' => 'Till', + 'Time slot created successfully.' => 'Tidslucka skapad.', + 'Unable to save this time slot.' => 'Kunde inte spara tidsluckan.', + 'Time slot removed successfully.' => 'Tidsluckan tog bort.', + 'Unable to remove this time slot.' => 'Kunde inte ta bort tidsluckan.', + 'Do you really want to remove this time slot?' => 'Vill du verkligen ta bort tidsluckan?', + 'Remove time slot' => 'Ta bort tidslucka', + 'Add new time slot' => 'Lägg till ny tidslucka', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Denna tidslucka används när kryssrutan "hela dagen" är kryssad vid schemalagd ledighet eller övertid.', + 'Files' => 'Filer', + 'Images' => 'Bilder', + 'Private project' => 'Privat projekt', + 'Amount' => 'Belopp', + 'AUD - Australian Dollar' => 'AUD - Australiska dollar', + 'Budget' => 'Budget', + 'Budget line' => 'Budgetlinje', + 'Budget line removed successfully.' => 'Budgetlinjen togs bort.', + 'Budget lines' => 'Budgetlinjer', + 'CAD - Canadian Dollar' => 'CAD - Kanadensiska dollar', + 'CHF - Swiss Francs' => 'CHF - Schweiziska Franc', + 'Cost' => 'Kostnad', + 'Cost breakdown' => 'Kostnadssammanställning', + 'Custom Stylesheet' => 'Anpassad stilmall', + 'download' => 'ladda ned', + 'Do you really want to remove this budget line?' => 'Vill du verkligen ta bort budgetlinjen?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Utgifter', + 'GBP - British Pound' => 'GBP - Brittiska Pund', + 'INR - Indian Rupee' => 'INR - Indiska Rupier', + 'JPY - Japanese Yen' => 'JPY - Japanska Yen', + 'New budget line' => 'Ny budgetlinje', + 'NZD - New Zealand Dollar' => 'NZD - Nya Zeeländska Dollar', + 'Remove a budget line' => 'Ta bort en budgetlinje', + 'Remove budget line' => 'Ta bort budgetlinje', + 'RSD - Serbian dinar' => 'RSD - Serbiska Dinarer', + 'The budget line have been created successfully.' => 'Budgetlinjen har skapats.', + 'Unable to create the budget line.' => 'Kunde inte skapa budgetlinjen.', + 'Unable to remove this budget line.' => 'Kunde inte ta bort budgetlinjen.', + 'USD - US Dollar' => 'USD - Amerikanska Dollar', + 'Remaining' => 'Återstående', + 'Destination column' => 'Målkolumn', + 'Move the task to another column when assigned to a user' => 'Flytta uppgiften till en annan kolumn när den tilldelats en användare', + 'Move the task to another column when assignee is cleared' => 'Flytta uppgiften till en annan kolumn när tilldelningen tas bort.', + 'Source column' => 'Källkolumn', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => 'Övergångar', + 'Executer' => 'Verkställare', + 'Time spent in the column' => 'Tid i kolumnen.', + 'Task transitions' => 'Uppgiftsövergångar', + 'Task transitions export' => 'Export av uppgiftsövergångar', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Denna rapport innehåller alla kolumnförflyttningar för varje uppgift med datum, användare och nedlagd tid vid varje övergång.', + 'Currency rates' => 'Valutakurser', + 'Rate' => 'Kurs', + 'Change reference currency' => 'Ändra referenskurs', + 'Add a new currency rate' => 'Lägg till ny valutakurs', + 'Currency rates are used to calculate project budget.' => 'Valutakurser används för att beräkna projektbudget.', + 'Reference currency' => 'Referensvaluta', + 'The currency rate have been added successfully.' => 'Valutakursen har lagts till.', + 'Unable to add this currency rate.' => 'Kunde inte lägga till valutakursen.', + 'Send notifications to a Slack channel' => 'Skicka notiser till en Slack kanal', + 'Webhook URL' => 'Webhook URL', + 'Help on Slack integration' => 'Hjälp för Slack integration', + '%s remove the assignee of the task %s' => '%s ta bort tilldelningen av uppgiften %s', + 'Send notifications to Hipchat' => 'Skicka notiser till Hipchat', + 'API URL' => 'API URL', + 'Room API ID or name' => 'Room API ID eller namn', + 'Room notification token' => 'Room notistoken', + 'Help on Hipchat integration' => 'Hjälp för Hipchat integration', + 'Enable Gravatar images' => 'Aktivera Gravatar bilder', + 'Information' => 'Information', + 'Check two factor authentication code' => 'Kolla tvåfaktorsverifieringskod', + 'The two factor authentication code is not valid.' => 'Tvåfaktorsverifieringskoden är inte giltig.', + 'The two factor authentication code is valid.' => 'Tvåfaktorsverifieringskoden är giltig.', + 'Code' => 'Kod', + 'Two factor authentication' => 'Tvåfaktorsverifiering', + 'Enable/disable two factor authentication' => 'Aktivera/avaktivera tvåfaktorsverifiering', + 'This QR code contains the key URI: ' => 'Denna QR-kod innehåller nyckel-URI:n', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Spara säkerhetsnyckeln i din TOTP mjukvara (med exempelvis Google Authenticator eller FreeOTP).', + 'Check my code' => 'Kolla min kod', + 'Secret key: ' => 'Säkerhetsnyckel:', + 'Test your device' => 'Testa din enhet', + 'Assign a color when the task is moved to a specific column' => 'Tilldela en färg när uppgiften flyttas till en specifik kolumn', + '%s via Kanboard' => '%s via Kanboard', + 'uploaded by: %s' => 'uppladdad av: %s', + 'uploaded on: %s' => 'uppladdad på: %s', + 'size: %s' => 'storlek', + 'Burndown chart for "%s"' => 'Burndown diagram för "%s"', + 'Burndown chart' => 'Burndown diagram', + 'This chart show the task complexity over the time (Work Remaining).' => 'Diagrammet visar uppgiftens svårighet över tid (återstående arbete).', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + 'SEK - Swedish Krona' => 'SEK - Svensk Krona', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php new file mode 100644 index 00000000..0c5a06dd --- /dev/null +++ b/app/Locale/th_TH/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => 'ไม่มี', + 'edit' => 'แก้ไข', + 'Edit' => 'แก้ไข', + 'remove' => 'ลบ', + 'Remove' => 'ลบ', + 'Update' => 'ปรับปรุง', + 'Yes' => 'ใช่', + 'No' => 'ไม่', + 'cancel' => 'ยกเลิก', + 'or' => 'หรือ', + 'Yellow' => 'สีเหลือง', + 'Blue' => 'สีน้ำเงิน', + 'Green' => 'สีเขียว', + 'Purple' => 'สีม่วง', + 'Red' => 'สีแดง', + 'Orange' => 'สีส้ม', + 'Grey' => 'สีเทา', + 'Save' => 'บันทึก', + 'Login' => 'เข้าสู่ระบบ', + 'Official website:' => 'เวบไซต์อย่างเป็นทางการ:', + 'Unassigned' => 'ไม่กำหนด', + 'View this task' => 'รายละเอียดงานนี้', + 'Remove user' => 'เอาผู้ใช้ออก', + 'Do you really want to remove this user: "%s"?' => 'คุณต้องการเอาผู้ใช้ « %s » ออกใช่หรือไม่?', + 'New user' => 'ผู้ใช้ใหม่', + 'All users' => 'ผู้ใช้ทั้งหมด', + 'Username' => 'ชื่อผู้ใช้', + 'Password' => 'รหัสผ่าน', + 'Default project' => 'โปรเจคเริ่มต้น', + 'Administrator' => 'ผู้ดูแลระบบ', + 'Sign in' => 'เข้าสู่ระบบ', + 'Users' => 'ผู้ใช้', + 'No user' => 'ไม่มีผู้ใช้', + 'Forbidden' => 'ไม่อนุญาติ', + 'Access Forbidden' => 'ไม่อนุญาติให้เข้า', + 'Only administrators can access to this page.' => 'หน้าสำหรับผู้ดูแลระบบเท่านั้น', + 'Edit user' => 'แก้ไขผู้ใช้', + 'Logout' => 'ออกจากระบบ', + 'Bad username or password' => 'ชื่อผู้ใช่หรือรหัสผ่านผิด', + 'users' => 'ผู้ใช้', + 'projects' => 'โปรเจค', + 'Edit project' => 'แก้ไขโปรเจค', + 'Name' => 'ชื่อ', + 'Activated' => 'เปิดใช้งาน', + 'Projects' => 'โปรเจค', + 'No project' => 'ไม่มีโปรเจค', + 'Project' => 'โปรเจค', + 'Status' => 'สถานะ', + 'Tasks' => 'งาน', + 'Board' => 'บอร์ด', + 'Actions' => 'การกระทำ', + 'Inactive' => 'ไม่เปิดใช้งาน', + 'Active' => 'เปิดใช้งาน', + 'Column %d' => 'คอลัมน์ %d', + 'Add this column' => 'เพิ่มคอลัมน์', + '%d tasks on the board' => '%d งานบนบอร์ด', + '%d tasks in total' => '%d งานทั้งหมด', + 'Unable to update this board.' => 'ไม่สามารถปรับปรุงบอร์ดได้.', + 'Edit board' => 'แก้ไขบอร์ด', + 'Disable' => 'ปิด', + 'Enable' => 'เปิด', + 'New project' => 'โปรเจคใหม่', + 'Do you really want to remove this project: "%s"?' => 'คุณต้องการเอาโปรเจค « %s » ออกใช่หรือไม่?', + 'Remove project' => 'ลบโปรเจค', + 'Boards' => 'บอร์ด', + 'Edit the board for "%s"' => 'แก้ไขบอร์ดสำหรับ « %s »', + 'All projects' => 'โปรเจคทั้งหมด', + 'Change columns' => 'เปลี่ยนคอลัมน์', + 'Add a new column' => 'เพิ่มคอลัมน์ใหม่', + 'Title' => 'หัวเรื่อง', + 'Add Column' => 'เพิ่มคอลัมน์', + 'Project "%s"' => 'โปรเจค « %s »', + 'Nobody assigned' => 'ไม่กำหนดใคร', + 'Assigned to %s' => 'กำหนดให้ %s', + 'Remove a column' => 'ลบคอลัมน์', + 'Remove a column from a board' => 'ลบคอลัมน์ออกจากบอร์ด', + 'Unable to remove this column.' => 'ไม่สามารถลบคอลัมน์นี้', + 'Do you really want to remove this column: "%s"?' => 'คุณต้องการลบคอลัมน์ « %s » ออกใช่หรือไม่?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'การกระทำนี้จะลบงานที่เกี่ยวข้องกับคอลัมน์นี้', + 'Settings' => 'ตั้งค่า', + 'Application settings' => 'ตั้งค่าการทำงาน', + 'Language' => 'ภาษา', + // 'Webhook token:' => '', + 'API token:' => 'API token:', + 'More information' => 'ข้อมูลเพิ่มเติม', + 'Database size:' => 'ขนาดฐานข้อมูล:', + 'Download the database' => 'ดาวน์โหลดฐานข้อมูล', + 'Optimize the database' => 'ปรับปรุงฐานข้อมูล', + '(VACUUM command)' => '(VACUUM command)', + '(Gzip compressed Sqlite file)' => '(Gzip compressed Sqlite file)', + 'User settings' => 'ตั้งค่าผู้ใช้', + 'My default project:' => 'โปรเจคเริ่มต้นของฉัน:', + 'Close a task' => 'ปิดงาน', + 'Do you really want to close this task: "%s"?' => 'คุณต้องการปิดงาน « %s » ใช่หรือไม่?', + 'Edit a task' => 'แก้ไขงาน', + 'Column' => 'คอลัมน์', + 'Color' => 'สี', + 'Assignee' => 'กำหนดให้', + 'Create another task' => 'สร้างงานอื่น', + 'New task' => 'งานใหม่', + 'Open a task' => 'เปิดงาน', + 'Do you really want to open this task: "%s"?' => 'คุณต้องการเปิดงาน: « %s » ใช่หรือไม่?', + 'Back to the board' => 'กลับไปที่บอร์ด', + 'Created on %B %e, %Y at %k:%M %p' => 'สร้างวันที่ %d/%m/%Y เวลา %H:%M', + 'There is nobody assigned' => 'ไม่มีใครถูกกำหนด', + 'Column on the board:' => 'คอลัมน์บนบอร์ด:', + 'Status is open' => 'สถานะเปิด', + 'Status is closed' => 'สถานะปิด', + 'Close this task' => 'ปิดงานนี้', + 'Open this task' => 'เปิดงานนี้', + 'There is no description.' => 'ไม่มีคำอธิบาย', + 'Add a new task' => 'เพิ่มงานใหม่', + 'The username is required' => 'ต้องการชื่อผู้ใช้', + 'The maximum length is %d characters' => 'จำนวนตัวอักษรสูงสุด %d ตัวอักษร', + 'The minimum length is %d characters' => 'จำนวนตัวอักษรน้อยสุด %d ตัวอักษร', + 'The password is required' => 'ต้องการรหัสผ่าน', + 'This value must be an integer' => 'ต้องเป็นตัวเลข', + 'The username must be unique' => 'ชื่อผู้ใช้ต้องไม่ซ้ำ', + 'The username must be alphanumeric' => 'ชื่อผู้ใช้ต้องเป็นตัวอักษรหรือตัวเลข', + 'The user id is required' => 'ต้องการไอดีผู้ใช้', + 'Passwords don\'t match' => 'รหัสผ่านไม่ถูกต้อง', + 'The confirmation is required' => 'ต้องการการยืนยัน', + 'The column is required' => 'ต้องการคอลัมน์', + 'The project is required' => 'ต้องการโปรเจค', + 'The color is required' => 'ต้องการสี', + 'The id is required' => 'ต้องการไอดี', + 'The project id is required' => 'ต้องการไอดีโปรเจค', + 'The project name is required' => 'ต้องการชื่อโปรเจค', + 'This project must be unique' => 'ชื่อโปรเจคต้องไม่ซ้ำ', + 'The title is required' => 'ต้องการหัวเรื่อง', + 'The language is required' => 'ต้องการภาษา', + 'There is no active project, the first step is to create a new project.' => 'ไม่มีโปรเจคที่ทำงานอยู่, ต้องการสร้างโปรเจคใหม่', + 'Settings saved successfully.' => 'บันทึกการตั้งค่าเรียบร้อยแล้ว', + 'Unable to save your settings.' => 'ไม่สามารถบันทึกการตั้งค่าได้', + 'Database optimization done.' => 'ปรับปรุงฐานข้อมูลเรียบร้อยแล้ว', + 'Your project have been created successfully.' => 'สร้างโปรเจคเรียบร้อยแล้ว', + 'Unable to create your project.' => 'ไม่สามารถสร้างโปรเจคได้', + 'Project updated successfully.' => 'ปรับปรุงโปรเจคเรียบร้อยแล้ว', + 'Unable to update this project.' => 'ไม่สามารถปรับปรุงโปรเจคได้', + 'Unable to remove this project.' => 'ไม่สามารถลบโปรเจคได้', + 'Project removed successfully.' => 'ลบโปรเจคเรียบร้อยแล้ว', + 'Project activated successfully.' => 'เปิดใช้งานโปรเจคเรียบร้อยแล้ว', + 'Unable to activate this project.' => 'ไม่สามารถเปิดใช้งานโปรเจคได้', + 'Project disabled successfully.' => 'ปิดโปรเจคเรียบร้อยแล้ว', + 'Unable to disable this project.' => 'ไม่สามารถปิดโปรเจคได้', + 'Unable to open this task.' => 'ไม่สามารถเปิดงานนี้', + 'Task opened successfully.' => 'เปิดงานเรียบร้อยแล้ว', + 'Unable to close this task.' => 'ไม่สามารถปิดงานนี้', + 'Task closed successfully.' => 'ปิดงานเรียบร้อยแล้ว', + 'Unable to update your task.' => 'ไม่สามารถปรับปรุงงานได้', + 'Task updated successfully.' => 'ปรับปรุงงานเรียบร้อยแล้ว', + 'Unable to create your task.' => 'ไม่สามารถสร้างงานได้', + 'Task created successfully.' => 'สร้างงานเรียบร้อยแล้ว', + 'User created successfully.' => 'สร้างผู้ใช้เรียบร้อยแล้ว', + 'Unable to create your user.' => 'ไม่สามารถสร้างผู้ใช้ได้', + 'User updated successfully.' => 'ปรับปรุงผู้ใช้เรียบร้อยแล้ว', + 'Unable to update your user.' => 'ไม่สามารถปรับปรุงผู้ใช้ได้', + 'User removed successfully.' => 'ลบผู้ใช้เรียบร้อยแล้ว', + 'Unable to remove this user.' => 'ไม่สามารถลบผู้ใช้ได้', + 'Board updated successfully.' => 'ปรับปรุงบอร์ดเรียบร้อยแล้ว', + 'Ready' => 'พร้อม', + 'Backlog' => 'งานค้าง', + 'Work in progress' => 'กำลังทำ', + 'Done' => 'เสร็จ', + 'Application version:' => 'แอพเวอร์ชัน:', + 'Completed on %B %e, %Y at %k:%M %p' => 'เรียบร้อยวันที่ %d/%m/%Y เวลา %H:%M', + '%B %e, %Y at %k:%M %p' => '%d/%m/%Y เวลา %H:%M', + 'Date created' => 'สร้างวันที่', + 'Date completed' => 'เรียบร้อยวันที่', + 'Id' => 'ไอดี', + 'No task' => 'ไม่มีงาน', + 'Completed tasks' => 'งานที่เสร็จแล้ว', + 'List of projects' => 'รายชื่อโปรเจค', + 'Completed tasks for "%s"' => 'งานที่เสร็จแล้วสำหรับ « %s »', + '%d closed tasks' => '%d งานที่ปิด', + 'No task for this project' => 'ไม่มีงานสำหรับโปรเจคนี้', + 'Public link' => 'ลิงค์สาธารณะ', + 'There is no column in your project!' => 'ไม่มีคอลัมน์ในโปรเจคของคุณ', + 'Change assignee' => 'เปลี่ยนการกำหนด', + 'Change assignee for the task "%s"' => 'เปลี่ยนการกำหนดสำหรับงาน « %s »', + 'Timezone' => 'เขตเวลา', + 'Sorry, I didn\'t find this information in my database!' => 'เสียใจด้วย ไม่สามารถหาข้อมูลในฐานข้อมูลได้', + 'Page not found' => 'ไม่พบหน้า', + 'Complexity' => 'ความซับซ้อน', + 'limit' => 'จำกัด', + 'Task limit' => 'จำกัดงาน', + // 'Task count' => '', + 'This value must be greater than %d' => 'ค่าต้องมากกว่า %d', + 'Edit project access list' => 'แก้ไขการเข้าถึงรายชื่อโปรเจค', + 'Edit users access' => 'แก้ไขการเข้าถึงผู้ใช้', + 'Allow this user' => 'อนุญาตผู้ใช้นี้', + 'Only those users have access to this project:' => 'ผู้ใช้ที่สามารถเข้าถึงโปรเจคนี้:', + 'Don\'t forget that administrators have access to everything.' => 'อย่าลืมผู้ดูแลระบบสามารถเข้าถึงได้ทุกอย่าง', + 'Revoke' => 'ยกเลิก', + 'List of authorized users' => 'รายชื่อผู้ใช้ที่ได้รับการยืนยัน', + 'User' => 'ผู้ใช้', + // 'Nobody have access to this project.' => '', + 'You are not allowed to access to this project.' => 'คุณไม่ได้รับอนุญาตให้เข้าถึงโปรเจคนี้', + 'Comments' => 'ความคิดเห็น', + 'Post comment' => 'แสดงความคิดเห็น', + 'Write your text in Markdown' => 'เขียนข้อความในรูปแบบ Markdown', + 'Leave a comment' => 'ออกความคิดเห็น', + 'Comment is required' => 'ต้องการความคิดเห็น', + 'Leave a description' => 'แสดงคำอธิบาย', + 'Comment added successfully.' => 'เพิ่มความคิดเห็นเรียบร้อยแล้ว', + 'Unable to create your comment.' => 'ไม่สามารถสร้างความคิดเห็น', + 'The description is required' => 'ต้องการคำอธิบาย', + 'Edit this task' => 'แก้ไขงาน', + 'Due Date' => 'วันที่ครบกำหนด', + 'Invalid date' => 'วันที่ผิด', + 'Must be done before %B %e, %Y' => 'ต้องทำให้เสร็จก่อน %d/%m/%Y', + '%B %e, %Y' => '%d/%m/%Y', + // '%b %e, %Y' => '', + 'Automatic actions' => 'การกระทำอัตโนมัติ', + 'Your automatic action have been created successfully.' => 'การกระทำอัตโนมัติสร้างเรียบร้อยแล้ว', + 'Unable to create your automatic action.' => 'ไม่สามารถสร้างการกระทำอัตโนมัติได้', + 'Remove an action' => 'ลบการกระทำ', + 'Unable to remove this action.' => 'ไม่สามารถลบการกระทำ', + 'Action removed successfully.' => 'ลบการกระทำเรียบร้อยแล้ว', + 'Automatic actions for the project "%s"' => 'การกระทำอัตโนมัติสำหรับโปรเจค « %s »', + 'Defined actions' => 'กำหนดการกระทำ', + 'Add an action' => 'เพิ่มการกระทำ', + 'Event name' => 'ชื่อเหตุกาณ์', + 'Action name' => 'ชื่อการกระทำ', + 'Action parameters' => 'พารามิเตอร์ของการกระทำ', + 'Action' => 'การกระทำ', + 'Event' => 'เหตุการณ์', + 'When the selected event occurs execute the corresponding action.' => 'เหตุการ์ที่เลือกจะเกิดขึ้นเมื่อมีการกระทำที่สอดคล้องกัน', + 'Next step' => 'ขั้นตอนต่อไป', + 'Define action parameters' => 'กำหนดพารามิเตอร์ของการกระทำ', + 'Save this action' => 'บันทึกการกระทำนี้', + 'Do you really want to remove this action: "%s"?' => 'คุณต้องการลบการกระทำ « %s » ใช่หรือไม่?', + 'Remove an automatic action' => 'ลบการกระทำอัตโนมัติ', + 'Close the task' => 'ปิดงาน', + 'Assign the task to a specific user' => 'กำหนดงานให้ผู้ใช้แบบเจาะจง', + 'Assign the task to the person who does the action' => 'กำหนดงานให้ผู้ใช้งานปัจจุบัน', + 'Duplicate the task to another project' => 'ทำซ้ำงานนี้ในโปรเจคอื่น', + 'Move a task to another column' => 'ย้ายงานไปคอลัมน์อื่น', + 'Move a task to another position in the same column' => 'ย้ายงานไปตำแหน่งอื่นในคอลัมน์เดียวกัน', + 'Task modification' => 'แก้ไขงาน', + 'Task creation' => 'สร้างงาน', + 'Open a closed task' => 'เปิดงานที่ปิดอยู่', + 'Closing a task' => 'กำลังปิดงาน', + 'Assign a color to a specific user' => 'กำหนดสีให้ผู้ใช้แบบเจาะจง', + 'Column title' => 'หัวเรื่องคอลัมน์', + 'Position' => 'ตำแหน่ง', + 'Move Up' => 'ย้ายขึ้น', + 'Move Down' => 'ย้ายลง', + 'Duplicate to another project' => 'ทำซ้ำในโปรเจคอื่น', + 'Duplicate' => 'ทำซ้ำ', + 'link' => 'ลิงค์', + 'Update this comment' => 'ปรับปรุงความคิดเห็นนี้', + 'Comment updated successfully.' => 'ปรับปรุงความคิดเห็นเรียบร้อยแล้ว', + 'Unable to update your comment.' => 'ไม่สามารถปรับปรุงความคิดเห็นได้', + 'Remove a comment' => 'ลบความคิดเห็น', + 'Comment removed successfully.' => 'ลบความคิดเห็นเรียบร้อยแล้ว', + 'Unable to remove this comment.' => 'ไม่สามารถลบความคิดเห็นได้', + 'Do you really want to remove this comment?' => 'คุณต้องการลบความคิดเห็น', + 'Only administrators or the creator of the comment can access to this page.' => 'เฉพาะผู้ดูแลระบบหรือผู้สร้างความคิดเห็นเข้าถึงหน้านี้', + 'Details' => 'รายละเอียด', + 'Current password for the user "%s"' => 'รหัสผ่านปัจจุบันของผู้ใช้ « %s »', + 'The current password is required' => 'ต้องการรหัสผ่านปัจจุบัน', + 'Wrong password' => 'รหัสผ่านผิด', + 'Reset all tokens' => 'รีเซตโทเคนทั้งหมด ', + 'All tokens have been regenerated.' => 'โทเคนทั้งหมดทำการสร้างใหม่', + 'Unknown' => 'ไม่ทราบ', + 'Last logins' => 'เข้าใช้ล่าสุด', + 'Login date' => 'วันที่เข้าใข้', + 'Authentication method' => 'วิธีการยืนยันตัวตน', + 'IP address' => 'ไอพี แอดเดรส', + 'User agent' => 'User agent', + 'Persistent connections' => 'Persistent connections', + 'No session.' => 'No session.', + 'Expiration date' => 'หมดอายุวันที่', + 'Remember Me' => 'จดจำฉัน', + 'Creation date' => 'สร้างวันที่', + 'Filter by user' => 'กรองตามผู้ใช้', + 'Filter by due date' => 'กรองตามวันครบกำหนด', + 'Everybody' => 'ทุกคน', + 'Open' => 'เปิด', + 'Closed' => 'ปิด', + 'Search' => 'ค้นหา', + 'Nothing found.' => 'ค้นหาไม่พบ.', + 'Search in the project "%s"' => 'ค้นหาในโปรเจค "%s"', + 'Due date' => 'วันที่ครบกำหนด', + 'Others formats accepted: %s and %s' => 'รูปแบบอื่นที่ได้รับการยอมรับ: %s และ %s', + 'Description' => 'คำอธิบาย', + '%d comments' => '%d ความคิดเห็น', + '%d comment' => '%d ความคิดเห็น', + 'Email address invalid' => 'อีเมลผิด', + 'Your Google Account is not linked anymore to your profile.' => 'กูเกิลแอคเคาท์ไม่ได้เชื่อมต่อกับประวัติของคุณ', + 'Unable to unlink your Google Account.' => 'ไม่สามารถยกเลิกการเชื่อมต่อกับกูเกิลแอคเคาท์', + 'Google authentication failed' => 'การยืนยันกับกูเกิลผิดพลาด', + 'Unable to link your Google Account.' => 'ไม่สามารถเชื่อมต่อกับกูเกิลแอคเคาท์', + 'Your Google Account is linked to your profile successfully.' => 'กูเกลิแอคเคาท์เชื่อมต่อกับประวัติของคุณเรียบร้อยแล้ว', + 'Email' => 'อีเมล', + 'Link my Google Account' => 'เชื่อมต่อกับกูเกิลแอคเคาท์', + 'Unlink my Google Account' => 'ไม่เชื่อมต่อกับกูเกิลแอคเคาท์', + 'Login with my Google Account' => 'เข้าใช้ด้วยกูเกิลแอคเคาท์', + 'Project not found.' => 'หาโปรเจคไม่พบ', + 'Task #%d' => 'งานที่ %d', + 'Task removed successfully.' => 'ลบงานเรียบร้อยแล้ว', + 'Unable to remove this task.' => 'ไม่สามารถลบงานนี้', + 'Remove a task' => 'ลบงาาน', + 'Do you really want to remove this task: "%s"?' => 'คุณต้องการลบงาน "%s" ออกใช่หรือไม่?', + 'Assign automatically a color based on a category' => 'กำหนดสีอัตโนมัติขึ้นอยู่กับกลุ่ม', + 'Assign automatically a category based on a color' => 'กำหนดกลุ่มอัตโนมัติขึ้นอยู่กับสี', + 'Task creation or modification' => 'สร้างหรือแก้ไขงาน', + 'Category' => 'กลุ่ม', + 'Category:' => 'กลุ่ม:', + 'Categories' => 'กลุ่ม', + 'Category not found.' => 'ไม่พบกลุ่ม.', + 'Your category have been created successfully.' => 'สร้างกลุ่มเรียบร้อยแล้ว', + 'Unable to create your category.' => 'ไม่สามารถสร้างกลุ่มได้', + 'Your category have been updated successfully.' => 'ปรับปรุงกลุ่มเรียบร้อยแล้ว', + 'Unable to update your category.' => 'ไม่สามารถปรับปรุงกลุ่มได้', + 'Remove a category' => 'ลบกลุ่ม', + 'Category removed successfully.' => 'ลบกลุ่มเรียบร้อยแล้ว', + 'Unable to remove this category.' => 'ไม่สามารถลบกลุ่มได้', + 'Category modification for the project "%s"' => 'แก้ไขกลุ่มสำหรับโปรเจค "%s"', + 'Category Name' => 'ชื่อกลุ่ม', + 'Categories for the project "%s"' => 'กลุ่มสำหรับโปรเจค "%s"', + 'Add a new category' => 'เพิ่มกลุ่มใหม่', + 'Do you really want to remove this category: "%s"?' => 'คุณต้องการลบกลุ่ม "%s" ใช่หรือไม่?', + 'Filter by category' => 'กรองตามกลุ่ม', + 'All categories' => 'กลุ่มทั้งหมด', + 'No category' => 'ไม่มีกลุ่ม', + 'The name is required' => 'ต้องการชื่อ', + 'Remove a file' => 'ลบไฟล์', + 'Unable to remove this file.' => 'ไม่สามารถลบไฟล์ได้', + 'File removed successfully.' => 'ลบไฟล์เรียบร้อยแล้ว', + 'Attach a document' => 'แนบเอกสาร', + 'Do you really want to remove this file: "%s"?' => 'คุณต้องการลบไฟล์ "%s" ใช่หรือไม่?', + 'open' => 'เปิด', + 'Attachments' => 'แนบ', + 'Edit the task' => 'แก้ไขงาน', + 'Edit the description' => 'แก้ไขคำอธิบาย', + 'Add a comment' => 'เพิ่มความคิดเห็น', + 'Edit a comment' => 'แก้ไขความคิดเห็น', + 'Summary' => 'สรุป', + 'Time tracking' => 'การติดตามเวลา', + 'Estimate:' => 'ประมาณ:', + 'Spent:' => 'ใช้:', + 'Do you really want to remove this sub-task?' => 'คุณต้องการลบงานย่อยใช่หรือไม่?', + 'Remaining:' => 'เหลือ:', + 'hours' => 'ชั่วโมง', + 'spent' => 'ใช้', + 'estimated' => 'ประมาณ', + 'Sub-Tasks' => 'งานย่อย', + 'Add a sub-task' => 'เพิ่มงานย่อย', + // 'Original estimate' => '', + 'Create another sub-task' => 'สร้างงานย่อยอื่น', + // 'Time spent' => '', + 'Edit a sub-task' => 'แก้ไขงานย่อย', + 'Remove a sub-task' => 'ลบงานย่อย', + 'The time must be a numeric value' => 'เวลาที่ต้องเป็นตัวเลข', + 'Todo' => 'สิ่งที่ต้องทำ', + 'In progress' => 'กำลังดำเนินการ', + 'Sub-task removed successfully.' => 'ลบงานย่อยเรียบร้อยแล้ว', + 'Unable to remove this sub-task.' => 'ไม่สามารถลบงานย่อยได้', + 'Sub-task updated successfully.' => 'ปรับปรุงงานย่อย่่เรียบร้อยแล้ว', + 'Unable to update your sub-task.' => 'ไม่สามารถปรับปรุงานย่อยได้', + 'Unable to create your sub-task.' => 'ไม่สามารถสร้างงานย่อยได้', + 'Sub-task added successfully.' => 'เพิ่มงานย่อยเรียบร้อยแล้ว', + 'Maximum size: ' => 'ขนาดสูงสุด:', + 'Unable to upload the file.' => 'ไม่สามารถอัพโหลดไฟล์ได้', + 'Display another project' => 'แสดงโปรเจคอื่น', + 'Your GitHub account was successfully linked to your profile.' => 'กิทฮับแอคเคาท์เชื่อมต่อกับประวัติเรียบร้อยแล้ว', + 'Unable to link your GitHub Account.' => 'ไม่สามารถเชื่อมต่อกับกิทฮับแอคเคาท์ได้', + 'GitHub authentication failed' => 'การยืนยันกิทฮับผิดพลาด', + 'Your GitHub account is no longer linked to your profile.' => 'กิทฮับแอคเคาท์ไม่ได้มีการเชื่อมโยงไปยังโปรไฟล์ของคุณ', + 'Unable to unlink your GitHub Account.' => 'ไม่สามารถยกเลิกการเชื่อมต่อกิทฮับแอคเคาท์ได้', + 'Login with my GitHub Account' => 'เข้าใช้ด้วยกิทฮับแอคเคาท์', + 'Link my GitHub Account' => 'เชื่อมกับกิทฮับแอคเคาท์', + 'Unlink my GitHub Account' => 'ยกเลิกการเชื่อมกับกิทอับแอคเคาท์', + 'Created by %s' => 'สร้างโดย %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'แก้ไขล่าสุดวันที่ %B %e, %Y เวลา %k:%M %p', + 'Tasks Export' => 'ส่งออกงาน', + 'Tasks exportation for "%s"' => 'ส่งออกงานสำหรับ "%s"', + 'Start Date' => 'เริ่มวันที่', + 'End Date' => 'สิ้นสุดวันที่', + 'Execute' => 'ประมวลผล', + 'Task Id' => 'งาน ไอดี', + 'Creator' => 'ผู้สร้าง', + 'Modification date' => 'วันที่แก้ไข', + 'Completion date' => 'วันที่เสร็จสิ้น', + 'Clone' => 'เลียนแบบ', + // 'Clone Project' => '', + 'Project cloned successfully.' => 'เลียนแบบโปรเจคเรียบร้อยแล้ว', + 'Unable to clone this project.' => 'ไม่สามารถเลียบแบบโปรเจคได้', + 'Email notifications' => 'อีเมลแจ้งเตือน', + 'Enable email notifications' => 'เปิดอีเมลแจ้งเตือน', + 'Task position:' => 'ตำแหน่งงาน', + 'The task #%d have been opened.' => 'งานที่ #%d ถุกเปิด', + 'The task #%d have been closed.' => 'งานที่ #%d ถูกปิด', + 'Sub-task updated' => 'ปรับปรุงงานย่อย', + 'Title:' => 'หัวเรื่อง:', + 'Status:' => 'สถานะ:', + 'Assignee:' => 'กำหนดให้:', + 'Time tracking:' => 'การติดตามเวลา:', + 'New sub-task' => 'งานย่อยใหม่', + 'New attachment added "%s"' => 'เพิ่มการแนบใหม่ "%s"', + 'Comment updated' => 'ปรับปรุงความคิดเห็น', + 'New comment posted by %s' => 'ความคิดเห็นใหม่จาก %s', + 'List of due tasks for the project "%s"' => 'รายการงานสำหรับโปรเจค "%s"', + // 'New attachment' => '', + // 'New comment' => '', + // 'New subtask' => '', + // 'Subtask updated' => '', + // 'Task updated' => '', + // 'Task closed' => '', + // 'Task opened' => '', + '[%s][Due tasks]' => '[%s][งานปัจจุบัน]', + '[Kanboard] Notification' => '[Kanboard] แจ้งเตือน', + 'I want to receive notifications only for those projects:' => 'ฉันต้องการรับการแจ้งเตือนสำหรับโปรเจค:', + 'view the task on Kanboard' => 'แสดงงานบน Kanboard', + 'Public access' => 'การเข้าถึงสาธารณะ', + // 'Category management' => '', + // 'User management' => '', + 'Active tasks' => 'งานที่กำลังใช้งาน', + 'Disable public access' => 'ปิดการเข้าถึงสาธารณะ', + 'Enable public access' => 'เปิดการเข้าถึงสาธารณะ', + 'Active projects' => 'เปิดโปรเจค', + 'Inactive projects' => 'ปิดโปรเจค', + 'Public access disabled' => 'การเข้าถึงสาธารณะถูกปิด', + 'Do you really want to disable this project: "%s"?' => 'คุณต้องการปิดการใช้งานโปรเจคนี้: "%s" ใช่หรือไม่?', + 'Do you really want to duplicate this project: "%s"?' => 'คุณต้องการทำซ้ำโปรเจคนี้ "%s" ใช่หรือไม่?', + 'Do you really want to enable this project: "%s"?' => 'คุณต้องการเปิดการใช้งานโปรเจคนี้: "%s" ใช่หรือไม่?', + 'Project activation' => 'การ เปิด/ปิด ใช้งานโปรเจค', + 'Move the task to another project' => 'ย้ายงานไปโปรเจคอื่น', + 'Move to another project' => 'ย้ายไปโปรเจคอื่น', + 'Do you really want to duplicate this task?' => 'คุณต้องการทำซ้ำงานนี้ใช่หรือไม่?', + 'Duplicate a task' => 'ทำซ้ำงาน', + 'External accounts' => 'บัญชีภายนอก', + 'Account type' => 'ประเภทบัญชี', + 'Local' => 'ท้องถิ่น', + 'Remote' => 'รีโมท', + 'Enabled' => 'เปิดการใช้', + 'Disabled' => 'ปิดการใช้', + 'Google account linked' => 'เชื่อมกับกูเกิลแอคเคาท์', + 'Github account linked' => 'เชื่อมกับกิทฮับแอคเคาท์', + 'Username:' => 'ชื่อผู้ใช้:', + 'Name:' => 'ชื่อ:', + 'Email:' => 'อีเมล:', + 'Default project:' => 'โปรเจคเริ่มต้น:', + 'Notifications:' => 'แจ้งเตือน:', + 'Notifications' => 'การแจ้งเตือน', + 'Group:' => 'กลุ่ม:', + 'Regular user' => 'ผู้ใช้ปกติ:', + 'Account type:' => 'ชนิดบัญชี:', + 'Edit profile' => 'แก้ไขประวัติ', + 'Change password' => 'เปลี่ยนรหัสผ่าน', + 'Password modification' => 'แก้ไขรหัสผ่าน', + 'External authentications' => 'การยืนยันภายนอก', + 'Google Account' => 'กูเกิลแอคเคาท์', + 'Github Account' => 'กิทฮับแอคเคาท์', + 'Never connected.' => 'ไม่เชื่อมต่อ', + 'No account linked.' => 'แอคเคาท์ไม่มีการเชื่อม', + 'Account linked.' => 'แอคเคาท์เชื่อมต่อแล้ว', + 'No external authentication enabled.' => 'ไม่เปิดการใช้งานการยืนยันภายนอก', + 'Password modified successfully.' => 'แก้ไขรหัสผ่านเรียบร้อยแล้ว', + 'Unable to change the password.' => 'ไม่สามารถเปลี่ยนรหัสผ่านได้', + 'Change category for the task "%s"' => 'เปลี่ยนกลุ่มสำหรับงาน "%s"', + 'Change category' => 'เปลี่ยนกลุ่ม', + '%s updated the task %s' => '%s ปรับปรุงงานแล้ว %s', + '%s opened the task %s' => '%s เปิดงานแล้ว %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s ย้ายงานแล้ว %s ไปตำแหน่ง #%d ในคอลัมน์ "%s"', + '%s moved the task %s to the column "%s"' => '%s ย้ายงานแล้ว %s ไปคอลัมน์ "%s"', + '%s created the task %s' => '%s สร้างงานแล้ว %s', + '%s closed the task %s' => '%s ปิดงานแล้ว %s', + '%s created a subtask for the task %s' => '%s สร้างงานย่อยสำหรับงานแล้ว %s', + '%s updated a subtask for the task %s' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว %s', + 'Assigned to %s with an estimate of %s/%sh' => 'กำหนดให้ %s โดยประมาณแล้ว %s/%sh', + 'Not assigned, estimate of %sh' => 'ไม่กำหนดแล้ว, ประมาณเวลาที่ใช้ %s ชั่วโมง', + '%s updated a comment on the task %s' => '%s ปรับปรุงความคิดเห็นในงานแล้ว %s', + '%s commented the task %s' => '%s แสดงความคิดเห็นของงานแล้ว %s', + '%s\'s activity' => 'กิจกรรม %s', + 'No activity.' => 'ไม่มีกิจกรรม', + 'RSS feed' => 'RSS feed', + '%s updated a comment on the task #%d' => '%s ปรับปรุงความคิดเห็นบนงานแล้ว #%d', + '%s commented on the task #%d' => '%s แสดงความคิดเห็นบนงานแล้ว #%d', + '%s updated a subtask for the task #%d' => '%s ปรับปรุงงานย่อยสำหรับงานแล้ว #%d', + '%s created a subtask for the task #%d' => '%s สร้างงานย่อยสำหรับงานแล้ว #%d', + '%s updated the task #%d' => '%s ปรับปรุงงานแล้ว #%d', + '%s created the task #%d' => '%s สร้างงานแล้ว #%d', + '%s closed the task #%d' => '%s ปิดงานแล้ว #%d', + '%s open the task #%d' => '%s เปิดงานแล้ว #%d', + '%s moved the task #%d to the column "%s"' => '%s ย้ายงานแล้ว #%d ไปที่คอลัมน์ "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s ย้ายงานแล้ว #%d ไปตำแหน่ง %d ในคอลัมน์ที่ "%s"', + 'Activity' => 'กิจกรรม', + 'Default values are "%s"' => 'ค่าเริ่มต้น "%s"', + 'Default columns for new projects (Comma-separated)' => 'คอลัมน์เริ่มต้นสำหรับโปรเจคใหม่ (Comma-separated)', + 'Task assignee change' => 'เปลี่ยนการกำหนดบุคคลของงาน', + // '%s change the assignee of the task #%d to %s' => '', + // '%s changed the assignee of the task %s to %s' => '', + // 'Column Change' => '', + // 'Position Change' => '', + // 'Assignee Change' => '', + 'New password for the user "%s"' => 'รหัสผ่านใหม่สำหรับผู้ใช้ "%s"', + // 'Choose an event' => '', + // 'Github commit received' => '', + // 'Github issue opened' => '', + // 'Github issue closed' => '', + // 'Github issue reopened' => '', + // 'Github issue assignee change' => '', + // 'Github issue label change' => '', + // 'Create a task from an external provider' => '', + // 'Change the assignee based on an external username' => '', + // 'Change the category based on an external label' => '', + // 'Reference' => '', + // 'Reference: %s' => '', + // 'Label' => '', + // 'Database' => '', + // 'About' => '', + // 'Database driver:' => '', + // 'Board settings' => '', + // 'URL and token' => '', + // 'Webhook settings' => '', + // 'URL for task creation:' => '', + // 'Reset token' => '', + // 'API endpoint:' => '', + // 'Refresh interval for private board' => '', + // 'Refresh interval for public board' => '', + // 'Task highlight period' => '', + // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', + // 'Frequency in second (60 seconds by default)' => '', + // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', + // 'Application URL' => '', + // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', + // 'Token regenerated.' => '', + // 'Date format' => '', + // 'ISO format is always accepted, example: "%s" and "%s"' => '', + // 'New private project' => '', + // 'This project is private' => '', + // 'Type here to create a new sub-task' => '', + // 'Add' => '', + // 'Estimated time: %s hours' => '', + // 'Time spent: %s hours' => '', + // 'Started on %B %e, %Y' => '', + // 'Start date' => '', + // 'Time estimated' => '', + // 'There is nothing assigned to you.' => '', + // 'My tasks' => '', + // 'Activity stream' => '', + // 'Dashboard' => '', + 'Confirmation' => 'ยืนยันรหัสผ่าน', + // 'Allow everybody to access to this project' => '', + 'Everybody have access to this project.' => 'ทุกคนสามารถเข้าถึงโปรเจคนี้', + // 'Webhooks' => '', + // 'API' => '', + // 'Integration' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', + // 'Create a comment from an external provider' => '', + // 'Github issue comment created' => '', + // 'Configure' => '', + // 'Project management' => '', + // 'My projects' => '', + // 'Columns' => '', + // 'Task' => '', + // 'Your are not member of any project.' => '', + // 'Percentage' => '', + // 'Number of tasks' => '', + // 'Task distribution' => '', + // 'Reportings' => '', + // 'Task repartition for "%s"' => '', + // 'Analytics' => '', + // 'Subtask' => '', + // 'My subtasks' => '', + // 'User repartition' => '', + // 'User repartition for "%s"' => '', + // 'Clone this project' => '', + // 'Column removed successfully.' => '', + // 'Edit Project' => '', + // 'Github Issue' => '', + // 'Not enough data to show the graph.' => '', + // 'Previous' => '', + // 'The id must be an integer' => '', + // 'The project id must be an integer' => '', + // 'The status must be an integer' => '', + // 'The subtask id is required' => '', + // 'The subtask id must be an integer' => '', + // 'The task id is required' => '', + // 'The task id must be an integer' => '', + // 'The user id must be an integer' => '', + // 'This value is required' => '', + // 'This value must be numeric' => '', + // 'Unable to create this task.' => '', + // 'Cumulative flow diagram' => '', + // 'Cumulative flow diagram for "%s"' => '', + // 'Daily project summary' => '', + // 'Daily project summary export' => '', + // 'Daily project summary export for "%s"' => '', + // 'Exports' => '', + // 'This export contains the number of tasks per column grouped per day.' => '', + // 'Nothing to preview...' => '', + // 'Preview' => '', + // 'Write' => '', + // 'Active swimlanes' => '', + // 'Add a new swimlane' => '', + // 'Change default swimlane' => '', + // 'Default swimlane' => '', + // 'Do you really want to remove this swimlane: "%s"?' => '', + // 'Inactive swimlanes' => '', + // 'Set project manager' => '', + // 'Set project member' => '', + // 'Remove a swimlane' => '', + // 'Rename' => '', + // 'Show default swimlane' => '', + // 'Swimlane modification for the project "%s"' => '', + // 'Swimlane not found.' => '', + // 'Swimlane removed successfully.' => '', + // 'Swimlanes' => '', + // 'Swimlane updated successfully.' => '', + // 'The default swimlane have been updated successfully.' => '', + // 'Unable to create your swimlane.' => '', + // 'Unable to remove this swimlane.' => '', + // 'Unable to update this swimlane.' => '', + // 'Your swimlane have been created successfully.' => '', + // 'Example: "Bug, Feature Request, Improvement"' => '', + // 'Default categories for new projects (Comma-separated)' => '', + // 'Gitlab commit received' => '', + // 'Gitlab issue opened' => '', + // 'Gitlab issue closed' => '', + // 'Gitlab webhooks' => '', + // 'Help on Gitlab webhooks' => '', + // 'Integrations' => '', + // 'Integration with third-party services' => '', + // 'Role for this project' => '', + // 'Project manager' => '', + // 'Project member' => '', + // 'A project manager can change the settings of the project and have more privileges than a standard user.' => '', + // 'Gitlab Issue' => '', + // 'Subtask Id' => '', + // 'Subtasks' => '', + // 'Subtasks Export' => '', + // 'Subtasks exportation for "%s"' => '', + // 'Task Title' => '', + // 'Untitled' => '', + // 'Application default' => '', + // 'Language:' => '', + // 'Timezone:' => '', + // 'All columns' => '', + // 'Calendar for "%s"' => '', + // 'Filter by column' => '', + // 'Filter by status' => '', + // 'Calendar' => '', + // 'Next' => '', + // '#%d' => '', + // 'Filter by color' => '', + // 'Filter by swimlane' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Add a comment logging moving the task between columns' => '', + // 'Moved to column %s' => '', + // 'Change description' => '', + // 'User dashboard' => '', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Enable time tracking for subtasks' => '', + // 'Select the new status of the subtask: "%s"' => '', + // 'Subtask timesheet' => '', + // 'There is nothing to show.' => '', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + // 'Which parts of the project do you want to duplicate?' => '', + // 'Change dashboard view' => '', + // 'Show/hide activities' => '', + // 'Show/hide projects' => '', + // 'Show/hide subtasks' => '', + // 'Show/hide tasks' => '', + // 'Disable login form' => '', + // 'Show/hide calendar' => '', + // 'User calendar' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + // 'Task age in days' => '', + // 'Days in this column' => '', + // '%dd' => '', + // 'Add a link' => '', + // 'Add a new link' => '', + // 'Do you really want to remove this link: "%s"?' => '', + // 'Do you really want to remove this link with task #%d?' => '', + // 'Field required' => '', + // 'Link added successfully.' => '', + // 'Link updated successfully.' => '', + // 'Link removed successfully.' => '', + // 'Link labels' => '', + // 'Link modification' => '', + // 'Links' => '', + // 'Link settings' => '', + // 'Opposite label' => '', + // 'Remove a link' => '', + // 'Task\'s links' => '', + // 'The labels must be different' => '', + // 'There is no link.' => '', + // 'This label must be unique' => '', + // 'Unable to create your link.' => '', + // 'Unable to update your link.' => '', + // 'Unable to remove this link.' => '', + // 'relates to' => '', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + // 'This task' => '', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + // 'Expand tasks' => '', + // 'Collapse tasks' => '', + // 'Expand/collapse tasks' => '', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + // 'Keyboard shortcuts' => '', + // 'Open board switcher' => '', + // 'Application' => '', + // 'Filter recently updated' => '', + // 'since %B %e, %Y at %k:%M %p' => '', + // 'More filters' => '', + // 'Compact view' => '', + // 'Horizontal scrolling' => '', + // 'Compact/wide view' => '', + // 'No results match:' => '', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + // 'Hourly rates' => '', + // 'Hourly rate' => '', + // 'Currency' => '', + // 'Effective date' => '', + // 'Add new rate' => '', + // 'Rate removed successfully.' => '', + // 'Unable to remove this rate.' => '', + // 'Unable to save the hourly rate.' => '', + // 'Hourly rate created successfully.' => '', + // 'Start time' => '', + // 'End time' => '', + // 'Comment' => '', + // 'All day' => '', + // 'Day' => '', + // 'Manage timetable' => '', + // 'Overtime timetable' => '', + // 'Time off timetable' => '', + // 'Timetable' => '', + // 'Work timetable' => '', + // 'Week timetable' => '', + // 'Day timetable' => '', + // 'From' => '', + // 'To' => '', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + // 'Remove time slot' => '', + // 'Add new time slot' => '', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + // 'Files' => '', + // 'Images' => '', + // 'Private project' => '', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + // 'Budget' => '', + // 'Budget line' => '', + // 'Budget line removed successfully.' => '', + // 'Budget lines' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + // 'Cost' => '', + // 'Cost breakdown' => '', + // 'Custom Stylesheet' => '', + // 'download' => '', + // 'Do you really want to remove this budget line?' => '', + // 'EUR - Euro' => '', + // 'Expenses' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'New budget line' => '', + // 'NZD - New Zealand Dollar' => '', + // 'Remove a budget line' => '', + // 'Remove budget line' => '', + // 'RSD - Serbian dinar' => '', + // 'The budget line have been created successfully.' => '', + // 'Unable to create the budget line.' => '', + // 'Unable to remove this budget line.' => '', + // 'USD - US Dollar' => '', + // 'Remaining' => '', + // 'Destination column' => '', + // 'Move the task to another column when assigned to a user' => '', + // 'Move the task to another column when assignee is cleared' => '', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php new file mode 100644 index 00000000..f3249ae2 --- /dev/null +++ b/app/Locale/tr_TR/translations.php @@ -0,0 +1,925 @@ +<?php + +return array( + // 'number.decimals_separator' => '', + // 'number.thousands_separator' => '', + 'None' => 'Hiçbiri', + 'edit' => 'düzenle', + 'Edit' => 'Düzenle', + 'remove' => 'sil', + 'Remove' => 'Sil', + 'Update' => 'Güncelle', + 'Yes' => 'Evet', + 'No' => 'Hayır', + 'cancel' => 'İptal', + 'or' => 'veya', + 'Yellow' => 'Sarı', + 'Blue' => 'Mavi', + 'Green' => 'Yeşil', + 'Purple' => 'Mor', + 'Red' => 'Kırmızı', + 'Orange' => 'Turuncu', + 'Grey' => 'Gri', + 'Save' => 'Kaydet', + 'Login' => 'Giriş', + 'Official website:' => 'Resmi internet sitesi:', + 'Unassigned' => 'Atanmamış', + 'View this task' => 'Bu görevi görüntüle', + 'Remove user' => 'Kullanıcıyı kaldır', + 'Do you really want to remove this user: "%s"?' => 'Bu kullanıcıyı gerçekten silmek istiyor musunuz: "%s"?', + 'New user' => 'Yeni kullanıcı', + 'All users' => 'Tüm kullanıcılar', + 'Username' => 'Kullanıcı adı', + 'Password' => 'Şifre', + // 'Default project' => '', + 'Administrator' => 'Yönetici', + 'Sign in' => 'Giriş yap', + 'Users' => 'Kullanıcılar', + 'No user' => 'Kullanıcı yok', + 'Forbidden' => 'Yasak', + 'Access Forbidden' => 'Erişim yasak', + 'Only administrators can access to this page.' => 'Bu sayfaya yalnızca yöneticiler erişebilir.', + 'Edit user' => 'Kullanıcıyı düzenle', + 'Logout' => 'Çıkış yap', + 'Bad username or password' => 'Hatalı kullanıcı adı veya şifre', + 'users' => 'kullanıcılar', + 'projects' => 'projeler', + 'Edit project' => 'Projeyi düzenle', + 'Name' => 'İsim', + 'Activated' => 'Aktif', + 'Projects' => 'Projeler', + 'No project' => 'Proje yok', + 'Project' => 'Proje', + 'Status' => 'Durum', + 'Tasks' => 'Görevler', + 'Board' => 'Tablo', + 'Actions' => 'İşlemler', + 'Inactive' => 'Aktif değil', + 'Active' => 'Aktif', + 'Column %d' => 'Sütun %d', + 'Add this column' => 'Bu sütunu ekle', + '%d tasks on the board' => '%d görev bu tabloda', + '%d tasks in total' => '%d görev toplam', + 'Unable to update this board.' => 'Bu tablo güncellenemiyor.', + 'Edit board' => 'Tabloyu düzenle', + 'Disable' => 'Devre dışı bırak', + 'Enable' => 'Etkinleştir', + 'New project' => 'Yeni proje', + 'Do you really want to remove this project: "%s"?' => 'Bu projeyi gerçekten silmek istiyor musunuz: "%s"?', + 'Remove project' => 'Projeyi sil', + 'Boards' => 'Tablolar', + 'Edit the board for "%s"' => 'Tabloyu "%s" için güncelle', + 'All projects' => 'Tüm projeler', + 'Change columns' => 'Sütunları değiştir', + 'Add a new column' => 'Yeni sütun ekle', + 'Title' => 'Başlık', + 'Add Column' => 'Sütun ekle', + 'Project "%s"' => 'Proje "%s"', + 'Nobody assigned' => 'Kullanıcı atanmamış', + 'Assigned to %s' => '%s kullanıcısına atanmış', + 'Remove a column' => 'Bir sütunu sil', + 'Remove a column from a board' => 'Tablodan bir sütunu sil', + 'Unable to remove this column.' => 'Bu sütun silinemiyor.', + 'Do you really want to remove this column: "%s"?' => 'Bu sütunu gerçekten silmek istiyor musunuz: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Bu komut sütun içindeki TÜM GÖREVLERİ silecek!', + 'Settings' => 'Ayarlar', + 'Application settings' => 'Uygulama ayarları', + 'Language' => 'Dil', + // 'Webhook token:' => '', + 'API token:' => 'API Token:', + 'More information' => 'Daha fazla bilgi', + 'Database size:' => 'Veritabanı boyutu :', + 'Download the database' => 'Veritabanını indir', + 'Optimize the database' => 'Veritabanını optimize et', + '(VACUUM command)' => '(VACUUM komutu)', + '(Gzip compressed Sqlite file)' => '(Gzip ile sıkıştırılmış Sqlite dosyası)', + 'User settings' => 'Kullanıcı ayarları', + 'My default project:' => 'Benim varsayılan projem:', + 'Close a task' => 'Bir görevi kapat', + 'Do you really want to close this task: "%s"?' => 'Bu görevi gerçekten kapatmak istiyor musunuz: "%s"?', + 'Edit a task' => 'Bir görevi düzenle', + 'Column' => 'Sütun', + 'Color' => 'Renk', + 'Assignee' => 'Atanan', + 'Create another task' => 'Başka bir görev oluştur', + 'New task' => 'Nouvelle tâche', + 'Open a task' => 'Bir görevi aç', + 'Do you really want to open this task: "%s"?' => 'Bu görevi gerçekten açmak istiyor musunuz: "%s"?', + 'Back to the board' => 'Tabloya dön', + // 'Created on %B %e, %Y at %k:%M %p' => '', + 'There is nobody assigned' => 'Kimse atanmamış', + 'Column on the board:' => 'Tablodaki sütun:', + 'Status is open' => 'Açık durumda', + 'Status is closed' => 'Kapalı durumda', + 'Close this task' => 'Görevi kapat', + 'Open this task' => 'Görevi aç', + 'There is no description.' => 'Açıklama yok.', + 'Add a new task' => 'Yeni görev ekle', + 'The username is required' => 'Kullanıcı adı gerekli', + 'The maximum length is %d characters' => 'Maksimum uzunluk %d karakterdir', + 'The minimum length is %d characters' => 'Minimum uzunluk %d karakterdir', + 'The password is required' => 'Şifre gerekli', + 'This value must be an integer' => 'Bu değer bir rakam olmak zorunda', + 'The username must be unique' => 'Kullanıcı adı daha önceden var', + 'The username must be alphanumeric' => 'Kullanıcı adı alfanumerik olmalı (geçersiz karakter var)', + 'The user id is required' => 'Kullanıcı kodu gerekli', + 'Passwords don\'t match' => 'Şifreler uyuşmuyor', + 'The confirmation is required' => 'Onay gerekli', + 'The column is required' => 'Sütun gerekli', + 'The project is required' => 'Proje gerekli', + 'The color is required' => 'Renk gerekli', + 'The id is required' => 'Kod gerekli', + 'The project id is required' => 'Proje kodu gerekli', + 'The project name is required' => 'Proje adı gerekli', + 'This project must be unique' => 'Bu projenin tekil olması gerekli', + 'The title is required' => 'Başlık gerekli', + 'The language is required' => 'Dil seçimi gerekli', + 'There is no active project, the first step is to create a new project.' => 'Aktif bir proje yok. İlk aşama yeni bir proje oluşturmak olmalı.', + 'Settings saved successfully.' => 'Ayarlar başarıyla kaydedildi.', + 'Unable to save your settings.' => 'Ayarlarınız kaydedilemedi.', + 'Database optimization done.' => 'Veritabanı optimizasyonu tamamlandı.', + 'Your project have been created successfully.' => 'Projeniz başarıyla oluşturuldu.', + 'Unable to create your project.' => 'Proje oluşturulamadı.', + 'Project updated successfully.' => 'Proje başarıyla güncellendi.', + 'Unable to update this project.' => 'Bu proje güncellenemedi.', + 'Unable to remove this project.' => 'Bu proje silinemedi.', + 'Project removed successfully.' => 'Proje başarıyla silindi.', + 'Project activated successfully.' => 'Proje başarıyla aktive edildi.', + 'Unable to activate this project.' => 'Bu proje aktive edilemedi.', + 'Project disabled successfully.' => 'Proje devre dışı bırakıldı.', + 'Unable to disable this project.' => 'Bu proje devre dışı bırakılamadı.', + 'Unable to open this task.' => 'Bu görev açılamıyor.', + 'Task opened successfully.' => 'Görev başarıyla açıldı.', + 'Unable to close this task.' => 'Bu görev kapatılamıyor.', + 'Task closed successfully.' => 'Görev başarıyla kapatıldı.', + 'Unable to update your task.' => 'Görev güncellenemiyor.', + 'Task updated successfully.' => 'Görev başarıyla güncellendi.', + 'Unable to create your task.' => 'Görev oluşturulamadı.', + 'Task created successfully.' => 'Görev başarıyla oluşturuldu.', + 'User created successfully.' => 'Kullanıcı başarıyla oluşturuldu', + 'Unable to create your user.' => 'Kullanıcı oluşturulamıyor.', + 'User updated successfully.' => 'Kullanıcı başarıyla güncellendi.', + 'Unable to update your user.' => 'Kullanıcı güncellenemiyor.', + 'User removed successfully.' => 'Kullanıcı silindi.', + 'Unable to remove this user.' => 'Bu kullanıcı silinemiyor.', + 'Board updated successfully.' => 'Tablo başarıyla güncellendi.', + 'Ready' => 'Hazır', + 'Backlog' => 'Bekleme listesi', + 'Work in progress' => 'İşlemde', + 'Done' => 'Tamamlandı', + 'Application version:' => 'Uygulama versiyonu:', + // 'Completed on %B %e, %Y at %k:%M %p' => '', + // '%B %e, %Y at %k:%M %p' => '', + 'Date created' => 'Oluşturulma tarihi', + 'Date completed' => 'Tamamlanma tarihi', + 'Id' => 'Kod', + 'No task' => 'Görev yok', + 'Completed tasks' => 'Tamamlanan görevler', + 'List of projects' => 'Proje listesi', + 'Completed tasks for "%s"' => '"%s" için tamamlanan görevler', + '%d closed tasks' => '%d kapatılmış görevler', + // 'No task for this project' => '', + 'Public link' => 'Dışa açık link', + 'There is no column in your project!' => 'Projenizde hiç sütun yok', + 'Change assignee' => 'Atanmış Kullanıcıyı değiştir', + 'Change assignee for the task "%s"' => '"%s" görevi için atanmış kullanıcıyı değiştir', + 'Timezone' => 'Saat dilimi', + // 'Sorry, I didn\'t find this information in my database!' => '', + 'Page not found' => 'Sayfa bulunamadı', + 'Complexity' => 'Zorluk seviyesi', + 'limit' => 'limit', + 'Task limit' => 'Görev limiti', + 'Task count' => 'Görev sayısı', + 'This value must be greater than %d' => 'Bu değer %d den büyük olmalı', + 'Edit project access list' => 'Proje erişim listesini düzenle', + 'Edit users access' => 'Kullanıcı erişim haklarını düzenle', + 'Allow this user' => 'Bu kullanıcıya izin ver', + 'Only those users have access to this project:' => 'Bu projeye yalnızca şu kullanıcılar erişebilir:', + 'Don\'t forget that administrators have access to everything.' => 'Dikkat: Yöneticilerin herşeye erişimi olduğunu unutmayın!', + 'Revoke' => 'Iptal et', + 'List of authorized users' => 'Yetkili kullanıcıların listesi', + 'User' => 'Kullanıcı', + 'Nobody have access to this project.' => 'Bu projeye kimsenin erişimi yok.', + 'You are not allowed to access to this project.' => 'Bu projeye giriş yetkiniz yok.', + 'Comments' => 'Yorumlar', + 'Post comment' => 'Yorum ekle', + 'Write your text in Markdown' => 'Yazınızı Markdown ile yazın', + 'Leave a comment' => 'Bir yorum ekle', + 'Comment is required' => 'Yorum gerekli', + 'Leave a description' => 'Açıklama ekleyin', + 'Comment added successfully.' => 'Yorum eklendi', + 'Unable to create your comment.' => 'Yorumunuz oluşturulamadı', + 'The description is required' => 'Açıklama gerekli', + 'Edit this task' => 'Bu görevi değiştir', + 'Due Date' => 'Termin', + 'Invalid date' => 'Geçersiz tarihi', + // 'Must be done before %B %e, %Y' => '', + '%B %e, %Y' => '%d %B %Y', + '%b %e, %Y' => '%d/%m/%Y', + 'Automatic actions' => 'Otomatik işlemler', + 'Your automatic action have been created successfully.' => 'Otomatik işlem başarıyla oluşturuldu', + 'Unable to create your automatic action.' => 'Otomatik işleminiz oluşturulamadı', + 'Remove an action' => 'Bir işlemi sil', + 'Unable to remove this action.' => 'Bu işlem silinemedi', + 'Action removed successfully.' => 'İşlem başarıyla silindi', + 'Automatic actions for the project "%s"' => '"%s" projesi için otomatik işlemler', + 'Defined actions' => 'Tanımlanan işlemler', + 'Add an action' => 'İşlem ekle', + 'Event name' => 'Durum adı', + 'Action name' => 'İşlem adı', + 'Action parameters' => 'İşlem parametreleri', + 'Action' => 'İşlem', + 'Event' => 'Durum', + 'When the selected event occurs execute the corresponding action.' => 'Seçilen durum oluştuğunda ilgili eylemi gerçekleştir.', + 'Next step' => 'Sonraki adım', + 'Define action parameters' => 'İşlem parametrelerini düzenle', + 'Save this action' => 'Bu işlemi kaydet', + 'Do you really want to remove this action: "%s"?' => 'Bu işlemi silmek istediğinize emin misiniz: "%s"?', + 'Remove an automatic action' => 'Bir otomatik işlemi sil', + 'Close the task' => 'Görevi kapat', + 'Assign the task to a specific user' => 'Görevi bir kullanıcıya ata', + 'Assign the task to the person who does the action' => 'Görevi, işlemi gerçekleştiren kullanıcıya ata', + 'Duplicate the task to another project' => 'Görevi bir başka projeye kopyala', + 'Move a task to another column' => 'Bir görevi başka bir sütuna taşı', + 'Move a task to another position in the same column' => 'Bir görevin aynı sütunda yerini değiştir', + 'Task modification' => 'Görev düzenleme', + 'Task creation' => 'Görev oluşturma', + 'Open a closed task' => 'Kapalı bir görevi aç', + 'Closing a task' => 'Bir görev kapatılıyor', + 'Assign a color to a specific user' => 'Bir kullanıcıya renk tanımla', + 'Column title' => 'Sütun başlığı', + 'Position' => 'Pozisyon', + 'Move Up' => 'Yukarı taşı', + 'Move Down' => 'Aşağı taşı', + 'Duplicate to another project' => 'Başka bir projeye kopyala', + 'Duplicate' => 'Kopya oluştur', + 'link' => 'link', + 'Update this comment' => 'Bu yorumu güncelle', + 'Comment updated successfully.' => 'Yorum güncellendi.', + 'Unable to update your comment.' => 'Yorum güncellenemedi.', + 'Remove a comment' => 'Bir yorumu sil', + 'Comment removed successfully.' => 'Yorum silindi.', + 'Unable to remove this comment.' => 'Bu yorum silinemiyor.', + 'Do you really want to remove this comment?' => 'Bu yorumu silmek istediğinize emin misiniz?', + 'Only administrators or the creator of the comment can access to this page.' => 'Bu sayfaya yalnızca yorum sahibi ve yöneticiler erişebilir.', + 'Details' => 'Detaylar', + 'Current password for the user "%s"' => 'Kullanıcı için mevcut şifre "%s"', + 'The current password is required' => 'Mevcut şifre gerekli', + 'Wrong password' => 'Yanlış Şifre', + 'Reset all tokens' => 'Tüm fişleri sıfırla', + 'All tokens have been regenerated.' => 'Tüm fişler yeniden oluşturuldu.', + 'Unknown' => 'Bilinmeyen', + 'Last logins' => 'Son kullanıcı girişleri', + 'Login date' => 'Giriş tarihi', + 'Authentication method' => 'Doğrulama yöntemi', + 'IP address' => 'IP adresi', + 'User agent' => 'Kullanıcı sistemi', + 'Persistent connections' => 'Kalıcı bağlantılar', + // 'No session.' => '', + 'Expiration date' => 'Geçerlilik sonu', + 'Remember Me' => 'Beni hatırla', + 'Creation date' => 'Oluşturulma tarihi', + 'Filter by user' => 'Kullanıcıya göre filtrele', + 'Filter by due date' => 'Termine göre filtrele', + 'Everybody' => 'Herkes', + 'Open' => 'Açık', + 'Closed' => 'Kapalı', + 'Search' => 'Ara', + 'Nothing found.' => 'Hiçbir şey bulunamadı', + 'Search in the project "%s"' => '"%s" Projesinde ara', + 'Due date' => 'Termin', + 'Others formats accepted: %s and %s' => 'Diğer kabul edilen formatlar: %s ve %s', + 'Description' => 'Açıklama', + '%d comments' => '%d yorumlar', + '%d comment' => '%d yorum', + 'Email address invalid' => 'E-Posta adresi geçersiz', + 'Your Google Account is not linked anymore to your profile.' => 'Google hesabınız artık profilinize bağlı değil', + 'Unable to unlink your Google Account.' => 'Google hesabınızla bağ koparılamadı', + 'Google authentication failed' => 'Google hesap doğrulaması başarısız', + 'Unable to link your Google Account.' => 'Google hesabınızla bağ oluşturulamadı', + 'Your Google Account is linked to your profile successfully.' => 'Google hesabınız profilinize başarıyla bağlandı', + 'Email' => 'E-Posta', + 'Link my Google Account' => 'Google hesabımla bağ oluştur', + 'Unlink my Google Account' => 'Google hesabımla bağı kaldır', + 'Login with my Google Account' => 'Google hesabımla giriş yap', + 'Project not found.' => 'Proje bulunamadı', + 'Task #%d' => 'Görev #%d', + 'Task removed successfully.' => 'Görev silindi', + 'Unable to remove this task.' => 'Görev silinemiyor', + 'Remove a task' => 'Bir görevi sil', + 'Do you really want to remove this task: "%s"?' => 'Bu görevi silmek istediğinize emin misiniz: "%s"?', + 'Assign automatically a color based on a category' => 'Kategoriye göre otomatik renk ata', + 'Assign automatically a category based on a color' => 'Rengine göre otomatik kategori ata', + 'Task creation or modification' => 'Görev oluşturma veya değiştirme', + 'Category' => 'Kategori', + 'Category:' => 'Kategori:', + 'Categories' => 'Kategoriler', + 'Category not found.' => 'Kategori bulunamadı', + 'Your category have been created successfully.' => 'Kategori oluşturuldu', + 'Unable to create your category.' => 'Kategori oluşturulamadı', + 'Your category have been updated successfully.' => 'Kategori başarıyla güncellendi', + 'Unable to update your category.' => 'Kategori güncellenemedi', + 'Remove a category' => 'Bir kategoriyi sil', + 'Category removed successfully.' => 'Kategori silindi', + 'Unable to remove this category.' => 'Bu kategori silinemedi', + 'Category modification for the project "%s"' => '"%s" projesi için kategori değiştirme', + 'Category Name' => 'Kategori adı', + 'Categories for the project "%s"' => '"%s" Projesi için kategoriler', + 'Add a new category' => 'Yeni kategori ekle', + 'Do you really want to remove this category: "%s"?' => 'Bu kategoriyi silmek istediğinize emin misiniz: "%s"?', + 'Filter by category' => 'Kategoriye göre filtrele', + 'All categories' => 'Tüm kategoriler', + 'No category' => 'Kategori Yok', + 'The name is required' => 'İsim gerekli', + 'Remove a file' => 'Dosya sil', + 'Unable to remove this file.' => 'Dosya silinemedi', + 'File removed successfully.' => 'Dosya silindi', + 'Attach a document' => 'Dosya ekle', + 'Do you really want to remove this file: "%s"?' => 'Bu dosyayı silmek istediğinize emin misiniz: "%s"?', + 'open' => 'aç', + 'Attachments' => 'Ekler', + 'Edit the task' => 'Görevi değiştir', + 'Edit the description' => 'Açıklamayı değiştir', + 'Add a comment' => 'Yorum ekle', + 'Edit a comment' => 'Yorum değiştir', + 'Summary' => 'Özet', + 'Time tracking' => 'Zaman takibi', + 'Estimate:' => 'Tahmini:', + 'Spent:' => 'Harcanan:', + 'Do you really want to remove this sub-task?' => 'Bu alt görevi silmek istediğinize emin misiniz', + 'Remaining:' => 'Kalan', + 'hours' => 'saat', + 'spent' => 'harcanan', + 'estimated' => 'tahmini', + 'Sub-Tasks' => 'Alt Görev', + 'Add a sub-task' => 'Alt görev ekle', + // 'Original estimate' => '', + 'Create another sub-task' => 'Başka bir alt görev daha oluştur', + // 'Time spent' => '', + 'Edit a sub-task' => 'Alt görev düzenle', + 'Remove a sub-task' => 'Alt görev sil', + 'The time must be a numeric value' => 'Zaman alfanumerik bir değer olmalı', + 'Todo' => 'Yapılacaklar', + 'In progress' => 'İşlemde', + 'Sub-task removed successfully.' => 'Alt görev silindi', + 'Unable to remove this sub-task.' => 'Alt görev silinemedi', + 'Sub-task updated successfully.' => 'Alt görev güncellendi', + 'Unable to update your sub-task.' => 'Alt görev güncellenemiyor', + 'Unable to create your sub-task.' => 'Alt görev oluşturulamadı', + 'Sub-task added successfully.' => 'Alt görev başarıyla eklendii', + 'Maximum size: ' => 'Maksimum boyutu', + 'Unable to upload the file.' => 'Karşıya yükleme başarısız', + 'Display another project' => 'Başka bir proje göster', + 'Your GitHub account was successfully linked to your profile.' => 'GitHub Hesabınız Profilinize bağlandı.', + 'Unable to link your GitHub Account.' => 'GitHub hesabınızla bağ oluşturulamadı.', + // 'GitHub authentication failed' => '', + // 'Your GitHub account is no longer linked to your profile.' => '', + // 'Unable to unlink your GitHub Account.' => '', + // 'Login with my GitHub Account' => '', + // 'Link my GitHub Account' => '', + // 'Unlink my GitHub Account' => '', + 'Created by %s' => '%s tarafından oluşturuldu', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Son değişiklik tarihi %d.%m.%Y, saati %H:%M', + 'Tasks Export' => 'Görevleri dışa aktar', + 'Tasks exportation for "%s"' => '"%s" için görevleri dışa aktar', + 'Start Date' => 'Başlangıç tarihi', + 'End Date' => 'Bitiş tarihi', + 'Execute' => 'Gerçekleştir', + 'Task Id' => 'Görev No', + 'Creator' => 'Oluşturan', + 'Modification date' => 'Değişiklik tarihi', + 'Completion date' => 'Tamamlanma tarihi', + 'Clone' => 'Kopya oluştur', + 'Clone Project' => 'Projenin kopyasını oluştur', + 'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.', + 'Unable to clone this project.' => 'Proje kopyası oluşturulamadı.', + 'Email notifications' => 'E-Posta bilgilendirmesi', + 'Enable email notifications' => 'E-Posta bilgilendirmesini aç', + 'Task position:' => 'Görev pozisyonu', + 'The task #%d have been opened.' => '#%d numaralı görev açıldı.', + 'The task #%d have been closed.' => '#%d numaralı görev kapatıldı.', + 'Sub-task updated' => 'Alt görev güncellendi', + 'Title:' => 'Başlık', + 'Status:' => 'Durum', + 'Assignee:' => 'Sorumlu:', + 'Time tracking:' => 'Zaman takibi', + 'New sub-task' => 'Yeni alt görev', + 'New attachment added "%s"' => 'Yeni dosya "%s" eklendi.', + 'Comment updated' => 'Yorum güncellendi', + 'New comment posted by %s' => '%s tarafından yeni yorum eklendi', + 'List of due tasks for the project "%s"' => '"%s" projesi için ilgili görevlerin listesi', + 'New attachment' => 'Yeni dosya eki', + 'New comment' => 'Yeni yorum', + 'New subtask' => 'Yeni alt görev', + 'Subtask updated' => 'Alt görev güncellendi', + 'Task updated' => 'Görev güncellendi', + 'Task closed' => 'Görev kapatıldı', + 'Task opened' => 'Görev açıldı', + '[%s][Due tasks]' => '[%s][İlgili görevler]', + '[Kanboard] Notification' => '[Kanboard] Bildirim', + 'I want to receive notifications only for those projects:' => 'Yalnızca bu projelerle ilgili bildirim almak istiyorum:', + 'view the task on Kanboard' => 'bu görevi Kanboard\'da göster', + 'Public access' => 'Dışa açık erişim', + 'Category management' => 'Kategori yönetimi', + 'User management' => 'Kullanıcı yönetimi', + 'Active tasks' => 'Aktif görevler', + 'Disable public access' => 'Dışa açık erişimi kapat', + 'Enable public access' => 'Dışa açık erişimi aç', + 'Active projects' => 'Aktif projeler', + 'Inactive projects' => 'Aktif olmayan projeler', + 'Public access disabled' => 'Dışa açık erişim kapatıldı', + 'Do you really want to disable this project: "%s"?' => 'Bu projeyi devre dışı bırakmak istediğinize emin misiniz?: "%s"', + 'Do you really want to duplicate this project: "%s"?' => 'Bu projenin kopyasını oluşturmak istediğinize emin misiniz?: "%s"', + 'Do you really want to enable this project: "%s"?' => 'Bu projeyi aktive etmek istediğinize emin misiniz?: "%s"', + 'Project activation' => 'Proje aktivasyonu', + 'Move the task to another project' => 'Görevi başka projeye taşı', + 'Move to another project' => 'Başka projeye taşı', + 'Do you really want to duplicate this task?' => 'Bu görevin kopyasını oluşturmak istediğinize emin misiniz?', + 'Duplicate a task' => 'Görevin kopyasını oluştur', + 'External accounts' => 'Dış hesaplar', + 'Account type' => 'Hesap türü', + 'Local' => 'Yerel', + 'Remote' => 'Uzak', + 'Enabled' => 'Etkinleştirildi', + 'Disabled' => 'Devre dışı bırakıldı', + 'Google account linked' => 'Google hesabıyla bağlı', + 'Github account linked' => 'Github hesabıyla bağlı', + 'Username:' => 'Kullanıcı adı', + 'Name:' => 'Ad', + 'Email:' => 'E-Posta', + 'Default project:' => 'Varsayılan Proje:', + 'Notifications:' => 'Bildirimler:', + 'Notifications' => 'Bildirimler', + 'Group:' => 'Grup', + 'Regular user' => 'Varsayılan kullanıcı', + 'Account type:' => 'Hesap türü:', + 'Edit profile' => 'Profili değiştir', + 'Change password' => 'Şifre değiştir', + 'Password modification' => 'Şifre değişimi', + 'External authentications' => 'Dış kimlik doğrulamaları', + 'Google Account' => 'Google hesabı', + 'Github Account' => 'Github hesabı', + 'Never connected.' => 'Hiç bağlanmamış.', + 'No account linked.' => 'Bağlanmış hesap yok.', + 'Account linked.' => 'Hesap bağlandı', + 'No external authentication enabled.' => 'Dış kimlik doğrulama kapalı.', + 'Password modified successfully.' => 'Şifre başarıyla değiştirildi.', + 'Unable to change the password.' => 'Şifre değiştirilemedi.', + 'Change category for the task "%s"' => '"%s" görevi için kategori değiştirme', + 'Change category' => 'Kategori değiştirme', + '%s updated the task %s' => '%s kullanıcısı %s görevini güncelledi', + '%s opened the task %s' => '%s kullanıcısı %s görevini açtı', + '%s moved the task %s to the position #%d in the column "%s"' => '%s kullanıcısı %s görevini #%d pozisyonu "%s" sütununa taşıdı', + '%s moved the task %s to the column "%s"' => '%s kullanıcısı %s görevini "%s" sütununa taşıdı', + '%s created the task %s' => '%s kullanıcısı %s görevini oluşturdu', + '%s closed the task %s' => '%s kullanıcısı %s görevini kapattı', + '%s created a subtask for the task %s' => '%s kullanıcısı %s görevi için bir alt görev oluşturdu', + '%s updated a subtask for the task %s' => '%s kullanıcısı %s görevinin bir alt görevini güncelledi', + 'Assigned to %s with an estimate of %s/%sh' => '%s kullanıcısına tahmini %s/%s saat tamamlanma süresi ile atanmış', + 'Not assigned, estimate of %sh' => 'Kimseye atanmamış, tahmini süre %s saat', + '%s updated a comment on the task %s' => '%s kullanıcısı %s görevinde bir yorumu güncelledi', + '%s commented the task %s' => '%s kullanıcısı %s görevine yorum ekledi', + '%s\'s activity' => '%s\'in aktivitesi', + 'No activity.' => 'Aktivite yok.', + 'RSS feed' => 'RSS kaynağı', + '%s updated a comment on the task #%d' => '%s kullanıcısı #%d nolu görevde bir yorumu güncelledi', + '%s commented on the task #%d' => '%s kullanıcısı #%d nolu göreve yorum ekledi', + '%s updated a subtask for the task #%d' => '%s kullanıcısı #%d nolu görevin bir alt görevini güncelledi', + '%s created a subtask for the task #%d' => '%s kullanıcısı #%d nolu göreve bir alt görev ekledi', + '%s updated the task #%d' => '%s kullanıcısı #%d nolu görevi güncelledi', + '%s created the task #%d' => '%s kullanıcısı #%d nolu görevi oluşturdu', + '%s closed the task #%d' => '%s kullanıcısı #%d nolu görevi kapattı', + '%s open the task #%d' => '%s kullanıcısı #%d nolu görevi açtı', + '%s moved the task #%d to the column "%s"' => '%s kullanıcısı #%d nolu görevi "%s" sütununa taşıdı', + '%s moved the task #%d to the position %d in the column "%s"' => '%s kullanıcısı #%d nolu görevi %d pozisyonu "%s" sütununa taşıdı', + 'Activity' => 'Aktivite', + 'Default values are "%s"' => 'Varsayılan değerler "%s"', + 'Default columns for new projects (Comma-separated)' => 'Yeni projeler için varsayılan sütunlar (virgül ile ayrılmış)', + 'Task assignee change' => 'Göreve atanan kullanıcı değişikliği', + '%s change the assignee of the task #%d to %s' => '%s kullanıcısı #%d nolu görevin sorumlusunu %s olarak değiştirdi', + '%s changed the assignee of the task %s to %s' => '%s kullanıcısı %s görevinin sorumlusunu %s olarak değiştirdi', + 'Column Change' => 'Sütun değişikliği', + 'Position Change' => 'Konum değişikliği', + 'Assignee Change' => 'Sorumlu değişikliği', + 'New password for the user "%s"' => '"%s" kullanıcısı için yeni şifre', + 'Choose an event' => 'Bir durum seçin', + // 'Github commit received' => '', + // 'Github issue opened' => '', + // 'Github issue closed' => '', + // 'Github issue reopened' => '', + // 'Github issue assignee change' => '', + // 'Github issue label change' => '', + 'Create a task from an external provider' => 'Dış sağlayıcı ile bir görev oluştur', + 'Change the assignee based on an external username' => 'Dış kaynaklı kullanıcı adı ile göreve atananı değiştir', + 'Change the category based on an external label' => 'Dış kaynaklı bir etiket ile kategori değiştir', + 'Reference' => 'Referans', + 'Reference: %s' => 'Referans: %s', + 'Label' => 'Etiket', + 'Database' => 'Veri bankası', + 'About' => 'Hakkında', + 'Database driver:' => 'Veri bankası sürücüsü', + 'Board settings' => 'Tablo ayarları', + 'URL and token' => 'URL veya Token', + 'Webhook settings' => 'Webhook ayarları', + 'URL for task creation:' => 'Görev oluşturma için URL', + 'Reset token' => 'Reset Token', + 'API endpoint:' => 'API endpoint', + 'Refresh interval for private board' => 'Özel tablolar için yenileme sıklığı', + 'Refresh interval for public board' => 'Dışa açık tablolar için yenileme sıklığı', + 'Task highlight period' => 'Görevi öne çıkarma süresi', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Bir görevin yeni değiştirilmiş sayılması için süre (saniye olarak) (Bu özelliği iptal etmek için 0, varsayılan değer 2 gün)', + 'Frequency in second (60 seconds by default)' => 'Saniye olarak frekans (varsayılan 60 saniye)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Saniye olarak frekans (Bu özelliği iptal etmek için 0, varsayılan değer 10 saniye)', + 'Application URL' => 'Uygulama URL', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Örneğin: http://example.kanboard.net/ (E-posta bildirimleri için kullanılıyor)', + 'Token regenerated.' => 'Token yeniden oluşturuldu.', + 'Date format' => 'Tarih formatı', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formatı her zaman kabul edilir, örneğin: "%s" ve "%s"', + 'New private project' => 'Yeni özel proje', + 'This project is private' => 'Bu proje özel', + 'Type here to create a new sub-task' => 'Yeni bir alt görev oluşturmak için buraya yazın', + 'Add' => 'Ekle', + 'Estimated time: %s hours' => 'Tahmini süre: %s Saat', + 'Time spent: %s hours' => 'Kullanılan süre: %s Saat', + 'Started on %B %e, %Y' => '%B %e %Y tarihinde başlatıldı', + 'Start date' => 'Başlangıç tarihi', + 'Time estimated' => 'Tahmini süre', + 'There is nothing assigned to you.' => 'Size atanan hiçbir şey yok.', + 'My tasks' => 'Görevlerim', + 'Activity stream' => 'Güncel olay akışı', + 'Dashboard' => 'Anasayfa', + 'Confirmation' => 'Onay', + 'Allow everybody to access to this project' => 'Bu projeye herkesin erişimine izin ver', + 'Everybody have access to this project.' => 'Bu projeye herkesin erişimi var.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Integration' => 'Entegrasyon', + 'Github webhooks' => 'Github Webhook', + 'Help on Github webhooks' => 'Github Webhooks hakkında yardım', + 'Create a comment from an external provider' => 'Dış sağlayıcı ile bir yorum oluştur', + 'Github issue comment created' => 'Github hata yorumu oluşturuldu', + 'Configure' => 'Ayarla', + 'Project management' => 'Proje yönetimi', + 'My projects' => 'Projelerim', + 'Columns' => 'Sütunlar', + 'Task' => 'Görev', + 'Your are not member of any project.' => 'Hiç bir projenin üyesi değilsiniz.', + 'Percentage' => 'Yüzde', + 'Number of tasks' => 'Görev sayısı', + 'Task distribution' => 'Görev dağılımı', + 'Reportings' => 'Raporlar', + 'Task repartition for "%s"' => '"%s" için görev dağılımı', + 'Analytics' => 'Analiz', + 'Subtask' => 'Alt görev', + 'My subtasks' => 'Alt görevlerim', + 'User repartition' => 'Kullanıcı dağılımı', + 'User repartition for "%s"' => '"%s" için kullanıcı dağılımı', + 'Clone this project' => 'Projenin kopyasını oluştur', + 'Column removed successfully.' => 'Sütun başarıyla kaldırıldı.', + 'Edit Project' => 'Projeyi düzenle', + 'Github Issue' => 'Github Issue', + 'Not enough data to show the graph.' => 'Grafik gösterimi için yeterli veri yok.', + 'Previous' => 'Önceki', + 'The id must be an integer' => 'ID bir tamsayı olmalı', + 'The project id must be an integer' => 'Proje numarası bir tam sayı olmalı', + 'The status must be an integer' => 'Durum bir tam sayı olmalı', + 'The subtask id is required' => 'Alt görev numarası gerekli', + 'The subtask id must be an integer' => 'Alt görev numarası bir tam sayı olmalı', + 'The task id is required' => 'Görev numarası gerekli', + 'The task id must be an integer' => 'Görev numarası bir tam sayı olmalı', + 'The user id must be an integer' => 'Kullanıcı numarası bir tam sayı olmalı', + 'This value is required' => 'Bu değer gerekli', + 'This value must be numeric' => 'Bu değer sayı olmalı', + 'Unable to create this task.' => 'Bu görev oluşturulamıyor.', + 'Cumulative flow diagram' => 'Kümülatif akış diyagramı', + 'Cumulative flow diagram for "%s"' => '"%s" için kümülatif akış diyagramı', + 'Daily project summary' => 'Günlük proje özeti', + 'Daily project summary export' => 'Günlük proje özetini dışa aktar', + 'Daily project summary export for "%s"' => '"%s" için günlük proje özetinin dışa', + 'Exports' => 'Dışa aktarımlar', + 'This export contains the number of tasks per column grouped per day.' => 'Bu dışa aktarım günlük gruplanmış olarak her sütundaki görev sayısını içerir.', + 'Nothing to preview...' => 'Önizleme yapılacak bir şey yok ...', + 'Preview' => 'Önizleme', + 'Write' => 'Değiştir', + 'Active swimlanes' => 'Aktif Kulvar', + 'Add a new swimlane' => 'Yeni bir Kulvar ekle', + 'Change default swimlane' => 'Varsayılan Kulvarı değiştir', + 'Default swimlane' => 'Varsayılan Kulvar', + 'Do you really want to remove this swimlane: "%s"?' => 'Bu Kulvarı silmek istediğinize emin misiniz?: "%s"?', + 'Inactive swimlanes' => 'Pasif Kulvarlar', + 'Set project manager' => 'Proje yöneticisi olarak ata', + 'Set project member' => 'Proje üyesi olarak ata', + 'Remove a swimlane' => 'Kulvarı sil', + 'Rename' => 'Yeniden adlandır', + 'Show default swimlane' => 'Varsayılan Kulvarı göster', + 'Swimlane modification for the project "%s"' => '"% s" Projesi için Kulvar değişikliği', + 'Swimlane not found.' => 'Kulvar bulunamadı', + 'Swimlane removed successfully.' => 'Kulvar başarıyla kaldırıldı.', + 'Swimlanes' => 'Kulvarlar', + 'Swimlane updated successfully.' => 'Kulvar başarıyla güncellendi.', + 'The default swimlane have been updated successfully.' => 'Varsayılan Kulvarlar başarıyla güncellendi.', + 'Unable to create your swimlane.' => 'Bu Kulvarı oluşturmak mümkün değil.', + 'Unable to remove this swimlane.' => 'Bu Kulvarı silmek mümkün değil.', + 'Unable to update this swimlane.' => 'Bu Kulvarı değiştirmek mümkün değil.', + 'Your swimlane have been created successfully.' => 'Kulvar başarıyla oluşturuldu.', + 'Example: "Bug, Feature Request, Improvement"' => 'Örnek: "Sorun, Özellik talebi, İyileştirme"', + 'Default categories for new projects (Comma-separated)' => 'Yeni projeler için varsayılan kategoriler (Virgül ile ayrılmış)', + // 'Gitlab commit received' => '', + // 'Gitlab issue opened' => '', + // 'Gitlab issue closed' => '', + // 'Gitlab webhooks' => '', + // 'Help on Gitlab webhooks' => '', + 'Integrations' => 'Entegrasyon', + 'Integration with third-party services' => 'Dış servislerle entegrasyon', + 'Role for this project' => 'Bu proje için rol', + 'Project manager' => 'Proje Yöneticisi', + 'Project member' => 'Proje Üyesi', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Bir Proje Yöneticisi proje ayarlarını değiştirebilir ve bir üyeden daha fazla yetkiye sahiptir.', + // 'Gitlab Issue' => '', + 'Subtask Id' => 'Alt görev No:', + 'Subtasks' => 'Alt görevler', + 'Subtasks Export' => 'Alt görevleri dışa aktar', + 'Subtasks exportation for "%s"' => '"%s" için alt görevleri dışa aktarımı', + 'Task Title' => 'Görev Başlığı', + 'Untitled' => 'Başlıksız', + 'Application default' => 'Uygulama varsayılanları', + 'Language:' => 'Dil:', + 'Timezone:' => 'Saat dilimi:', + 'All columns' => 'Tüm sütunlar', + 'Calendar for "%s"' => '"%s" için takvim', + 'Filter by column' => 'Sütuna göre filtrele', + 'Filter by status' => 'Duruma göre filtrele', + 'Calendar' => 'Takvim', + 'Next' => 'Sonraki', + '#%d' => '#%d', + 'Filter by color' => 'Renklere göre filtrele', + 'Filter by swimlane' => 'Kulvara göre filtrele', + 'All swimlanes' => 'Tüm Kulvarlar', + 'All colors' => 'Tüm Renkler', + 'All status' => 'Tüm Durumlar', + 'Add a comment logging moving the task between columns' => 'Sütun değiştiğinde kayıt olarak yorum ekle', + 'Moved to column %s' => '%s Sütununa taşındı', + 'Change description' => 'Açıklamayı değiştir', + 'User dashboard' => 'Kullanıcı Anasayfası', + 'Allow only one subtask in progress at the same time for a user' => 'Bir kullanıcı için aynı anda yalnızca bir alt göreve izin ver', + 'Edit column "%s"' => '"%s" sütununu değiştir', + 'Enable time tracking for subtasks' => 'Alt görevler için zaman takibini etkinleştir', + 'Select the new status of the subtask: "%s"' => '"%s" alt görevi için yeni durum seçin.', + 'Subtask timesheet' => 'Alt görev için zaman takip tablosu', + 'There is nothing to show.' => 'Gösterilecek hiçbir şey yok.', + 'Time Tracking' => 'Zaman takibi', + 'You already have one subtask in progress' => 'Zaten işlemde olan bir alt görev var', + 'Which parts of the project do you want to duplicate?' => 'Projenin hangi kısımlarının kopyasını oluşturmak istiyorsunuz?', + 'Change dashboard view' => 'Anasayfa görünümünü değiştir', + 'Show/hide activities' => 'Aktiviteleri göster/gizle', + 'Show/hide projects' => 'Projeleri göster/gizle', + 'Show/hide subtasks' => 'Alt görevleri göster/gizle', + 'Show/hide tasks' => 'Görevleri göster/gizle', + 'Disable login form' => 'Giriş formunu devre dışı bırak', + 'Show/hide calendar' => 'Takvimi göster/gizle', + 'User calendar' => 'Kullanıcı takvimi', + 'Bitbucket commit received' => 'Bitbucket commit alındı', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Bitbucket webhooks için yardım', + 'Start' => 'Başlangıç', + 'End' => 'Son', + 'Task age in days' => 'Görev yaşı gün olarak', + 'Days in this column' => 'Bu sütunda geçirilen gün', + '%dd' => '%dG', + 'Add a link' => 'Link ekle', + 'Add a new link' => 'Yeni link ekle', + 'Do you really want to remove this link: "%s"?' => '"%s" linkini gerçekten silmek istiyor musunuz?', + 'Do you really want to remove this link with task #%d?' => '#%d numaralı görev ile linki gerçekten silmek istiyor musunuz?', + 'Field required' => 'Bu alan gerekli', + 'Link added successfully.' => 'Link başarıyla eklendi.', + 'Link updated successfully.' => 'Link başarıyla güncellendi.', + 'Link removed successfully.' => 'Link başarıyla silindi.', + 'Link labels' => 'Link etiketleri', + 'Link modification' => 'Link değiştirme', + 'Links' => 'Links', + 'Link settings' => 'Link ayarları', + 'Opposite label' => 'Zıt etiket', + 'Remove a link' => 'Bir link silmek', + 'Task\'s links' => 'Görevin linkleri', + 'The labels must be different' => 'Etiketler farklı olmalı', + 'There is no link.' => 'Hiç bir bağ yok', + 'This label must be unique' => 'Bu etiket tek olmalı', + 'Unable to create your link.' => 'Link oluşturulamadı.', + 'Unable to update your link.' => 'Link güncellenemiyor.', + 'Unable to remove this link.' => 'Link kaldırılamıyor', + 'relates to' => 'şununla ilgili', + 'blocks' => 'şunu engelliyor', + 'is blocked by' => 'şunun tarafından engelleniyor', + 'duplicates' => 'şunun kopyasını oluşturuyor', + 'is duplicated by' => 'şunun tarafından kopyası oluşturuluyor', + 'is a child of' => 'şunun astı', + 'is a parent of' => 'şunun üstü', + 'targets milestone' => 'şu kilometre taşını hedefliyor', + 'is a milestone of' => 'şunun için bir kilometre taşı', + 'fixes' => 'düzeltiyor', + 'is fixed by' => 'şunun tarafından düzeltildi', + 'This task' => 'Bu görev', + '<1h' => '<1s', + '%dh' => '%ds', + // '%b %e' => '', + 'Expand tasks' => 'Görevleri genişlet', + 'Collapse tasks' => 'Görevleri daralt', + 'Expand/collapse tasks' => 'Görevleri genişlet/daralt', + 'Close dialog box' => 'İletiyi kapat', + 'Submit a form' => 'Formu gönder', + 'Board view' => 'Tablo görünümü', + 'Keyboard shortcuts' => 'Klavye kısayolları', + 'Open board switcher' => 'Tablo seçim listesini aç', + 'Application' => 'Uygulama', + 'Filter recently updated' => 'Son güncellenenleri göster', + 'since %B %e, %Y at %k:%M %p' => '%B %e, %Y saat %k:%M %p\'den beri', + 'More filters' => 'Daha fazla filtre', + 'Compact view' => 'Ekrana sığdır', + 'Horizontal scrolling' => 'Geniş görünüm', + 'Compact/wide view' => 'Ekrana sığdır / Geniş görünüm', + // 'No results match:' => '', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + // 'Hourly rates' => '', + // 'Hourly rate' => '', + // 'Currency' => '', + // 'Effective date' => '', + // 'Add new rate' => '', + // 'Rate removed successfully.' => '', + // 'Unable to remove this rate.' => '', + // 'Unable to save the hourly rate.' => '', + // 'Hourly rate created successfully.' => '', + // 'Start time' => '', + // 'End time' => '', + // 'Comment' => '', + // 'All day' => '', + // 'Day' => '', + // 'Manage timetable' => '', + // 'Overtime timetable' => '', + // 'Time off timetable' => '', + // 'Timetable' => '', + // 'Work timetable' => '', + // 'Week timetable' => '', + // 'Day timetable' => '', + // 'From' => '', + // 'To' => '', + // 'Time slot created successfully.' => '', + // 'Unable to save this time slot.' => '', + // 'Time slot removed successfully.' => '', + // 'Unable to remove this time slot.' => '', + // 'Do you really want to remove this time slot?' => '', + // 'Remove time slot' => '', + // 'Add new time slot' => '', + // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + // 'Files' => '', + // 'Images' => '', + // 'Private project' => '', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + // 'Budget' => '', + // 'Budget line' => '', + // 'Budget line removed successfully.' => '', + // 'Budget lines' => '', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + // 'Cost' => '', + // 'Cost breakdown' => '', + // 'Custom Stylesheet' => '', + // 'download' => '', + // 'Do you really want to remove this budget line?' => '', + // 'EUR - Euro' => '', + // 'Expenses' => '', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + // 'New budget line' => '', + // 'NZD - New Zealand Dollar' => '', + // 'Remove a budget line' => '', + // 'Remove budget line' => '', + // 'RSD - Serbian dinar' => '', + // 'The budget line have been created successfully.' => '', + // 'Unable to create the budget line.' => '', + // 'Unable to remove this budget line.' => '', + // 'USD - US Dollar' => '', + // 'Remaining' => '', + // 'Destination column' => '', + // 'Move the task to another column when assigned to a user' => '', + // 'Move the task to another column when assignee is cleared' => '', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', + // 'Enable Gravatar images' => '', + // 'Information' => '', + // 'Check two factor authentication code' => '', + // 'The two factor authentication code is not valid.' => '', + // 'The two factor authentication code is valid.' => '', + // 'Code' => '', + // 'Two factor authentication' => '', + // 'Enable/disable two factor authentication' => '', + // 'This QR code contains the key URI: ' => '', + // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '', + // 'Check my code' => '', + // 'Secret key: ' => '', + // 'Test your device' => '', + // 'Assign a color when the task is moved to a specific column' => '', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', +); diff --git a/app/Locales/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index fbaef229..1b2a5ef4 100644 --- a/app/Locales/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -1,6 +1,8 @@ <?php return array( + 'number.decimals_separator' => '.', + 'number.thousands_separator' => ',', 'None' => '无', 'edit' => '编辑', 'Edit' => '编辑', @@ -24,7 +26,7 @@ return array( 'Unassigned' => '未指定', 'View this task' => '查看该任务', 'Remove user' => '移除用户', - 'Do you really want to remove this user: "%s"?' => '你确定要移除这个用户吗:"%s"?', + 'Do you really want to remove this user: "%s"?' => '确定要删除用户"%s"吗?', 'New user' => '新建用户', 'All users' => '所有用户', 'Username' => '用户名', @@ -63,7 +65,7 @@ return array( 'Disable' => '停用', 'Enable' => '启用', 'New project' => '新建项目', - 'Do you really want to remove this project: "%s"?' => '你确定要移除该项目吗:"%s"?', + 'Do you really want to remove this project: "%s"?' => '确定要移除项目"%s"吗?', 'Remove project' => '移除项目', 'Boards' => '看板', 'Edit the board for "%s"' => '为"%s"修改看板', @@ -78,7 +80,7 @@ return array( 'Remove a column' => '移除一个栏目', 'Remove a column from a board' => '从看板移除一个栏目', 'Unable to remove this column.' => '无法移除该栏目。', - 'Do you really want to remove this column: "%s"?' => '你确定要移除该栏目:"%s"吗?', + 'Do you really want to remove this column: "%s"?' => '确定要移除栏目"%s"吗?', 'This action will REMOVE ALL TASKS associated to this column!' => '该动作将移除与该栏目相关的所有项目!', 'Settings' => '设置', 'Application settings' => '应用设置', @@ -182,18 +184,19 @@ return array( 'Change assignee' => '变更负责人', 'Change assignee for the task "%s"' => '更改任务"%s"的负责人', 'Timezone' => '时区', - 'Sorry, I didn\'t found this information in my database!' => '抱歉,无法在数据库中找到该信息!', + 'Sorry, I didn\'t find this information in my database!' => '抱歉,无法在数据库中找到该信息!', 'Page not found' => '页面未找到', 'Complexity' => '复杂度', 'limit' => '限制', 'Task limit' => '任务限制', + 'Task count' => '任务数', 'This value must be greater than %d' => '该数值必须大于%d', 'Edit project access list' => '编辑项目存取列表', 'Edit users access' => '编辑用户存取权限', 'Allow this user' => '允许该用户', 'Only those users have access to this project:' => '只有这些用户有该项目的存取权限:', 'Don\'t forget that administrators have access to everything.' => '别忘了管理员有一切的权限。', - 'revoke' => '撤销', + 'Revoke' => '撤销', 'List of authorized users' => '已授权的用户列表', 'User' => '用户', 'Nobody have access to this project.' => '无用户可以访问此项目.', @@ -212,6 +215,7 @@ return array( 'Invalid date' => '无效日期', 'Must be done before %B %e, %Y' => '必须在%Y/%m/%d前完成', '%B %e, %Y' => '%Y/%m/%d', + // '%b %e, %Y' => '', 'Automatic actions' => '自动动作', 'Your automatic action have been created successfully.' => '您的自动动作已成功创建', 'Unable to create your automatic action.' => '无法为您创建自动动作。', @@ -230,7 +234,7 @@ return array( 'Next step' => '下一步', 'Define action parameters' => '定义动作参数', 'Save this action' => '保存该动作', - 'Do you really want to remove this action: "%s"?' => '确定要移除该动作"%s"吗?', + 'Do you really want to remove this action: "%s"?' => '确定要移除动作"%s"吗?', 'Remove an automatic action' => '移除一个自动动作', 'Close the task' => '关闭任务', 'Assign the task to a specific user' => '将该任务指派给一个用户', @@ -284,17 +288,17 @@ return array( 'Nothing found.' => '没找到。', 'Search in the project "%s"' => '在项目"%s"中查找', 'Due date' => '到期时间', - 'Others formats accepted: %s and %s' => '允许其他格式:%s 和 %s', + 'Others formats accepted: %s and %s' => '可以使用的其它格式:%s 和 %s', 'Description' => '描述', '%d comments' => '%d个评论', '%d comment' => '%d个评论', - 'Email address invalid' => 'Email地址无效', + 'Email address invalid' => '电子邮件地址无效', 'Your Google Account is not linked anymore to your profile.' => '您的google帐号不再与您的账户配置关联。', 'Unable to unlink your Google Account.' => '无法去除您google帐号的关联', 'Google authentication failed' => 'google验证失败', 'Unable to link your Google Account.' => '无法关联您的google帐号。', 'Your Google Account is linked to your profile successfully.' => '您的google帐号已成功与账户配置关联。', - 'Email' => 'Email', + 'Email' => '电子邮件', 'Link my Google Account' => '关联我的google帐号', 'Unlink my Google Account' => '去除我的google帐号关联', 'Login with my Google Account' => '用我的google帐号登录', @@ -322,7 +326,7 @@ return array( 'Category Name' => '分类名称', 'Categories for the project "%s"' => '项目"%s"的分类', 'Add a new category' => '加入新分类', - 'Do you really want to remove this category: "%s"?' => '您确定要移除分类"%s"吗?', + 'Do you really want to remove this category: "%s"?' => '确定要移除分类"%s"吗?', 'Filter by category' => '按分类过滤', 'All categories' => '所有分类', 'No category' => '无分类', @@ -331,7 +335,7 @@ return array( 'Unable to remove this file.' => '无法移除该文件。', 'File removed successfully.' => '文件成功移除。', 'Attach a document' => '附加文档', - 'Do you really want to remove this file: "%s"?' => '您确定要移除文件"%s"吗?', + 'Do you really want to remove this file: "%s"?' => '确定要移除文件"%s"吗?', 'open' => '打开', 'Attachments' => '附件', 'Edit the task' => '修改任务', @@ -374,10 +378,10 @@ return array( 'Login with my GitHub Account' => '用Github账号登录', 'Link my GitHub Account' => '链接GitHub账号', 'Unlink my GitHub Account' => '取消GitHub账号链接', - 'Created by %s' => '创建者:', + 'Created by %s' => '创建者:%s', 'Last modified on %B %e, %Y at %k:%M %p' => '最后修改:%Y/%m/%d/ %H:%M', 'Tasks Export' => '任务导出', - 'Tasks exportation for "%s"' => '导出任务"%s"', + 'Tasks exportation for "%s"' => '导出"%s"的任务', 'Start Date' => '开始时间', 'End Date' => '结束时间', 'Execute' => '执行', @@ -385,8 +389,6 @@ return array( 'Creator' => '创建者', 'Modification date' => '修改日期', 'Completion date' => '完成日期', - 'Webhook URL for task creation' => '创建任务的Webhook URL', - 'Webhook URL for task modification' => '修改任务的Webhook URL', 'Clone' => '克隆', 'Clone Project' => '复制项目', 'Project cloned successfully.' => '成功复制项目。', @@ -406,15 +408,13 @@ return array( 'Comment updated' => '更新了评论', 'New comment posted by %s' => '%s 的新评论', 'List of due tasks for the project "%s"' => '项目"%s"的到期任务列表', - '[%s][New attachment] %s (#%d)' => '[%s][新附件] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][新评论] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][评论更新] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][新子任务] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][子任务更新] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][新任务] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][任务更新] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][任务关闭] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][任务开启] %s (#%d)', + 'New attachment' => '新建附件', + 'New comment' => '新建评论', + 'New subtask' => '新建子任务', + 'Subtask updated' => '子任务更新', + 'Task updated' => '任务更新', + 'Task closed' => '任务关闭', + 'Task opened' => '任务开启', '[%s][Due tasks]' => '[%s][到期任务]', '[Kanboard] Notification' => '[Kanboard] 通知', 'I want to receive notifications only for those projects:' => '我仅需要收到下面项目的通知:', @@ -467,20 +467,20 @@ return array( 'Unable to change the password.' => '无法修改密码。', 'Change category for the task "%s"' => '变更任务 "%s" 的分类', 'Change category' => '变更分类', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 更新了任务 <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 开启了任务 <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s 将任务 <a href="?controller=task&action=show&task_id=%d">#%d</a> 移动到了"%s"的第#%d个位置', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s 移动任务 <a href="?controller=task&action=show&task_id=%d">#%d</a> 到栏目 "%s"', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 创建了任务 <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 关闭了任务 <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 创建了 <a href="?controller=task&action=show&task_id=%d">#%d</a>的子任务', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 更新了 <a href="?controller=task&action=show&task_id=%d">#%d</a>的子任务', + '%s updated the task %s' => '%s 更新了任务 %s', + '%s opened the task %s' => '%s 开启了任务 %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s 将任务 %s 移动到了"%s"的第#%d个位置', + '%s moved the task %s to the column "%s"' => '%s 移动任务 %s 到栏目 "%s"', + '%s created the task %s' => '%s 创建了任务 %s', + '%s closed the task %s' => '%s 关闭了任务 %s', + '%s created a subtask for the task %s' => '%s 创建了 %s的子任务', + '%s updated a subtask for the task %s' => '%s 更新了 %s的子任务', 'Assigned to %s with an estimate of %s/%sh' => '分配给 %s,预估需要 %s/%s 小时', 'Not assigned, estimate of %sh' => '未分配,预估需要 %s 小时', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 更新了任务 <a href="?controller=task&action=show&task_id=%d">#%d</a>的评论', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s 评论了任务 <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s\'s activity' => '%s的活动', - 'No activity.' => '无活动', + '%s updated a comment on the task %s' => '%s 更新了任务 %s的评论', + '%s commented the task %s' => '%s 评论了任务 %s', + '%s\'s activity' => '%s的动态', + 'No activity.' => '无动态', 'RSS feed' => 'RSS 链接', '%s updated a comment on the task #%d' => '%s 更新了任务 #%d 的评论', '%s commented on the task #%d' => '%s 评论了任务 #%d', @@ -492,15 +492,15 @@ return array( '%s open the task #%d' => '%s 开启了任务 #%d', '%s moved the task #%d to the column "%s"' => '%s 将任务 #%d 移动到栏目 "%s"', '%s moved the task #%d to the position %d in the column "%s"' => '%s将任务#%d移动到"%s"的第 %d 列', - 'Activity' => '活动', + 'Activity' => '动态', 'Default values are "%s"' => '默认值为 "%s"', 'Default columns for new projects (Comma-separated)' => '新建项目的默认栏目(用逗号分开)', 'Task assignee change' => '任务分配变更', '%s change the assignee of the task #%d to %s' => '%s 将任务 #%d 分配给了 %s', - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '%s 将任务 <a href="?controller=task&action=show&task_id=%d">#%d</a> 分配给 %s', - '[%s][Column Change] %s (#%d)' => '[%s][栏目变更] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][位置变更] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][任务分配变更] %s (#%d)', + '%s changed the assignee of the task %s to %s' => '%s 将任务 %s 分配给 %s', + 'Column Change' => '栏目变更', + 'Position Change' => '位置变更', + 'Assignee Change' => '负责人变更', 'New password for the user "%s"' => '用户"%s"的新密码', 'Choose an event' => '选择一个事件', 'Github commit received' => '收到了Github提交', @@ -546,9 +546,380 @@ return array( 'Time estimated' => '预计时间', 'There is nothing assigned to you.' => '无任务指派给你。', 'My tasks' => '我的任务', - 'Activity stream' => '活动流', + 'Activity stream' => '动态记录', 'Dashboard' => '面板', 'Confirmation' => '确认', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', + 'Allow everybody to access to this project' => '允许所有人访问此项目', + 'Everybody have access to this project.' => '所有人都可以访问此项目', + 'Webhooks' => '网络钩子', + 'API' => '应用程序接口', + 'Integration' => '整合', + 'Github webhooks' => 'Github 网络钩子', + 'Help on Github webhooks' => 'Github 网络钩子帮助', + 'Create a comment from an external provider' => '从外部创建一个评论', + 'Github issue comment created' => '已经创建了Github问题评论', + 'Configure' => '配置', + 'Project management' => '项目管理', + 'My projects' => '我的项目', + 'Columns' => '栏目', + 'Task' => '任务', + 'Your are not member of any project.' => '您尚未加入任何项目', + 'Percentage' => '百分比', + 'Number of tasks' => '任务数', + 'Task distribution' => '任务分布', + 'Reportings' => '报告', + 'Task repartition for "%s"' => '"%s"的任务分析', + 'Analytics' => '分析', + 'Subtask' => '子任务', + 'My subtasks' => '我的子任务', + 'User repartition' => '用户分析', + 'User repartition for "%s"' => '"%s"的用户分析', + 'Clone this project' => '复制此项目', + 'Column removed successfully.' => '成功删除了栏目。', + 'Edit Project' => '编辑项目', + 'Github Issue' => 'Github 任务报告', + 'Not enough data to show the graph.' => '数据不足,无法绘图。', + 'Previous' => '后退', + 'The id must be an integer' => '编号必须为整数', + 'The project id must be an integer' => '项目编号必须为整数', + 'The status must be an integer' => '状态必须为整数', + 'The subtask id is required' => '必须提供子任务编号', + 'The subtask id must be an integer' => '子任务编号必须为整数', + 'The task id is required' => '需要任务编号', + 'The task id must be an integer' => '任务编号必须为整数', + 'The user id must be an integer' => '用户编号必须为整数', + 'This value is required' => '必须给出这个值', + 'This value must be numeric' => '这个值必须为数字', + 'Unable to create this task.' => '无法创建此任务。', + 'Cumulative flow diagram' => '累积流图表', + 'Cumulative flow diagram for "%s"' => '"%s"的累积流图表', + 'Daily project summary' => '每日项目汇总', + 'Daily project summary export' => '导出每日项目汇总', + 'Daily project summary export for "%s"' => '导出项目"%s"的每日汇总', + 'Exports' => '导出', + 'This export contains the number of tasks per column grouped per day.' => '此导出包含每列的任务数,按天分组', + 'Nothing to preview...' => '没有需要预览的内容', + 'Preview' => '预览', + 'Write' => '书写', + 'Active swimlanes' => '活动泳道', + 'Add a new swimlane' => '添加新泳道', + 'Change default swimlane' => '修改默认泳道', + 'Default swimlane' => '默认泳道', + 'Do you really want to remove this swimlane: "%s"?' => '确定要删除泳道:"%s"?', + 'Inactive swimlanes' => '非活动泳道', + 'Set project manager' => '设为项目经理', + 'Set project member' => '设为项目成员', + 'Remove a swimlane' => '删除泳道', + 'Rename' => '重命名', + 'Show default swimlane' => '显示默认泳道', + 'Swimlane modification for the project "%s"' => '项目"%s"的泳道变更', + 'Swimlane not found.' => '未找到泳道。', + 'Swimlane removed successfully.' => '成功删除泳道', + 'Swimlanes' => '泳道', + 'Swimlane updated successfully.' => '成功更新了泳道。', + 'The default swimlane have been updated successfully.' => '成功更新了默认泳道。', + 'Unable to create your swimlane.' => '无法创建泳道。', + 'Unable to remove this swimlane.' => '无法删除此泳道', + 'Unable to update this swimlane.' => '无法更新此泳道', + 'Your swimlane have been created successfully.' => '已经成功创建泳道。', + 'Example: "Bug, Feature Request, Improvement"' => '示例:“缺陷,功能需求,提升', + 'Default categories for new projects (Comma-separated)' => '新项目的默认分类(用逗号分隔)', + 'Gitlab commit received' => '收到 Gitlab 提交', + 'Gitlab issue opened' => '开启 Gitlab 问题', + 'Gitlab issue closed' => '关闭 Gitlab 问题', + 'Gitlab webhooks' => 'Gitlab 网络钩子', + 'Help on Gitlab webhooks' => 'Gitlab 网络钩子帮助', + 'Integrations' => '整合', + 'Integration with third-party services' => '与第三方服务进行整合', + 'Role for this project' => '项目角色', + 'Project manager' => '项目管理员', + 'Project member' => '项目成员', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => '项目经理可以修改项目的设置,比标准用户多了一些权限', + 'Gitlab Issue' => 'Gitlab 问题', + 'Subtask Id' => '子任务 Id', + 'Subtasks' => '子任务', + 'Subtasks Export' => '子任务导出', + 'Subtasks exportation for "%s"' => '导出"%s"的子任务', + 'Task Title' => '任务标题', + 'Untitled' => '无标题', + 'Application default' => '程序默认', + 'Language:' => '语言:', + 'Timezone:' => '时区:', + 'All columns' => '全部栏目', + 'Calendar for "%s"' => '"%s"的日程表', + 'Filter by column' => '按栏目过滤', + 'Filter by status' => '按状态过滤', + 'Calendar' => '日程表', + 'Next' => '前进', + '#%d' => '#%d', + 'Filter by color' => '按颜色过滤', + 'Filter by swimlane' => '按泳道过滤', + 'All swimlanes' => '全部泳道', + 'All colors' => '全部颜色', + 'All status' => '全部状态', + 'Add a comment logging moving the task between columns' => '在不同栏目间移动任务时添加一个评论', + 'Moved to column %s' => '移动到栏目 %s', + 'Change description' => '修改描述', + 'User dashboard' => '用户仪表板', + 'Allow only one subtask in progress at the same time for a user' => '每用户同时仅有一个活动子任务', + 'Edit column "%s"' => '编辑栏目"%s"', + 'Enable time tracking for subtasks' => '启用子任务的时间记录', + 'Select the new status of the subtask: "%s"' => '选择子任务的新状态:"%s"', + 'Subtask timesheet' => '子任务时间', + 'There is nothing to show.' => '无内容。', + 'Time Tracking' => '时间记录', + 'You already have one subtask in progress' => '你已经有了一个进行中的子任务', + 'Which parts of the project do you want to duplicate?' => '要复制项目的哪些内容?', + 'Change dashboard view' => '修改仪表板视图', + 'Show/hide activities' => '显示/隐藏活动', + 'Show/hide projects' => '显示/隐藏项目', + 'Show/hide subtasks' => '显示/隐藏子任务', + 'Show/hide tasks' => '显示/隐藏任务', + 'Disable login form' => '禁用登录界面', + 'Show/hide calendar' => '显示/隐藏日程表', + 'User calendar' => '用户日程表', + 'Bitbucket commit received' => '收到Bitbucket提交', + 'Bitbucket webhooks' => 'Bitbucket网络钩子', + 'Help on Bitbucket webhooks' => 'Bitbucket网络钩子帮助', + 'Start' => '开始', + 'End' => '结束', + 'Task age in days' => '任务存在天数', + 'Days in this column' => '在此栏目的天数', + '%dd' => '%d天', + 'Add a link' => '添加一个关联', + 'Add a new link' => '添加一个新关联', + 'Do you really want to remove this link: "%s"?' => '确认要删除此关联吗:"%s"?', + 'Do you really want to remove this link with task #%d?' => '确认要删除到任务 #%d 的关联吗?', + 'Field required' => '必须的字段', + 'Link added successfully.' => '成功添加关联。', + 'Link updated successfully.' => '成功更新关联。', + 'Link removed successfully.' => '成功删除关联。', + 'Link labels' => '关联标签', + 'Link modification' => '关联修改', + 'Links' => '关联', + 'Link settings' => '关联设置', + 'Opposite label' => '反向标签', + 'Remove a link' => '删除关联', + 'Task\'s links' => '任务的关联', + 'The labels must be different' => '标签不能一样', + 'There is no link.' => '没有关联', + 'This label must be unique' => '关联必须唯一', + 'Unable to create your link.' => '无法创建关联。', + 'Unable to update your link.' => '无法更新关联。', + 'Unable to remove this link.' => '无法删除关联。', + 'relates to' => '关联到', + // 'blocks' => '', + // 'is blocked by' => '', + // 'duplicates' => '', + // 'is duplicated by' => '', + // 'is a child of' => '', + // 'is a parent of' => '', + // 'targets milestone' => '', + // 'is a milestone of' => '', + // 'fixes' => '', + // 'is fixed by' => '', + 'This task' => '此任务', + '<1h' => '<1h', + '%dh' => '%h', + // '%b %e' => '', + 'Expand tasks' => '展开任务', + 'Collapse tasks' => '收缩任务', + 'Expand/collapse tasks' => '展开/收缩任务', + 'Close dialog box' => '关闭对话框', + 'Submit a form' => '提交表单', + 'Board view' => '面板视图', + 'Keyboard shortcuts' => '键盘快捷方式', + 'Open board switcher' => '打开面板切换器', + 'Application' => '应用程序', + 'Filter recently updated' => '过滤最近的更新', + // 'since %B %e, %Y at %k:%M %p' => '', + 'More filters' => '更多过滤', + 'Compact view' => '紧凑视图', + 'Horizontal scrolling' => '水平滚动', + 'Compact/wide view' => '紧凑/宽视图', + 'No results match:' => '无匹配结果:', + 'Remove hourly rate' => '删除小时工资', + 'Do you really want to remove this hourly rate?' => '确定要删除此计时工资吗?', + 'Hourly rates' => '小时工资', + 'Hourly rate' => '小时工资', + 'Currency' => '货币', + 'Effective date' => '开始时间', + 'Add new rate' => '添加小时工资', + 'Rate removed successfully.' => '成功删除工资。', + 'Unable to remove this rate.' => '无法删除此小时工资。', + 'Unable to save the hourly rate.' => '无法删除小时工资。', + 'Hourly rate created successfully.' => '成功创建小时工资。', + 'Start time' => '开始时间', + 'End time' => '结束时1间', + 'Comment' => '注释', + 'All day' => '全天', + 'Day' => '日期', + 'Manage timetable' => '管理时间表', + // 'Overtime timetable' => '', + 'Time off timetable' => '加班时间表', + 'Timetable' => '时间表', + 'Work timetable' => '工作时间表', + 'Week timetable' => '周时间表', + 'Day timetable' => '日时间表', + 'From' => '从', + 'To' => '到', + 'Time slot created successfully.' => '成功创建时间段。', + 'Unable to save this time slot.' => '无法保存此时间段。', + 'Time slot removed successfully.' => '成功删除时间段。', + 'Unable to remove this time slot.' => '无法删除此时间段。', + 'Do you really want to remove this time slot?' => '确认要删除此时间段吗?', + 'Remove time slot' => '删除时间段', + 'Add new time slot' => '添加新时间段', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '如果在放假和加班计划中选择全天,则会使用这里配置的时间段。', + 'Files' => '文件', + 'Images' => '图片', + 'Private project' => '私人项目', + 'Amount' => '数量', + // 'AUD - Australian Dollar' => '', + 'Budget' => '预算', + 'Budget line' => '预算线', + 'Budget line removed successfully.' => '成功删除预算线', + 'Budget lines' => '预算线', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + 'Cost' => '成本', + 'Cost breakdown' => '成本分解', + 'Custom Stylesheet' => '自定义样式表', + 'download' => '下载', + 'Do you really want to remove this budget line?' => '确定要删除此预算线吗?', + // 'EUR - Euro' => '', + 'Expenses' => '花费', + // 'GBP - British Pound' => '', + // 'INR - Indian Rupee' => '', + // 'JPY - Japanese Yen' => '', + 'New budget line' => '新预算线', + // 'NZD - New Zealand Dollar' => '', + 'Remove a budget line' => '删除预算线', + 'Remove budget line' => '删除预算线', + // 'RSD - Serbian dinar' => '', + 'The budget line have been created successfully.' => '成功创建预算线。', + 'Unable to create the budget line.' => '无法创建预算线。', + 'Unable to remove this budget line.' => '无法删除此预算线。', + // 'USD - US Dollar' => '', + 'Remaining' => '剩余', + 'Destination column' => '目标栏目', + 'Move the task to another column when assigned to a user' => '指定负责人时移动到其它栏目', + 'Move the task to another column when assignee is cleared' => '移除负责人时移动到其它栏目', + 'Source column' => '原栏目', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => '变更', + 'Executer' => '执行者', + 'Time spent in the column' => '栏目中的时间消耗', + 'Task transitions' => '任务变更', + 'Task transitions export' => '导出任务变更', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '此报告记录任务的变更,包含日期、用户和时间消耗。', + 'Currency rates' => '汇率', + 'Rate' => '汇率', + 'Change reference currency' => '修改参考货币', + 'Add a new currency rate' => '添加新汇率', + 'Currency rates are used to calculate project budget.' => '汇率会用来计算项目预算。', + 'Reference currency' => '参考货币', + 'The currency rate have been added successfully.' => '成功添加汇率。', + 'Unable to add this currency rate.' => '无法添加此汇率', + 'Send notifications to a Slack channel' => '发送通知到 Slack 频道', + 'Webhook URL' => '网络钩子 URL', + 'Help on Slack integration' => 'Slack 整合帮助', + '%s remove the assignee of the task %s' => '%s删除了任务%s的负责人', + 'Send notifications to Hipchat' => '发送通知到 Hipchat', + 'API URL' => 'API URL', + 'Room API ID or name' => '房间 API ID 或名称', + 'Room notification token' => '房间通知令牌', + 'Help on Hipchat integration' => 'Hipchat 整合帮助', + 'Enable Gravatar images' => '启用 Gravatar 图像', + 'Information' => '信息', + 'Check two factor authentication code' => '检查双重认证码', + 'The two factor authentication code is not valid.' => '双重认证码不正确。', + 'The two factor authentication code is valid.' => '双重认证码正确。', + 'Code' => '认证码', + 'Two factor authentication' => '双重认证', + 'Enable/disable two factor authentication' => '启用/禁用双重认证', + 'This QR code contains the key URI: ' => '此二维码包含密码 URI:', + 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '将密码保存到 TOTP 软件(例如Google 认证或 FreeOTP)', + 'Check my code' => '检查我的认证码', + 'Secret key: ' => '密码:', + 'Test your device' => '测试设备', + 'Assign a color when the task is moved to a specific column' => '任务移动到指定栏目时设置颜色', + // '%s via Kanboard' => '', + // 'uploaded by: %s' => '', + // 'uploaded on: %s' => '', + // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', + // 'Screenshot taken %s' => '', + // 'Add a screenshot' => '', + // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', + // 'Screenshot uploaded successfully.' => '', + // 'SEK - Swedish Krona' => '', + // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', + // 'Identifier' => '', + // 'Postmark (incoming emails)' => '', + // 'Help on Postmark integration' => '', + // 'Mailgun (incoming emails)' => '', + // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', + // 'Disable two factor authentication' => '', + // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', + // 'Edit link' => '', + // 'Start to type task title...' => '', + // 'A task cannot be linked to itself' => '', + // 'The exact same link already exists' => '', + // 'Recurrent task is scheduled to be generated' => '', + // 'Recurring information' => '', + // 'Score' => '', + // 'The identifier must be unique' => '', + // 'This linked task id doesn\'t exists' => '', + // 'This value must be alphanumeric' => '', + // 'Edit recurrence' => '', + // 'Generate recurrent task' => '', + // 'Trigger to generate recurrent task' => '', + // 'Factor to calculate new due date' => '', + // 'Timeframe to calculate new due date' => '', + // 'Base date to calculate new due date' => '', + // 'Action date' => '', + // 'Base date to calculate new due date: ' => '', + // 'This task has created this child task: ' => '', + // 'Day(s)' => '', + // 'Existing due date' => '', + // 'Factor to calculate new due date: ' => '', + // 'Month(s)' => '', + // 'Recurrence' => '', + // 'This task has been created by: ' => '', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + // 'When task is closed' => '', + // 'When task is moved from first column' => '', + // 'When task is moved to last column' => '', + // 'Year(s)' => '', + // 'Jabber (XMPP)' => '', + // 'Send notifications to Jabber' => '', + // 'XMPP server address' => '', + // 'Jabber domain' => '', + // 'Jabber nickname' => '', + // 'Multi-user chat room' => '', + // 'Help on Jabber integration' => '', + // 'The server address must use this format: "tcp://hostname:5222"' => '', + // 'Calendar settings' => '', + // 'Project calendar view' => '', + // 'Project settings' => '', + // 'Show subtasks based on the time tracking' => '', + // 'Show tasks based on the creation date' => '', + // 'Show tasks based on the start date' => '', + // 'Subtasks time tracking' => '', + // 'User calendar view' => '', + // 'Automatically update the start date' => '', + // 'iCal feed' => '', + // 'Preferences' => '', + // 'Security' => '', + // 'Two factor authentication disabled' => '', + // 'Two factor authentication enabled' => '', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', ); diff --git a/app/Locales/fi_FI/translations.php b/app/Locales/fi_FI/translations.php deleted file mode 100644 index 4811b832..00000000 --- a/app/Locales/fi_FI/translations.php +++ /dev/null @@ -1,554 +0,0 @@ -<?php - -return array( - 'None' => 'Ei mikään', - 'edit' => 'muokkaa', - 'Edit' => 'Muokkaa', - 'remove' => 'poista', - 'Remove' => 'Poista', - 'Update' => 'Päivitä', - 'Yes' => 'Kyllä', - 'No' => 'Ei', - 'cancel' => 'peruuta', - 'or' => 'tai', - 'Yellow' => 'Keltainen', - 'Blue' => 'Sininen', - 'Green' => 'Vihreä', - 'Purple' => 'Violetti', - 'Red' => 'Punainen', - 'Orange' => 'Oranssi', - 'Grey' => 'Harmaa', - 'Save' => 'Tallenna', - 'Login' => 'Sisäänkirjautuminen', - 'Official website:' => 'Virallinen verkkosivu:', - 'Unassigned' => 'Ei suorittajaa', - 'View this task' => 'Näytä tämä tehtävä', - 'Remove user' => 'Poista käyttäjä', - 'Do you really want to remove this user: "%s"?' => 'Oletko varma että haluat poistaa käyttäjän "%s"?', - 'New user' => 'Uusi käyttäjä', - 'All users' => 'Kaikki käyttäjät', - 'Username' => 'Käyttäjänimi', - 'Password' => 'Salasana', - 'Default project' => 'Oletusprojekti', - 'Administrator' => 'Ylläpitäjä', - 'Sign in' => 'Kirjaudu sisään', - 'Users' => 'Käyttäjät', - 'No user' => 'Ei käyttäjää', - 'Forbidden' => 'Estetty', - 'Access Forbidden' => 'Pääsy estetty', - 'Only administrators can access to this page.' => 'Vain ylläpitäjillä on pääsy tälle sivulle.', - 'Edit user' => 'Muokkaa käyttäjää', - 'Logout' => 'Kirjaudu ulos', - 'Bad username or password' => 'Väärä käyttäjätunnus tai salasana', - 'users' => 'käyttäjät', - 'projects' => 'projektit', - 'Edit project' => 'Muokkaa projektia', - 'Name' => 'Nimi', - 'Activated' => 'Aktivoitu', - 'Projects' => 'Projektit', - 'No project' => 'Ei projektia', - 'Project' => 'Projekti', - 'Status' => 'Status', - 'Tasks' => 'Tehtävät', - 'Board' => 'Taulu', - 'Actions' => 'Toiminnot', - 'Inactive' => 'Ei aktiivinen', - 'Active' => 'Aktiivinen', - 'Column %d' => 'Sarake %d', - 'Add this column' => 'Lisää tämä sarake', - '%d tasks on the board' => '%d tehtävää taululla', - '%d tasks in total' => '%d tehtävää yhteensä', - 'Unable to update this board.' => 'Taulun muuttaminen ei onnistunut.', - 'Edit board' => 'Muuta taulua', - 'Disable' => 'Disabloi', - 'Enable' => 'Aktivoi', - 'New project' => 'Uusi projekti', - 'Do you really want to remove this project: "%s"?' => 'Haluatko varmasti poistaa projektin: "%s"?', - 'Remove project' => 'Poista projekti', - 'Boards' => 'Taulut', - 'Edit the board for "%s"' => 'Muokkaa taulua projektille "%s"', - 'All projects' => 'Kaikki projektit', - 'Change columns' => 'Muokkaa sarakkeita', - 'Add a new column' => 'Lisää uusi sarake', - 'Title' => 'Nimi', - 'Add Column' => 'Lisää sarake', - 'Project "%s"' => 'Projekti "%s"', - 'Nobody assigned' => 'Ei suorittajaa', - 'Assigned to %s' => 'Tekijä: %s', - 'Remove a column' => 'Poista sarake', - 'Remove a column from a board' => 'Poista sarake taulusta', - 'Unable to remove this column.' => 'Sarakkeen poistaminen ei onnistunut.', - 'Do you really want to remove this column: "%s"?' => 'Haluatko varmasti poistaa sarakkeen "%s"?', - 'This action will REMOVE ALL TASKS associated to this column!' => 'Tämä toiminto POISTAA KAIKKI TEHTÄVÄT tästä sarakkeesta!', - 'Settings' => 'Asetukset', - 'Application settings' => 'Ohjelman asetukset', - 'Language' => 'Kieli', - 'Webhook token:' => 'Webhooks avain:', - // 'API token:' => '', - 'More information' => 'Lisätietoja', - 'Database size:' => 'Tietokannan koko:', - 'Download the database' => 'Lataa tietokanta', - 'Optimize the database' => 'Optimoi tietokanta', - '(VACUUM command)' => '(VACUUM-komento)', - '(Gzip compressed Sqlite file)' => '(Gzip-pakattu Sqlite-tiedosto)', - 'User settings' => 'Käyttäjän asetukset', - 'My default project:' => 'Oletusprojektini: ', - 'Close a task' => 'Sulje tehtävä', - 'Do you really want to close this task: "%s"?' => 'Haluatko varmasti sulkea tehtävän: "%s"?', - 'Edit a task' => 'Muokkaa tehtävää', - 'Column' => 'Sarake', - 'Color' => 'Väri', - 'Assignee' => 'Suorittaja', - 'Create another task' => 'Luo toinen tehtävä', - 'New task' => 'Uusi tehtävä', - 'Open a task' => 'Avaa tehtävä', - 'Do you really want to open this task: "%s"?' => 'Haluatko varmasti avata tehtävän: "%s"?', - 'Back to the board' => 'Takaisin tauluun', - 'Created on %B %e, %Y at %k:%M %p' => 'Luotu %d.%m.%Y kello %H:%M', - 'There is nobody assigned' => 'Ei suorittajaa', - 'Column on the board:' => 'Sarake taululla: ', - 'Status is open' => 'Status on avoin', - 'Status is closed' => 'Status on suljettu', - 'Close this task' => 'Sulje tämä tehtävä', - 'Open this task' => 'Avaa tämä tehtävä', - 'There is no description.' => 'Ei kuvausta.', - 'Add a new task' => 'Lisää uusi tehtävä', - 'The username is required' => 'Käyttäjätunnut vaaditaan', - 'The maximum length is %d characters' => 'Maksimipituus on %d merkkiä', - 'The minimum length is %d characters' => 'Vähimmäispituus on %d merkkiä', - 'The password is required' => 'Salasana vaaditaan', - 'This value must be an integer' => 'Tämän arvon täytyy olla numero', - 'The username must be unique' => 'Käyttäjänimi täytyy olla uniikki', - 'The username must be alphanumeric' => 'Käyttäjänimen täytyy olla alfanumeerinen', - 'The user id is required' => 'Käyttäjän id on pakollinen', - // 'Passwords don\'t match' => '', - 'The confirmation is required' => 'Varmistus vaaditaan', - 'The column is required' => 'Sarake on pakollinen', - 'The project is required' => 'Projekti on pakollinen', - 'The color is required' => 'Väri on pakollinen', - 'The id is required' => 'ID vaaditaan', - 'The project id is required' => 'Projektin ID on pakollinen', - 'The project name is required' => 'Projektin nimi on pakollinen', - 'This project must be unique' => 'Projektin nimi täytyy olla uniikki', - 'The title is required' => 'Otsikko vaaditaan', - 'The language is required' => 'Kieli on pakollinen', - 'There is no active project, the first step is to create a new project.' => 'Aktiivista projektia ei ole, ensimmäinen vaihe on luoda uusi projekti.', - 'Settings saved successfully.' => 'Asetukset tallennettu onnistuneesti.', - 'Unable to save your settings.' => 'Asetusten tallentaminen epäonnistui.', - 'Database optimization done.' => 'Tietokannan optimointi suoritettu.', - 'Your project have been created successfully.' => 'Projekti luotiin onnistuneesti.', - 'Unable to create your project.' => 'Projektin luominen epäonnistui.', - 'Project updated successfully.' => 'Projekti päivitettiin onnistuneesti.', - 'Unable to update this project.' => 'Projektin muuttaminen epäonnistui.', - 'Unable to remove this project.' => 'Projektin poistaminen epäonnistui.', - 'Project removed successfully.' => 'Projekti poistettiin onnistuneesti.', - 'Project activated successfully.' => 'Projekti aktivoitiin onnistuneesti.', - 'Unable to activate this project.' => 'Projektin aktivoiminen epäonnistui.', - 'Project disabled successfully.' => 'Projektin disabloiminen onnistui.', - 'Unable to disable this project.' => 'Projektin disabloiminen epäonnistui.', - 'Unable to open this task.' => 'Tehtävän avaus epäonnistui.', - 'Task opened successfully.' => 'Tehtävä avattiin onnistuneesti.', - 'Unable to close this task.' => 'Tehtävän sulkeminen epäonnistui.', - 'Task closed successfully.' => 'Tehtävä suljettiin onnistuneesti.', - 'Unable to update your task.' => 'Tehtävän muokkaaminen epäonnistui.', - 'Task updated successfully.' => 'Tehtävä päivitettiin onnistuneesti.', - 'Unable to create your task.' => 'Tehtävän luominen epäonnistui.', - 'Task created successfully.' => 'Tehtävä luotiin onnistuneesti.', - 'User created successfully.' => 'Käyttäjä lisättiin onnistuneesti.', - 'Unable to create your user.' => 'Käyttäjän lisäys epäonnistui.', - 'User updated successfully.' => 'Käyttäjätietojen päivitys onnistui.', - 'Unable to update your user.' => 'Käyttäjätietojen päivitys epäonnistui.', - 'User removed successfully.' => 'Käyttäjä poistettiin onnistuneesti.', - 'Unable to remove this user.' => 'Käyttäjän poistaminen epäonnistui.', - 'Board updated successfully.' => 'Taulu päivitettiin onnistuneesti.', - 'Ready' => 'Valmis', - 'Backlog' => 'Tehtäväjono', - 'Work in progress' => 'Työnalla', - 'Done' => 'Tehty', - 'Application version:' => 'Ohjelman versio:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Valmistunut %d.%m.%Y kello %H:%M', - '%B %e, %Y at %k:%M %p' => '%d.%m.%Y kello %H:%M', - 'Date created' => 'Luomispäivä', - 'Date completed' => 'Valmistumispäivä', - 'Id' => 'Id', - 'No task' => 'Ei tehtävää', - 'Completed tasks' => 'Valmiit tehtävät', - 'List of projects' => 'Projektit', - 'Completed tasks for "%s"' => 'Suoritetut tehtävät projektille %s', - '%d closed tasks' => '%d suljettua tehtävää', - 'No task for this project' => 'Ei tehtävää tälle projektille', - 'Public link' => 'Julkinen linkki', - 'There is no column in your project!' => 'Projektilta puuttuu sarakkeet!', - 'Change assignee' => 'Vaihda suorittajaa', - 'Change assignee for the task "%s"' => 'Vaihda suorittajaa tehtävälle %s', - 'Timezone' => 'Aikavyöhyke', - 'Sorry, I didn\'t found this information in my database!' => 'Anteeksi, en löytänyt tätä tietoa tietokannastani', - 'Page not found' => 'Sivua ei löydy', - 'Complexity' => 'Monimutkaisuus', - 'limit' => 'raja', - 'Task limit' => 'Tehtävien maksimimäärä', - 'This value must be greater than %d' => 'Arvon täytyy olla suurempi kuin %d', - 'Edit project access list' => 'Muuta projektin käyttäjiä', - 'Edit users access' => 'Muuta käyttäjien pääsyä', - 'Allow this user' => 'Salli tämä projekti', - 'Only those users have access to this project:' => 'Vain näillä käyttäjillä on pääsy projektiin:', - 'Don\'t forget that administrators have access to everything.' => 'Muista että ylläpitäjät pääsevät kaikkialle.', - 'revoke' => 'poista', - 'List of authorized users' => 'Sallittujen käyttäjien lista', - 'User' => 'Käyttäjät', - // 'Nobody have access to this project.' => '', - 'You are not allowed to access to this project.' => 'Sinulla ei ole pääsyä tähän projektiin.', - 'Comments' => 'Kommentit', - 'Post comment' => 'Lisää kommentti', - 'Write your text in Markdown' => 'Kirjoita kommenttisi Markdownilla', - 'Leave a comment' => 'Lisää kommentti', - 'Comment is required' => 'Kommentti vaaditaan', - 'Leave a description' => 'Lisää kuvaus', - 'Comment added successfully.' => 'Kommentti lisättiin onnistuneesti.', - 'Unable to create your comment.' => 'Kommentin lisäys epäonnistui.', - 'The description is required' => 'Kuvaus vaaditaan', - 'Edit this task' => 'Muokkaa tehtävää', - 'Due Date' => 'Deadline', - 'Invalid date' => 'Virheellinen päiväys', - 'Must be done before %B %e, %Y' => 'Täytyy suorittaa ennen %d.%m.%Y', - '%B %e, %Y' => '%d.%m.%Y', - 'Automatic actions' => 'Automaattiset toiminnot', - 'Your automatic action have been created successfully.' => 'Toiminto suoritettiin onnistuneesti.', - 'Unable to create your automatic action.' => 'Automaattisen toiminnon luominen epäonnistui.', - 'Remove an action' => 'Poista toiminto', - 'Unable to remove this action.' => 'Toiminnon poistaminen epäonnistui.', - 'Action removed successfully.' => 'Toiminto poistettiin onnistuneesti.', - 'Automatic actions for the project "%s"' => 'Automaattiset toiminnot projektille "%s"', - 'Defined actions' => 'Määritellyt toiminnot', - // 'Add an action' => '', - 'Event name' => 'Tapahtuman nimi', - 'Action name' => 'Toiminnon nimi', - 'Action parameters' => 'Toiminnon parametrit', - 'Action' => 'Toiminto', - 'Event' => 'Tapahtuma', - 'When the selected event occurs execute the corresponding action.' => 'Kun valittu tapahtuma tapahtuu, suorita vastaava toiminto.', - 'Next step' => 'Seuraava vaihe', - 'Define action parameters' => 'Määrittele toiminnon parametrit', - 'Save this action' => 'Tallenna toiminto', - 'Do you really want to remove this action: "%s"?' => 'Oletko varma että haluat poistaa toiminnon "%s"?', - 'Remove an automatic action' => 'Poista automaattintn toiminto', - 'Close the task' => 'Sulje tehtävä', - 'Assign the task to a specific user' => 'Osoita tehtävä käyttäjälle', - 'Assign the task to the person who does the action' => 'Määritä suorittaja tehtävälle', - 'Duplicate the task to another project' => 'Monista tehtävä toiselle projektille', - 'Move a task to another column' => 'Siirrä tehtävä toiseen sarakkeeseen', - 'Move a task to another position in the same column' => 'Siirrä tehtävä eri järjestykseen samassa sarakkeessa', - 'Task modification' => 'Tehtävän muokkaus', - 'Task creation' => 'Tehtävän luominen', - 'Open a closed task' => 'Avaa jo suljettu tehtävä', - 'Closing a task' => 'Tehtävää suljetaan', - 'Assign a color to a specific user' => 'Valitse väri käyttäjälle', - 'Column title' => 'Sarakkeen nimi', - 'Position' => 'Positio', - 'Move Up' => 'Siirrä ylös', - 'Move Down' => 'Siirrä alas', - 'Duplicate to another project' => 'Kopioi toiseen projektiin', - 'Duplicate' => 'Monista', - 'link' => 'linkki', - 'Update this comment' => 'Muuta projektia', - 'Comment updated successfully.' => 'Kommentti päivitettiin onnistuneesti.', - 'Unable to update your comment.' => 'Kommentin päivitys epäonnistui.', - 'Remove a comment' => 'Poista kommentti', - 'Comment removed successfully.' => 'Kommentti poistettiin onnistuneesti.', - 'Unable to remove this comment.' => 'Kommentin poistaminen epäonnistui.', - 'Do you really want to remove this comment?' => 'Haluatko varmasti poistaa tämän kommentin?', - 'Only administrators or the creator of the comment can access to this page.' => 'Vain ylläpitäjillä tai kommentin jättäjällä on pääsy tälle sivulle.', - 'Details' => 'Tiedot', - 'Current password for the user "%s"' => 'Käyttäjän "%s" salasana', - 'The current password is required' => 'Salasana vaaditaan', - 'Wrong password' => 'Väärä salasana', - 'Reset all tokens' => 'Resetoi kaikki tokenit', - 'All tokens have been regenerated.' => 'Kaikki tokenit luotiin uudelleen.', - 'Unknown' => 'Tuntematon', - 'Last logins' => 'Viimeisimmät kirjautumiset', - 'Login date' => 'Kirjautumispäivä', - 'Authentication method' => 'Autentikointimenetelmä', - 'IP address' => 'IP-Osoite', - 'User agent' => 'Selain', - 'Persistent connections' => 'Voimassa olevat yhteydet', - 'No session.' => 'Ei sessioita.', - 'Expiration date' => 'Vanhentumispäivä', - 'Remember Me' => 'Muista minut', - 'Creation date' => 'Luomispäivä', - 'Filter by user' => 'Rajaa käyttäjän mukaan', - 'Filter by due date' => 'Rajaa deadlinen mukaan', - 'Everybody' => 'Kaikki', - 'Open' => 'Avoin', - 'Closed' => 'Suljettu', - 'Search' => 'Etsi', - 'Nothing found.' => 'Ei löytynyt.', - 'Search in the project "%s"' => 'Etsi projektista "%s"', - 'Due date' => 'Deadline', - 'Others formats accepted: %s and %s' => 'Muut hyväksytyt muodot: %s ja %s', - 'Description' => 'Kuvaus', - '%d comments' => '%d kommenttia', - '%d comment' => '%d kommentti', - 'Email address invalid' => 'Email ei kelpaa', - 'Your Google Account is not linked anymore to your profile.' => 'Google tunnustasi ei ole enää linkattu profiiliisi', - 'Unable to unlink your Google Account.' => 'Google tunnuksen linkkaamisen poistaminen epäonnistui.', - 'Google authentication failed' => 'Google autentikointi epäonnistui', - 'Unable to link your Google Account.' => 'Google tunnuksen linkkaaminen epäonnistui.', - 'Your Google Account is linked to your profile successfully.' => 'Google tunnuksesi linkitettiin profiiliisi onnistuneesti.', - 'Email' => 'Sähköposti', - 'Link my Google Account' => 'Linkitä Google-tili', - 'Unlink my Google Account' => 'Poista Google-tilin linkitys', - 'Login with my Google Account' => 'Kirjaudu Google tunnuksella', - 'Project not found.' => 'Projektia ei löytynyt.', - 'Task #%d' => 'Tehtävä #%d', - 'Task removed successfully.' => 'Tehtävä poistettiin onnistuneesti.', - 'Unable to remove this task.' => 'Tehtävän poistaminen epäonnistui.', - 'Remove a task' => 'Poista tehtävä', - 'Do you really want to remove this task: "%s"?' => 'Haluatko varmasti poistaa tehtävän: "%s"?', - 'Assign automatically a color based on a category' => 'Aseta väri automaattisesti kategorian mukaan', - 'Assign automatically a category based on a color' => 'Aseta kategoria automaattisesti värin mukaan', - 'Task creation or modification' => 'Tehtävän luonti tai muuttaminen', - 'Category' => 'Kategoria', - 'Category:' => 'Kategoria:', - 'Categories' => 'Kategoriat', - 'Category not found.' => 'Kategoriaa ei löytynyt.', - 'Your category have been created successfully.' => 'Kategoria luotiin onnistuneesti.', - 'Unable to create your category.' => 'Kategorian luonti epäonnistui.', - 'Your category have been updated successfully.' => 'Kategoriaa muokattiin onnistuneesti.', - 'Unable to update your category.' => 'Kategorian muokkaaminen epäonnistui.', - 'Remove a category' => 'Poista kategoria', - 'Category removed successfully.' => 'Kategoria poistettu onnistuneesti.', - 'Unable to remove this category.' => 'Kategorian poisto epäonnistui.', - 'Category modification for the project "%s"' => 'Kategorian muutos projektissa "%s"', - 'Category Name' => 'Kategorian nimi', - 'Categories for the project "%s"' => 'Kategoriat projektille "%s"', - 'Add a new category' => 'Lisää uusi kategoria', - 'Do you really want to remove this category: "%s"?' => 'Haluatko varmasti poistaa kategorian: "%s"?', - 'Filter by category' => 'Rajaa kategorian mukaan', - 'All categories' => 'Kaikki kategoriat', - 'No category' => 'Kategoriaa ei löydy', - 'The name is required' => 'Nimi vaaditaan', - 'Remove a file' => 'Poista tiedosto', - 'Unable to remove this file.' => 'Tiedoston poistaminen epäonnistui.', - 'File removed successfully.' => 'Tiedosto poistettiin onnistuneesti.', - 'Attach a document' => 'Liitä dokumentti', - 'Do you really want to remove this file: "%s"?' => 'Haluatko varmasti poistaa tiedoston: "%s"?', - 'open' => 'avaa', - 'Attachments' => 'Liitteet', - 'Edit the task' => 'Muokkaa tehtävää', - 'Edit the description' => 'Muokkaa kuvausta', - 'Add a comment' => 'Lisää kommentti', - 'Edit a comment' => 'Muokkaa kommenttia', - 'Summary' => 'Yhteenveto', - 'Time tracking' => 'Ajan seuranta', - 'Estimate:' => 'Arvio:', - 'Spent:' => 'Käytetty:', - 'Do you really want to remove this sub-task?' => 'Haluatko varmasti poistaa tämän alitehtävän?', - 'Remaining:' => 'Jäljellä', - 'hours' => 'tuntia', - 'spent' => 'käytetty', - 'estimated' => 'estimoitu', - 'Sub-Tasks' => 'Alitehtävät', - 'Add a sub-task' => 'Lisää alitehtävä', - 'Original estimate' => 'Alkuperäinen estimaatti', - 'Create another sub-task' => 'Lisää toinen alitehtävä', - 'Time spent' => 'Käytetty aika', - 'Edit a sub-task' => 'Muokkaa alitehtävää', - 'Remove a sub-task' => 'Poista alitehtävä', - 'The time must be a numeric value' => 'Ajan pitää olla numero', - 'Todo' => 'Todo', - 'In progress' => 'Työnalla', - 'Sub-task removed successfully.' => 'Alitehtävä poistettu onnistuneesti.', - 'Unable to remove this sub-task.' => 'Alitehtävän poistaminen epäonnistui.', - 'Sub-task updated successfully.' => 'Alitehtävä päivitettiin onnistuneesti.', - 'Unable to update your sub-task.' => 'Alitehtävän päivitys epäonnistui.', - 'Unable to create your sub-task.' => 'Alitehtävän luonti epäonnistui.', - 'Sub-task added successfully.' => 'Alitehtävä luotiin onnistuneesti.', - 'Maximum size: ' => 'Maksimikoko: ', - 'Unable to upload the file.' => 'Tiedoston lataus epäonnistui.', - 'Display another project' => 'Näytä toinen projekti', - // 'Your GitHub account was successfully linked to your profile.' => '', - // 'Unable to link your GitHub Account.' => '', - // 'GitHub authentication failed' => '', - // 'Your GitHub account is no longer linked to your profile.' => '', - // 'Unable to unlink your GitHub Account.' => '', - // 'Login with my GitHub Account' => '', - // 'Link my GitHub Account' => '', - // 'Unlink my GitHub Account' => '', - 'Created by %s' => 'Luonut: %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Viimeksi muokattu %B %e, %Y kello %H:%M', - 'Tasks Export' => 'Tehtävien vienti', - 'Tasks exportation for "%s"' => 'Tehtävien vienti projektilta "%s"', - 'Start Date' => 'Aloituspäivä', - 'End Date' => 'Lopetuspäivä', - 'Execute' => 'Suorita', - 'Task Id' => 'Tehtävän ID', - 'Creator' => 'Luonut', - 'Modification date' => 'Muokkauspäivä', - 'Completion date' => 'Valmistumispäivä', - 'Webhook URL for task creation' => 'Webhook URL tehtävän luomiselle', - 'Webhook URL for task modification' => 'Webhook URL tehtävän muokkaamiselle', - // 'Clone' => '', - // 'Clone Project' => '', - // 'Project cloned successfully.' => '', - // 'Unable to clone this project.' => '', - // 'Email notifications' => '', - // 'Enable email notifications' => '', - // 'Task position:' => '', - // 'The task #%d have been opened.' => '', - // 'The task #%d have been closed.' => '', - // 'Sub-task updated' => '', - // 'Title:' => '', - // 'Status:' => '', - // 'Assignee:' => '', - // 'Time tracking:' => '', - // 'New sub-task' => '', - // 'New attachment added "%s"' => '', - // 'Comment updated' => '', - // 'New comment posted by %s' => '', - // 'List of due tasks for the project "%s"' => '', - // '[%s][New attachment] %s (#%d)' => '', - // '[%s][New comment] %s (#%d)' => '', - // '[%s][Comment updated] %s (#%d)' => '', - // '[%s][New subtask] %s (#%d)' => '', - // '[%s][Subtask updated] %s (#%d)' => '', - // '[%s][New task] %s (#%d)' => '', - // '[%s][Task updated] %s (#%d)' => '', - // '[%s][Task closed] %s (#%d)' => '', - // '[%s][Task opened] %s (#%d)' => '', - // '[%s][Due tasks]' => '', - // '[Kanboard] Notification' => '', - // 'I want to receive notifications only for those projects:' => '', - // 'view the task on Kanboard' => '', - // 'Public access' => '', - // 'Category management' => '', - // 'User management' => '', - // 'Active tasks' => '', - // 'Disable public access' => '', - // 'Enable public access' => '', - // 'Active projects' => '', - // 'Inactive projects' => '', - // 'Public access disabled' => '', - // 'Do you really want to disable this project: "%s"?' => '', - // 'Do you really want to duplicate this project: "%s"?' => '', - // 'Do you really want to enable this project: "%s"?' => '', - // 'Project activation' => '', - // 'Move the task to another project' => '', - // 'Move to another project' => '', - // 'Do you really want to duplicate this task?' => '', - // 'Duplicate a task' => '', - // 'External accounts' => '', - // 'Account type' => '', - // 'Local' => '', - // 'Remote' => '', - // 'Enabled' => '', - // 'Disabled' => '', - // 'Google account linked' => '', - // 'Github account linked' => '', - // 'Username:' => '', - // 'Name:' => '', - // 'Email:' => '', - // 'Default project:' => '', - // 'Notifications:' => '', - // 'Notifications' => '', - // 'Group:' => '', - // 'Regular user' => '', - // 'Account type:' => '', - // 'Edit profile' => '', - // 'Change password' => '', - // 'Password modification' => '', - // 'External authentications' => '', - // 'Google Account' => '', - // 'Github Account' => '', - // 'Never connected.' => '', - // 'No account linked.' => '', - // 'Account linked.' => '', - // 'No external authentication enabled.' => '', - // 'Password modified successfully.' => '', - // 'Unable to change the password.' => '', - // 'Change category for the task "%s"' => '', - // 'Change category' => '', - // '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '', - // '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // 'Assigned to %s with an estimate of %s/%sh' => '', - // 'Not assigned, estimate of %sh' => '', - // '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s\'s activity' => '', - // 'No activity.' => '', - // 'RSS feed' => '', - // '%s updated a comment on the task #%d' => '', - // '%s commented on the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated the task #%d' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s moved the task #%d to the position %d in the column "%s"' => '', - // 'Activity' => '', - // 'Default values are "%s"' => '', - // 'Default columns for new projects (Comma-separated)' => '', - // 'Task assignee change' => '', - // '%s change the assignee of the task #%d to %s' => '', - // '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', - // 'New password for the user "%s"' => '', - // 'Choose an event' => '', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', - // 'Reference' => '', - // 'Reference: %s' => '', - // 'Label' => '', - // 'Database' => '', - // 'About' => '', - // 'Database driver:' => '', - // 'Board settings' => '', - // 'URL and token' => '', - // 'Webhook settings' => '', - // 'URL for task creation:' => '', - // 'Reset token' => '', - // 'API endpoint:' => '', - // 'Refresh interval for private board' => '', - // 'Refresh interval for public board' => '', - // 'Task highlight period' => '', - // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', - // 'Frequency in second (60 seconds by default)' => '', - // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', - // 'Application URL' => '', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', - // 'Date format' => '', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', - // 'New private project' => '', - // 'This project is private' => '', - // 'Type here to create a new sub-task' => '', - // 'Add' => '', - // 'Estimated time: %s hours' => '', - // 'Time spent: %s hours' => '', - // 'Started on %B %e, %Y' => '', - // 'Start date' => '', - // 'Time estimated' => '', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', -); diff --git a/app/Locales/it_IT/translations.php b/app/Locales/it_IT/translations.php deleted file mode 100644 index 4d2cfc91..00000000 --- a/app/Locales/it_IT/translations.php +++ /dev/null @@ -1,554 +0,0 @@ -<?php - -return array( - 'None' => 'Nessuno', - 'edit' => 'modificare', - 'Edit' => 'Modificare', - 'remove' => 'cancellare', - 'Remove' => 'Cancellare', - 'Update' => 'Aggiornare', - 'Yes' => 'Si', - 'No' => 'No', - 'cancel' => 'annullare', - 'or' => 'o', - 'Yellow' => 'Giallo', - 'Blue' => 'Blu', - 'Green' => 'Verde', - 'Purple' => 'Viola', - 'Red' => 'Rosso', - 'Orange' => 'Arancione', - 'Grey' => 'Grigio', - 'Save' => 'Salvare', - 'Login' => 'Entra', - 'Official website:' => 'Sito web ufficiale:', - 'Unassigned' => 'Non assegnato', - 'View this task' => 'Vedere questo compito', - 'Remove user' => 'Cancellare un utente', - 'Do you really want to remove this user: "%s"?' => 'Veramente vuoi cancellare questo utente: « %s » ?', - 'New user' => 'Aggiungere un utente', - 'All users' => 'Tutti gli utenti', - 'Username' => 'Nome utente', - 'Password' => 'Password', - 'Default project' => 'Progetto predefinito', - 'Administrator' => 'Amministratore', - 'Sign in' => 'Iscriversi', - 'Users' => 'Utenti', - 'No user' => 'Nessun utente', - 'Forbidden' => 'Vietato', - 'Access Forbidden' => 'Accesso vietato', - 'Only administrators can access to this page.' => 'Solo gli amministratori possono accedere a questa pagina.', - 'Edit user' => 'Modificare un utente', - 'Logout' => 'Uscire', - 'Bad username or password' => 'Utente o password errati', - 'users' => 'utenti', - 'projects' => 'progetti', - 'Edit project' => 'Modificare progetto', - 'Name' => 'Nome', - 'Activated' => 'Attivo', - 'Projects' => 'Progetti', - 'No project' => 'Nessun progetto', - 'Project' => 'Progetto', - 'Status' => 'Stato', - 'Tasks' => 'Compiti', - 'Board' => 'Bacheca', - // 'Actions' => '', - 'Inactive' => 'Inattivo', - 'Active' => 'Attivo', - 'Column %d' => 'Colonna %d', - 'Add this column' => 'Aggiungere questa colonna', - '%d tasks on the board' => '%d compiti sulla bacheca', - '%d tasks in total' => '%d compiti in totale', - 'Unable to update this board.' => 'Non si può aggiornare questa bacheca.', - 'Edit board' => 'Modificare questa bacheca', - 'Disable' => 'Disattivare', - 'Enable' => 'Attivare', - 'New project' => 'Nuovo progetto', - 'Do you really want to remove this project: "%s"?' => 'Veramente vuoi eliminare questo progetto: « %s » ?', - 'Remove project' => 'Cancellare il progetto', - 'Boards' => 'Bacheche', - 'Edit the board for "%s"' => 'Modificare la bacheca per « %s »', - 'All projects' => 'Tutti i progetti', - 'Change columns' => 'Cambiare le colonne', - 'Add a new column' => 'Aggiungere una nuova colonna', - 'Title' => 'Titolo', - 'Add Column' => 'Aggiungere colonna', - 'Project "%s"' => 'progetto « %s »', - 'Nobody assigned' => 'Nessuno assegnato', - 'Assigned to %s' => 'Assegnato a %s', - 'Remove a column' => 'Cancellare questa colonna', - 'Remove a column from a board' => 'Cancellare una colonna da una bacheca', - 'Unable to remove this column.' => 'Non si può cancellare questa colonna.', - 'Do you really want to remove this column: "%s"?' => 'Veramente desideri cancellare questa colonna : « %s » ?', - 'This action will REMOVE ALL TASKS associated to this column!' => 'Questa azione cancellerà TUTTI I COMPITI legati a questa colonna!', - 'Settings' => 'Impostazioni', - 'Application settings' => 'Impostazioni dell\'applicazione', - 'Language' => 'Lingua', - 'Webhook token:' => 'Identificatore (token) per i webhooks :', - // 'API token:' => '', - 'More information' => 'Più informazioni', - 'Database size:' => 'Dimensioni della base dati:', - 'Download the database' => 'Scaricare la base dati', - 'Optimize the database' => 'Ottimizare la base dati', - '(VACUUM command)' => '(Comando VACUUM)', - '(Gzip compressed Sqlite file)' => '(File Sqlite compresso in Gzip)', - 'User settings' => 'Impostazioni di utente', - 'My default project:' => 'Il mio progetto predefinito:', - 'Close a task' => 'Chiudere un compito', - 'Do you really want to close this task: "%s"?' => 'Veramente desideri chiudere questo compito: « %s » ?', - 'Edit a task' => 'Modificare un compito', - 'Column' => 'colonna', - // 'Color' => '', - 'Assignee' => 'Persona assegnata', - 'Create another task' => 'Creare un nuovo compito', - 'New task' => 'Nuovo compito', - 'Open a task' => 'Aprire un compito', - 'Do you really want to open this task: "%s"?' => 'Veramente desideri aprire questo compito: « %s » ?', - 'Back to the board' => 'Tornare alla bacheca', - // 'Created on %B %e, %Y at %k:%M %p' => '', - 'There is nobody assigned' => 'Non c\'è nessuno assegnato a questo compito', - 'Column on the board:' => 'Colonna sulla bacheca: ', - 'Status is open' => 'Stato aperto', - 'Status is closed' => 'stato chiuso', - 'Close this task' => 'Chiudere questo compito', - 'Open this task' => 'Aprire questo compito', - 'There is no description.' => 'Non c\'è descrizione.', - 'Add a new task' => 'Aggiungere un nuovo compito', - 'The username is required' => 'Si richiede un nome di utente', - 'The maximum length is %d characters' => 'La lunghezza massima è di %d caratteri', - 'The minimum length is %d characters' => 'La lunghezza minima è di %d caratteri', - 'The password is required' => 'Si richiede una password', - 'This value must be an integer' => 'questo valore deve essere un intero', - 'The username must be unique' => 'Il nome di utente deve essere unico', - 'The username must be alphanumeric' => 'Il nome di utente deve essere alfanumerico', - 'The user id is required' => 'Si richiede l\'identificatore dell\'utente', - // 'Passwords don\'t match' => '', - 'The confirmation is required' => 'Si richiede una conferma', - 'The column is required' => 'Si richiede una colonna', - 'The project is required' => 'Si richiede il progetto', - 'The color is required' => 'Si richiede il colore', - 'The id is required' => 'Si richiede l\'identificatore', - 'The project id is required' => 'Si richiede l\'identificatore del progetto', - 'The project name is required' => 'Si richiede il nome del progetto', - 'This project must be unique' => 'Il nome del progetto deve essere unico', - 'The title is required' => 'Si richiede un titolo', - 'The language is required' => 'Si richiede una lingua', - 'There is no active project, the first step is to create a new project.' => 'Non ci sono progetti attivi, il primo passo consiste in creare un nuovo progetto.', - 'Settings saved successfully.' => 'Impostazioni salvate correttamente.', - 'Unable to save your settings.' => 'Non si possono salvare le impostazioni.', - 'Database optimization done.' => 'Ottimizzazione della base dati conclusa.', - 'Your project have been created successfully.' => 'Il tuo progetto è stato creato correttamente.', - 'Unable to create your project.' => 'Non si può creare il progetto.', - 'Project updated successfully.' => 'Progetto aggiornato correttamente.', - 'Unable to update this project.' => 'Non si può aggiornare il progetto.', - 'Unable to remove this project.' => 'Non si può cancellare questo progetto.', - 'Project removed successfully.' => 'Progetto cancellato correttamente.', - 'Project activated successfully.' => 'Progetto attivato correttamente.', - 'Unable to activate this project.' => 'Non si può attivare il progetto.', - 'Project disabled successfully.' => 'Progetto disattivato correttamente.', - 'Unable to disable this project.' => 'Non si può disattivare il progetto.', - 'Unable to open this task.' => 'Non si può aprire questo compito.', - 'Task opened successfully.' => 'Il compito è stato aperto correttamente.', - 'Unable to close this task.' => 'Non si può chiudere questo compito.', - 'Task closed successfully.' => 'Compito chiuso correttamente.', - 'Unable to update your task.' => 'Non si può modificare questo compito.', - 'Task updated successfully.' => 'Compito modificato correttamente.', - 'Unable to create your task.' => 'Non si può creare questo compito.', - 'Task created successfully.' => 'Compito creato correttamente.', - 'User created successfully.' => 'Utente creato correttamente.', - 'Unable to create your user.' => 'Non si può creare l\'utente.', - 'User updated successfully.' => 'Utente aggiornato correttamente.', - 'Unable to update your user.' => 'Non si può aggiornare questo utente.', - 'User removed successfully.' => 'Utente cancellato correttamente.', - 'Unable to remove this user.' => 'Non si può cancellare questo utente.', - 'Board updated successfully.' => 'Bacheca aggiornata correttamente.', - 'Ready' => 'Pronto', - 'Backlog' => 'In attesa', - 'Work in progress' => 'In corso', - 'Done' => 'Fatto', - 'Application version:' => 'Versione dell\'applicazione:', - // 'Completed on %B %e, %Y at %k:%M %p' => '', - // '%B %e, %Y at %k:%M %p' => '', - 'Date created' => 'Data di creazione', - 'Date completed' => 'Data di termine', - 'Id' => 'Identificatore', - 'No task' => 'Nessun compito', - 'Completed tasks' => 'Compiti fatti', - 'List of projects' => 'Lista di progetti', - 'Completed tasks for "%s"' => 'Compiti fatti da « %s »', - '%d closed tasks' => '%d compiti chiusi', - 'No task for this project' => 'Nessun compito per questo progetto', - 'Public link' => 'Link pubblico', - 'There is no column in your project!' => 'Non c\'è nessuna colonna per questo progetto!', - 'Change assignee' => 'Cambiare la persona assegnata', - 'Change assignee for the task "%s"' => 'Cambiare la persona assegnata per il compito « %s »', - 'Timezone' => 'Fuso orario', - 'Sorry, I didn\'t found this information in my database!' => 'Mi dispiace, non ho trovato questa informazione sulla base dati!', - 'Page not found' => 'Pagina non trovata', - // 'Complexity' => '', - 'limit' => 'limite', - 'Task limit' => 'Numero massimo di compiti', - 'This value must be greater than %d' => 'questo valore deve essere maggiore di %d', - 'Edit project access list' => 'Modificare i permessi del progetto', - 'Edit users access' => 'Modificare i permessi degli utenti', - 'Allow this user' => 'Permettere a questo utente', - 'Only those users have access to this project:' => 'Solo questi utenti hanno accesso a questo progetto:', - 'Don\'t forget that administrators have access to everything.' => 'Non dimenticare che gli amministratori hanno accesso a tutto.', - 'revoke' => 'revocare', - 'List of authorized users' => 'Lista di utenti autorizzati', - 'User' => 'Utente', - // 'Nobody have access to this project.' => '', - 'You are not allowed to access to this project.' => 'Non hai l\'accesso a questo progetto.', - 'Comments' => 'Commenti', - 'Post comment' => 'Mandare commento', - 'Write your text in Markdown' => 'Scrivi il testo in Markdown', - 'Leave a comment' => 'Lasciare un commento', - 'Comment is required' => 'Si richiede un commento', - 'Leave a description' => 'Lasciare una descrizione', - 'Comment added successfully.' => 'Commenti aggiunti correttamente.', - 'Unable to create your comment.' => 'Non si può creare questo commento.', - 'The description is required' => 'Si richiede una descrizione', - 'Edit this task' => 'Modificare questo compito', - 'Due Date' => 'Data di scadenza', - 'Invalid date' => 'Data sbagliata', - // 'Must be done before %B %e, %Y' => '', - // '%B %e, %Y' => '', - 'Automatic actions' => 'Azioni automatiche', - 'Your automatic action have been created successfully.' => 'l\'azione automatica è stata creata correttamente.', - 'Unable to create your automatic action.' => 'Non si può creare quest\'azione automatica.', - 'Remove an action' => 'Cancellare un\'azione', - 'Unable to remove this action.' => 'Non si può cancellare questa azione.', - 'Action removed successfully.' => 'Azione cancellata correttamente.', - 'Automatic actions for the project "%s"' => 'Azioni automatiche per questo progetto « %s »', - 'Defined actions' => 'Azioni definite', - // 'Add an action' => '', - 'Event name' => 'Nome dell\'evento', - 'Action name' => 'Nome dell\'azione', - 'Action parameters' => 'Parametri d\'azione', - 'Action' => 'Azione', - 'Event' => 'Evento', - 'When the selected event occurs execute the corresponding action.' => 'Quando accade l\'evento selezionato, eseguire l\'azione corrispondente.', - 'Next step' => 'Passo seguente', - 'Define action parameters' => 'Definire i parametri dell\'azione', - 'Save this action' => 'Salvare questa azione', - 'Do you really want to remove this action: "%s"?' => 'Veramente vuole cancellare questa azione « %s » ?', - 'Remove an automatic action' => 'Cancellare un\'azione automatica', - 'Close the task' => 'Chiudere questo compito', - 'Assign the task to a specific user' => 'Assegnare questo compito a un utente specifico', - 'Assign the task to the person who does the action' => 'Assegnare il compito all\'utente che svolge l\'azione', - 'Duplicate the task to another project' => 'Duplicare il compito in altro progetto', - 'Move a task to another column' => 'Muovere un compito in un\'altra colonna', - 'Move a task to another position in the same column' => 'Muovere un compito in un\'altra posizione sulla stessa colonna', - 'Task modification' => 'Modifica di un compito', - 'Task creation' => 'Creazione di un compito', - 'Open a closed task' => 'Riaprire un compito', - 'Closing a task' => 'Chiudere un compito', - // 'Assign a color to a specific user' => '', - 'Column title' => 'Titolo della colonna', - 'Position' => 'Posizione', - 'Move Up' => 'Alzare', - 'Move Down' => 'Abassare', - 'Duplicate to another project' => 'Duplicare in un altro progetto', - 'Duplicate' => 'Duplicare', - 'link' => 'link', - 'Update this comment' => 'Aggiornare questo commento', - 'Comment updated successfully.' => 'Commento aggiornato correttamente.', - 'Unable to update your comment.' => 'Non si può aggiornare questo commento.', - 'Remove a comment' => 'Cancellare un commento', - 'Comment removed successfully.' => 'Commento cancellato correttamente.', - 'Unable to remove this comment.' => 'Non si può cancellare questo commento.', - 'Do you really want to remove this comment?' => 'Desidera cancellare questo commento?', - 'Only administrators or the creator of the comment can access to this page.' => 'Solo gli amministratori o l\'autore del commento hanno accesso a questa pagina.', - 'Details' => 'Dettagli', - 'Current password for the user "%s"' => 'Password attuale per l\'utente: « %s »', - 'The current password is required' => 'Si richiede la password attuale', - 'Wrong password' => 'password sbagliata', - 'Reset all tokens' => 'Azzerare gli identificatori (tokens) di sicurezza ', - 'All tokens have been regenerated.' => 'Tutti gli identificatori (tokens) sono stati rigenerati.', - 'Unknown' => 'Sconociuto', - 'Last logins' => 'Ultimi ingressi', - 'Login date' => 'Data di ingresso', - 'Authentication method' => 'Metodo di autenticazzione', - 'IP address' => 'Indirizzo IP', - 'User agent' => 'Navigatore', - 'Persistent connections' => 'Connessioni persistenti', - 'No session.' => 'Non esiste sessione.', - 'Expiration date' => 'Data di scadenza', - 'Remember Me' => 'Ricordami', - 'Creation date' => 'Data di creazione', - 'Filter by user' => 'Filtrato mediante utente', - 'Filter by due date' => 'Filtrare attraverso data di scadenza', - 'Everybody' => 'Tutti', - 'Open' => 'Aperto', - 'Closed' => 'Chiuso', - 'Search' => 'Cercare', - 'Nothing found.' => 'Non si è trovato nulla.', - 'Search in the project "%s"' => 'Cercare nel progetto "%s"', - 'Due date' => 'Data di scadenza', - 'Others formats accepted: %s and %s' => 'Altri formati accettati: %s y %s', - 'Description' => 'Descrizione', - '%d comments' => '%d commenti', - '%d comment' => '%d commento', - 'Email address invalid' => 'Indirizzo e-mail sbagliato', - 'Your Google Account is not linked anymore to your profile.' => 'Il suo account Google non è più collegato al suo profilo', - 'Unable to unlink your Google Account.' => 'Non si può svincolare l\'account di Google.', - 'Google authentication failed' => 'Autenticazione con Google non riuscita', - 'Unable to link your Google Account.' => 'Non si può collegare il tuo account di Google.', - 'Your Google Account is linked to your profile successfully.' => 'Il tuo account di Google è stato collegato correttamente al tuo profilo.', - 'Email' => 'E-mail', - 'Link my Google Account' => 'Collegare il mio Account di Google', - 'Unlink my Google Account' => 'Scollegare il mio account di Google', - 'Login with my Google Account' => 'Entra con il mio Account di Google', - 'Project not found.' => 'progetto non trovato.', - 'Task #%d' => 'Compito numero %d', - 'Task removed successfully.' => 'Compito cancellato correttamente.', - 'Unable to remove this task.' => 'Non si può cancellare questo compito.', - 'Remove a task' => 'Cancellare un compito', - 'Do you really want to remove this task: "%s"?' => 'Veramente vuoi cancellare questo compito: "%s"?', - 'Assign automatically a color based on a category' => 'Assegnare un colore in modo automatico basandosi sulla categoria', - 'Assign automatically a category based on a color' => 'Assegnare una categoria in modo automatico basandosi sul colore', - 'Task creation or modification' => 'Creazione o modifica di compito', - 'Category' => 'Categoria', - 'Category:' => 'Categoria:', - 'Categories' => 'Categorie', - 'Category not found.' => 'Categoria non trovata.', - 'Your category have been created successfully.' => 'La tua categoria è stata creata correttamente.', - 'Unable to create your category.' => 'Non si può creare la tua categoria.', - 'Your category have been updated successfully.' => 'La tua categoria è stata aggiornata correttamente.', - 'Unable to update your category.' => 'Non si può aggiornare la tua categoria.', - 'Remove a category' => 'Cancellare una categoria', - 'Category removed successfully.' => 'Categoria cancellata correttamente.', - 'Unable to remove this category.' => 'Non si può cancellare questa categoria.', - 'Category modification for the project "%s"' => 'Modifica di categoria per il progetto "%s"', - 'Category Name' => 'Nome di categoria', - 'Categories for the project "%s"' => 'Categorie per il progetto', - 'Add a new category' => 'Aggiungere una nuova categoria', - 'Do you really want to remove this category: "%s"?' => 'Vuoi veramente cancellare questa categoria: "%s"?', - 'Filter by category' => 'Filtrare attraverso categoria', - 'All categories' => 'Tutte le categorie', - 'No category' => 'Senza categoria', - 'The name is required' => 'Si richiede un nome', - 'Remove a file' => 'Cancellare un file', - 'Unable to remove this file.' => 'Non si può cancellare questo file.', - 'File removed successfully.' => 'File cancellato correttamente.', - 'Attach a document' => 'Allegare un documento', - 'Do you really want to remove this file: "%s"?' => 'Vuoi veramente cancellare questo file: "%s"?', - 'open' => 'aprire', - 'Attachments' => 'Allegati', - 'Edit the task' => 'Modificare il compito', - 'Edit the description' => 'Modificare la descrizione', - 'Add a comment' => 'Aggiungere un commento', - 'Edit a comment' => 'Modificare un commento', - 'Summary' => 'Sommario', - 'Time tracking' => 'Time tracking', - 'Estimate:' => 'Stimato:', - 'Spent:' => 'Trascorso:', - 'Do you really want to remove this sub-task?' => 'Vuoi veramente cancellare questo sotto-compito?', - 'Remaining:' => 'Rimangono', - 'hours' => 'ore', - 'spent' => 'trascorse', - 'estimated' => 'stimate', - 'Sub-Tasks' => 'Sotto-compiti', - 'Add a sub-task' => 'Aggiungere un sotto-compito', - 'Original estimate' => 'Stima originale', - 'Create another sub-task' => 'Creare un altro sotto-compito', - 'Time spent' => 'Tempo Trascorso', - 'Edit a sub-task' => 'Modificare un sotto-compito', - 'Remove a sub-task' => 'Cancellare un sotto-compito', - 'The time must be a numeric value' => 'Il tempo deve essere un valore numerico', - 'Todo' => 'Da fare', - 'In progress' => 'In corso', - 'Sub-task removed successfully.' => 'Sotto-compito cancellato correttamente.', - 'Unable to remove this sub-task.' => 'Non si può cancellare questo sotto-compito.', - 'Sub-task updated successfully.' => 'Sotto-compito aggiornato correttamente.', - 'Unable to update your sub-task.' => 'Non si può aggiornare il tuo sotto-compito.', - 'Unable to create your sub-task.' => 'Non si può creare il tuo sotto-compito.', - 'Sub-task added successfully.' => 'Sotto-compito aggiunto correttamente.', - 'Maximum size: ' => 'Dimensioni massime', - 'Unable to upload the file.' => 'Non si può caricare il file.', - 'Display another project' => 'Mostrare un altro progetto', - 'Your GitHub account was successfully linked to your profile.' => 'Il suo account di Github è stato collegato correttamente col tuo profilo.', - 'Unable to link your GitHub Account.' => 'Non si può collegarre il tuo account di Github.', - 'GitHub authentication failed' => 'Autenticazione con GitHub non riuscita', - 'Your GitHub account is no longer linked to your profile.' => 'Il tuo account di Github non è più collegato al tuo profilo.', - 'Unable to unlink your GitHub Account.' => 'Non si può collegare il tuo account di Github.', - 'Login with my GitHub Account' => 'Entrare col tuo account di Github', - 'Link my GitHub Account' => 'Collegare il mio account Github', - 'Unlink my GitHub Account' => 'Scollegare il mio account di Github', - 'Created by %s' => 'Creato da %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Ultima modifica il %d/%m/%Y alle %H:%M', - 'Tasks Export' => 'Esportazione di compiti', - 'Tasks exportation for "%s"' => 'Esportazione di compiti per « %s »', - 'Start Date' => 'Data d\'inizio', - 'End Date' => 'Data di fine', - 'Execute' => 'Eseguire', - 'Task Id' => 'Identificatore del compito', - 'Creator' => 'Creatore', - 'Modification date' => 'Data di modifica', - 'Completion date' => 'Data di termine', - // 'Webhook URL for task creation' => '', - // 'Webhook URL for task modification' => '', - // 'Clone' => '', - // 'Clone Project' => '', - // 'Project cloned successfully.' => '', - // 'Unable to clone this project.' => '', - // 'Email notifications' => '', - // 'Enable email notifications' => '', - // 'Task position:' => '', - // 'The task #%d have been opened.' => '', - // 'The task #%d have been closed.' => '', - // 'Sub-task updated' => '', - // 'Title:' => '', - // 'Status:' => '', - // 'Assignee:' => '', - // 'Time tracking:' => '', - // 'New sub-task' => '', - 'New attachment added "%s"' => 'Nuovo allegato aggiunto « %s »', - 'Comment updated' => 'Commento aggiornato', - 'New comment posted by %s' => 'Nuovo commento aggiunto da « %s »', - 'List of due tasks for the project "%s"' => 'Lista dei compiti scaduti per il progetto « %s »', - '[%s][New attachment] %s (#%d)' => '[%s][Nuovo allegato] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Nuovo commento] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Commento aggiornato] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Nuovo sotto-compito] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Sotto-compito aggiornato] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Nuovo compito] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Compito aggiornato] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Compito chiuso] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Compito aperto] %s (#%d)', - '[%s][Due tasks]' => '[%s][Compiti scaduti]', - '[Kanboard] Notification' => '[Kanboard] Notifica', - 'I want to receive notifications only for those projects:' => 'Vorrei ricevere le notifiche solo da questi progetti:', - 'view the task on Kanboard' => 'vedi il compito su Kanboard', - // 'Public access' => '', - // 'Category management' => '', - // 'User management' => '', - // 'Active tasks' => '', - // 'Disable public access' => '', - // 'Enable public access' => '', - // 'Active projects' => '', - // 'Inactive projects' => '', - // 'Public access disabled' => '', - // 'Do you really want to disable this project: "%s"?' => '', - // 'Do you really want to duplicate this project: "%s"?' => '', - // 'Do you really want to enable this project: "%s"?' => '', - // 'Project activation' => '', - // 'Move the task to another project' => '', - // 'Move to another project' => '', - // 'Do you really want to duplicate this task?' => '', - // 'Duplicate a task' => '', - // 'External accounts' => '', - // 'Account type' => '', - // 'Local' => '', - // 'Remote' => '', - // 'Enabled' => '', - // 'Disabled' => '', - // 'Google account linked' => '', - // 'Github account linked' => '', - // 'Username:' => '', - // 'Name:' => '', - // 'Email:' => '', - // 'Default project:' => '', - // 'Notifications:' => '', - // 'Notifications' => '', - // 'Group:' => '', - // 'Regular user' => '', - // 'Account type:' => '', - // 'Edit profile' => '', - // 'Change password' => '', - // 'Password modification' => '', - // 'External authentications' => '', - // 'Google Account' => '', - // 'Github Account' => '', - // 'Never connected.' => '', - // 'No account linked.' => '', - // 'Account linked.' => '', - // 'No external authentication enabled.' => '', - // 'Password modified successfully.' => '', - // 'Unable to change the password.' => '', - // 'Change category for the task "%s"' => '', - // 'Change category' => '', - // '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '', - // '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // 'Assigned to %s with an estimate of %s/%sh' => '', - // 'Not assigned, estimate of %sh' => '', - // '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s\'s activity' => '', - // 'No activity.' => '', - // 'RSS feed' => '', - // '%s updated a comment on the task #%d' => '', - // '%s commented on the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated the task #%d' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s moved the task #%d to the position %d in the column "%s"' => '', - // 'Activity' => '', - // 'Default values are "%s"' => '', - // 'Default columns for new projects (Comma-separated)' => '', - // 'Task assignee change' => '', - // '%s change the assignee of the task #%d to %s' => '', - // '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', - // 'New password for the user "%s"' => '', - // 'Choose an event' => '', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', - // 'Reference' => '', - // 'Reference: %s' => '', - // 'Label' => '', - // 'Database' => '', - // 'About' => '', - // 'Database driver:' => '', - // 'Board settings' => '', - // 'URL and token' => '', - // 'Webhook settings' => '', - // 'URL for task creation:' => '', - // 'Reset token' => '', - // 'API endpoint:' => '', - // 'Refresh interval for private board' => '', - // 'Refresh interval for public board' => '', - // 'Task highlight period' => '', - // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', - // 'Frequency in second (60 seconds by default)' => '', - // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', - // 'Application URL' => '', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', - // 'Date format' => '', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', - // 'New private project' => '', - // 'This project is private' => '', - // 'Type here to create a new sub-task' => '', - // 'Add' => '', - // 'Estimated time: %s hours' => '', - // 'Time spent: %s hours' => '', - // 'Started on %B %e, %Y' => '', - // 'Start date' => '', - // 'Time estimated' => '', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', -); diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php deleted file mode 100644 index 1ca201c1..00000000 --- a/app/Locales/pl_PL/translations.php +++ /dev/null @@ -1,554 +0,0 @@ -<?php - -return array( - 'None' => 'Brak', - 'edit' => 'edytuj', - 'Edit' => 'Edytuj', - 'remove' => 'usuń', - 'Remove' => 'Usuń', - 'Update' => 'Aktualizuj', - 'Yes' => 'Tak', - 'No' => 'Nie', - 'cancel' => 'anuluj', - 'or' => 'lub', - 'Yellow' => 'Żółty', - 'Blue' => 'Niebieski', - 'Green' => 'Zielony', - 'Purple' => 'Fioletowy', - 'Red' => 'Czerwony', - 'Orange' => 'Pomarańczowy', - 'Grey' => 'Szary', - 'Save' => 'Zapisz', - 'Login' => 'Login', - 'Official website:' => 'Oficjalna strona:', - 'Unassigned' => 'Nieprzypisany', - 'View this task' => 'Zobacz zadanie', - 'Remove user' => 'Usuń użytkownika', - 'Do you really want to remove this user: "%s"?' => 'Na pewno chcesz usunąć użytkownika: "%s"?', - 'New user' => 'Nowy użytkownik', - 'All users' => 'Wszyscy użytkownicy', - 'Username' => 'Nazwa użytkownika', - 'Password' => 'Hasło', - 'Default project' => 'Domyślny projekt', - 'Administrator' => 'Administrator', - 'Sign in' => 'Zaloguj', - 'Users' => 'Użytkownicy', - 'No user' => 'Brak użytkowników', - 'Forbidden' => 'Zabroniony', - 'Access Forbidden' => 'Dostęp zabroniony', - 'Only administrators can access to this page.' => 'Tylko administrator może wejść na tą stronę.', - 'Edit user' => 'Edytuj użytkownika', - 'Logout' => 'Wyloguj', - 'Bad username or password' => 'Zła nazwa uyżytkownika lub hasło', - 'users' => 'użytkownicy', - 'projects' => 'projekty', - 'Edit project' => 'Edytuj projekt', - 'Name' => 'Nazwa', - 'Activated' => 'Aktywny', - 'Projects' => 'Projekty', - 'No project' => 'Brak projektów', - 'Project' => 'Projekt', - 'Status' => 'Status', - 'Tasks' => 'Zadania', - 'Board' => 'Tablica', - 'Actions' => 'Akcje', - 'Inactive' => 'Nieaktywny', - 'Active' => 'Aktywny', - 'Column %d' => 'Kolumna %d', - 'Add this column' => 'Dodaj kolumnę', - '%d tasks on the board' => '%d zadań na tablicy', - '%d tasks in total' => '%d wszystkich zadań', - 'Unable to update this board.' => 'Nie można zaktualizować tablicy.', - 'Edit board' => 'Edytuj tablicę', - 'Disable' => 'Wyłącz', - 'Enable' => 'Włącz', - 'New project' => 'Nowy projekt', - 'Do you really want to remove this project: "%s"?' => 'Na pewno chcesz usunąć projekt: "%s"?', - 'Remove project' => 'Usuń projekt', - 'Boards' => 'Tablice', - 'Edit the board for "%s"' => 'Edytuj tablię dla "%s"', - 'All projects' => 'Wszystkie projekty', - 'Change columns' => 'Zmień kolumny', - 'Add a new column' => 'Dodaj nową kolumnę', - 'Title' => 'Tytuł', - 'Add Column' => 'Dodaj kolumnę', - 'Project "%s"' => 'Projekt "%s"', - 'Nobody assigned' => 'Nikt nie przypisany', - 'Assigned to %s' => 'Przypisane do %s', - 'Remove a column' => 'Usuń kolumnę', - 'Remove a column from a board' => 'Usuń kolumnę z tablicy', - 'Unable to remove this column.' => 'Nie udało się usunąć kolumny.', - 'Do you really want to remove this column: "%s"?' => 'Na pewno chcesz usunąć kolumnę: "%s"?', - 'This action will REMOVE ALL TASKS associated to this column!' => 'Wszystkie zadania w kolumnie zostaną usunięte!', - 'Settings' => 'Ustawienia', - 'Application settings' => 'Ustawienia aplikacji', - 'Language' => 'Język', - 'Webhook token:' => 'Token :', - // 'API token:' => '', - 'More information' => 'Więcej informacji', - 'Database size:' => 'Rozmiar bazy danych :', - 'Download the database' => 'Pobierz bazę danych', - 'Optimize the database' => 'Optymalizuj bazę danych', - '(VACUUM command)' => '(komenda VACUUM)', - '(Gzip compressed Sqlite file)' => '(baza danych spakowana Gzip)', - 'User settings' => 'Ustawienia użytkownika', - 'My default project:' => 'Mój domyślny projekt:', - 'Close a task' => 'Zakończ zadanie', - 'Do you really want to close this task: "%s"?' => 'Na pewno chcesz zakończyć to zadanie: "%s"?', - 'Edit a task' => 'Edytuj zadanie', - 'Column' => 'Kolumna', - 'Color' => 'Kolor', - 'Assignee' => 'Odpowiedzialny', - 'Create another task' => 'Dodaj kolejne zadanie', - 'New task' => 'Nowe zadanie', - 'Open a task' => 'Otwórz zadanie', - 'Do you really want to open this task: "%s"?' => 'Na pewno chcesz otworzyć zadanie: "%s"?', - 'Back to the board' => 'Powrót do tablicy', - 'Created on %B %e, %Y at %k:%M %p' => 'Utworzono dnia %e %B %Y o %k:%M', - 'There is nobody assigned' => 'Nikt nie jest przypisany', - 'Column on the board:' => 'Kolumna na tablicy:', - 'Status is open' => 'Status otwarty', - 'Status is closed' => 'Status zamknięty', - 'Close this task' => 'Zamknij zadanie', - 'Open this task' => 'Otwórz zadanie', - 'There is no description.' => 'Brak opisu.', - 'Add a new task' => 'Dodaj zadanie', - 'The username is required' => 'Nazwa użytkownika jest wymagana', - 'The maximum length is %d characters' => 'Maksymalna długość wynosi %d znaków', - 'The minimum length is %d characters' => 'Minimalna długość wynosi %d znaków', - 'The password is required' => 'Hasło jest wymagane', - 'This value must be an integer' => 'Wartość musi być liczbą całkowitą', - 'The username must be unique' => 'Nazwa użytkownika musi być unikalna', - 'The username must be alphanumeric' => 'Nazwa użytkownika musi być alfanumeryczna', - 'The user id is required' => 'ID użytkownika jest wymagane', - 'Passwords don\'t match' => 'Hasła nie pasują do siebie', - 'The confirmation is required' => 'Wymagane jest potwierdzenie', - 'The column is required' => 'Kolumna jest wymagana', - 'The project is required' => 'Projekt jest wymagany', - 'The color is required' => 'Kolor jest wymagany', - 'The id is required' => 'ID jest wymagane', - 'The project id is required' => 'ID projektu jest wymagane', - 'The project name is required' => 'Nazwa projektu jest wymagana', - 'This project must be unique' => 'Projekt musi być unikalny', - 'The title is required' => 'Tutył jest wymagany', - 'The language is required' => 'Język jest wymagany', - 'There is no active project, the first step is to create a new project.' => 'Brak aktywnych projektów. Pierwszym krokiem jest utworzenie nowego projektu.', - 'Settings saved successfully.' => 'Ustawienia zapisane.', - 'Unable to save your settings.' => 'Nie udało się zapisać ustawień.', - 'Database optimization done.' => 'Optymalizacja bazy danych zakończona.', - 'Your project have been created successfully.' => 'Projekt został pomyślnie utworzony.', - 'Unable to create your project.' => 'Nie udało się stworzyć projektu.', - 'Project updated successfully.' => 'Projekt zaktualizowany.', - 'Unable to update this project.' => 'Nie można zaktualizować projektu.', - 'Unable to remove this project.' => 'Nie można usunąć projektu.', - 'Project removed successfully.' => 'Projekt usunięty.', - 'Project activated successfully.' => 'Projekt aktywowany.', - 'Unable to activate this project.' => 'Nie można aktywować projektu.', - 'Project disabled successfully.' => 'Projekt wyłączony.', - 'Unable to disable this project.' => 'Nie można wyłączyć projektu.', - 'Unable to open this task.' => 'Nie można otworzyć tego zadania.', - 'Task opened successfully.' => 'Zadanie otwarte.', - 'Unable to close this task.' => 'Nie można zamknąć tego zadania.', - 'Task closed successfully.' => 'Zadanie zamknięte.', - 'Unable to update your task.' => 'Nie można zaktualizować tego zadania.', - 'Task updated successfully.' => 'Zadanie zaktualizowane.', - 'Unable to create your task.' => 'Nie można dodać zadania.', - 'Task created successfully.' => 'Zadanie zostało utworzone.', - 'User created successfully.' => 'Użytkownik dodany', - 'Unable to create your user.' => 'Nie udało się dodać użytkownika.', - 'User updated successfully.' => 'Użytkownik zaktualizowany.', - 'Unable to update your user.' => 'Nie udało się zaktualizować użytkownika.', - 'User removed successfully.' => 'Użytkownik usunięty.', - 'Unable to remove this user.' => 'Nie udało się usunąć użytkownika.', - 'Board updated successfully.' => 'Tablica została zaktualizowana.', - 'Ready' => 'Gotowe', - 'Backlog' => 'Log', - 'Work in progress' => 'W trakcie', - 'Done' => 'Zakończone', - 'Application version:' => 'Wersja aplikacji:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Zakończono dnia %e %B %Y o %k:%M', - '%B %e, %Y at %k:%M %p' => '%e %B %Y o %k:%M', - 'Date created' => 'Data utworzenia', - 'Date completed' => 'Data zakończenia', - 'Id' => 'Ident', - 'No task' => 'Brak zadań', - 'Completed tasks' => 'Ukończone zadania', - 'List of projects' => 'Lista projektów', - 'Completed tasks for "%s"' => 'Zadania zakończone dla "%s"', - '%d closed tasks' => '%d zamkniętych zadań', - 'No task for this project' => 'Brak zadań dla tego projektu', - 'Public link' => 'Link publiczny', - 'There is no column in your project!' => 'Brak kolumny w Twoim projekcie', - 'Change assignee' => 'Zmień odpowiedzialną osobę', - 'Change assignee for the task "%s"' => 'Zmień odpowiedzialną osobę dla zadania "%s"', - 'Timezone' => 'Strefa czasowa', - 'Sorry, I didn\'t found this information in my database!' => 'Niestety nie znaleziono tej informacji w bazie danych', - 'Page not found' => 'Strona nie istnieje', - 'Complexity' => 'Poziom trudności', - 'limit' => 'limit', - 'Task limit' => 'Limit zadań', - 'This value must be greater than %d' => 'Wartość musi być większa niż %d', - 'Edit project access list' => 'Edycja list dostępu dla projektu', - 'Edit users access' => 'Edytuj dostęp', - 'Allow this user' => 'Dodaj użytkownika', - 'Only those users have access to this project:' => 'Użytkownicy mający dostęp:', - 'Don\'t forget that administrators have access to everything.' => 'Pamiętaj: Administratorzy mają zawsze dostęp do wszystkiego!', - 'revoke' => 'odbierz dostęp', - 'List of authorized users' => 'Lista użytkowników mających dostęp', - 'User' => 'Użytkownik', - // 'Nobody have access to this project.' => '', - 'You are not allowed to access to this project.' => 'Nie masz dostępu do tego projektu.', - 'Comments' => 'Komentarze', - 'Post comment' => 'Dodaj komentarz', - 'Write your text in Markdown' => 'Możesz użyć Markdown', - 'Leave a comment' => 'Zostaw komentarz', - 'Comment is required' => 'Komentarz jest wymagany', - // 'Leave a description' => '', - 'Comment added successfully.' => 'Komentarz dodany', - 'Unable to create your comment.' => 'Nie udało się dodać komentarza', - 'The description is required' => 'Opis jest wymagany', - 'Edit this task' => 'Edytuj zadanie', - 'Due Date' => 'Termin', - 'Invalid date' => 'Błędna data', - 'Must be done before %B %e, %Y' => 'Termin do %e %B %Y', - '%B %e, %Y' => '%e %B %Y', - 'Automatic actions' => 'Akcje automatyczne', - 'Your automatic action have been created successfully.' => 'Twoja akcja została dodana', - 'Unable to create your automatic action.' => 'Nie udało się utworzyć akcji', - 'Remove an action' => 'Usuń akcję', - 'Unable to remove this action.' => 'Nie można usunąć akcji', - 'Action removed successfully.' => 'Akcja usunięta', - 'Automatic actions for the project "%s"' => 'Akcje automatyczne dla projektu "%s"', - 'Defined actions' => 'Zdefiniowane akcje', - 'Add an action' => 'Nowa akcja', - 'Event name' => 'Nazwa zdarzenia', - 'Action name' => 'Nazwa akcji', - 'Action parameters' => 'Parametry akcji', - 'Action' => 'Akcja', - 'Event' => 'Zdarzenie', - 'When the selected event occurs execute the corresponding action.' => 'Gdy następuje wybrane zdarzenie, uruchom odpowiednią akcję', - 'Next step' => 'Następny krok', - 'Define action parameters' => 'Zdefiniuj parametry akcji', - 'Save this action' => 'Zapisz akcję', - 'Do you really want to remove this action: "%s"?' => 'Na pewno chcesz usunąć akcję "%s"?', - 'Remove an automatic action' => 'Usuń akcję automatyczną', - 'Close the task' => 'Zamknij zadanie', - 'Assign the task to a specific user' => 'Przypisz zadanie do wybranego użytkownika', - 'Assign the task to the person who does the action' => 'Przypisz zadanie to osoby wykonującej akcję', - 'Duplicate the task to another project' => 'Kopiuj zadanie do innego projektu', - 'Move a task to another column' => 'Przeniesienie zadania do innej kolumny', - 'Move a task to another position in the same column' => 'Zmiania pozycji zadania w kolumnie', - 'Task modification' => 'Modyfikacja zadania', - 'Task creation' => 'Tworzenie zadania', - 'Open a closed task' => 'Otwarcie zamkniętego zadania', - 'Closing a task' => 'Zamknięcie zadania', - 'Assign a color to a specific user' => 'Przypisz kolor do wybranego użytkownika', - 'Column title' => 'Tytuł kolumny', - 'Position' => 'Pozycja', - 'Move Up' => 'Przenieś wyżej', - 'Move Down' => 'Przenieś niżej', - 'Duplicate to another project' => 'Skopiuj do innego projektu', - 'Duplicate' => 'Utwórz kopię', - 'link' => 'link', - 'Update this comment' => 'Zapisz komentarz', - 'Comment updated successfully.' => 'Komentarz został zapisany.', - 'Unable to update your comment.' => 'Nie udało się zapisanie komentarza.', - 'Remove a comment' => 'Usuń komentarz', - 'Comment removed successfully.' => 'Komentarz został usunięty.', - 'Unable to remove this comment.' => 'Nie udało się usunąć komentarza.', - 'Do you really want to remove this comment?' => 'Czy na pewno usunąć ten komentarz?', - 'Only administrators or the creator of the comment can access to this page.' => 'Tylko administratorzy oraz autor komentarza ma dostęp do tej strony.', - 'Details' => 'Szczegóły', - 'Current password for the user "%s"' => 'Aktualne hasło dla użytkownika "%s"', - 'The current password is required' => 'Wymanage jest aktualne hasło', - 'Wrong password' => 'Błędne hasło', - 'Reset all tokens' => 'Zresetuj wszystkie tokeny', - 'All tokens have been regenerated.' => 'Wszystkie tokeny zostały zresetowane.', - 'Unknown' => 'Nieznany', - 'Last logins' => 'Ostatnie logowania', - 'Login date' => 'Data logowania', - 'Authentication method' => 'Sposób autentykacji', - 'IP address' => 'Adres IP', - 'User agent' => 'Przeglądarka', - 'Persistent connections' => 'Stałe połączenia', - 'No session.' => 'Brak sesji.', - 'Expiration date' => 'Data zakończenia', - 'Remember Me' => 'Pamiętaj mnie', - 'Creation date' => 'Data utworzenia', - // 'Filter by user' => '', - // 'Filter by due date' => '', - // 'Everybody' => '', - // 'Open' => '', - // 'Closed' => '', - // 'Search' => '', - // 'Nothing found.' => '', - // 'Search in the project "%s"' => '', - // 'Due date' => '', - // 'Others formats accepted: %s and %s' => '', - 'Description' => 'Opis', - // '%d comments' => '', - // '%d comment' => '', - // 'Email address invalid' => '', - // 'Your Google Account is not linked anymore to your profile.' => '', - // 'Unable to unlink your Google Account.' => '', - // 'Google authentication failed' => '', - // 'Unable to link your Google Account.' => '', - // 'Your Google Account is linked to your profile successfully.' => '', - // 'Email' => '', - // 'Link my Google Account' => '', - // 'Unlink my Google Account' => '', - // 'Login with my Google Account' => '', - // 'Project not found.' => '', - // 'Task #%d' => '', - // 'Task removed successfully.' => '', - // 'Unable to remove this task.' => '', - // 'Remove a task' => '', - // 'Do you really want to remove this task: "%s"?' => '', - // 'Assign automatically a color based on a category' => '', - // 'Assign automatically a category based on a color' => '', - // 'Task creation or modification' => '', - // 'Category' => '', - // 'Category:' => '', - // 'Categories' => '', - // 'Category not found.' => '', - // 'Your category have been created successfully.' => '', - // 'Unable to create your category.' => '', - // 'Your category have been updated successfully.' => '', - // 'Unable to update your category.' => '', - // 'Remove a category' => '', - // 'Category removed successfully.' => '', - // 'Unable to remove this category.' => '', - // 'Category modification for the project "%s"' => '', - // 'Category Name' => '', - // 'Categories for the project "%s"' => '', - // 'Add a new category' => '', - // 'Do you really want to remove this category: "%s"?' => '', - // 'Filter by category' => '', - // 'All categories' => '', - // 'No category' => '', - // 'The name is required' => '', - // 'Remove a file' => '', - // 'Unable to remove this file.' => '', - // 'File removed successfully.' => '', - // 'Attach a document' => '', - // 'Do you really want to remove this file: "%s"?' => '', - // 'open' => '', - // 'Attachments' => '', - // 'Edit the task' => '', - // 'Edit the description' => '', - // 'Add a comment' => '', - // 'Edit a comment' => '', - // 'Summary' => '', - // 'Time tracking' => '', - // 'Estimate:' => '', - // 'Spent:' => '', - // 'Do you really want to remove this sub-task?' => '', - // 'Remaining:' => '', - // 'hours' => '', - // 'spent' => '', - // 'estimated' => '', - // 'Sub-Tasks' => '', - // 'Add a sub-task' => '', - // 'Original estimate' => '', - // 'Create another sub-task' => '', - // 'Time spent' => '', - // 'Edit a sub-task' => '', - // 'Remove a sub-task' => '', - // 'The time must be a numeric value' => '', - // 'Todo' => '', - // 'In progress' => '', - // 'Sub-task removed successfully.' => '', - // 'Unable to remove this sub-task.' => '', - // 'Sub-task updated successfully.' => '', - // 'Unable to update your sub-task.' => '', - // 'Unable to create your sub-task.' => '', - // 'Sub-task added successfully.' => '', - // 'Maximum size: ' => '', - // 'Unable to upload the file.' => '', - // 'Display another project' => '', - // 'Your GitHub account was successfully linked to your profile.' => '', - // 'Unable to link your GitHub Account.' => '', - // 'GitHub authentication failed' => '', - // 'Your GitHub account is no longer linked to your profile.' => '', - // 'Unable to unlink your GitHub Account.' => '', - // 'Login with my GitHub Account' => '', - // 'Link my GitHub Account' => '', - // 'Unlink my GitHub Account' => '', - // 'Created by %s' => '', - // 'Last modified on %B %e, %Y at %k:%M %p' => '', - // 'Tasks Export' => '', - // 'Tasks exportation for "%s"' => '', - // 'Start Date' => '', - // 'End Date' => '', - // 'Execute' => '', - // 'Task Id' => '', - // 'Creator' => '', - // 'Modification date' => '', - // 'Completion date' => '', - // 'Webhook URL for task creation' => '', - // 'Webhook URL for task modification' => '', - // 'Clone' => '', - // 'Clone Project' => '', - // 'Project cloned successfully.' => '', - // 'Unable to clone this project.' => '', - // 'Email notifications' => '', - // 'Enable email notifications' => '', - // 'Task position:' => '', - // 'The task #%d have been opened.' => '', - // 'The task #%d have been closed.' => '', - // 'Sub-task updated' => '', - // 'Title:' => '', - // 'Status:' => '', - // 'Assignee:' => '', - // 'Time tracking:' => '', - // 'New sub-task' => '', - // 'New attachment added "%s"' => '', - // 'Comment updated' => '', - // 'New comment posted by %s' => '', - // 'List of due tasks for the project "%s"' => '', - // '[%s][New attachment] %s (#%d)' => '', - // '[%s][New comment] %s (#%d)' => '', - // '[%s][Comment updated] %s (#%d)' => '', - // '[%s][New subtask] %s (#%d)' => '', - // '[%s][Subtask updated] %s (#%d)' => '', - // '[%s][New task] %s (#%d)' => '', - // '[%s][Task updated] %s (#%d)' => '', - // '[%s][Task closed] %s (#%d)' => '', - // '[%s][Task opened] %s (#%d)' => '', - // '[%s][Due tasks]' => '', - // '[Kanboard] Notification' => '', - // 'I want to receive notifications only for those projects:' => '', - // 'view the task on Kanboard' => '', - // 'Public access' => '', - // 'Category management' => '', - // 'User management' => '', - // 'Active tasks' => '', - // 'Disable public access' => '', - // 'Enable public access' => '', - // 'Active projects' => '', - // 'Inactive projects' => '', - // 'Public access disabled' => '', - // 'Do you really want to disable this project: "%s"?' => '', - // 'Do you really want to duplicate this project: "%s"?' => '', - // 'Do you really want to enable this project: "%s"?' => '', - // 'Project activation' => '', - // 'Move the task to another project' => '', - // 'Move to another project' => '', - // 'Do you really want to duplicate this task?' => '', - // 'Duplicate a task' => '', - // 'External accounts' => '', - // 'Account type' => '', - // 'Local' => '', - // 'Remote' => '', - // 'Enabled' => '', - // 'Disabled' => '', - // 'Google account linked' => '', - // 'Github account linked' => '', - // 'Username:' => '', - // 'Name:' => '', - // 'Email:' => '', - // 'Default project:' => '', - // 'Notifications:' => '', - // 'Notifications' => '', - // 'Group:' => '', - // 'Regular user' => '', - // 'Account type:' => '', - // 'Edit profile' => '', - // 'Change password' => '', - // 'Password modification' => '', - // 'External authentications' => '', - // 'Google Account' => '', - // 'Github Account' => '', - // 'Never connected.' => '', - // 'No account linked.' => '', - // 'Account linked.' => '', - // 'No external authentication enabled.' => '', - // 'Password modified successfully.' => '', - // 'Unable to change the password.' => '', - // 'Change category for the task "%s"' => '', - // 'Change category' => '', - // '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '', - // '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // 'Assigned to %s with an estimate of %s/%sh' => '', - // 'Not assigned, estimate of %sh' => '', - // '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s\'s activity' => '', - // 'No activity.' => '', - // 'RSS feed' => '', - // '%s updated a comment on the task #%d' => '', - // '%s commented on the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated the task #%d' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s moved the task #%d to the position %d in the column "%s"' => '', - // 'Activity' => '', - // 'Default values are "%s"' => '', - // 'Default columns for new projects (Comma-separated)' => '', - // 'Task assignee change' => '', - // '%s change the assignee of the task #%d to %s' => '', - // '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', - // 'New password for the user "%s"' => '', - // 'Choose an event' => '', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', - // 'Reference' => '', - // 'Reference: %s' => '', - // 'Label' => '', - // 'Database' => '', - // 'About' => '', - // 'Database driver:' => '', - // 'Board settings' => '', - // 'URL and token' => '', - // 'Webhook settings' => '', - // 'URL for task creation:' => '', - // 'Reset token' => '', - // 'API endpoint:' => '', - // 'Refresh interval for private board' => '', - // 'Refresh interval for public board' => '', - // 'Task highlight period' => '', - // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', - // 'Frequency in second (60 seconds by default)' => '', - // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', - // 'Application URL' => '', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', - // 'Date format' => '', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', - // 'New private project' => '', - // 'This project is private' => '', - // 'Type here to create a new sub-task' => '', - // 'Add' => '', - // 'Estimated time: %s hours' => '', - // 'Time spent: %s hours' => '', - // 'Started on %B %e, %Y' => '', - // 'Start date' => '', - // 'Time estimated' => '', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', -); diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php deleted file mode 100644 index 9bfc9cb3..00000000 --- a/app/Locales/pt_BR/translations.php +++ /dev/null @@ -1,554 +0,0 @@ -<?php - -return array( - 'None' => 'Nenhum', - 'edit' => 'editar', - 'Edit' => 'Editar', - 'remove' => 'apagar', - 'Remove' => 'Apagar', - 'Update' => 'Atualizar', - 'Yes' => 'Sim', - 'No' => 'Não', - 'cancel' => 'cancelar', - 'or' => 'ou', - 'Yellow' => 'Amarelo', - 'Blue' => 'Azul', - 'Green' => 'Verde', - 'Purple' => 'Violeta', - 'Red' => 'Vermelho', - 'Orange' => 'Laranja', - 'Grey' => 'Cinza', - 'Save' => 'Salvar', - 'Login' => 'Login', - 'Official website:' => 'Site web oficial :', - 'Unassigned' => 'Não Atribuída', - 'View this task' => 'Ver esta tarefa', - 'Remove user' => 'Remover usuário', - 'Do you really want to remove this user: "%s"?' => 'Quer realmente remover este usuário: "%s"?', - 'New user' => 'Novo usuário', - 'All users' => 'Todos os usuários', - 'Username' => 'Nome do usuário', - 'Password' => 'Senha', - 'Default project' => 'Projeto default', - 'Administrator' => 'Administrador', - 'Sign in' => 'Logar', - 'Users' => 'Usuários', - 'No user' => 'Sem usuário', - 'Forbidden' => 'Proibido', - 'Access Forbidden' => 'Acesso negado', - 'Only administrators can access to this page.' => 'Somente administradores têm acesso a esta página.', - 'Edit user' => 'Editar usuário', - 'Logout' => 'Logout', - 'Bad username or password' => 'Usuário ou senha inválidos', - 'users' => 'usuários', - 'projects' => 'projetos', - 'Edit project' => 'Editar projeto', - 'Name' => 'Nome', - 'Activated' => 'Ativo', - 'Projects' => 'Projetos', - 'No project' => 'Nenhum projeto', - 'Project' => 'Projeto', - 'Status' => 'Status', - 'Tasks' => 'Tarefas', - 'Board' => 'Quadro', - 'Actions' => 'Ações', - 'Inactive' => 'Inativo', - 'Active' => 'Ativo', - 'Column %d' => 'Coluna %d', - 'Add this column' => 'Adicionar esta coluna', - '%d tasks on the board' => '%d tarefas no quadro', - '%d tasks in total' => '%d tarefas no total', - 'Unable to update this board.' => 'Impossível atualizar este quadro.', - 'Edit board' => 'Modificar quadro', - 'Disable' => 'Desativar', - 'Enable' => 'Ativar', - 'New project' => 'Novo projeto', - 'Do you really want to remove this project: "%s"?' => 'Quer realmente remover este projeto: "%s" ?', - 'Remove project' => 'Remover projeto', - 'Boards' => 'Quadros', - 'Edit the board for "%s"' => 'Editar o quadro para "%s"', - 'All projects' => 'Todos os projetos', - 'Change columns' => 'Modificar colunas', - 'Add a new column' => 'Adicionar uma nova coluna', - 'Title' => 'Título', - 'Add Column' => 'Adicionar coluna', - 'Project "%s"' => 'Projeto "%s"', - 'Nobody assigned' => 'Ninguém designado', - 'Assigned to %s' => 'Designado para %s', - 'Remove a column' => 'Remover uma coluna', - 'Remove a column from a board' => 'Remover uma coluna do quadro', - 'Unable to remove this column.' => 'Impossível remover esta coluna.', - 'Do you really want to remove this column: "%s"?' => 'Quer realmente remover esta coluna: "%s"?', - 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta ação vai REMOVER TODAS AS TAREFAS associadas a esta coluna!', - 'Settings' => 'Preferências', - 'Application settings' => 'Preferências da aplicação', - 'Language' => 'Idioma', - 'Webhook token:' => 'Token de webhooks:', - 'API token:' => 'API Token:', - 'More information' => 'Mais informação', - 'Database size:' => 'Tamanho do banco de dados:', - 'Download the database' => 'Download do banco de dados', - 'Optimize the database' => 'Otimizar o banco de dados', - '(VACUUM command)' => '(Comando VACUUM)', - '(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)', - 'User settings' => 'Configurações do usuário', - 'My default project:' => 'Meu projeto default:', - 'Close a task' => 'Encerrar uma tarefa', - 'Do you really want to close this task: "%s"?' => 'Quer realmente encerrar esta tarefa: "%s"?', - 'Edit a task' => 'Editar uma tarefa', - 'Column' => 'Coluna', - 'Color' => 'Cor', - 'Assignee' => 'Designação', - 'Create another task' => 'Criar uma outra tarefa (aproveitando os dados preenchidos)', - 'New task' => 'Nova tarefa', - 'Open a task' => 'Abrir uma tarefa', - 'Do you really want to open this task: "%s"?' => 'Quer realmente abrir esta tarefa: "%s"?', - 'Back to the board' => 'Voltar ao quadro', - 'Created on %B %e, %Y at %k:%M %p' => 'Criado em %d %B %Y às %H:%M', - 'There is nobody assigned' => 'Não há ninguém designado', - 'Column on the board:' => 'Coluna no quadro:', - 'Status is open' => 'Status está aberto', - 'Status is closed' => 'Status está fechado', - 'Close this task' => 'Fechar esta tarefa', - 'Open this task' => 'Abrir esta tarefa', - 'There is no description.' => 'Não há descrição.', - 'Add a new task' => 'Adicionar uma nova tarefa', - 'The username is required' => 'O nome de usuário é obrigatório', - 'The maximum length is %d characters' => 'O tamanho máximo são %d caracteres', - 'The minimum length is %d characters' => 'O tamanho mínimo são %d caracteres', - 'The password is required' => 'A senha é obrigatória', - 'This value must be an integer' => 'O valor deve ser um inteiro', - 'The username must be unique' => 'O nome de usuário deve ser único', - 'The username must be alphanumeric' => 'O nome de usuário deve ser alfanumérico, sem espaços ou _', - 'The user id is required' => 'O id de usuário é obrigatório', - 'Passwords don\'t match' => 'As senhas não conferem', - 'The confirmation is required' => 'A confirmação é obrigatória', - 'The column is required' => 'A coluna é obrigatória', - 'The project is required' => 'O projeto é obrigatório', - 'The color is required' => 'A cor é obrigatória', - 'The id is required' => 'O id é obrigatório', - 'The project id is required' => 'O id do projeto é obrigatório', - 'The project name is required' => 'O nome do projeto é obrigatório', - 'This project must be unique' => 'Este projeto deve ser único', - 'The title is required' => 'O título é obrigatório', - 'The language is required' => 'O idioma é obrigatório', - 'There is no active project, the first step is to create a new project.' => 'Não há projeto ativo. O primeiro passo é criar um novo projeto.', - 'Settings saved successfully.' => 'Configurações salvas com sucesso.', - 'Unable to save your settings.' => 'Impossível salvar suas configurações.', - 'Database optimization done.' => 'Otimização do banco de dados terminada.', - 'Your project have been created successfully.' => 'Seu projeto foi criado com sucesso.', - 'Unable to create your project.' => 'Impossível criar seu projeto.', - 'Project updated successfully.' => 'Projeto atualizado com sucesso.', - 'Unable to update this project.' => 'Impossível atualizar este projeto.', - 'Unable to remove this project.' => 'Impossível remover este projeto.', - 'Project removed successfully.' => 'Projeto removido com sucesso.', - 'Project activated successfully.' => 'Projeto ativado com sucesso.', - 'Unable to activate this project.' => 'Impossível ativar este projeto.', - 'Project disabled successfully.' => 'Projeto desabilitado com sucesso.', - 'Unable to disable this project.' => 'Impossível desabilitar este projeto.', - 'Unable to open this task.' => 'Impossível abrir esta tarefa.', - 'Task opened successfully.' => 'Tarefa aberta com sucesso.', - 'Unable to close this task.' => 'Impossível encerrar esta tarefa.', - 'Task closed successfully.' => 'Tarefa encerrada com sucesso.', - 'Unable to update your task.' => 'Impossível atualizar sua tarefa.', - 'Task updated successfully.' => 'Tarefa atualizada com sucesso.', - 'Unable to create your task.' => 'Impossível criar sua tarefa.', - 'Task created successfully.' => 'Tarefa criada com sucesso.', - 'User created successfully.' => 'Usuário criado com sucesso.', - 'Unable to create your user.' => 'Impossível criar seu usuário.', - 'User updated successfully.' => 'Usuário atualizado com sucesso.', - 'Unable to update your user.' => 'Impossível atualizar seu usuário.', - 'User removed successfully.' => 'Usuário removido com sucesso.', - 'Unable to remove this user.' => 'Impossível remover este usuário.', - 'Board updated successfully.' => 'Quadro atualizado com sucesso.', - 'Ready' => 'Pronto', - 'Backlog' => 'Backlog', - 'Work in progress' => 'Em andamento', - 'Done' => 'Encerrado', - 'Application version:' => 'Versão da aplicação:', - 'Completed on %B %e, %Y at %k:%M %p' => 'Encerrado em %d %B %Y às %H:%M', - '%B %e, %Y at %k:%M %p' => '%d %B %Y às %H:%M', - 'Date created' => 'Data de criação', - 'Date completed' => 'Data de encerramento', - 'Id' => 'Id', - 'No task' => 'Nenhuma tarefa', - 'Completed tasks' => 'tarefas completadas', - 'List of projects' => 'Lista de projetos', - 'Completed tasks for "%s"' => 'Tarefas completadas por "%s"', - '%d closed tasks' => '%d tarefas encerradas', - 'No task for this project' => 'Nenhuma tarefa para este projeto', - 'Public link' => 'Link público', - 'There is no column in your project!' => 'Não há colunas no seu projeto!', - 'Change assignee' => 'Mudar a designação', - 'Change assignee for the task "%s"' => 'Modificar designação para a tarefa "%s"', - 'Timezone' => 'Fuso horário', - 'Sorry, I didn\'t found this information in my database!' => 'Desculpe, não encontrei esta informação no meu banco de dados!', - 'Page not found' => 'Página não encontrada', - 'Complexity' => 'Complexidade', - 'limit' => 'limite', - 'Task limit' => 'Limite da tarefa', - 'This value must be greater than %d' => 'Este valor deve ser maior que %d', - 'Edit project access list' => 'Editar lista de acesso ao projeto', - 'Edit users access' => 'Editar acesso de usuários', - 'Allow this user' => 'Permitir esse usuário', - 'Only those users have access to this project:' => 'Somente estes usuários têm acesso a este projeto:', - 'Don\'t forget that administrators have access to everything.' => 'Não esqueça que administradores têm acesso a tudo.', - 'revoke' => 'revogar', - 'List of authorized users' => 'Lista de usuários autorizados', - 'User' => 'Usuário', - // 'Nobody have access to this project.' => '', - 'You are not allowed to access to this project.' => 'Você não está autorizado a acessar este projeto.', - 'Comments' => 'Comentários', - 'Post comment' => 'Postar comentário', - 'Write your text in Markdown' => 'Escreva seu texto em Markdown', - 'Leave a comment' => 'Deixe um comentário', - 'Comment is required' => 'Comentário é obrigatório', - 'Leave a description' => 'Deixe uma descrição', - 'Comment added successfully.' => 'Cpmentário adicionado com sucesso.', - 'Unable to create your comment.' => 'Impossível criar seu comentário.', - 'The description is required' => 'A descrição é obrigatória', - 'Edit this task' => 'Editar esta tarefa', - 'Due Date' => 'Data de vencimento', - 'Invalid date' => 'Data inválida', - 'Must be done before %B %e, %Y' => 'Deve ser feito antes de %d %B %Y', - '%B %e, %Y' => '%d %B %Y', - 'Automatic actions' => 'Ações automáticas', - 'Your automatic action have been created successfully.' => 'Sua ação automética foi criada com sucesso.', - 'Unable to create your automatic action.' => 'Impossível criar sua ação automática.', - 'Remove an action' => 'Remover uma ação', - 'Unable to remove this action.' => 'Impossível remover esta ação', - 'Action removed successfully.' => 'Ação removida com sucesso.', - 'Automatic actions for the project "%s"' => 'Ações automáticas para o projeto "%s"', - 'Defined actions' => 'Ações definidas', - 'Add an action' => 'Adicionar Ação', - 'Event name' => 'Nome do evento', - 'Action name' => 'Nome da ação', - 'Action parameters' => 'Parâmetros da ação', - 'Action' => 'Ação', - 'Event' => 'Evento', - 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer, execute a ação correspondente', - 'Next step' => 'Próximo passo', - 'Define action parameters' => 'Definir parêmetros da ação', - 'Save this action' => 'Salvar esta ação', - 'Do you really want to remove this action: "%s"?' => 'Você quer realmente remover esta ação: "%s"?', - 'Remove an automatic action' => 'Remove uma ação automática', - 'Close the task' => 'Fechar tarefa', - 'Assign the task to a specific user' => 'Designar a tarefa para um usuário específico', - 'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a ação', - 'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projeto', - 'Move a task to another column' => 'Mover a tarefa para outra coluna', - 'Move a task to another position in the same column' => 'Mover a tarefa para outra posição, na mesma coluna', - 'Task modification' => 'Modificação de tarefa', - 'Task creation' => 'Criação de tarefa', - 'Open a closed task' => 'Reabrir uma tarefa fechada', - 'Closing a task' => 'Fechando uma tarefa', - 'Assign a color to a specific user' => 'Designar uma cor para um usuário específico', - 'Column title' => 'Título da coluna', - 'Position' => 'Posição', - 'Move Up' => 'Mover para cima', - 'Move Down' => 'Mover para baixo', - 'Duplicate to another project' => 'Duplicar para outro projeto', - 'Duplicate' => 'Duplicar', - 'link' => 'link', - 'Update this comment' => 'Atualizar este comentário', - 'Comment updated successfully.' => 'Comentário atualizado com sucesso.', - 'Unable to update your comment.' => 'Impossível atualizar seu comentário.', - 'Remove a comment' => 'Remover um comentário.', - 'Comment removed successfully.' => 'Comentário removido com sucesso.', - 'Unable to remove this comment.' => 'Impossível remover este comentário', - 'Do you really want to remove this comment?' => 'Você tem certeza de que quer remover este comentário?', - 'Only administrators or the creator of the comment can access to this page.' => 'Somente administradores ou o criator deste comentário tem acesso a esta página.', - 'Details' => 'Detalhes', - 'Current password for the user "%s"' => 'Senha atual para o usuário "%s"', - 'The current password is required' => 'A senha atual é obrigatória', - 'Wrong password' => 'Senha errada', - 'Reset all tokens' => 'Reiniciar todos os tokens', - 'All tokens have been regenerated.' => 'Todos os tokens foram gerados novamente', - 'Unknown' => 'Desconhecido', - 'Last logins' => 'Últimos logins', - 'Login date' => 'Data de login', - 'Authentication method' => 'Método de autenticação', - 'IP address' => 'Endereço IP', - 'User agent' => 'Agente usuário', - 'Persistent connections' => 'Conexões persistentes', - 'No session.' => 'Sem sessão.', - 'Expiration date' => 'Data de expiração', - 'Remember Me' => 'Lembre-se de mim', - 'Creation date' => 'Data de criação', - 'Filter by user' => 'Filtrar por usuário', - 'Filter by due date' => 'Filtrar por data de vencimento', - 'Everybody' => 'Todos', - 'Open' => 'Abrir', - 'Closed' => 'Fechado', - 'Search' => 'Pesquisar', - 'Nothing found.' => 'Não encontrado.', - 'Search in the project "%s"' => 'Procure no projeto "%s"', - 'Due date' => 'Data de vencimento', - 'Others formats accepted: %s and %s' => 'Outros formatos permitidos: %s e %s', - 'Description' => 'Descrição', - '%d comments' => '%d comentários', - '%d comment' => '%d comentário', - 'Email address invalid' => 'Endereço de e-mail inválido', - 'Your Google Account is not linked anymore to your profile.' => 'Sua conta Google não está mais associada ao seu perfil.', - 'Unable to unlink your Google Account.' => 'Impossível desassociar sua conta Google.', - 'Google authentication failed' => 'Autenticação do Google falhou.', - 'Unable to link your Google Account.' => 'Impossível associar a sua conta do Google.', - 'Your Google Account is linked to your profile successfully.' => 'Sua Conta do Google está ligada ao seu perfil com sucesso.', - 'Email' => 'E-mail', - 'Link my Google Account' => 'Vincular minha conta Google', - 'Unlink my Google Account' => 'Desvincular minha conta do Google', - 'Login with my Google Account' => 'Entrar com minha conta do Google', - 'Project not found.' => 'Projeto não encontrado.', - 'Task #%d' => 'Tarefa #%d', - 'Task removed successfully.' => 'Tarefa removida com sucesso.', - 'Unable to remove this task.' => 'Não foi possível remover esta tarefa.', - 'Remove a task' => 'Remover uma tarefa', - 'Do you really want to remove this task: "%s"?' => 'Você realmente deseja remover esta tarefa: "%s"', - 'Assign automatically a color based on a category' => 'Atribuir automaticamente uma cor com base em uma categoria', - 'Assign automatically a category based on a color' => 'Atribuir automaticamente uma categoria com base em uma cor', - 'Task creation or modification' => 'Criação ou modificação de tarefa', - 'Category' => 'Categoria', - 'Category:' => 'Categoria:', - 'Categories' => 'Categorias', - 'Category not found.' => 'Categoria não encontrada.', - 'Your category have been created successfully.' => 'Seu categoria foi criada com sucesso.', - 'Unable to create your category.' => 'Não é possível criar sua categoria.', - 'Your category have been updated successfully.' => 'A sua categoria foi atualizada com sucesso.', - 'Unable to update your category.' => 'Não foi possível atualizar a sua categoria.', - 'Remove a category' => 'Remover uma categoria', - 'Category removed successfully.' => 'Categoria removido com sucesso.', - 'Unable to remove this category.' => 'Não foi possível remover esta categoria.', - 'Category modification for the project "%s"' => 'Modificação de categoria para o projeto "%s"', - 'Category Name' => 'Nome da Categoria', - 'Categories for the project "%s"' => 'Categorias para o projeto "%s"', - 'Add a new category' => 'Adicionar uma nova categoria', - 'Do you really want to remove this category: "%s"?' => 'Você realmente deseja remover esta categoria: "%s"', - 'Filter by category' => 'Filtrar por categoria', - 'All categories' => 'Todas as categorias', - 'No category' => 'Sem categoria', - 'The name is required' => 'O nome é obrigatório', - 'Remove a file' => 'Remover um arquivo', - 'Unable to remove this file.' => 'Não foi possível remover este arquivo.', - 'File removed successfully.' => 'Arquivo removido com sucesso.', - 'Attach a document' => 'Anexar um documento', - 'Do you really want to remove this file: "%s"?' => 'Você realmente deseja remover este arquivo: "%s"', - 'open' => 'Aberto', - 'Attachments' => 'Anexos', - 'Edit the task' => 'Editar a tarefa', - 'Edit the description' => 'Editar a descrição', - 'Add a comment' => 'Adicionar um comentário', - 'Edit a comment' => 'Editar um comentário', - 'Summary' => 'Resumo', - 'Time tracking' => 'Rastreamento de tempo', - 'Estimate:' => 'Estimado:', - 'Spent:' => 'Gasto:', - 'Do you really want to remove this sub-task?' => 'Você realmente deseja remover esta sub-tarefa?', - 'Remaining:' => 'Restante:', - 'hours' => 'horas', - 'spent' => 'gasto', - 'estimated' => 'estimada', - 'Sub-Tasks' => 'Sub-tarefas', - 'Add a sub-task' => 'Adicionar uma sub-tarefa', - 'Original estimate' => 'Estimativa original', - 'Create another sub-task' => 'Criar uma outra sub-tarefa', - 'Time spent' => 'Tempo gasto', - 'Edit a sub-task' => 'Editar uma sub-tarefa', - 'Remove a sub-task' => 'Remover uma sub-tarefa', - 'The time must be a numeric value' => 'O tempo deve ser um valor numérico', - 'Todo' => 'A fazer', - 'In progress' => 'Em andamento', - 'Sub-task removed successfully.' => 'Sub-tarefa removido com sucesso.', - 'Unable to remove this sub-task.' => 'Não foi possível remover esta sub-tarefa.', - 'Sub-task updated successfully.' => 'Sub-tarefa atualizada com sucesso.', - 'Unable to update your sub-task.' => 'Não foi possível atualizar sua sub-tarefa.', - 'Unable to create your sub-task.' => 'Não é possível criar sua sub-tarefa.', - 'Sub-task added successfully.' => 'Sub-tarefa adicionada com sucesso.', - 'Maximum size: ' => 'O tamanho máximo:', - 'Unable to upload the file.' => 'Não foi possível carregar o arquivo.', - 'Display another project' => 'Mostrar um outro projeto', - 'Your GitHub account was successfully linked to your profile.' => 'A sua conta GitHub foi ligada com sucesso ao seu perfil.', - 'Unable to link your GitHub Account.' => 'Não foi possível vincular sua conta GitHub.', - 'GitHub authentication failed' => 'Falhou autenticação GitHub', - 'Your GitHub account is no longer linked to your profile.' => 'A sua conta GitHub já não está ligada ao seu perfil.', - 'Unable to unlink your GitHub Account.' => 'Não foi possível desvincular sua conta GitHub.', - 'Login with my GitHub Account' => 'Entrar com minha conta do GitHub', - 'Link my GitHub Account' => 'Vincular minha conta GitHub', - 'Unlink my GitHub Account' => 'Desvincular minha conta do GitHub', - 'Created by %s' => 'Criado por %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Última modificação em %B %e, %Y às %k: %M %p', - 'Tasks Export' => 'Tarefas Export', - 'Tasks exportation for "%s"' => 'Tarefas exportação para "%s"', - 'Start Date' => 'Data inicial', - 'End Date' => 'Data final', - 'Execute' => 'Executar', - 'Task Id' => 'Id da Tarefa', - 'Creator' => 'Criador', - 'Modification date' => 'Data de modificação', - 'Completion date' => 'Data de conclusão', - 'Webhook URL for task creation' => 'Webhook URL para criação de tarefas', - 'Webhook URL for task modification' => 'Webhook URL para modificação tarefa', - 'Clone' => 'Clone', - 'Clone Project' => 'Clonar Projeto', - 'Project cloned successfully.' => 'Projeto clonado com sucesso.', - 'Unable to clone this project.' => 'Impossível clonar este projeto.', - // 'Email notifications' => '', - // 'Enable email notifications' => '', - // 'Task position:' => '', - // 'The task #%d have been opened.' => '', - // 'The task #%d have been closed.' => '', - // 'Sub-task updated' => '', - // 'Title:' => '', - // 'Status:' => '', - // 'Assignee:' => '', - // 'Time tracking:' => '', - // 'New sub-task' => '', - // 'New attachment added "%s"' => '', - // 'Comment updated' => '', - // 'New comment posted by %s' => '', - // 'List of due tasks for the project "%s"' => '', - // '[%s][New attachment] %s (#%d)' => '', - // '[%s][New comment] %s (#%d)' => '', - // '[%s][Comment updated] %s (#%d)' => '', - // '[%s][New subtask] %s (#%d)' => '', - // '[%s][Subtask updated] %s (#%d)' => '', - // '[%s][New task] %s (#%d)' => '', - // '[%s][Task updated] %s (#%d)' => '', - // '[%s][Task closed] %s (#%d)' => '', - // '[%s][Task opened] %s (#%d)' => '', - // '[%s][Due tasks]' => '', - // '[Kanboard] Notification' => '', - // 'I want to receive notifications only for those projects:' => '', - // 'view the task on Kanboard' => '', - // 'Public access' => '', - // 'Category management' => '', - // 'User management' => '', - // 'Active tasks' => '', - // 'Disable public access' => '', - // 'Enable public access' => '', - // 'Active projects' => '', - // 'Inactive projects' => '', - // 'Public access disabled' => '', - // 'Do you really want to disable this project: "%s"?' => '', - // 'Do you really want to duplicate this project: "%s"?' => '', - // 'Do you really want to enable this project: "%s"?' => '', - // 'Project activation' => '', - // 'Move the task to another project' => '', - // 'Move to another project' => '', - // 'Do you really want to duplicate this task?' => '', - // 'Duplicate a task' => '', - // 'External accounts' => '', - // 'Account type' => '', - // 'Local' => '', - // 'Remote' => '', - // 'Enabled' => '', - // 'Disabled' => '', - // 'Google account linked' => '', - // 'Github account linked' => '', - // 'Username:' => '', - // 'Name:' => '', - // 'Email:' => '', - // 'Default project:' => '', - // 'Notifications:' => '', - // 'Notifications' => '', - // 'Group:' => '', - // 'Regular user' => '', - // 'Account type:' => '', - // 'Edit profile' => '', - // 'Change password' => '', - // 'Password modification' => '', - // 'External authentications' => '', - // 'Google Account' => '', - // 'Github Account' => '', - // 'Never connected.' => '', - // 'No account linked.' => '', - // 'Account linked.' => '', - // 'No external authentication enabled.' => '', - // 'Password modified successfully.' => '', - // 'Unable to change the password.' => '', - // 'Change category for the task "%s"' => '', - // 'Change category' => '', - // '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '', - // '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '', - // '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // 'Assigned to %s with an estimate of %s/%sh' => '', - // 'Not assigned, estimate of %sh' => '', - // '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '', - // '%s\'s activity' => '', - // 'No activity.' => '', - // 'RSS feed' => '', - // '%s updated a comment on the task #%d' => '', - // '%s commented on the task #%d' => '', - // '%s updated a subtask for the task #%d' => '', - // '%s created a subtask for the task #%d' => '', - // '%s updated the task #%d' => '', - // '%s created the task #%d' => '', - // '%s closed the task #%d' => '', - // '%s open the task #%d' => '', - // '%s moved the task #%d to the column "%s"' => '', - // '%s moved the task #%d to the position %d in the column "%s"' => '', - // 'Activity' => '', - // 'Default values are "%s"' => '', - // 'Default columns for new projects (Comma-separated)' => '', - // 'Task assignee change' => '', - // '%s change the assignee of the task #%d to %s' => '', - // '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '', - // '[%s][Column Change] %s (#%d)' => '', - // '[%s][Position Change] %s (#%d)' => '', - // '[%s][Assignee Change] %s (#%d)' => '', - // 'New password for the user "%s"' => '', - // 'Choose an event' => '', - // 'Github commit received' => '', - // 'Github issue opened' => '', - // 'Github issue closed' => '', - // 'Github issue reopened' => '', - // 'Github issue assignee change' => '', - // 'Github issue label change' => '', - // 'Create a task from an external provider' => '', - // 'Change the assignee based on an external username' => '', - // 'Change the category based on an external label' => '', - // 'Reference' => '', - // 'Reference: %s' => '', - // 'Label' => '', - // 'Database' => '', - // 'About' => '', - // 'Database driver:' => '', - // 'Board settings' => '', - // 'URL and token' => '', - // 'Webhook settings' => '', - // 'URL for task creation:' => '', - // 'Reset token' => '', - // 'API endpoint:' => '', - // 'Refresh interval for private board' => '', - // 'Refresh interval for public board' => '', - // 'Task highlight period' => '', - // 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => '', - // 'Frequency in second (60 seconds by default)' => '', - // 'Frequency in second (0 to disable this feature, 10 seconds by default)' => '', - // 'Application URL' => '', - // 'Example: http://example.kanboard.net/ (used by email notifications)' => '', - // 'Token regenerated.' => '', - // 'Date format' => '', - // 'ISO format is always accepted, example: "%s" and "%s"' => '', - // 'New private project' => '', - // 'This project is private' => '', - // 'Type here to create a new sub-task' => '', - // 'Add' => '', - // 'Estimated time: %s hours' => '', - // 'Time spent: %s hours' => '', - // 'Started on %B %e, %Y' => '', - // 'Start date' => '', - // 'Time estimated' => '', - // 'There is nothing assigned to you.' => '', - // 'My tasks' => '', - // 'Activity stream' => '', - // 'Dashboard' => '', - // 'Confirmation' => '', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', -); diff --git a/app/Locales/ru_RU/translations.php b/app/Locales/ru_RU/translations.php deleted file mode 100644 index 9d236947..00000000 --- a/app/Locales/ru_RU/translations.php +++ /dev/null @@ -1,554 +0,0 @@ -<?php - -return array( - 'None' => 'Отсутствует', - 'edit' => 'изменить', - 'Edit' => 'Изменить', - 'remove' => 'удалить', - 'Remove' => 'Удалить', - 'Update' => 'Обновить', - 'Yes' => 'Да', - 'No' => 'Нет', - 'cancel' => 'Отменить', - 'or' => 'или', - 'Yellow' => 'Желтый', - 'Blue' => 'Синий', - 'Green' => 'Зеленый', - 'Purple' => 'Фиолетовый', - 'Red' => 'Красный', - 'Orange' => 'Оранжевый', - 'Grey' => 'Серый', - 'Save' => 'Сохранить', - 'Login' => 'Вход', - 'Official website:' => 'Официальный сайт :', - 'Unassigned' => 'Не назначена', - 'View this task' => 'Посмотреть задачу', - 'Remove user' => 'Удалить пользователя', - 'Do you really want to remove this user: "%s"?' => 'Вы точно хотите удалить пользователя: « %s » ?', - 'New user' => 'Новый пользователь', - 'All users' => 'Все пользователи', - 'Username' => 'Имя пользователя', - 'Password' => 'Пароль', - 'Default project' => 'Проект по умолчанию', - 'Administrator' => 'Администратор', - 'Sign in' => 'Войти', - 'Users' => 'Пользователи', - 'No user' => 'Нет пользователя', - 'Forbidden' => 'Запрещено', - 'Access Forbidden' => 'Доступ запрещен', - 'Only administrators can access to this page.' => 'Только администраторы могут войти на эту страницу.', - 'Edit user' => 'Изменить пользователя', - 'Logout' => 'Выйти', - 'Bad username or password' => 'Неверное имя пользователя или пароль', - 'users' => 'пользователи', - 'projects' => 'проекты', - 'Edit project' => 'Изменить проект', - 'Name' => 'Имя', - 'Activated' => 'Активен', - 'Projects' => 'Проекты', - 'No project' => 'Нет проекта', - 'Project' => 'Проект', - 'Status' => 'Статус', - 'Tasks' => 'Задачи', - 'Board' => 'Доска', - 'Actions' => 'Действия', - 'Inactive' => 'Неактивен', - 'Active' => 'Активен', - 'Column %d' => 'Колонка %d', - 'Add this column' => 'Добавить колонку', - '%d tasks on the board' => 'Задач на доске - %d', - '%d tasks in total' => 'Задач всего - %d', - 'Unable to update this board.' => 'Не удалось обновить доску.', - 'Edit board' => 'Изменить доски', - 'Disable' => 'Деактивировать', - 'Enable' => 'Активировать', - 'New project' => 'Новый проект', - 'Do you really want to remove this project: "%s"?' => 'Вы точно хотите удалить этот проект? : « %s » ?', - 'Remove project' => 'Удалить проект', - 'Boards' => 'Доски', - 'Edit the board for "%s"' => 'Изменить доску для « %s »', - 'All projects' => 'Все проекты', - 'Change columns' => 'Изменить колонки', - 'Add a new column' => 'Добавить новую колонку', - 'Title' => 'Название', - 'Add Column' => 'Добавить колонку', - 'Project "%s"' => 'Проект « %s »', - 'Nobody assigned' => 'Никто не назначен', - 'Assigned to %s' => 'Исполнитель: %s', - 'Remove a column' => 'Удалить колонку', - 'Remove a column from a board' => 'Удалить колонку с доски', - 'Unable to remove this column.' => 'Не удалось удалить колонку.', - 'Do you really want to remove this column: "%s"?' => 'Вы точно хотите удалить эту колонку : « %s » ?', - 'This action will REMOVE ALL TASKS associated to this column!' => 'Вы УДАЛИТЕ ВСЕ ЗАДАЧИ находящиеся в этой колонке !', - 'Settings' => 'Настройки', - 'Application settings' => 'Настройки приложения', - 'Language' => 'Язык', - 'Webhook token:' => 'Webhooks токен :', - 'API token:' => 'API токен :', - 'More information' => 'Подробнее', - 'Database size:' => 'Размер базы данных :', - 'Download the database' => 'Скачать базу данных', - 'Optimize the database' => 'Оптимизировать базу данных', - '(VACUUM command)' => '(Команда VACUUM)', - '(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)', - 'User settings' => 'Настройки пользователя', - 'My default project:' => 'Мой проект по умолчанию : ', - 'Close a task' => 'Закрыть задачу', - 'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу : « %s » ?', - 'Edit a task' => 'Изменить задачу', - 'Column' => 'Колонка', - 'Color' => 'Цвет', - 'Assignee' => 'Назначена', - 'Create another task' => 'Создать другую задачу', - 'New task' => 'Новая задача', - 'Open a task' => 'Открыть задачу', - 'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу : « %s » ?', - 'Back to the board' => 'Вернуться на доску', - 'Created on %B %e, %Y at %k:%M %p' => 'Создано %d/%m/%Y в %H:%M', - 'There is nobody assigned' => 'Никто не назначен', - 'Column on the board:' => 'Колонка на доске : ', - 'Status is open' => 'Статус - открыт', - 'Status is closed' => 'Статус - закрыт', - 'Close this task' => 'Закрыть эту задачу', - 'Open this task' => 'Открыть эту задачу', - 'There is no description.' => 'Нет описания.', - 'Add a new task' => 'Добавить новую задачу', - 'The username is required' => 'Требуется имя пользователя', - 'The maximum length is %d characters' => 'Максимальная длина - %d знаков', - 'The minimum length is %d characters' => 'Минимальная длина - %d знаков', - 'The password is required' => 'Требуется пароль', - 'This value must be an integer' => 'Это значение должно быть целым', - 'The username must be unique' => 'Требуется уникальное имя пользователя', - 'The username must be alphanumeric' => 'Имя пользователя должно быть букво-цифровым', - 'The user id is required' => 'Требуется ID пользователя', - 'Passwords don\'t match' => 'Пароли не совпадают', - 'The confirmation is required' => 'Требуется подтверждение', - 'The column is required' => 'Требуется колонка', - 'The project is required' => 'Требуется проект', - 'The color is required' => 'Требуется цвет', - 'The id is required' => 'Требуется ID', - 'The project id is required' => 'Требуется ID проекта', - 'The project name is required' => 'Требуется имя проекта', - 'This project must be unique' => 'Проект должен быть уникальным', - 'The title is required' => 'Требуется заголовок', - 'The language is required' => 'Требуется язык', - 'There is no active project, the first step is to create a new project.' => 'Нет активного проекта, сначала создайте новый проект.', - 'Settings saved successfully.' => 'Параметры успешно сохранены.', - 'Unable to save your settings.' => 'Невозможно сохранить параметры.', - 'Database optimization done.' => 'База данных оптимизирована.', - 'Your project have been created successfully.' => 'Ваш проект успешно создан.', - 'Unable to create your project.' => 'Не удалось создать проект.', - 'Project updated successfully.' => 'Проект успешно обновлен.', - 'Unable to update this project.' => 'Не удалось обновить проект.', - 'Unable to remove this project.' => 'Не удалось удалить проект.', - 'Project removed successfully.' => 'Проект удален.', - 'Project activated successfully.' => 'Проект активирован.', - 'Unable to activate this project.' => 'Невозможно активировать проект.', - 'Project disabled successfully.' => 'Проект успешно выключен.', - 'Unable to disable this project.' => 'Не удалось выключить проект.', - 'Unable to open this task.' => 'Не удалось открыть задачу.', - 'Task opened successfully.' => 'Задача открыта.', - 'Unable to close this task.' => 'Не удалось закрыть задачу.', - 'Task closed successfully.' => 'Задача закрыта.', - 'Unable to update your task.' => 'Не удалось обновить вашу задачу.', - 'Task updated successfully.' => 'Задача обновлена.', - 'Unable to create your task.' => 'Не удалось создать задачу.', - 'Task created successfully.' => 'Задача создана.', - 'User created successfully.' => 'Пользователь создан.', - 'Unable to create your user.' => 'Не удалось создать пользователя.', - 'User updated successfully.' => 'Пользователь обновлен.', - 'Unable to update your user.' => 'Не удалось обновить пользователя.', - 'User removed successfully.' => 'Пользователь удален.', - 'Unable to remove this user.' => 'Не удалось удалить пользователя.', - 'Board updated successfully.' => 'Доска обновлена.', - 'Ready' => 'Готовые', - 'Backlog' => 'Ожидающие', - 'Work in progress' => 'В процессе', - 'Done' => 'Выполнена', - 'Application version:' => 'Версия приложения :', - 'Completed on %B %e, %Y at %k:%M %p' => 'Завершен %d/%m/%Y в %H:%M', - '%B %e, %Y at %k:%M %p' => '%d/%m/%Y в %H:%M', - 'Date created' => 'Дата создания', - 'Date completed' => 'Дата завершения', - 'Id' => 'ID', - 'No task' => 'Нет задачи', - 'Completed tasks' => 'Завершенные задачи', - 'List of projects' => 'Список проектов', - 'Completed tasks for "%s"' => 'Задачи завершенные для « %s »', - '%d closed tasks' => '%d завершенных задач', - 'No task for this project' => 'нет задач для этого проекта', - 'Public link' => 'Ссылка для просмотра', - 'There is no column in your project!' => 'Нет колонки в вашем проекте !', - 'Change assignee' => 'Сменить назначенного', - 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »', - 'Timezone' => 'Часовой пояс', - 'Sorry, I didn\'t found this information in my database!' => 'К сожалению, информация в базе данных не найдена !', - 'Page not found' => 'Страница не найдена', - 'Complexity' => 'Сложность', - 'limit' => 'лимит', - 'Task limit' => 'Лимит задач', - 'This value must be greater than %d' => 'Это значение должно быть больше %d', - 'Edit project access list' => 'Изменить доступ к проекту', - 'Edit users access' => 'Изменить доступ пользователей', - 'Allow this user' => 'Разрешить этого пользователя', - 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту :', - 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет доступ ко всему.', - 'revoke' => 'отозвать', - 'List of authorized users' => 'Список авторизованных пользователей', - 'User' => 'Пользователь', - 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту', - 'You are not allowed to access to this project.' => 'Вам запрешен доступ к этому проекту.', - 'Comments' => 'Комментарии', - 'Post comment' => 'Оставить комментарий', - 'Write your text in Markdown' => 'Справка по синтаксису Markdown', - 'Leave a comment' => 'Оставить комментарий 2', - 'Comment is required' => 'Нужен комментарий', - 'Leave a description' => 'Оставьте описание', - 'Comment added successfully.' => 'Комментарий успешно добавлен.', - 'Unable to create your comment.' => 'Невозможно создать комментарий.', - 'The description is required' => 'Требуется описание', - 'Edit this task' => 'Изменить задачу', - 'Due Date' => 'Сделать до', - 'Invalid date' => 'Неверная дата', - 'Must be done before %B %e, %Y' => 'Должно быть сделано до %d/%m/%Y', - '%B %e, %Y' => '%d/%m/%Y', - 'Automatic actions' => 'Автоматические действия', - 'Your automatic action have been created successfully.' => 'Автоматика настроена.', - 'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.', - 'Remove an action' => 'Удалить действие', - 'Unable to remove this action.' => 'Не удалось удалить действие', - 'Action removed successfully.' => 'Действие удалено.', - 'Automatic actions for the project "%s"' => 'Автоматические действия для проекта « %s »', - 'Defined actions' => 'Заданные действия', - 'Add an action' => 'Добавить действие', - 'Event name' => 'Имя события', - 'Action name' => 'Имя действия', - 'Action parameters' => 'Параметры действия', - 'Action' => 'Действие', - 'Event' => 'Событие', - 'When the selected event occurs execute the corresponding action.' => 'Когда случится ВЫБРАННОЕ событие выполняется СООТВЕТСТВУЮЩЕЕ действие.', - 'Next step' => 'Следующий шаг', - 'Define action parameters' => 'Задать параметры действия', - 'Save this action' => 'Сохранить это действие', - 'Do you really want to remove this action: "%s"?' => 'Вы точно хотите удалить это действие: « %s » ?', - 'Remove an automatic action' => 'Удалить автоматическое действие', - 'Close the task' => 'Закрыть задачу', - 'Assign the task to a specific user' => 'Назначить задачу определенному пользователю', - 'Assign the task to the person who does the action' => 'Назначить задачу тому кто выполнит действие', - 'Duplicate the task to another project' => 'Создать дубликат задачи в другом проекте', - 'Move a task to another column' => 'Переместить задачу в другую колонку', - 'Move a task to another position in the same column' => 'Переместить задачу в другое место этой же колонки', - 'Task modification' => 'Изменение задачи', - 'Task creation' => 'Создание задачи', - 'Open a closed task' => 'Открыть завершенную задачу', - 'Closing a task' => 'Завершение задачи', - 'Assign a color to a specific user' => 'Назначить определенный цвет пользователю', - 'Column title' => 'Название колонки', - 'Position' => 'Расположение', - 'Move Up' => 'Сдвинуть вверх', - 'Move Down' => 'Сдвинуть вниз', - 'Duplicate to another project' => 'Клонировать в другой проект', - 'Duplicate' => 'Клонировать', - 'link' => 'ссылка', - 'Update this comment' => 'Обновить комментарий', - 'Comment updated successfully.' => 'Комментарий обновлен.', - 'Unable to update your comment.' => 'Не удалось обновить ваш комментарий.', - 'Remove a comment' => 'Удалить комментарий', - 'Comment removed successfully.' => 'Комментарий удален.', - 'Unable to remove this comment.' => 'Не удалось удалить этот комментарий.', - 'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий ?', - 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор или автор комментарий могут получить доступ.', - 'Details' => 'Подробности', - 'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »', - 'The current password is required' => 'Требуется текущий пароль', - 'Wrong password' => 'Неверный пароль', - 'Reset all tokens' => 'Сброс всех токенов', - 'All tokens have been regenerated.' => 'Все токены пересозданы.', - 'Unknown' => 'Неизвестно', - 'Last logins' => 'Последние посещения', - 'Login date' => 'Дата входа', - 'Authentication method' => 'Способ аутентификации', - 'IP address' => 'IP адрес', - 'User agent' => 'User agent', - 'Persistent connections' => 'Постоянные соединения', - 'No session.' => 'Нет сеанса', - 'Expiration date' => 'Дата окончания', - 'Remember Me' => 'Запомнить меня', - 'Creation date' => 'Дата создания', - 'Filter by user' => 'Фильтр по пользователям', - 'Filter by due date' => 'Фильтр по сроку', - 'Everybody' => 'Все', - 'Open' => 'Открытый', - 'Closed' => 'Закрытый', - 'Search' => 'Поиск', - 'Nothing found.' => 'Ничего не найдено.', - 'Search in the project "%s"' => 'Искать в проекте « %s »', - 'Due date' => 'Срок', - 'Others formats accepted: %s and %s' => 'Другой формат приемлем : %s и %s', - 'Description' => 'Описание', - '%d comments' => '%d комментариев', - '%d comment' => '%d комментарий', - 'Email address invalid' => 'Adresse email invalide', - 'Your Google Account is not linked anymore to your profile.' => 'Ваш аккаунт в Google больше не привязан к вашему профилю.', - 'Unable to unlink your Google Account.' => 'Не удалось отвязать ваш профиль от Google.', - 'Google authentication failed' => 'Аутентификация Google не удалась', - 'Unable to link your Google Account.' => 'Не удалось привязать ваш профиль к Google.', - 'Your Google Account is linked to your profile successfully.' => 'Ваш профиль успешно привязан к Google.', - 'Email' => 'Email', - 'Link my Google Account' => 'Привязать мой профиль к Google', - 'Unlink my Google Account' => 'Отвязать мой профиль от Google', - 'Login with my Google Account' => 'Аутентификация через Google', - 'Project not found.' => 'Проект не найден.', - 'Task #%d' => 'Задача n°%d', - 'Task removed successfully.' => 'Задача удалена.', - 'Unable to remove this task.' => 'Не удалось удалить эту задачу.', - 'Remove a task' => 'Удалить задачу', - 'Do you really want to remove this task: "%s"?' => 'Вы точно хотите удалить эту задачу « %s » ?', - 'Assign automatically a color based on a category' => 'Автоматически назначать цвет по категории', - 'Assign automatically a category based on a color' => 'Автоматически назначать категорию по цвету ', - 'Task creation or modification' => 'Создание или изменение задачи', - 'Category' => 'Категория', - 'Category:' => 'Категория :', - 'Categories' => 'Категории', - 'Category not found.' => 'Категория не найдена', - 'Your category have been created successfully.' => 'Категория создана.', - 'Unable to create your category.' => 'Не удалось создать категорию.', - 'Your category have been updated successfully.' => 'Категория обновлена.', - 'Unable to update your category.' => 'Не удалось обновить категорию.', - 'Remove a category' => 'Удалить категорию', - 'Category removed successfully.' => 'Категория удалена.', - 'Unable to remove this category.' => 'Не удалось удалить категорию.', - 'Category modification for the project "%s"' => 'Изменение категории для проекта « %s »', - 'Category Name' => 'Название категории', - 'Categories for the project "%s"' => 'Категории для проекта « %s »', - 'Add a new category' => 'Добавить новую категорию', - 'Do you really want to remove this category: "%s"?' => 'Вы точно хотите удалить категорию « %s » ?', - 'Filter by category' => 'Фильтр по категориям', - 'All categories' => 'Все категории', - 'No category' => 'Нет категории', - 'The name is required' => 'Требуется название', - 'Remove a file' => 'Удалить файл', - 'Unable to remove this file.' => 'Не удалось удалить файл.', - 'File removed successfully.' => 'Файл удален.', - 'Attach a document' => 'Приложить документ', - 'Do you really want to remove this file: "%s"?' => 'Вы точно хотите удалить этот файл « %s » ?', - 'open' => 'открыть', - 'Attachments' => 'Приложение', - 'Edit the task' => 'Изменить задачу', - 'Edit the description' => 'Изменить описание', - 'Add a comment' => 'Добавить комментарий', - 'Edit a comment' => 'Изменить комментарий', - 'Summary' => 'Сводка', - 'Time tracking' => 'Отслеживание времени', - 'Estimate:' => 'Приблизительно :', - 'Spent:' => 'Затрачено :', - 'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу ?', - 'Remaining:' => 'Осталось :', - 'hours' => 'часов', - 'spent' => 'затрачено', - 'estimated' => 'расчетное', - 'Sub-Tasks' => 'Подзадачи', - 'Add a sub-task' => 'Добавить подзадачу', - 'Original estimate' => 'Первичная оценка', - 'Create another sub-task' => 'Создать другую подзадачу', - 'Time spent' => 'Времени затрачено', - 'Edit a sub-task' => 'Изменить подзадачу', - 'Remove a sub-task' => 'Удалить подзадачу', - 'The time must be a numeric value' => 'Время должно быть числом!', - 'Todo' => 'К исполнению', - 'In progress' => 'В процессе', - 'Sub-task removed successfully.' => 'Подзадача удалена.', - 'Unable to remove this sub-task.' => 'Не удалось удалить подзадачу.', - 'Sub-task updated successfully.' => 'Подзадача обновлена.', - 'Unable to update your sub-task.' => 'Не удалось обновить подзадачу.', - 'Unable to create your sub-task.' => 'Не удалось создать подзадачу.', - 'Sub-task added successfully.' => 'Подзадача добавлена.', - 'Maximum size: ' => 'Максимальный размер : ', - 'Unable to upload the file.' => 'Не удалось загрузить файл.', - 'Display another project' => 'Показать другой проект', - 'Your GitHub account was successfully linked to your profile.' => 'Ваш GitHub привязан к вашему профилю.', - 'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к Github.', - 'GitHub authentication failed' => 'Аутентификация в GitHub не удалась', - 'Your GitHub account is no longer linked to your profile.' => 'Ваш GitHub отвязан от вашего профиля.', - 'Unable to unlink your GitHub Account.' => 'Не удалось отвязать ваш профиль от GitHub.', - 'Login with my GitHub Account' => 'Аутентификация через GitHub', - 'Link my GitHub Account' => 'Привязать мой профиль к GitHub', - 'Unlink my GitHub Account' => 'Отвязать мой профиль от GitHub', - 'Created by %s' => 'Создано %s', - 'Last modified on %B %e, %Y at %k:%M %p' => 'Последнее изменение %d/%m/%Y в %H:%M', - 'Tasks Export' => 'Экспорт задач', - 'Tasks exportation for "%s"' => 'Задача экспортирована для « %s »', - 'Start Date' => 'Дата начала', - 'End Date' => 'Дата завершения', - 'Execute' => 'Выполнить', - 'Task Id' => 'ID задачи', - 'Creator' => 'Автор', - 'Modification date' => 'Дата изменения', - 'Completion date' => 'Дата завершения', - 'Webhook URL for task creation' => 'Webhook URL для создания задачи', - 'Webhook URL for task modification' => 'Webhook URL для изменения задачи', - 'Clone' => 'Клонировать', - 'Clone Project' => 'Клонировать проект', - 'Project cloned successfully.' => 'Проект клонирован.', - 'Unable to clone this project.' => 'Не удалось клонировать проект.', - 'Email notifications' => 'Уведомления по email', - 'Enable email notifications' => 'Включить уведомления по email', - 'Task position:' => 'Позиция задачи :', - 'The task #%d have been opened.' => 'Задача #%d была открыта.', - 'The task #%d have been closed.' => 'Задача #%d была закрыта.', - 'Sub-task updated' => 'Подзадача обновлена', - 'Title:' => 'Название :', - 'Status:' => 'Статус :', - 'Assignee:' => 'Назначена :', - 'Time tracking:' => 'Отслеживание времени :', - 'New sub-task' => 'Новая подзадача', - 'New attachment added "%s"' => 'Добавлено вложение « %s »', - 'Comment updated' => 'Комментарий обновлен', - 'New comment posted by %s' => 'Новый комментарий написан « %s »', - 'List of due tasks for the project "%s"' => 'Список сроков к проекту « %s »', - '[%s][New attachment] %s (#%d)' => '[%s][Новых вложений] %s (#%d)', - '[%s][New comment] %s (#%d)' => '[%s][Новых комментариев] %s (#%d)', - '[%s][Comment updated] %s (#%d)' => '[%s][Обновленых коментариев] %s (#%d)', - '[%s][New subtask] %s (#%d)' => '[%s][Новых подзадач] %s (#%d)', - '[%s][Subtask updated] %s (#%d)' => '[%s][Обновленных подзадач] %s (#%d)', - '[%s][New task] %s (#%d)' => '[%s][Новых задач] %s (#%d)', - '[%s][Task updated] %s (#%d)' => '[%s][Обновленных задач] %s (#%d)', - '[%s][Task closed] %s (#%d)' => '[%s][Закрытых задач] %s (#%d)', - '[%s][Task opened] %s (#%d)' => '[%s][Открытых задач] %s (#%d)', - '[%s][Due tasks]' => '[%s][Текущие задачи]', - '[Kanboard] Notification' => '[Kanboard] Оповещение', - 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам :', - 'view the task on Kanboard' => 'посмотреть задачу на Kanboard', - 'Public access' => 'Общий доступ', - 'Category management' => 'Управление категориями', - 'User management' => 'Управление пользователями', - 'Active tasks' => 'Активные задачи', - 'Disable public access' => 'Отключить общий доступ', - 'Enable public access' => 'Включить общий доступ', - 'Active projects' => 'Активные проекты', - 'Inactive projects' => 'Неактивные проекты', - 'Public access disabled' => 'Общий доступ отключен', - 'Do you really want to disable this project: "%s"?' => 'Вы точно хотите отключить проект: "%s"?', - 'Do you really want to duplicate this project: "%s"?' => 'Вы точно хотите клонировать проект: "%s"?', - 'Do you really want to enable this project: "%s"?' => 'Вы точно хотите включить проект: "%s"?', - 'Project activation' => 'Активация проекта', - 'Move the task to another project' => 'Переместить задачу в другой проект', - 'Move to another project' => 'Переместить в другой проект', - 'Do you really want to duplicate this task?' => 'Вы точно хотите клонировать задачу?', - 'Duplicate a task' => 'Клонировать задачу', - 'External accounts' => 'Внешняя аутентификация', - 'Account type' => 'Тип профиля', - 'Local' => 'Локальный', - 'Remote' => 'Удаленный', - 'Enabled' => 'Включен', - 'Disabled' => 'Выключены', - 'Google account linked' => 'Профиль Google связан', - 'Github account linked' => 'Профиль GitHub связан', - 'Username:' => 'Имя пользователя:', - 'Name:' => 'Имя:', - 'Email:' => 'Email:', - 'Default project:' => 'Проект по умолчанию:', - 'Notifications:' => 'Уведомления:', - 'Notifications' => 'Уведомления', - 'Group:' => 'Группа:', - 'Regular user' => 'Обычный пользователь', - 'Account type:' => 'Тип профиля:', - 'Edit profile' => 'Редактировать профиль', - 'Change password' => 'Сменить пароль', - 'Password modification' => 'Изменение пароля', - 'External authentications' => 'Внешняя аутентификация', - 'Google Account' => 'Профиль Google', - 'Github Account' => 'Профиль GitHub', - 'Never connected.' => 'Ранее не соединялось.', - 'No account linked.' => 'Нет связанных профилей.', - 'Account linked.' => 'Профиль связан.', - 'No external authentication enabled.' => 'Нет активной внешней аутентификации.', - 'Password modified successfully.' => 'Пароль изменен.', - 'Unable to change the password.' => 'Не удалось сменить пароль.', - 'Change category for the task "%s"' => 'Сменить категорию для задачи "%s"', - 'Change category' => 'Смена категории', - '%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s обновил задачу <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s открыл задачу <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"' => '%s перместил задачу <a href="?controller=task&action=show&task_id=%d">#%d</a> на позицию #%d в колонке "%s"', - '%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"' => '%s переместил задачу <a href="?controller=task&action=show&task_id=%d">#%d</a> в колонку "%s"', - '%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s создал задачу <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s закрыл задачу <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s создал подзадачу для задачи <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s обновил подзадачу для задачи <a href="?controller=task&action=show&task_id=%d">#%d</a>', - 'Assigned to %s with an estimate of %s/%sh' => 'Назначено %s с окончанием %s/%sh', - 'Not assigned, estimate of %sh' => 'Не назначено, окончание %sh', - '%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s обновил комментарий к задаче <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>' => '%s прокомментировал задачу <a href="?controller=task&action=show&task_id=%d">#%d</a>', - '%s\'s activity' => '%s активность', - 'No activity.' => 'Нет активности', - 'RSS feed' => 'RSS лента', - '%s updated a comment on the task #%d' => '%s обновил комментарий задачи #%d', - '%s commented on the task #%d' => '%s откомментировал задачу #%d', - '%s updated a subtask for the task #%d' => '%s обновил подзадачу задачи #%d', - '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d', - '%s updated the task #%d' => '%s обновил задачу #%d', - '%s created the task #%d' => '%s создал задачу #%d', - '%s closed the task #%d' => '%s закрыл задачу #%d', - '%s open the task #%d' => '%s открыл задачу #%d', - '%s moved the task #%d to the column "%s"' => '%s переместил задачу #%d в колонку "%s"', - '%s moved the task #%d to the position %d in the column "%s"' => '%s переместил задачу #%d на позицию %d в колонке "%s"', - 'Activity' => 'Активность', - 'Default values are "%s"' => 'Колонки по умолчанию: "%s"', - 'Default columns for new projects (Comma-separated)' => 'Колонки по умолчанию для новых проектов (разделять запятой)', - 'Task assignee change' => 'Изменен назначенный', - '%s change the assignee of the task #%d to %s' => '%s сменил назначенного для задачи #%d на %s', - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s' => '%s сменил назначенного для задачи <a href="?controller=task&action=show&task_id=%d">#%d</a> на %s', - '[%s][Column Change] %s (#%d)' => '[%s][Изменение колонки] %s (#%d)', - '[%s][Position Change] %s (#%d)' => '[%s][Изменение позиции] %s (#%d)', - '[%s][Assignee Change] %s (#%d)' => '[%s][Изменение назначеного] %s (#%d)', - 'New password for the user "%s"' => 'Новый пароль для пользователя %s"', - 'Choose an event' => 'Выберите событие', - 'Github commit received' => 'Github: коммит получен', - 'Github issue opened' => 'Github: новая проблема', - 'Github issue closed' => 'Github: проблема закрыта', - 'Github issue reopened' => 'Github: проблема переоткрыта', - 'Github issue assignee change' => 'Github: сменить ответственного за проблему', - 'Github issue label change' => 'Github: ярлык проблемы изменен', - 'Create a task from an external provider' => 'Создать задачу из внешнего источника', - 'Change the assignee based on an external username' => 'Изменить назначенного основываясь на внешнем имени пользователя', - 'Change the category based on an external label' => 'Изменить категорию основываясь на внешнем ярлыке', - 'Reference' => 'Ссылка', - 'Reference: %s' => 'Ссылка: %s', - 'Label' => 'Ярлык', - 'Database' => 'База данных', - 'About' => 'Информация', - 'Database driver:' => 'Драйвер базы данных', - 'Board settings' => 'Настройки доски', - 'URL and token' => 'URL и токен', - 'Webhook settings' => 'Параметры Webhook', - 'URL for task creation:' => 'URL для создания задачи:', - 'Reset token' => 'Перезагрузить токен', - 'API endpoint:' => 'API endpoint:', - 'Refresh interval for private board' => 'Период обновления для частных досок', - 'Refresh interval for public board' => 'Период обновления для публичных досок', - 'Task highlight period' => 'Время подсвечивания задачи', - 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Период (в секундах) в течении которого задача считается недавно измененной (0 для выключения, 2 дня по умолчанию)', - 'Frequency in second (60 seconds by default)' => 'Частота в секундах (60 секунд по умолчанию)', - 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Частота в секундах (0 для выключения, 10 секунд по умолчанию)', - 'Application URL' => 'URL приложения', - 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Пример: http://example.kanboard.net (используется в email уведомлениях)', - 'Token regenerated.' => 'Токен пересоздан', - 'Date format' => 'Формат даты', - 'ISO format is always accepted, example: "%s" and "%s"' => 'Время должно быть в ISO-формате, например: "%s" или "%s"', - 'New private project' => 'Новый проект с ограниченным доступом', - 'This project is private' => 'Это проект с ограниченным доступом', - 'Type here to create a new sub-task' => 'Печатайте сюда чтобы создать подзадачу', - 'Add' => 'Добавить', - 'Estimated time: %s hours' => 'Планируемое время: %s часов', - 'Time spent: %s hours' => 'Потрачено времени: %s часов', - 'Started on %B %e, %Y' => 'Начато %B %e, %Y', - 'Start date' => 'Дата начала', - 'Time estimated' => 'Планируемое время', - 'There is nothing assigned to you.' => 'Вам ничего не назначено', - 'My tasks' => 'Мои задачи', - 'Activity stream' => 'Текущая активность', - 'Dashboard' => 'Инфопанель', - 'Confirmation' => 'Подтверждение пароля', - // 'Allow everybody to access to this project' => '', - // 'Everybody have access to this project.' => '', -); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index 9a6866d3..8cfc7120 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -3,7 +3,7 @@ namespace Model; /** - * Acl model + * Access List * * @package model * @author Frederic Guillot @@ -16,149 +16,195 @@ class Acl extends Base * @access private * @var array */ - private $public_actions = array( - 'user' => array('login', 'check', 'google', 'github'), + private $public_acl = array( + 'auth' => array('login', 'check'), + 'user' => array('google', 'github'), 'task' => array('readonly'), 'board' => array('readonly'), 'project' => array('feed'), - 'webhook' => array('task', 'github'), + 'webhook' => '*', + 'app' => array('colors'), + 'ical' => '*', ); /** - * Controllers and actions allowed for regular users + * Controllers and actions for project members * * @access private * @var array */ - private $user_actions = array( - 'app' => array('index'), - 'board' => array('index', 'show', 'save', 'check', 'changeassignee', 'updateassignee', 'changecategory', 'updatecategory', 'movecolumn', 'edit', 'update', 'add', 'confirm', 'remove'), - 'project' => array('index', 'show', 'export', 'share', 'edit', 'update', 'users', 'remove', 'duplicate', 'disable', 'enable', 'activity', 'search', 'tasks', 'create', 'save'), - 'user' => array('edit', 'forbidden', 'logout', 'show', 'external', 'unlinkgoogle', 'unlinkgithub', 'sessions', 'removesession', 'last', 'notifications', 'password'), - 'comment' => array('create', 'save', 'confirm', 'remove', 'update', 'edit', 'forbidden'), - 'file' => array('create', 'save', 'download', 'confirm', 'remove', 'open', 'image'), - 'subtask' => array('create', 'save', 'edit', 'update', 'confirm', 'remove', 'togglestatus'), - 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'open', 'duplicate', 'remove', 'description', 'move', 'copy', 'time'), - 'category' => array('index', 'save', 'edit', 'update', 'confirm', 'remove'), - 'action' => array('index', 'event', 'params', 'create', 'confirm', 'remove'), + private $member_acl = array( + 'board' => '*', + 'comment' => '*', + 'file' => '*', + 'project' => array('show'), + 'projectinfo' => array('tasks', 'search', 'activity'), + 'subtask' => '*', + 'task' => '*', + 'tasklink' => '*', + 'calendar' => array('show', 'project'), ); /** - * Return true if the specified controller/action is allowed according to the given acl + * Controllers and actions for project managers * - * @access public - * @param array $acl Acl list - * @param string $controller Controller name - * @param string $action Action name - * @return bool + * @access private + * @var array */ - public function isAllowedAction(array $acl, $controller, $action) - { - if (isset($acl[$controller])) { - return in_array($action, $acl[$controller]); - } + private $manager_acl = array( + 'action' => '*', + 'analytic' => '*', + 'category' => '*', + 'column' => '*', + 'export' => array('tasks', 'subtasks', 'summary'), + 'project' => array('edit', 'update', 'share', 'integration', 'users', 'alloweverybody', 'allow', 'setowner', 'revoke', 'duplicate', 'disable', 'enable'), + 'swimlane' => '*', + 'budget' => '*', + ); - return false; - } + /** + * Controllers and actions for admins + * + * @access private + * @var array + */ + private $admin_acl = array( + 'app' => array('dashboard'), + 'user' => array('index', 'create', 'save', 'remove'), + 'config' => '*', + 'link' => '*', + 'project' => array('remove'), + 'hourlyrate' => '*', + 'currency' => '*', + 'twofactor' => array('disable'), + ); /** - * Return true if the given action is public + * Return true if the specified controller/action match the given acl * * @access public + * @param array $acl Acl list * @param string $controller Controller name * @param string $action Action name * @return bool */ - public function isPublicAction($controller, $action) + public function matchAcl(array $acl, $controller, $action) { - return $this->isAllowedAction($this->public_actions, $controller, $action); + $action = strtolower($action); + return isset($acl[$controller]) && $this->hasAction($action, $acl[$controller]); } /** - * Return true if the given action is allowed for a regular user + * Return true if the specified action is inside the list of actions * * @access public - * @param string $controller Controller name * @param string $action Action name + * @param mixed $action Actions list * @return bool */ - public function isUserAction($controller, $action) + public function hasAction($action, $actions) { - return $this->isAllowedAction($this->user_actions, $controller, $action); + if (is_array($actions)) { + return in_array($action, $actions); + } + + return $actions === '*'; } /** - * Return true if the logged user is admin + * Return true if the given action is public * * @access public + * @param string $controller Controller name + * @param string $action Action name * @return bool */ - public function isAdminUser() + public function isPublicAction($controller, $action) { - return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === true; + return $this->matchAcl($this->public_acl, $controller, $action); } /** - * Return true if the logged user is not admin + * Return true if the given action is for admins * * @access public + * @param string $controller Controller name + * @param string $action Action name * @return bool */ - public function isRegularUser() + public function isAdminAction($controller, $action) { - return isset($_SESSION['user']['is_admin']) && $_SESSION['user']['is_admin'] === false; + return $this->matchAcl($this->admin_acl, $controller, $action); } /** - * Get the connected user id + * Return true if the given action is for project managers * * @access public - * @return integer + * @param string $controller Controller name + * @param string $action Action name + * @return bool */ - public function getUserId() + public function isManagerAction($controller, $action) { - return isset($_SESSION['user']['id']) ? (int) $_SESSION['user']['id'] : 0; + return $this->matchAcl($this->manager_acl, $controller, $action); } /** - * Check is the user is connected + * Return true if the given action is for project members * * @access public + * @param string $controller Controller name + * @param string $action Action name * @return bool */ - public function isLogged() + public function isMemberAction($controller, $action) { - return ! empty($_SESSION['user']); + return $this->matchAcl($this->member_acl, $controller, $action); } /** - * Check is the user was authenticated with the RememberMe or set the value + * Return true if the visitor is allowed to access to the given page + * We suppose the user already authenticated * * @access public - * @param bool $value Set true if the user use the RememberMe + * @param string $controller Controller name + * @param string $action Action name + * @param integer $project_id Project id * @return bool */ - public function isRememberMe($value = null) + public function isAllowed($controller, $action, $project_id = 0) { - if ($value !== null) { - $_SESSION['is_remember_me'] = $value; + // If you are admin you have access to everything + if ($this->userSession->isAdmin()) { + return true; } - return empty($_SESSION['is_remember_me']) ? false : $_SESSION['is_remember_me']; + // If you access to an admin action, your are not allowed + if ($this->isAdminAction($controller, $action)) { + return false; + } + + // Check project manager permissions + if ($this->isManagerAction($controller, $action)) { + return $this->isManagerActionAllowed($project_id); + } + + // Check project member permissions + if ($this->isMemberAction($controller, $action)) { + return $project_id > 0 && $this->projectPermission->isMember($project_id, $this->userSession->getId()); + } + + // Other applications actions are allowed + return true; } - /** - * Check if an action is allowed for the logged user - * - * @access public - * @param string $controller Controller name - * @param string $action Action name - * @return bool - */ - public function isPageAccessAllowed($controller, $action) + public function isManagerActionAllowed($project_id) { - return $this->isPublicAction($controller, $action) || - $this->isAdminUser() || - ($this->isRegularUser() && $this->isUserAction($controller, $action)); + if ($this->userSession->isAdmin()) { + return true; + } + + return $project_id > 0 && $this->projectPermission->isManager($project_id, $this->userSession->getId()); } } diff --git a/app/Model/Action.php b/app/Model/Action.php index 56a1a2bb..3e8aa091 100644 --- a/app/Model/Action.php +++ b/app/Model/Action.php @@ -2,7 +2,9 @@ namespace Model; -use LogicException; +use Integration\GitlabWebhook; +use Integration\GithubWebhook; +use Integration\BitbucketWebhook; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -43,12 +45,18 @@ class Action extends Base 'TaskAssignCurrentUser' => t('Assign the task to the person who does the action'), 'TaskDuplicateAnotherProject' => t('Duplicate the task to another project'), 'TaskMoveAnotherProject' => t('Move the task to another project'), + 'TaskMoveColumnAssigned' => t('Move the task to another column when assigned to a user'), + 'TaskMoveColumnUnAssigned' => t('Move the task to another column when assignee is cleared'), + 'TaskAssignColorColumn' => t('Assign a color when the task is moved to a specific column'), 'TaskAssignColorUser' => t('Assign a color to a specific user'), 'TaskAssignColorCategory' => t('Assign automatically a color based on a category'), 'TaskAssignCategoryColor' => t('Assign automatically a category based on a color'), + 'CommentCreation' => t('Create a comment from an external provider'), 'TaskCreation' => t('Create a task from an external provider'), + 'TaskLogMoveAnotherColumn' => t('Add a comment logging moving the task between columns'), 'TaskAssignUser' => t('Change the assignee based on an external username'), 'TaskAssignCategoryLabel' => t('Change the category based on an external label'), + 'TaskUpdateStartDate' => t('Automatically update the start date'), ); asort($values); @@ -78,6 +86,11 @@ class Action extends Base GithubWebhook::EVENT_ISSUE_REOPENED => t('Github issue reopened'), GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE => t('Github issue assignee change'), GithubWebhook::EVENT_ISSUE_LABEL_CHANGE => t('Github issue label change'), + GithubWebhook::EVENT_ISSUE_COMMENT => t('Github issue comment created'), + GitlabWebhook::EVENT_COMMIT => t('Gitlab commit received'), + GitlabWebhook::EVENT_ISSUE_OPENED => t('Gitlab issue opened'), + GitlabWebhook::EVENT_ISSUE_CLOSED => t('Gitlab issue closed'), + BitbucketWebhook::EVENT_COMMIT => t('Bitbucket commit received'), ); asort($values); @@ -134,9 +147,17 @@ class Action extends Base public function getAll() { $actions = $this->db->table(self::TABLE)->findAll(); + $params = $this->db->table(self::TABLE_PARAMS)->findAll(); foreach ($actions as &$action) { - $action['params'] = $this->db->table(self::TABLE_PARAMS)->eq('action_id', $action['id'])->findAll(); + + $action['params'] = array(); + + foreach ($params as $param) { + if ($param['action_id'] === $action['id']) { + $action['params'][] = $param; + } + } } return $actions; @@ -185,6 +206,7 @@ class Action extends Base */ public function remove($action_id) { + // $this->container['fileCache']->remove('proxy_action_getAll'); return $this->db->table(self::TABLE)->eq('id', $action_id)->remove(); } @@ -193,7 +215,7 @@ class Action extends Base * * @access public * @param array $values Required parameters to save an action - * @return bool Success or not + * @return boolean|integer */ public function create(array $values) { @@ -228,7 +250,9 @@ class Action extends Base $this->db->closeTransaction(); - return true; + // $this->container['fileCache']->remove('proxy_action_getAll'); + + return $action_id; } /** @@ -238,7 +262,10 @@ class Action extends Base */ public function attachEvents() { - foreach ($this->getAll() as $action) { + //$actions = $this->container['fileCache']->proxy('action', 'getAll'); + $actions = $this->getAll(); + + foreach ($actions as $action) { $listener = $this->load($action['action_name'], $action['project_id'], $action['event_name']); @@ -246,7 +273,7 @@ class Action extends Base $listener->setParam($param['name'], $param['value']); } - $this->event->attach($action['event_name'], $listener); + $this->container['dispatcher']->addListener($action['event_name'], array($listener, 'execute')); } } @@ -262,7 +289,7 @@ class Action extends Base public function load($name, $project_id, $event) { $className = '\Action\\'.$name; - return new $className($this->registry, $project_id, $event); + return new $className($this->container, $project_id, $event); } /** @@ -301,6 +328,8 @@ class Action extends Base } } + // $this->container['fileCache']->remove('proxy_action_getAll'); + return true; } diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php index b9ebcfe2..86c1c43f 100644 --- a/app/Model/Authentication.php +++ b/app/Model/Authentication.php @@ -3,7 +3,6 @@ namespace Model; use Core\Request; -use Auth\Database; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -24,31 +23,31 @@ class Authentication extends Base */ public function backend($name) { - if (! isset($this->registry->$name)) { + if (! isset($this->container[$name])) { $class = '\Auth\\'.ucfirst($name); - $this->registry->$name = new $class($this->registry); + $this->container[$name] = new $class($this->container); } - return $this->registry->shared($name); + return $this->container[$name]; } /** * Check if the current user is authenticated * * @access public - * @param string $controller Controller - * @param string $action Action name * @return bool */ - public function isAuthenticated($controller, $action) + public function isAuthenticated() { - // If the action is public we don't need to do any checks - if ($this->acl->isPublicAction($controller, $action)) { - return true; - } - // If the user is already logged it's ok - if ($this->acl->isLogged()) { + if ($this->userSession->isLogged()) { + + // Check if the user session match an existing user + if (! $this->user->exists($this->userSession->getId())) { + $this->backend('rememberMe')->destroy($this->userSession->getId()); + $this->session->close(); + return false; + } // We update each time the RememberMe cookie tokens if ($this->backend('rememberMe')->hasCookie()) { @@ -118,7 +117,7 @@ class Authentication extends Base if (! empty($values['remember_me'])) { $credentials = $this->backend('rememberMe') - ->create($this->acl->getUserId(), Request::getIpAddress(), Request::getUserAgent()); + ->create($this->userSession->getId(), Request::getIpAddress(), Request::getUserAgent()); $this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']); } diff --git a/app/Model/Base.php b/app/Model/Base.php index 72d91c3c..51ae782d 100644 --- a/app/Model/Base.php +++ b/app/Model/Base.php @@ -2,44 +2,15 @@ namespace Model; -use Core\Event; -use Core\Tool; -use Core\Registry; -use PicoDb\Database; +use Pimple\Container; /** * Base model class * * @package model * @author Frederic Guillot - * - * @property \Model\Acl $acl - * @property \Model\Action $action - * @property \Model\Authentication $authentication - * @property \Model\Board $board - * @property \Model\Category $category - * @property \Model\Comment $comment - * @property \Model\CommentHistory $commentHistory - * @property \Model\Color $color - * @property \Model\Config $config - * @property \Model\DateParser $dateParser - * @property \Model\File $file - * @property \Model\LastLogin $lastLogin - * @property \Model\Notification $notification - * @property \Model\Project $project - * @property \Model\ProjectPermission $projectPermission - * @property \Model\SubTask $subTask - * @property \Model\SubtaskHistory $subtaskHistory - * @property \Model\Task $task - * @property \Model\TaskExport $taskExport - * @property \Model\TaskFinder $taskFinder - * @property \Model\TaskHistory $taskHistory - * @property \Model\TaskValidator $taskValidator - * @property \Model\TimeTracking $timeTracking - * @property \Model\User $user - * @property \Model\Webhook $webhook */ -abstract class Base +abstract class Base extends \Core\Base { /** * Database instance @@ -50,44 +21,35 @@ abstract class Base protected $db; /** - * Event dispatcher instance - * - * @access public - * @var \Core\Event - */ - public $event; - - /** - * Registry instance - * - * @access protected - * @var \Core\Registry - */ - protected $registry; - - /** * Constructor * * @access public - * @param \Core\Registry $registry Registry instance + * @param \Pimple\Container $container */ - public function __construct(Registry $registry) + public function __construct(Container $container) { - $this->registry = $registry; - $this->db = $this->registry->shared('db'); - $this->event = $this->registry->shared('event'); + $this->container = $container; + $this->db = $this->container['db']; } /** - * Load automatically models + * Save a record in the database * * @access public - * @param string $name Model name - * @return mixed + * @param string $table Table name + * @param array $values Form values + * @return boolean|integer */ - public function __get($name) + public function persist($table, array $values) { - return Tool::loadModel($this->registry, $name); + return $this->db->transaction(function($db) use ($table, $values) { + + if (! $db->table($table)->save($values)) { + return false; + } + + return (int) $db->getConnection()->getLastId(); + }); } /** @@ -95,7 +57,7 @@ abstract class Base * * @access public * @param array $values Input array - * @param array $keys List of keys to remove + * @param string[] $keys List of keys to remove */ public function removeFields(array &$values, array $keys) { @@ -110,8 +72,8 @@ abstract class Base * Force some fields to be at 0 if empty * * @access public - * @param array $values Input array - * @param array $keys List of keys + * @param array $values Input array + * @param string[] $keys List of keys */ public function resetFields(array &$values, array $keys) { @@ -126,8 +88,8 @@ abstract class Base * Force some fields to be integer * * @access public - * @param array $values Input array - * @param array $keys List of keys + * @param array $values Input array + * @param string[] $keys List of keys */ public function convertIntegerFields(array &$values, array $keys) { @@ -137,4 +99,67 @@ abstract class Base } } } + + /** + * Build SQL condition for a given time range + * + * @access protected + * @param string $start_time Start timestamp + * @param string $end_time End timestamp + * @param string $start_column Start column name + * @param string $end_column End column name + * @return string + */ + protected function getCalendarCondition($start_time, $end_time, $start_column, $end_column) + { + $start_column = $this->db->escapeIdentifier($start_column); + $end_column = $this->db->escapeIdentifier($end_column); + + $conditions = array( + "($start_column >= '$start_time' AND $start_column <= '$end_time')", + "($start_column <= '$start_time' AND $end_column >= '$start_time')", + "($start_column <= '$start_time' AND ($end_column = '0' OR $end_column IS NULL))", + ); + + return $start_column.' IS NOT NULL AND '.$start_column.' > 0 AND ('.implode(' OR ', $conditions).')'; + } + + /** + * Get common properties for task calendar events + * + * @access protected + * @param array $task + * @return array + */ + protected function getTaskCalendarProperties(array &$task) + { + return array( + 'timezoneParam' => $this->config->getCurrentTimezone(), + 'id' => $task['id'], + 'title' => t('#%d', $task['id']).' '.$task['title'], + 'backgroundColor' => $this->color->getBackgroundColor($task['color_id']), + 'borderColor' => $this->color->getBorderColor($task['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + ); + } + + /** + * Group a collection of records by a column + * + * @access public + * @param array $collection + * @param string $column + * @return array + */ + public function groupByColumn(array $collection, $column) + { + $result = array(); + + foreach ($collection as $item) { + $result[$item[$column]][] = $item; + } + + return $result; + } } diff --git a/app/Model/Board.php b/app/Model/Board.php index 4c78b0f6..eecbc91c 100644 --- a/app/Model/Board.php +++ b/app/Model/Board.php @@ -24,7 +24,7 @@ class Board extends Base * Get Kanboard default columns * * @access public - * @return array + * @return string[] */ public function getDefaultColumns() { @@ -47,7 +47,7 @@ class Board extends Base $column_name = trim($column_name); if (! empty($column_name)) { - $columns[] = array('title' => $column_name, 'task_limit' => 0); + $columns[] = array('title' => $column_name, 'task_limit' => 0, 'description' => ''); } } @@ -73,6 +73,7 @@ class Board extends Base 'position' => ++$position, 'project_id' => $project_id, 'task_limit' => $column['task_limit'], + 'description' => $column['description'], ); if (! $this->db->table(self::TABLE)->save($values)) { @@ -94,7 +95,7 @@ class Board extends Base public function duplicate($project_from, $project_to) { $columns = $this->db->table(Board::TABLE) - ->columns('title', 'task_limit') + ->columns('title', 'task_limit', 'description') ->eq('project_id', $project_from) ->asc('position') ->findAll(); @@ -109,61 +110,79 @@ class Board extends Base * @param integer $project_id Project id * @param string $title Column title * @param integer $task_limit Task limit - * @return boolean + * @param string $description Column description + * @return boolean|integer */ - public function addColumn($project_id, $title, $task_limit = 0) + public function addColumn($project_id, $title, $task_limit = 0, $description = '') { - return $this->db->table(self::TABLE)->save(array( + $values = array( 'project_id' => $project_id, 'title' => $title, - 'task_limit' => $task_limit, + 'task_limit' => intval($task_limit), 'position' => $this->getLastColumnPosition($project_id) + 1, - )); + 'description' => $description, + ); + + return $this->persist(self::TABLE, $values); } /** - * Update columns + * Update a column * * @access public - * @param array $values Form values + * @param integer $column_id Column id + * @param string $title Column title + * @param integer $task_limit Task limit + * @param string $description Optional description * @return boolean */ - public function update(array $values) + public function updateColumn($column_id, $title, $task_limit = 0, $description = '') { - $columns = array(); - - foreach (array('title', 'task_limit') as $field) { - foreach ($values[$field] as $column_id => $value) { - $columns[$column_id][$field] = $value; - } - } + return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( + 'title' => $title, + 'task_limit' => intval($task_limit), + 'description' => $description, + )); + } - $this->db->startTransaction(); + /** + * Get columns with consecutive positions + * + * If you remove a column, the positions are not anymore consecutives + * + * @access public + * @param integer $project_id + * @return array + */ + public function getNormalizedColumnPositions($project_id) + { + $columns = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'position'); + $position = 1; - foreach ($columns as $column_id => $values) { - $this->updateColumn($column_id, $values['title'], (int) $values['task_limit']); + foreach ($columns as $column_id => $column_position) { + $columns[$column_id] = $position++; } - $this->db->closeTransaction(); - - return true; + return $columns; } /** - * Update a column + * Save the new positions for a set of columns * * @access public - * @param integer $column_id Column id - * @param string $title Column title - * @param integer $task_limit Task limit + * @param array $columns Hashmap of column_id/column_position * @return boolean */ - public function updateColumn($column_id, $title, $task_limit = 0) + public function saveColumnPositions(array $columns) { - return $this->db->table(self::TABLE)->eq('id', $column_id)->update(array( - 'title' => $title, - 'task_limit' => $task_limit, - )); + return $this->db->transaction(function ($db) use ($columns) { + + foreach ($columns as $column_id => $position) { + if (! $db->table(Board::TABLE)->eq('id', $column_id)->update(array('position' => $position))) { + return false; + } + } + }); } /** @@ -176,7 +195,7 @@ class Board extends Base */ public function moveDown($project_id, $column_id) { - $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position'); + $columns = $this->getNormalizedColumnPositions($project_id); $positions = array_flip($columns); if (isset($columns[$column_id]) && $columns[$column_id] < count($columns)) { @@ -184,12 +203,7 @@ class Board extends Base $position = ++$columns[$column_id]; $columns[$positions[$position]]--; - $this->db->startTransaction(); - $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position)); - $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]])); - $this->db->closeTransaction(); - - return true; + return $this->saveColumnPositions($columns); } return false; @@ -205,7 +219,7 @@ class Board extends Base */ public function moveUp($project_id, $column_id) { - $columns = $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'position'); + $columns = $this->getNormalizedColumnPositions($project_id); $positions = array_flip($columns); if (isset($columns[$column_id]) && $columns[$column_id] > 1) { @@ -213,42 +227,79 @@ class Board extends Base $position = --$columns[$column_id]; $columns[$positions[$position]]++; - $this->db->startTransaction(); - $this->db->table(self::TABLE)->eq('id', $column_id)->update(array('position' => $position)); - $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $columns[$positions[$position]])); - $this->db->closeTransaction(); - - return true; + return $this->saveColumnPositions($columns); } return false; } /** - * Get all columns and tasks for a given project + * Get all tasks sorted by columns and swimlanes * * @access public * @param integer $project_id Project id - * @param array $filters * @return array */ - public function get($project_id, array $filters = array()) + public function getBoard($project_id) { + $swimlanes = $this->swimlane->getSwimlanes($project_id); $columns = $this->getColumns($project_id); - $tasks = $this->taskFinder->getTasksOnBoard($project_id); + $nb_columns = count($columns); - foreach ($columns as &$column) { + for ($i = 0, $ilen = count($swimlanes); $i < $ilen; $i++) { - $column['tasks'] = array(); + $swimlanes[$i]['columns'] = $columns; + $swimlanes[$i]['nb_columns'] = $nb_columns; + $swimlanes[$i]['nb_tasks'] = 0; - foreach ($tasks as &$task) { - if ($task['column_id'] == $column['id']) { - $column['tasks'][] = $task; - } + for ($j = 0; $j < $nb_columns; $j++) { + $swimlanes[$i]['columns'][$j]['tasks'] = $this->taskFinder->getTasksByColumnAndSwimlane($project_id, $columns[$j]['id'], $swimlanes[$i]['id']); + $swimlanes[$i]['columns'][$j]['nb_tasks'] = count($swimlanes[$i]['columns'][$j]['tasks']); + $swimlanes[$i]['columns'][$j]['score'] = $this->getColumnSum($swimlanes[$i]['columns'][$j]['tasks'], 'score'); + $swimlanes[$i]['nb_tasks'] += $swimlanes[$i]['columns'][$j]['nb_tasks']; } } - return $columns; + return $swimlanes; + } + + /** + * Calculate the sum of the defined field for a list of tasks + * + * @access public + * @param array $tasks + * @param string $field + * @return integer + */ + public function getColumnSum(array &$tasks, $field) + { + $sum = 0; + + foreach ($tasks as $task) { + $sum += $task['score']; + } + + return $sum; + } + + /** + * Get the total of tasks per column + * + * @access public + * @param integer $project_id + * @param boolean $prepend Prepend default value + * @return array + */ + public function getColumnStats($project_id, $prepend = false) + { + $listing = $this->db + ->hashtable(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->groupBy('column_id') + ->getAll('column_id', 'COUNT(*) AS total'); + + return $prepend ? array(-1 => t('All columns')) + $listing : $listing; } /** @@ -264,15 +315,29 @@ class Board extends Base } /** + * Get the last column id for a given project + * + * @access public + * @param integer $project_id Project id + * @return integer + */ + public function getLastColumn($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('position')->findOneColumn('id'); + } + + /** * Get the list of columns sorted by position [ column_id => title ] * * @access public * @param integer $project_id Project id + * @param boolean $prepend Prepend a default value * @return array */ - public function getColumnsList($project_id) + public function getColumnsList($project_id, $prepend = false) { - return $this->db->table(self::TABLE)->eq('project_id', $project_id)->asc('position')->listing('id', 'title'); + $listing = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->asc('position')->getAll('id', 'title'); + return $prepend ? array(-1 => t('All columns')) + $listing : $listing; } /** @@ -343,22 +408,16 @@ class Board extends Base * Validate column modification * * @access public - * @param array $columns Original columns List * @param array $values Required parameters to update a column * @return array $valid, $errors [0] = Success or not, [1] = List of errors */ - public function validateModification(array $columns, array $values) + public function validateModification(array $values) { - $rules = array(); - - foreach ($columns as $column_id => $column_title) { - $rules[] = new Validators\Integer('task_limit['.$column_id.']', t('This value must be an integer')); - $rules[] = new Validators\GreaterThan('task_limit['.$column_id.']', t('This value must be greater than %d', 0), 0); - $rules[] = new Validators\Required('title['.$column_id.']', t('The title is required')); - $rules[] = new Validators\MaxLength('title['.$column_id.']', t('The maximum length is %d characters', 50), 50); - } - - $v = new Validator($values, $rules); + $v = new Validator($values, array( + new Validators\Integer('task_limit', t('This value must be an integer')), + new Validators\Required('title', t('The title is required')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 50), 50), + )); return array( $v->execute(), diff --git a/app/Model/Budget.php b/app/Model/Budget.php new file mode 100644 index 00000000..d74dd870 --- /dev/null +++ b/app/Model/Budget.php @@ -0,0 +1,214 @@ +<?php + +namespace Model; + +use DateInterval; +use DateTime; +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Budget + * + * @package model + * @author Frederic Guillot + */ +class Budget extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'budget_lines'; + + /** + * Get all budget lines for a project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAll($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->desc('date')->findAll(); + } + + /** + * Get the current total of the budget + * + * @access public + * @param integer $project_id + * @return float + */ + public function getTotal($project_id) + { + $result = $this->db->table(self::TABLE)->columns('SUM(amount) as total')->eq('project_id', $project_id)->findOne(); + return isset($result['total']) ? (float) $result['total'] : 0; + } + + /** + * Get breakdown by tasks/subtasks/users + * + * @access public + * @param integer $project_id + * @return \PicoDb\Table + */ + public function getSubtaskBreakdown($project_id) + { + return $this->db + ->table(SubtaskTimeTracking::TABLE) + ->columns( + SubtaskTimeTracking::TABLE.'.id', + SubtaskTimeTracking::TABLE.'.user_id', + SubtaskTimeTracking::TABLE.'.subtask_id', + SubtaskTimeTracking::TABLE.'.start', + SubtaskTimeTracking::TABLE.'.time_spent', + Subtask::TABLE.'.task_id', + Subtask::TABLE.'.title AS subtask_title', + Task::TABLE.'.title AS task_title', + Task::TABLE.'.project_id', + User::TABLE.'.username', + User::TABLE.'.name' + ) + ->join(Subtask::TABLE, 'id', 'subtask_id') + ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->join(User::TABLE, 'id', 'user_id') + ->eq(Task::TABLE.'.project_id', $project_id) + ->filter(array($this, 'applyUserRate')); + } + + /** + * Gather necessary information to display the budget graph + * + * @access public + * @param integer $project_id + * @return array + */ + public function getDailyBudgetBreakdown($project_id) + { + $out = array(); + $in = $this->db->hashtable(self::TABLE)->eq('project_id', $project_id)->gt('amount', 0)->asc('date')->getAll('date', 'amount'); + $time_slots = $this->getSubtaskBreakdown($project_id)->findAll(); + + foreach ($time_slots as $slot) { + $date = date('Y-m-d', $slot['start']); + + if (! isset($out[$date])) { + $out[$date] = 0; + } + + $out[$date] += $slot['cost']; + } + + $start = key($in) ?: key($out); + $end = new DateTime; + $left = 0; + $serie = array(); + + for ($today = new DateTime($start); $today <= $end; $today->add(new DateInterval('P1D'))) { + + $date = $today->format('Y-m-d'); + $today_in = isset($in[$date]) ? (int) $in[$date] : 0; + $today_out = isset($out[$date]) ? (int) $out[$date] : 0; + + if ($today_in > 0 || $today_out > 0) { + + $left += $today_in; + $left -= $today_out; + + $serie[] = array( + 'date' => $date, + 'in' => $today_in, + 'out' => -$today_out, + 'left' => $left, + ); + } + } + + return $serie; + } + + /** + * Filter callback to apply the rate according to the effective date + * + * @access public + * @param array $records + * @return array + */ + public function applyUserRate(array $records) + { + $rates = $this->hourlyRate->getAllByProject($records[0]['project_id']); + + foreach ($records as &$record) { + + $hourly_price = 0; + + foreach ($rates as $rate) { + + if ($rate['user_id'] == $record['user_id'] && date('Y-m-d', $rate['date_effective']) <= date('Y-m-d', $record['start'])) { + $hourly_price = $this->currency->getPrice($rate['currency'], $rate['rate']); + break; + } + } + + $record['cost'] = $hourly_price * $record['time_spent']; + } + + return $records; + } + + /** + * Add a new budget line in the database + * + * @access public + * @param integer $project_id + * @param float $amount + * @param string $comment + * @param string $date + * @return boolean|integer + */ + public function create($project_id, $amount, $comment, $date = '') + { + $values = array( + 'project_id' => $project_id, + 'amount' => $amount, + 'comment' => $comment, + 'date' => $date ?: date('Y-m-d'), + ); + + return $this->persist(self::TABLE, $values); + } + + /** + * Remove a specific budget line + * + * @access public + * @param integer $budget_id + * @return boolean + */ + public function remove($budget_id) + { + return $this->db->table(self::TABLE)->eq('id', $budget_id)->remove(); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('project_id', t('Field required')), + new Validators\Required('amount', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +}
\ No newline at end of file diff --git a/app/Model/Category.php b/app/Model/Category.php index fb54594b..c8ba7251 100644 --- a/app/Model/Category.php +++ b/app/Model/Category.php @@ -46,6 +46,66 @@ class Category extends Base } /** + * Get the category name by the id + * + * @access public + * @param integer $category_id Category id + * @return string + */ + public function getNameById($category_id) + { + return $this->db->table(self::TABLE)->eq('id', $category_id)->findOneColumn('name') ?: ''; + } + + /** + * Get a category id by the project and the name + * + * @access public + * @param integer $project_id Project id + * @param string $category_name Category name + * @return integer + */ + public function getIdByName($project_id, $category_name) + { + return (int) $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('name', $category_name) + ->findOneColumn('id'); + } + + /** + * Prepare categories to be displayed on the board + * + * @access public + * @param integer $project_id + * @return array + */ + public function getBoardCategories($project_id) + { + $descriptions = array(); + + $listing = array( + -1 => t('All categories'), + 0 => t('No category'), + ); + + $categories = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->asc('name') + ->findAll(); + + foreach ($categories as $category) { + $listing[$category['id']] = $category['name']; + $descriptions[$category['id']] = $category['description']; + } + + return array( + $listing, + $descriptions, + ); + } + + /** * Return the list of all categories * * @access public @@ -56,10 +116,10 @@ class Category extends Base */ public function getList($project_id, $prepend_none = true, $prepend_all = false) { - $listing = $this->db->table(self::TABLE) + $listing = $this->db->hashtable(self::TABLE) ->eq('project_id', $project_id) ->asc('name') - ->listing('id', 'name'); + ->getAll('id', 'name'); $prepend = array(); @@ -90,15 +150,38 @@ class Category extends Base } /** - * Create a category + * Create default cetegories during project creation (transaction already started in Project::create()) + * + * @access public + * @param integer $project_id + */ + public function createDefaultCategories($project_id) + { + $categories = explode(',', $this->config->get('project_categories')); + + foreach ($categories as $category) { + + $category = trim($category); + + if (! empty($category)) { + $this->db->table(self::TABLE)->insert(array( + 'project_id' => $project_id, + 'name' => $category, + )); + } + } + } + + /** + * Create a category (run inside a transaction) * * @access public * @param array $values Form values - * @return bool + * @return bool|integer */ public function create(array $values) { - return $this->db->table(self::TABLE)->save($values); + return $this->persist(self::TABLE, $values); } /** @@ -137,26 +220,26 @@ class Category extends Base } /** - * Duplicate categories from a project to another one + * Duplicate categories from a project to another one, must be executed inside a transaction * * @author Antonio Rabelo - * @param integer $project_from Project Template - * @return integer $project_to Project that receives the copy + * @param integer $src_project_id Source project id + * @return integer $dst_project_id Destination project id * @return boolean */ - public function duplicate($project_from, $project_to) + public function duplicate($src_project_id, $dst_project_id) { $categories = $this->db->table(self::TABLE) ->columns('name') - ->eq('project_id', $project_from) + ->eq('project_id', $src_project_id) ->asc('name') ->findAll(); foreach ($categories as $category) { - $category['project_id'] = $project_to; + $category['project_id'] = $dst_project_id; - if (! $this->category->create($category)) { + if (! $this->db->table(self::TABLE)->save($category)) { return false; } } diff --git a/app/Model/Color.php b/app/Model/Color.php index f414e837..241a97c7 100644 --- a/app/Model/Color.php +++ b/app/Model/Color.php @@ -3,7 +3,7 @@ namespace Model; /** - * Color model (TODO: model for the future color picker) + * Color model * * @package model * @author Frederic Guillot @@ -11,14 +11,60 @@ namespace Model; class Color extends Base { /** + * Default colors + * + * @access private + * @var array + */ + private $default_colors = array( + 'yellow' => array( + 'name' => 'Yellow', + 'background' => 'rgb(245, 247, 196)', + 'border' => 'rgb(223, 227, 45)', + ), + 'blue' => array( + 'name' => 'Blue', + 'background' => 'rgb(219, 235, 255)', + 'border' => 'rgb(168, 207, 255)', + ), + 'green' => array( + 'name' => 'Green', + 'background' => 'rgb(189, 244, 203)', + 'border' => 'rgb(74, 227, 113)', + ), + 'purple' => array( + 'name' => 'Purple', + 'background' => 'rgb(223, 176, 255)', + 'border' => 'rgb(205, 133, 254)', + ), + 'red' => array( + 'name' => 'Red', + 'background' => 'rgb(255, 187, 187)', + 'border' => 'rgb(255, 151, 151)', + ), + 'orange' => array( + 'name' => 'Orange', + 'background' => 'rgb(255, 215, 179)', + 'border' => 'rgb(255, 172, 98)', + ), + 'grey' => array( + 'name' => 'Grey', + 'background' => 'rgb(238, 238, 238)', + 'border' => 'rgb(204, 204, 204)', + ), + ); + + /** * Get available colors * * @access public * @return array */ - public function getList() + public function getList($prepend = false) { - return array( + $listing = $prepend ? array('' => t('All colors')) : array(); + + return $listing + array( 'yellow' => t('Yellow'), 'blue' => t('Blue'), 'green' => t('Green'), @@ -28,4 +74,68 @@ class Color extends Base 'grey' => t('Grey'), ); } + + /** + * Get the default color + * + * @access public + * @return string + */ + public function getDefaultColor() + { + return 'yellow'; // TODO: make this parameter configurable + } + + /** + * Get Bordercolor from string + * + * @access public + * @param string $color_id Color id + * @return string + */ + public function getBorderColor($color_id) + { + if (isset($this->default_colors[$color_id])) { + return $this->default_colors[$color_id]['border']; + } + + return $this->default_colors[$this->getDefaultColor()]['border']; + } + + /** + * Get background color from the color_id + * + * @access public + * @param string $color_id Color id + * @return string + */ + public function getBackgroundColor($color_id) + { + if (isset($this->default_colors[$color_id])) { + return $this->default_colors[$color_id]['background']; + } + + return $this->default_colors[$this->getDefaultColor()]['background']; + } + + /** + * Get CSS stylesheet of all colors + * + * @access public + * @return string + */ + public function getCss() + { + $buffer = ''; + + foreach ($this->default_colors as $color => $values) { + $buffer .= 'td.color-'.$color.','; + $buffer .= 'div.color-'.$color.' {'; + $buffer .= 'background-color: '.$values['background'].';'; + $buffer .= 'border-color: '.$values['border']; + $buffer .= '}'; + } + + return $buffer; + } } diff --git a/app/Model/Comment.php b/app/Model/Comment.php index cd361b1d..3aa9c027 100644 --- a/app/Model/Comment.php +++ b/app/Model/Comment.php @@ -2,6 +2,7 @@ namespace Model; +use Event\CommentEvent; use SimpleValidator\Validator; use SimpleValidator\Validators; @@ -46,7 +47,8 @@ class Comment extends Base self::TABLE.'.user_id', self::TABLE.'.comment', User::TABLE.'.username', - User::TABLE.'.name' + User::TABLE.'.name', + User::TABLE.'.email' ) ->join(User::TABLE, 'id', 'user_id') ->orderBy(self::TABLE.'.date', 'ASC') @@ -95,24 +97,22 @@ class Comment extends Base } /** - * Save a comment in the database + * Create a new comment * * @access public * @param array $values Form values - * @return boolean + * @return boolean|integer */ public function create(array $values) { $values['date'] = time(); + $comment_id = $this->persist(self::TABLE, $values); - if ($this->db->table(self::TABLE)->save($values)) { - - $values['id'] = $this->db->getConnection()->getLastId(); - $this->event->trigger(self::EVENT_CREATE, $values); - return true; + if ($comment_id) { + $this->container['dispatcher']->dispatch(self::EVENT_CREATE, new CommentEvent(array('id' => $comment_id) + $values)); } - return false; + return $comment_id; } /** @@ -129,7 +129,9 @@ class Comment extends Base ->eq('id', $values['id']) ->update(array('comment' => $values['comment'])); - $this->event->trigger(self::EVENT_UPDATE, $values); + if ($result) { + $this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values)); + } return $result; } diff --git a/app/Model/Config.php b/app/Model/Config.php index 066d3993..813cc84f 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -2,8 +2,6 @@ namespace Model; -use SimpleValidator\Validator; -use SimpleValidator\Validators; use Core\Translator; use Core\Security; use Core\Session; @@ -24,41 +22,132 @@ class Config extends Base const TABLE = 'settings'; /** + * Get available currencies + * + * @access public + * @return array + */ + public function getCurrencies() + { + return array( + 'USD' => t('USD - US Dollar'), + 'EUR' => t('EUR - Euro'), + 'GBP' => t('GBP - British Pound'), + 'CHF' => t('CHF - Swiss Francs'), + 'CAD' => t('CAD - Canadian Dollar'), + 'AUD' => t('AUD - Australian Dollar'), + 'NZD' => t('NZD - New Zealand Dollar'), + 'INR' => t('INR - Indian Rupee'), + 'JPY' => t('JPY - Japanese Yen'), + 'RSD' => t('RSD - Serbian dinar'), + 'SEK' => t('SEK - Swedish Krona'), + ); + } + + /** * Get available timezones * * @access public + * @param boolean $prepend Prepend a default value * @return array */ - public function getTimezones() + public function getTimezones($prepend = false) { $timezones = timezone_identifiers_list(); - return array_combine(array_values($timezones), $timezones); + $listing = array_combine(array_values($timezones), $timezones); + + if ($prepend) { + return array('' => t('Application default')) + $listing; + } + + return $listing; } /** * Get available languages * * @access public + * @param boolean $prepend Prepend a default value * @return array */ - public function getLanguages() + public function getLanguages($prepend = false) { // Sorted by value - return array( + $languages = array( 'da_DK' => 'Dansk', 'de_DE' => 'Deutsch', 'en_US' => 'English', 'es_ES' => 'Español', 'fr_FR' => 'Français', 'it_IT' => 'Italiano', + 'hu_HU' => 'Magyar', + 'nl_NL' => 'Nederlands', 'pl_PL' => 'Polski', 'pt_BR' => 'Português (Brasil)', 'ru_RU' => 'Русский', + 'sr_Latn_RS' => 'Srpski', 'fi_FI' => 'Suomi', 'sv_SE' => 'Svenska', + 'tr_TR' => 'Türkçe', 'zh_CN' => '中文(简体)', 'ja_JP' => '日本語', + 'th_TH' => 'ไทย', + ); + + if ($prepend) { + return array('' => t('Application default')) + $languages; + } + + return $languages; + } + + /** + * Get javascript language code + * + * @access public + * @return string + */ + public function getJsLanguageCode() + { + $languages = array( + 'da_DK' => 'da', + 'de_DE' => 'de', + 'en_US' => 'en', + 'es_ES' => 'es', + 'fr_FR' => 'fr', + 'it_IT' => 'it', + 'hu_HU' => 'hu', + 'nl_NL' => 'nl', + 'pl_PL' => 'pl', + 'pt_BR' => 'pt-br', + 'ru_RU' => 'ru', + 'sr_Latn_RS' => 'sr', + 'fi_FI' => 'fi', + 'sv_SE' => 'sv', + 'tr_TR' => 'tr', + 'zh_CN' => 'zh-cn', + 'ja_JP' => 'ja', + 'th_TH' => 'th', ); + + $lang = $this->getCurrentLanguage(); + + return isset($languages[$lang]) ? $languages[$lang] : 'en'; + } + + /** + * Get current language + * + * @access public + * @return string + */ + public function getCurrentLanguage() + { + if ($this->userSession->isLogged() && ! empty($this->session['user']['language'])) { + return $this->session['user']['language']; + } + + return $this->get('application_language', 'en_US'); } /** @@ -76,12 +165,13 @@ class Config extends Base return $value ?: $default_value; } - if (! isset($_SESSION['config'][$name])) { - $_SESSION['config'] = $this->getAll(); + // Cache config in session + if (! isset($this->session['config'][$name])) { + $this->session['config'] = $this->getAll(); } - if (! empty($_SESSION['config'][$name])) { - return $_SESSION['config'][$name]; + if (! empty($this->session['config'][$name])) { + return $this->session['config'][$name]; } return $default_value; @@ -95,7 +185,7 @@ class Config extends Base */ public function getAll() { - return $this->db->table(self::TABLE)->listing('option', 'value'); + return $this->db->hashtable(self::TABLE)->getAll('option', 'value'); } /** @@ -126,7 +216,7 @@ class Config extends Base */ public function reload() { - $_SESSION['config'] = $this->getAll(); + $this->session['config'] = $this->getAll(); $this->setupTranslations(); } @@ -137,11 +227,22 @@ class Config extends Base */ public function setupTranslations() { - $language = $this->get('application_language', 'en_US'); + Translator::load($this->getCurrentLanguage()); + } - if ($language !== 'en_US') { - Translator::load($language); + /** + * Get current timezone + * + * @access public + * @return string + */ + public function getCurrentTimezone() + { + if ($this->userSession->isLogged() && ! empty($this->session['user']['timezone'])) { + return $this->session['user']['timezone']; } + + return $this->get('application_timezone', 'UTC'); } /** @@ -151,7 +252,7 @@ class Config extends Base */ public function setupTimezone() { - date_default_timezone_set($this->get('application_timezone', 'UTC')); + date_default_timezone_set($this->getCurrentTimezone()); } /** diff --git a/app/Model/Currency.php b/app/Model/Currency.php new file mode 100644 index 00000000..6ae842e7 --- /dev/null +++ b/app/Model/Currency.php @@ -0,0 +1,106 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Currency + * + * @package model + * @author Frederic Guillot + */ +class Currency extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'currencies'; + + /** + * Get all currency rates + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db->table(self::TABLE)->findAll(); + } + + /** + * Calculate the price for the reference currency + * + * @access public + * @param string $currency + * @param double $price + * @return double + */ + public function getPrice($currency, $price) + { + static $rates = null; + $reference = $this->config->get('application_currency', 'USD'); + + if ($reference !== $currency) { + $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array(); + $rate = isset($rates[$currency]) ? $rates[$currency] : 1; + + return $rate * $price; + } + + return $price; + } + + /** + * Add a new currency rate + * + * @access public + * @param string $currency + * @param float $rate + * @return boolean|integer + */ + public function create($currency, $rate) + { + if ($this->db->table(self::TABLE)->eq('currency', $currency)->count() === 1) { + return $this->update($currency, $rate); + } + + return $this->persist(self::TABLE, compact('currency', 'rate')); + } + + /** + * Update a currency rate + * + * @access public + * @param string $currency + * @param float $rate + * @return boolean + */ + public function update($currency, $rate) + { + return $this->db->table(self::TABLE)->eq('currency', $currency)->update(array('rate' => $rate)); + } + + /** + * Validate + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validate(array $values) + { + $v = new Validator($values, array( + new Validators\Required('currency', t('Field required')), + new Validators\Required('rate', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/DateParser.php b/app/Model/DateParser.php index 38265f98..be79a92e 100644 --- a/app/Model/DateParser.php +++ b/app/Model/DateParser.php @@ -13,6 +13,47 @@ use DateTime; class DateParser extends Base { /** + * Return true if the date is within the date range + * + * @access public + * @param DateTime $date + * @param DateTime $start + * @param DateTime $end + * @return boolean + */ + public function withinDateRange(DateTime $date, DateTime $start, DateTime $end) + { + return $date >= $start && $date <= $end; + } + + /** + * Get the total number of hours between 2 datetime objects + * Minutes are rounded to the nearest quarter + * + * @access public + * @param DateTime $d1 + * @param DateTime $d2 + * @return float + */ + public function getHours(DateTime $d1, DateTime $d2) + { + $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp())); + return round($seconds / 3600, 2); + } + + /** + * Round the timestamp to the nearest quarter + * + * @access public + * @param integer $seconds Timestamp + * @return integer + */ + public function getRoundedSeconds($seconds) + { + return (int) round($seconds / (15 * 60)) * (15 * 60); + } + + /** * Return a timestamp if the given date format is correct otherwise return 0 * * @access public @@ -60,7 +101,7 @@ class DateParser extends Base * Return the list of supported date formats (for the parser) * * @access public - * @return array + * @return string[] */ public function getDateFormats() { @@ -87,23 +128,35 @@ class DateParser extends Base } /** - * For a given timestamp, reset the date to midnight + * Remove the time from a timestamp * * @access public * @param integer $timestamp Timestamp * @return integer */ - public function resetDateToMidnight($timestamp) + public function removeTimeFromTimestamp($timestamp) { return mktime(0, 0, 0, date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp)); } /** + * Get a timetstamp from an ISO date format + * + * @access public + * @param string $date Date format + * @return integer + */ + public function getTimestampFromIsoFormat($date) + { + return $this->removeTimeFromTimestamp(ctype_digit($date) ? $date : strtotime($date)); + } + + /** * Format date (form display) * * @access public * @param array $values Database values - * @param array $fields Date fields + * @param string[] $fields Date fields * @param string $format Date format */ public function format(array &$values, array $fields, $format = '') @@ -128,14 +181,14 @@ class DateParser extends Base * * @access public * @param array $values Database values - * @param array $fields Date fields + * @param string[] $fields Date fields */ public function convert(array &$values, array $fields) { foreach ($fields as $field) { if (! empty($values[$field]) && ! is_numeric($values[$field])) { - $values[$field] = $this->getTimestamp($values[$field]); + $values[$field] = $this->removeTimeFromTimestamp($this->getTimestamp($values[$field])); } } } diff --git a/app/Model/File.php b/app/Model/File.php index d5a0c7cd..1f62a55e 100644 --- a/app/Model/File.php +++ b/app/Model/File.php @@ -2,6 +2,8 @@ namespace Model; +use Event\FileEvent; + /** * File model * @@ -15,14 +17,7 @@ class File extends Base * * @var string */ - const TABLE = 'task_has_files'; - - /** - * Directory where are stored files - * - * @var string - */ - const BASE_PATH = 'data/files/'; + const TABLE = 'files'; /** * Events @@ -54,7 +49,7 @@ class File extends Base { $file = $this->getbyId($file_id); - if (! empty($file) && @unlink(self::BASE_PATH.$file['path'])) { + if (! empty($file) && @unlink(FILES_DIR.$file['path'])) { return $this->db->table(self::TABLE)->eq('id', $file_id)->remove(); } @@ -85,17 +80,24 @@ class File extends Base * @param string $name Filename * @param string $path Path on the disk * @param bool $is_image Image or not + * @param integer $size File size * @return bool */ - public function create($task_id, $name, $path, $is_image) + public function create($task_id, $name, $path, $is_image, $size) { - $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id, 'name' => $name)); + $this->container['dispatcher']->dispatch( + self::EVENT_CREATE, + new FileEvent(array('task_id' => $task_id, 'name' => $name)) + ); return $this->db->table(self::TABLE)->save(array( 'task_id' => $task_id, - 'name' => $name, + 'name' => substr($name, 0, 255), 'path' => $path, 'is_image' => $is_image ? '1' : '0', + 'size' => $size, + 'user_id' => $this->userSession->getId() ?: 0, + 'date' => time(), )); } @@ -108,9 +110,83 @@ class File extends Base */ public function getAll($task_id) { - return $this->db->table(self::TABLE) + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.name', + self::TABLE.'.path', + self::TABLE.'.is_image', + self::TABLE.'.task_id', + self::TABLE.'.date', + self::TABLE.'.user_id', + self::TABLE.'.size', + User::TABLE.'.username', + User::TABLE.'.name as user_name' + ) + ->join(User::TABLE, 'id', 'user_id') + ->eq('task_id', $task_id) + ->asc(self::TABLE.'.name') + ->findAll(); + } + + /** + * Get all images for a given task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAllImages($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.name', + self::TABLE.'.path', + self::TABLE.'.is_image', + self::TABLE.'.task_id', + self::TABLE.'.date', + self::TABLE.'.user_id', + self::TABLE.'.size', + User::TABLE.'.username', + User::TABLE.'.name as user_name' + ) + ->join(User::TABLE, 'id', 'user_id') + ->eq('task_id', $task_id) + ->eq('is_image', 1) + ->asc(self::TABLE.'.name') + ->findAll(); + } + + /** + * Get all files without images for a given task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAllDocuments($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.name', + self::TABLE.'.path', + self::TABLE.'.is_image', + self::TABLE.'.task_id', + self::TABLE.'.date', + self::TABLE.'.user_id', + self::TABLE.'.size', + User::TABLE.'.username', + User::TABLE.'.name as user_name' + ) + ->join(User::TABLE, 'id', 'user_id') ->eq('task_id', $task_id) - ->asc('name') + ->eq('is_image', 0) + ->asc(self::TABLE.'.name') ->findAll(); } @@ -123,7 +199,17 @@ class File extends Base */ public function isImage($filename) { - return getimagesize($filename) !== false; + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + switch ($extension) { + case 'jpeg': + case 'jpg': + case 'png': + case 'gif': + return true; + } + + return false; } /** @@ -147,14 +233,14 @@ class File extends Base */ public function setup() { - if (! is_dir(self::BASE_PATH)) { - if (! mkdir(self::BASE_PATH, 0755, true)) { - die('Unable to create the upload directory: "'.self::BASE_PATH.'"'); + if (! is_dir(FILES_DIR)) { + if (! mkdir(FILES_DIR, 0755, true)) { + die('Unable to create the upload directory: "'.FILES_DIR.'"'); } } - if (! is_writable(self::BASE_PATH)) { - die('The directory "'.self::BASE_PATH.'" must be writeable by your webserver user'); + if (! is_writable(FILES_DIR)) { + die('The directory "'.FILES_DIR.'" must be writeable by your webserver user'); } } @@ -162,9 +248,9 @@ class File extends Base * Handle file upload * * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param string $form_name File form name + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $form_name File form name * @return bool */ public function upload($project_id, $task_id, $form_name) @@ -182,15 +268,16 @@ class File extends Base $uploaded_filename = $_FILES[$form_name]['tmp_name'][$key]; $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); - @mkdir(self::BASE_PATH.dirname($destination_filename), 0755, true); + @mkdir(FILES_DIR.dirname($destination_filename), 0755, true); - if (@move_uploaded_file($uploaded_filename, self::BASE_PATH.$destination_filename)) { + if (@move_uploaded_file($uploaded_filename, FILES_DIR.$destination_filename)) { $result[] = $this->create( $task_id, $original_filename, $destination_filename, - $this->isImage(self::BASE_PATH.$destination_filename) + $this->isImage($original_filename), + $_FILES[$form_name]['size'][$key] ); } } @@ -199,4 +286,144 @@ class File extends Base return count(array_unique($result)) === 1; } + + /** + * Handle screenshot upload + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $blob Base64 encoded image + * @return bool + */ + public function uploadScreenshot($project_id, $task_id, $blob) + { + $data = base64_decode($blob); + + if (empty($data)) { + return false; + } + + $original_filename = e('Screenshot taken %s', dt('%B %e, %Y at %k:%M %p', time())).'.png'; + $destination_filename = $this->generatePath($project_id, $task_id, $original_filename); + + @mkdir(FILES_DIR.dirname($destination_filename), 0755, true); + @file_put_contents(FILES_DIR.$destination_filename, $data); + + return $this->create( + $task_id, + $original_filename, + $destination_filename, + true, + strlen($data) + ); + } + + /** + * Handle file upload (base64 encoded content) + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $filename Filename + * @param bool $is_image Is image file? + * @param string $blob Base64 encoded image + * @return bool + */ + public function uploadContent($project_id, $task_id, $filename, $is_image, &$blob) + { + $data = base64_decode($blob); + + if (empty($data)) { + return false; + } + + $destination_filename = $this->generatePath($project_id, $task_id, $filename); + + @mkdir(FILES_DIR.dirname($destination_filename), 0755, true); + @file_put_contents(FILES_DIR.$destination_filename, $data); + + return $this->create( + $task_id, + $filename, + $destination_filename, + $is_image, + strlen($data) + ); + } + + /** + * Generate a jpeg thumbnail from an image (output directly the image) + * + * @access public + * @param string $filename Source image + * @param integer $resize_width Desired image width + * @param integer $resize_height Desired image height + */ + public function generateThumbnail($filename, $resize_width, $resize_height) + { + $metadata = getimagesize($filename); + $src_width = $metadata[0]; + $src_height = $metadata[1]; + $dst_y = 0; + $dst_x = 0; + + if (empty($metadata['mime'])) { + return; + } + + if ($resize_width == 0 && $resize_height == 0) { + $resize_width = 100; + $resize_height = 100; + } + + if ($resize_width > 0 && $resize_height == 0) { + $dst_width = $resize_width; + $dst_height = floor($src_height * ($resize_width / $src_width)); + $dst_image = imagecreatetruecolor($dst_width, $dst_height); + } + elseif ($resize_width == 0 && $resize_height > 0) { + $dst_width = floor($src_width * ($resize_height / $src_height)); + $dst_height = $resize_height; + $dst_image = imagecreatetruecolor($dst_width, $dst_height); + } + else { + + $src_ratio = $src_width / $src_height; + $resize_ratio = $resize_width / $resize_height; + + if ($src_ratio <= $resize_ratio) { + $dst_width = $resize_width; + $dst_height = floor($src_height * ($resize_width / $src_width)); + + $dst_y = ($dst_height - $resize_height) / 2 * (-1); + } + else { + $dst_width = floor($src_width * ($resize_height / $src_height)); + $dst_height = $resize_height; + + $dst_x = ($dst_width - $resize_width) / 2 * (-1); + } + + $dst_image = imagecreatetruecolor($resize_width, $resize_height); + } + + switch ($metadata['mime']) { + case 'image/jpeg': + case 'image/jpg': + $src_image = imagecreatefromjpeg($filename); + break; + case 'image/png': + $src_image = imagecreatefrompng($filename); + break; + case 'image/gif': + $src_image = imagecreatefromgif($filename); + break; + default: + return; + } + + imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, 0, 0, $dst_width, $dst_height, $src_width, $src_height); + imagejpeg($dst_image); + } } diff --git a/app/Model/HourlyRate.php b/app/Model/HourlyRate.php new file mode 100644 index 00000000..1550bdae --- /dev/null +++ b/app/Model/HourlyRate.php @@ -0,0 +1,121 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Hourly Rate + * + * @package model + * @author Frederic Guillot + */ +class HourlyRate extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'hourly_rates'; + + /** + * Get all user rates for a given project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAllByProject($project_id) + { + $members = $this->projectPermission->getMembers($project_id); + + if (empty($members)) { + return array(); + } + + return $this->db->table(self::TABLE)->in('user_id', array_keys($members))->desc('date_effective')->findAll(); + } + + /** + * Get all rates for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getAllByUser($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_effective')->findAll(); + } + + /** + * Get current rate for a given user + * + * @access public + * @param integer $user_id User id + * @return float + */ + public function getCurrentRate($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_effective')->findOneColumn('rate') ?: 0; + } + + /** + * Add a new rate in the database + * + * @access public + * @param integer $user_id User id + * @param float $rate Hourly rate + * @param string $currency Currency code + * @param string $date ISO8601 date format + * @return boolean|integer + */ + public function create($user_id, $rate, $currency, $date) + { + $values = array( + 'user_id' => $user_id, + 'rate' => $rate, + 'currency' => $currency, + 'date_effective' => $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($date)), + ); + + return $this->persist(self::TABLE, $values); + } + + /** + * Remove a specific rate + * + * @access public + * @param integer $rate_id + * @return boolean + */ + public function remove($rate_id) + { + return $this->db->table(self::TABLE)->eq('id', $rate_id)->remove(); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('user_id', t('Field required')), + new Validators\Required('rate', t('Field required')), + new Validators\Numeric('rate', t('This value must be numeric')), + new Validators\Required('date_effective', t('Field required')), + new Validators\Required('currency', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php index 3391db50..dd64284e 100644 --- a/app/Model/LastLogin.php +++ b/app/Model/LastLogin.php @@ -32,7 +32,7 @@ class LastLogin extends Base * @param integer $user_id User id * @param string $ip IP Address * @param string $user_agent User Agent - * @return array + * @return boolean */ public function create($auth_type, $user_id, $ip, $user_agent) { diff --git a/app/Model/Link.php b/app/Model/Link.php new file mode 100644 index 00000000..e0e5184e --- /dev/null +++ b/app/Model/Link.php @@ -0,0 +1,223 @@ +<?php + +namespace Model; + +use PDO; +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Link model + * + * @package model + * @author Olivier Maridat + * @author Frederic Guillot + */ +class Link extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'links'; + + /** + * Get a link by id + * + * @access public + * @param integer $link_id Link id + * @return array + */ + public function getById($link_id) + { + return $this->db->table(self::TABLE)->eq('id', $link_id)->findOne(); + } + + /** + * Get a link by name + * + * @access public + * @param string $label + * @return array + */ + public function getByLabel($label) + { + return $this->db->table(self::TABLE)->eq('label', $label)->findOne(); + } + + /** + * Get the opposite link id + * + * @access public + * @param integer $link_id Link id + * @return integer + */ + public function getOppositeLinkId($link_id) + { + return $this->db->table(self::TABLE)->eq('id', $link_id)->findOneColumn('opposite_id') ?: $link_id; + } + + /** + * Get all links + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db->table(self::TABLE)->findAll(); + } + + /** + * Get merged links + * + * @access public + * @return array + */ + public function getMergedList() + { + return $this->db + ->execute(' + SELECT + links.id, links.label, opposite.label as opposite_label + FROM links + LEFT JOIN links AS opposite ON opposite.id=links.opposite_id + ') + ->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Get label list + * + * @access public + * @param integer $exclude_id Exclude this link + * @param booelan $prepend Prepend default value + * @return array + */ + public function getList($exclude_id = 0, $prepend = true) + { + $labels = $this->db->hashtable(self::TABLE)->neq('id', $exclude_id)->asc('id')->getAll('id', 'label'); + + foreach ($labels as &$value) { + $value = t($value); + } + + return $prepend ? array('') + $labels : $labels; + } + + /** + * Create a new link label + * + * @access public + * @param string $label + * @param string $opposite_label + * @return boolean|integer + */ + public function create($label, $opposite_label = '') + { + $this->db->startTransaction(); + + if (! $this->db->table(self::TABLE)->insert(array('label' => $label))) { + $this->db->cancelTransaction(); + return false; + } + + $label_id = $this->db->getConnection()->getLastId(); + + if (! empty($opposite_label)) { + + $this->db + ->table(self::TABLE) + ->insert(array( + 'label' => $opposite_label, + 'opposite_id' => $label_id, + )); + + $this->db + ->table(self::TABLE) + ->eq('id', $label_id) + ->update(array( + 'opposite_id' => $this->db->getConnection()->getLastId() + )); + } + + $this->db->closeTransaction(); + + return (int) $label_id; + } + + /** + * Update a link + * + * @access public + * @param array $values + * @return boolean + */ + public function update(array $values) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'label' => $values['label'], + 'opposite_id' => $values['opposite_id'], + )); + } + + /** + * Remove a link a the relation to its opposite + * + * @access public + * @param integer $link_id + * @return boolean + */ + public function remove($link_id) + { + $this->db->table(self::TABLE)->eq('opposite_id', $link_id)->update(array('opposite_id' => 0)); + return $this->db->table(self::TABLE)->eq('id', $link_id)->remove(); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('label', t('Field required')), + new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), + new Validators\NotEquals('label', 'opposite_label', t('The labels must be different')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $v = new Validator($values, array( + new Validators\Required('id', t('Field required')), + new Validators\Required('opposite_id', t('Field required')), + new Validators\Required('label', t('Field required')), + new Validators\Unique('label', t('This label must be unique'), $this->db->getConnection(), self::TABLE), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/Notification.php b/app/Model/Notification.php index 0a80e335..7255a414 100644 --- a/app/Model/Notification.php +++ b/app/Model/Notification.php @@ -4,11 +4,6 @@ namespace Model; use Core\Session; use Core\Translator; -use Core\Template; -use Event\NotificationListener; -use Swift_Message; -use Swift_Mailer; -use Swift_TransportException; /** * Notification model @@ -26,189 +21,326 @@ class Notification extends Base const TABLE = 'user_has_notifications'; /** - * Get a list of people with notifications enabled + * User filters + * + * @var integer + */ + const FILTER_NONE = 1; + const FILTER_ASSIGNEE = 2; + const FILTER_CREATOR = 3; + const FILTER_BOTH = 4; + + /** + * Send overdue tasks * * @access public - * @param integer $project_id Project id - * @param array $exlude_users List of user_id to exclude - * @return array */ - public function getUsersWithNotification($project_id, array $exclude_users = array()) + public function sendOverdueTaskNotifications() { - return $this->db - ->table(ProjectPermission::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email') - ->join(User::TABLE, 'id', 'user_id') - ->eq('project_id', $project_id) - ->eq('notifications_enabled', '1') - ->neq('email', '') - ->notin(User::TABLE.'.id', $exclude_users) - ->findAll(); + $tasks = $this->taskFinder->getOverdueTasks(); + $projects = array(); + + foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { + + // Get the list of users that should receive notifications for each projects + $users = $this->notification->getUsersWithNotificationEnabled($project_id); + + foreach ($users as $user) { + $this->sendUserOverdueTaskNotifications($user, $project_tasks); + } + } + + return $tasks; } /** - * Get the list of users to send the notification for a given project + * Send overdue tasks for a given user * * @access public - * @param integer $project_id Project id - * @param array $exlude_users List of user_id to exclude - * @return array + * @param array $user + * @param array $tasks */ - public function getUsersList($project_id, array $exclude_users = array()) + public function sendUserOverdueTaskNotifications(array $user, array $tasks) { - // Exclude the connected user - if (Session::isOpen()) { - $exclude_users[] = $this->acl->getUserId(); + $user_tasks = array(); + + foreach ($tasks as $task) { + if ($this->notification->shouldReceiveNotification($user, array('task' => $task))) { + $user_tasks[] = $task; + } } - $users = $this->getUsersWithNotification($project_id, $exclude_users); + if (! empty($user_tasks)) { + $this->sendEmailNotification( + $user, + Task::EVENT_OVERDUE, + array('tasks' => $user_tasks, 'project_name' => $tasks[0]['project_name']) + ); + } + } - foreach ($users as $index => $user) { + /** + * Send notifications to people + * + * @access public + * @param string $event_name + * @param array $event_data + */ + public function sendNotifications($event_name, array $event_data) + { + $logged_user_id = $this->userSession->isLogged() ? $this->userSession->getId() : 0; + $users = $this->notification->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id); - $projects = $this->db->table(self::TABLE) - ->eq('user_id', $user['id']) - ->findAllByColumn('project_id'); + foreach ($users as $user) { + if ($this->shouldReceiveNotification($user, $event_data)) { + $this->sendEmailNotification($user, $event_name, $event_data); + } + } - // The user have selected only some projects - if (! empty($projects)) { + // Restore locales + $this->config->setupTranslations(); + } - // If the user didn't select this project we remove that guy from the list - if (! in_array($project_id, $projects)) { - unset($users[$index]); - } - } + /** + * Send email notification to someone + * + * @access public + * @param array $user User + * @param string $event_name + * @param array $event_data + */ + public function sendEmailNotification(array $user, $event_name, array $event_data) + { + // Use the user language otherwise use the application language (do not use the session language) + if (! empty($user['language'])) { + Translator::load($user['language']); + } + else { + Translator::load($this->config->get('application_language', 'en_US')); } - return $users; + $this->emailClient->send( + $user['email'], + $user['name'] ?: $user['username'], + $this->getMailSubject($event_name, $event_data), + $this->getMailContent($event_name, $event_data) + ); } /** - * Attach events + * Return true if the user should receive notification * * @access public + * @param array $user + * @param array $event_data + * @return boolean */ - public function attachEvents() + public function shouldReceiveNotification(array $user, array $event_data) { - $events = array( - Task::EVENT_CREATE => 'notification_task_creation', - Task::EVENT_UPDATE => 'notification_task_update', - Task::EVENT_CLOSE => 'notification_task_close', - Task::EVENT_OPEN => 'notification_task_open', - Task::EVENT_MOVE_COLUMN => 'notification_task_move_column', - Task::EVENT_MOVE_POSITION => 'notification_task_move_position', - Task::EVENT_ASSIGNEE_CHANGE => 'notification_task_assignee_change', - SubTask::EVENT_CREATE => 'notification_subtask_creation', - SubTask::EVENT_UPDATE => 'notification_subtask_update', - Comment::EVENT_CREATE => 'notification_comment_creation', - Comment::EVENT_UPDATE => 'notification_comment_update', - File::EVENT_CREATE => 'notification_file_creation', + $filters = array( + 'filterNone', + 'filterAssignee', + 'filterCreator', + 'filterBoth', ); - foreach ($events as $event_name => $template_name) { + foreach ($filters as $filter) { + if ($this->$filter($user, $event_data)) { + return $this->filterProject($user, $event_data); + } + } - $listener = new NotificationListener($this->registry); - $listener->setTemplate($template_name); + return false; + } - $this->event->attach($event_name, $listener); - } + /** + * Return true if the user will receive all notifications + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterNone(array $user, array $event_data) + { + return $user['notifications_filter'] == self::FILTER_NONE; } /** - * Send the email notifications + * Return true if the user is the assignee and selected the filter "assignee" * * @access public - * @param string $template Template name - * @param array $users List of users - * @param array $data Template data + * @param array $user + * @param array $event_data + * @return boolean */ - public function sendEmails($template, array $users, array $data) + public function filterAssignee(array $user, array $event_data) { - try { - $transport = $this->registry->shared('mailer'); - $mailer = Swift_Mailer::newInstance($transport); + return $user['notifications_filter'] == self::FILTER_ASSIGNEE && $event_data['task']['owner_id'] == $user['id']; + } - $message = Swift_Message::newInstance() - ->setSubject($this->getMailSubject($template, $data)) - ->setFrom(array(MAIL_FROM => 'Kanboard')) - ->setBody($this->getMailContent($template, $data), 'text/html'); + /** + * Return true if the user is the creator and enabled the filter "creator" + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterCreator(array $user, array $event_data) + { + return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id']; + } - foreach ($users as $user) { - $message->setTo(array($user['email'] => $user['name'] ?: $user['username'])); - $mailer->send($message); - } + /** + * Return true if the user is the assignee or the creator and selected the filter "both" + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterBoth(array $user, array $event_data) + { + return $user['notifications_filter'] == self::FILTER_BOTH && + ($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id']); + } + + /** + * Return true if the user want to receive notification for the selected project + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterProject(array $user, array $event_data) + { + $projects = $this->db->table(self::TABLE)->eq('user_id', $user['id'])->findAllByColumn('project_id'); + + if (! empty($projects)) { + return in_array($event_data['task']['project_id'], $projects); } - catch (Swift_TransportException $e) { - debug($e->getMessage()); + + return true; + } + + /** + * Get a list of people with notifications enabled + * + * @access public + * @param integer $project_id Project id + * @param array $exclude_user_id User id to exclude + * @return array + */ + public function getUsersWithNotificationEnabled($project_id, $exclude_user_id = 0) + { + if ($this->projectPermission->isEverybodyAllowed($project_id)) { + + return $this->db + ->table(User::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') + ->eq('notifications_enabled', '1') + ->neq('email', '') + ->neq(User::TABLE.'.id', $exclude_user_id) + ->findAll(); } + + return $this->db + ->table(ProjectPermission::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') + ->join(User::TABLE, 'id', 'user_id') + ->eq('project_id', $project_id) + ->eq('notifications_enabled', '1') + ->neq('email', '') + ->neq(User::TABLE.'.id', $exclude_user_id) + ->findAll(); + } + + /** + * Get the mail content for a given template name + * + * @access public + * @param string $event_name Event name + * @param array $event_data Event data + * @return string + */ + public function getMailContent($event_name, array $event_data) + { + return $this->template->render( + 'notification/'.str_replace('.', '_', $event_name), + $event_data + array('application_url' => $this->config->get('application_url')) + ); } /** * Get the mail subject for a given template name * * @access public - * @param string $template Template name - * @param array $data Template data + * @param string $event_name Event name + * @param array $event_data Event data + * @return string */ public function getMailSubject($template, array $data) { switch ($template) { - case 'notification_file_creation': - $subject = e('[%s][New attachment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New attachment'), $data); break; - case 'notification_comment_creation': - $subject = e('[%s][New comment] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Comment::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New comment'), $data); break; - case 'notification_comment_update': - $subject = e('[%s][Comment updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Comment::EVENT_UPDATE: + $subject = $this->getStandardMailSubject(e('Comment updated'), $data); break; - case 'notification_subtask_creation': - $subject = e('[%s][New subtask] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Subtask::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New subtask'), $data); break; - case 'notification_subtask_update': - $subject = e('[%s][Subtask updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Subtask::EVENT_UPDATE: + $subject = $this->getStandardMailSubject(e('Subtask updated'), $data); break; - case 'notification_task_creation': - $subject = e('[%s][New task] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New task'), $data); break; - case 'notification_task_update': - $subject = e('[%s][Task updated] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_UPDATE: + $subject = $this->getStandardMailSubject(e('Task updated'), $data); break; - case 'notification_task_close': - $subject = e('[%s][Task closed] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_CLOSE: + $subject = $this->getStandardMailSubject(e('Task closed'), $data); break; - case 'notification_task_open': - $subject = e('[%s][Task opened] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_OPEN: + $subject = $this->getStandardMailSubject(e('Task opened'), $data); break; - case 'notification_task_move_column': - $subject = e('[%s][Column Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_MOVE_COLUMN: + $subject = $this->getStandardMailSubject(e('Column Change'), $data); break; - case 'notification_task_move_position': - $subject = e('[%s][Position Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_MOVE_POSITION: + $subject = $this->getStandardMailSubject(e('Position Change'), $data); break; - case 'notification_task_assignee_change': - $subject = e('[%s][Assignee Change] %s (#%d)', $data['task']['project_name'], $data['task']['title'], $data['task']['id']); + case Task::EVENT_ASSIGNEE_CHANGE: + $subject = $this->getStandardMailSubject(e('Assignee Change'), $data); break; - case 'notification_task_due': - $subject = e('[%s][Due tasks]', $data['project']); + case Task::EVENT_OVERDUE: + $subject = e('[%s] Overdue tasks', $data['project_name']); break; default: - $subject = e('[Kanboard] Notification'); + $subject = e('Notification'); } return $subject; } /** - * Get the mail content for a given template name + * Get the mail subject for a given label * - * @access public - * @param string $template Template name + * @access private + * @param string $label Label * @param array $data Template data + * @return string */ - public function getMailContent($template, array $data) + private function getStandardMailSubject($label, array $data) { - $tpl = new Template; - return $tpl->load($template, $data + array('application_url' => $this->config->get('application_url'))); + return sprintf('[%s][%s] %s (#%d)', $data['task']['project_name'], $label, $data['task']['title'], $data['task']['id']); } /** @@ -227,7 +359,8 @@ class Notification extends Base // Activate notifications $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( - 'notifications_enabled' => '1' + 'notifications_enabled' => '1', + 'notifications_filter' => empty($values['notifications_filter']) ? self::FILTER_BOTH : $values['notifications_filter'], )); // Save selected projects @@ -259,9 +392,7 @@ class Notification extends Base */ public function readSettings($user_id) { - $values = array(); - $values['notifications_enabled'] = $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_enabled'); - + $values = $this->db->table(User::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne(); $projects = $this->db->table(self::TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id'); foreach ($projects as $project_id) { diff --git a/app/Model/Project.php b/app/Model/Project.php index 32b7fcbe..71c660b9 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -4,7 +4,6 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; -use Event\ProjectModificationDateListener; use Core\Security; /** @@ -52,12 +51,28 @@ class Project extends Base * Get a project by the name * * @access public - * @param string $project_name Project name + * @param string $name Project name * @return array */ - public function getByName($project_name) + public function getByName($name) { - return $this->db->table(self::TABLE)->eq('name', $project_name)->findOne(); + return $this->db->table(self::TABLE)->eq('name', $name)->findOne(); + } + + /** + * Get a project by the identifier (code) + * + * @access public + * @param string $identifier + * @return array|boolean + */ + public function getByIdentifier($identifier) + { + if (empty($identifier)) { + return false; + } + + return $this->db->table(self::TABLE)->eq('identifier', strtoupper($identifier))->findOne(); } /** @@ -65,10 +80,14 @@ class Project extends Base * * @access public * @param string $token Token - * @return array + * @return array|boolean */ public function getByToken($token) { + if (empty($token)) { + return false; + } + return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_public', 1)->findOne(); } @@ -96,27 +115,25 @@ class Project extends Base } /** - * Get all projects, optionaly fetch stats for each project and can check users permissions + * Get all projects * * @access public - * @param bool $filter_permissions If true, remove projects not allowed for the current user * @return array */ - public function getAll($filter_permissions = false) + public function getAll() { - $projects = $this->db->table(self::TABLE)->asc('name')->findAll(); - - if ($filter_permissions) { - - foreach ($projects as $key => $project) { - - if (! $this->projectPermission->isUserAllowed($project['id'], $this->acl->getUserId())) { - unset($projects[$key]); - } - } - } + return $this->db->table(self::TABLE)->asc('name')->findAll(); + } - return $projects; + /** + * Get all project ids + * + * @access public + * @return array + */ + public function getAllIds() + { + return $this->db->table(self::TABLE)->asc('name')->findAllByColumn('id'); } /** @@ -129,10 +146,10 @@ class Project extends Base public function getList($prepend = true) { if ($prepend) { - return array(t('None')) + $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + return array(t('None')) + $this->db->hashtable(self::TABLE)->asc('name')->getAll('id', 'name'); } - return $this->db->table(self::TABLE)->asc('name')->listing('id', 'name'); + return $this->db->hashtable(self::TABLE)->asc('name')->getAll('id', 'name'); } /** @@ -161,10 +178,10 @@ class Project extends Base public function getListByStatus($status) { return $this->db - ->table(self::TABLE) + ->hashtable(self::TABLE) ->asc('name') ->eq('is_active', $status) - ->listing('id', 'name'); + ->getAll('id', 'name'); } /** @@ -189,14 +206,15 @@ class Project extends Base * @param integer $project_id Project id * @return array */ - public function getStats($project_id) + public function getTaskStats($project_id) { $stats = array(); - $columns = $this->board->getcolumns($project_id); $stats['nb_active_tasks'] = 0; + $columns = $this->board->getColumns($project_id); + $column_stats = $this->board->getColumnStats($project_id); foreach ($columns as &$column) { - $column['nb_active_tasks'] = $this->taskFinder->countByColumnId($project_id, $column['id']); + $column['nb_active_tasks'] = isset($column_stats[$column['id']]) ? $column_stats[$column['id']] : 0; $stats['nb_active_tasks'] += $column['nb_active_tasks']; } @@ -208,73 +226,69 @@ class Project extends Base } /** - * Create a project from another one. + * Get stats for each column of a project * - * @author Antonio Rabelo - * @param integer $project_id Project Id - * @return integer Cloned Project Id + * @access public + * @param array $project + * @return array */ - public function createProjectFromAnotherProject($project_id) + public function getColumnStats(array &$project) { - $project = $this->getById($project_id); - - $values = array( - 'name' => $project['name'].' ('.t('Clone').')', - 'is_active' => true, - 'last_modified' => 0, - 'token' => '', - 'is_public' => 0, - 'is_private' => empty($project['is_private']) ? 0 : 1, - ); + $project['columns'] = $this->board->getColumns($project['id']); + $stats = $this->board->getColumnStats($project['id']); - if (! $this->db->table(self::TABLE)->save($values)) { - return false; + foreach ($project['columns'] as &$column) { + $column['nb_tasks'] = isset($stats[$column['id']]) ? $stats[$column['id']] : 0; } - return $this->db->getConnection()->getLastId(); + return $project; } /** - * Clone a project + * Apply column stats to a collection of projects (filter callback) * - * @author Antonio Rabelo - * @param integer $project_id Project Id - * @return integer Cloned Project Id + * @access public + * @param array $projects + * @return array */ - public function duplicate($project_id) + public function applyColumnStats(array $projects) { - $this->db->startTransaction(); - - // Get the cloned project Id - $clone_project_id = $this->createProjectFromAnotherProject($project_id); - - if (! $clone_project_id) { - $this->db->cancelTransaction(); - return false; + foreach ($projects as &$project) { + $this->getColumnStats($project); } - foreach (array('board', 'category', 'projectPermission', 'action') as $model) { + return $projects; + } - if (! $this->$model->duplicate($project_id, $clone_project_id)) { - $this->db->cancelTransaction(); - return false; - } + /** + * Get project summary for a list of project + * + * @access public + * @param array $project_ids List of project id + * @return \PicoDb\Table + */ + public function getQueryColumnStats(array $project_ids) + { + if (empty($project_ids)) { + return $this->db->table(Project::TABLE)->limit(0); } - $this->db->closeTransaction(); - - return (int) $clone_project_id; + return $this->db + ->table(Project::TABLE) + ->in('id', $project_ids) + ->filter(array($this, 'applyColumnStats')); } /** * Create a project * * @access public - * @param array $values Form values - * @param integer $user_id User who create the project - * @return integer Project id + * @param array $values Form values + * @param integer $user_id User who create the project + * @param bool $add_user Automatically add the user + * @return integer Project id */ - public function create(array $values, $user_id = 0) + public function create(array $values, $user_id = 0, $add_user = false) { $this->db->startTransaction(); @@ -282,6 +296,10 @@ class Project extends Base $values['last_modified'] = time(); $values['is_private'] = empty($values['is_private']) ? 0 : 1; + if (! empty($values['identifier'])) { + $values['identifier'] = strtoupper($values['identifier']); + } + if (! $this->db->table(self::TABLE)->save($values)) { $this->db->cancelTransaction(); return false; @@ -294,10 +312,12 @@ class Project extends Base return false; } - if ($values['is_private'] && $user_id) { - $this->projectPermission->allowUser($project_id, $user_id); + if ($add_user && $user_id) { + $this->projectPermission->addManager($project_id, $user_id); } + $this->category->createDefaultCategories($project_id); + $this->db->closeTransaction(); return (int) $project_id; @@ -342,6 +362,10 @@ class Project extends Base */ public function update(array $values) { + if (! empty($values['identifier'])) { + $values['identifier'] = strtoupper($values['identifier']); + } + return $this->exists($values['id']) && $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); } @@ -447,7 +471,10 @@ class Project extends Base new Validators\Integer('is_active', t('This value must be an integer')), new Validators\Required('name', t('The project name is required')), new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50), + new Validators\MaxLength('identifier', t('The maximum length is %d characters', 50), 50), + new Validators\AlphaNumeric('identifier', t('This value must be alphanumeric')) , new Validators\Unique('name', t('This project must be unique'), $this->db->getConnection(), self::TABLE), + new Validators\Unique('identifier', t('The identifier must be unique'), $this->db->getConnection(), self::TABLE), ); } @@ -460,6 +487,10 @@ class Project extends Base */ public function validateCreation(array $values) { + if (! empty($values['identifier'])) { + $values['identifier'] = strtoupper($values['identifier']); + } + $v = new Validator($values, $this->commonValidationRules()); return array( @@ -477,6 +508,10 @@ class Project extends Base */ public function validateModification(array $values) { + if (! empty($values['identifier'])) { + $values['identifier'] = strtoupper($values['identifier']); + } + $rules = array( new Validators\Required('id', t('This value is required')), ); @@ -488,34 +523,4 @@ class Project extends Base $v->getErrors() ); } - - /** - * Attach events - * - * @access public - */ - public function attachEvents() - { - $events = array( - Task::EVENT_CREATE_UPDATE, - Task::EVENT_CLOSE, - Task::EVENT_OPEN, - Task::EVENT_MOVE_COLUMN, - Task::EVENT_MOVE_POSITION, - Task::EVENT_ASSIGNEE_CHANGE, - GithubWebhook::EVENT_ISSUE_OPENED, - GithubWebhook::EVENT_ISSUE_CLOSED, - GithubWebhook::EVENT_ISSUE_REOPENED, - GithubWebhook::EVENT_ISSUE_ASSIGNEE_CHANGE, - GithubWebhook::EVENT_ISSUE_LABEL_CHANGE, - GithubWebhook::EVENT_ISSUE_COMMENT, - GithubWebhook::EVENT_COMMIT, - ); - - $listener = new ProjectModificationDateListener($this->registry); - - foreach ($events as $event_name) { - $this->event->attach($event_name, $listener); - } - } } diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 6d6ef454..27f1cfcd 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -2,9 +2,6 @@ namespace Model; -use Core\Template; -use Event\ProjectActivityListener; - /** * Project activity model * @@ -25,7 +22,7 @@ class ProjectActivity extends Base * * @var integer */ - const MAX_EVENTS = 5000; + const MAX_EVENTS = 1000; /** * Add a new event for the project @@ -46,7 +43,7 @@ class ProjectActivity extends Base 'creator_id' => $creator_id, 'event_name' => $event_name, 'date_creation' => time(), - 'data' => serialize($data), + 'data' => json_encode($data), ); $this->cleanup(self::MAX_EVENTS - 1); @@ -59,42 +56,101 @@ class ProjectActivity extends Base * @access public * @param integer $project_id Project id * @param integer $limit Maximum events number + * @param integer $start Timestamp of earliest activity + * @param integer $end Timestamp of latest activity * @return array */ - public function getProject($project_id, $limit = 50) + public function getProject($project_id, $limit = 50, $start = null, $end = null) { - return $this->getProjects(array($project_id), $limit); + return $this->getProjects(array($project_id), $limit, $start, $end); } /** * Get all events for the given projects list * * @access public - * @param integer $project_id Project id + * @param integer[] $project_ids Projects id * @param integer $limit Maximum events number + * @param integer $start Timestamp of earliest activity + * @param integer $end Timestamp of latest activity * @return array */ - public function getProjects(array $projects, $limit = 50) + public function getProjects(array $project_ids, $limit = 50, $start = null, $end = null) { - if (empty($projects)) { + if (empty($project_ids)) { return array(); } - $events = $this->db->table(self::TABLE) - ->columns( - self::TABLE.'.*', - User::TABLE.'.username AS author_username', - User::TABLE.'.name AS author_name' - ) - ->in('project_id', $projects) - ->join(User::TABLE, 'id', 'creator_id') - ->desc('id') - ->limit($limit) - ->findAll(); + $query = $this + ->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.*', + User::TABLE.'.username AS author_username', + User::TABLE.'.name AS author_name', + User::TABLE.'.email' + ) + ->in('project_id', $project_ids) + ->join(User::TABLE, 'id', 'creator_id') + ->desc(self::TABLE.'.id') + ->limit($limit); + + return $this->getEvents($query, $start, $end); + } + + /** + * Get all events for the given task + * + * @access public + * @param integer $task_id Task id + * @param integer $limit Maximum events number + * @param integer $start Timestamp of earliest activity + * @param integer $end Timestamp of latest activity + * @return array + */ + public function getTask($task_id, $limit = 50, $start = null, $end = null) + { + $query = $this + ->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.*', + User::TABLE.'.username AS author_username', + User::TABLE.'.name AS author_name', + User::TABLE.'.email' + ) + ->eq('task_id', $task_id) + ->join(User::TABLE, 'id', 'creator_id') + ->desc(self::TABLE.'.id') + ->limit($limit); + + return $this->getEvents($query, $start, $end); + } + + /** + * Common function to return events + * + * @access public + * @param \PicoDb\Table $query PicoDb Query + * @param integer $start Timestamp of earliest activity + * @param integer $end Timestamp of latest activity + * @return array + */ + private function getEvents(\PicoDb\Table $query, $start, $end) + { + if (! is_null($start)){ + $query->gte('date_creation', $start); + } + + if (! is_null($end)){ + $query->lte('date_creation', $end); + } + + $events = $query->findAll(); foreach ($events as &$event) { - $event += unserialize($event['data']); + $event += $this->decode($event['data']); unset($event['data']); $event['author'] = $event['author_name'] ?: $event['author_username']; @@ -127,34 +183,6 @@ class ProjectActivity extends Base } /** - * Attach events to be able to record the history - * - * @access public - */ - public function attachEvents() - { - $events = array( - Task::EVENT_ASSIGNEE_CHANGE, - Task::EVENT_UPDATE, - Task::EVENT_CREATE, - Task::EVENT_CLOSE, - Task::EVENT_OPEN, - Task::EVENT_MOVE_COLUMN, - Task::EVENT_MOVE_POSITION, - Comment::EVENT_UPDATE, - Comment::EVENT_CREATE, - SubTask::EVENT_UPDATE, - SubTask::EVENT_CREATE, - ); - - $listener = new ProjectActivityListener($this->registry); - - foreach ($events as $event_name) { - $this->event->attach($event_name, $listener); - } - } - - /** * Get the event html content * * @access public @@ -163,8 +191,10 @@ class ProjectActivity extends Base */ public function getContent(array $params) { - $tpl = new Template; - return $tpl->load('event_'.str_replace('.', '_', $params['event_name']), $params); + return $this->template->render( + 'event/'.str_replace('.', '_', $params['event_name']), + $params + ); } /** @@ -178,7 +208,13 @@ class ProjectActivity extends Base { switch ($event['event_name']) { case Task::EVENT_ASSIGNEE_CHANGE: - return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $event['task']['assignee_name'] ?: $event['task']['assignee_username']); + $assignee = $event['task']['assignee_name'] ?: $event['task']['assignee_username']; + + if (! empty($assignee)) { + return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $assignee); + } + + return t('%s remove the assignee of the task %s', $event['author'], e('#%d', $event['task']['id'])); case Task::EVENT_UPDATE: return t('%s updated the task #%d', $event['author'], $event['task']['id']); case Task::EVENT_CREATE: @@ -191,9 +227,9 @@ class ProjectActivity extends Base return t('%s moved the task #%d to the column "%s"', $event['author'], $event['task']['id'], $event['task']['column_title']); case Task::EVENT_MOVE_POSITION: return t('%s moved the task #%d to the position %d in the column "%s"', $event['author'], $event['task']['id'], $event['task']['position'], $event['task']['column_title']); - case SubTask::EVENT_UPDATE: + case Subtask::EVENT_UPDATE: return t('%s updated a subtask for the task #%d', $event['author'], $event['task']['id']); - case SubTask::EVENT_CREATE: + case Subtask::EVENT_CREATE: return t('%s created a subtask for the task #%d', $event['author'], $event['task']['id']); case Comment::EVENT_UPDATE: return t('%s updated a comment on the task #%d', $event['author'], $event['task']['id']); @@ -203,4 +239,20 @@ class ProjectActivity extends Base return ''; } } + + /** + * Decode event data, supports unserialize() and json_decode() + * + * @access public + * @param string $data Serialized data + * @return array + */ + public function decode($data) + { + if ($data{0} === 'a') { + return unserialize($data); + } + + return json_decode($data, true) ?: array(); + } } diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php new file mode 100644 index 00000000..a663f921 --- /dev/null +++ b/app/Model/ProjectAnalytic.php @@ -0,0 +1,90 @@ +<?php + +namespace Model; + +/** + * Project analytic model + * + * @package model + * @author Frederic Guillot + */ +class ProjectAnalytic extends Base +{ + /** + * Get tasks repartition + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getTaskRepartition($project_id) + { + $metrics = array(); + $total = 0; + $columns = $this->board->getColumns($project_id); + + foreach ($columns as $column) { + + $nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']); + $total += $nb_tasks; + + $metrics[] = array( + 'column_title' => $column['title'], + 'nb_tasks' => $nb_tasks, + ); + } + + if ($total === 0) { + return array(); + } + + foreach ($metrics as &$metric) { + $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); + } + + return $metrics; + } + + /** + * Get users repartition + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getUserRepartition($project_id) + { + $metrics = array(); + $total = 0; + $tasks = $this->taskFinder->getAll($project_id); + $users = $this->projectPermission->getMemberList($project_id); + + foreach ($tasks as $task) { + + $user = isset($users[$task['owner_id']]) ? $users[$task['owner_id']] : $users[0]; + $total++; + + if (! isset($metrics[$user])) { + $metrics[$user] = array( + 'nb_tasks' => 0, + 'percentage' => 0, + 'user' => $user, + ); + } + + $metrics[$user]['nb_tasks']++; + } + + if ($total === 0) { + return array(); + } + + foreach ($metrics as &$metric) { + $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); + } + + ksort($metrics); + + return array_values($metrics); + } +} diff --git a/app/Model/ProjectDailySummary.php b/app/Model/ProjectDailySummary.php new file mode 100644 index 00000000..65a612f6 --- /dev/null +++ b/app/Model/ProjectDailySummary.php @@ -0,0 +1,192 @@ +<?php + +namespace Model; + +/** + * Project daily summary + * + * @package model + * @author Frederic Guillot + */ +class ProjectDailySummary extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_daily_summaries'; + + /** + * Update daily totals for the project + * + * "total" is the number open of tasks in the column + * "score" is the sum of tasks score in the column + * + * @access public + * @param integer $project_id Project id + * @param string $date Record date (YYYY-MM-DD) + * @return boolean + */ + public function updateTotals($project_id, $date) + { + return $this->db->transaction(function($db) use ($project_id, $date) { + + $column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id'); + + foreach ($column_ids as $column_id) { + + // This call will fail if the record already exists + // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE) + $db->table(ProjectDailySummary::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'column_id' => $column_id, + 'total' => 0, + 'score' => 0, + )); + + $db->table(ProjectDailySummary::TABLE) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('day', $date) + ->update(array( + 'score' => $db->table(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('is_active', Task::STATUS_OPEN) + ->sum('score'), + 'total' => $db->table(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->count() + )); + } + }); + } + + /** + * Count the number of recorded days for the data range + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return integer + */ + public function countDays($project_id, $from, $to) + { + $rq = $this->db->execute( + 'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ? AND project_id=?', + array($from, $to, $project_id) + ); + + return $rq !== false ? $rq->fetchColumn(0) : 0; + } + + /** + * Get raw metrics for the project within a data range + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return array + */ + public function getRawMetrics($project_id, $from, $to) + { + return $this->db->table(ProjectDailySummary::TABLE) + ->columns( + ProjectDailySummary::TABLE.'.column_id', + ProjectDailySummary::TABLE.'.day', + ProjectDailySummary::TABLE.'.total', + ProjectDailySummary::TABLE.'.score', + Board::TABLE.'.title AS column_title' + ) + ->join(Board::TABLE, 'id', 'column_id') + ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(ProjectDailySummary::TABLE.'.day') + ->findAll(); + } + + /** + * Get raw metrics for the project within a data range grouped by day + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return array + */ + public function getRawMetricsByDay($project_id, $from, $to) + { + return $this->db->table(ProjectDailySummary::TABLE) + ->columns( + ProjectDailySummary::TABLE.'.day', + 'SUM('.ProjectDailySummary::TABLE.'.total) AS total', + 'SUM('.ProjectDailySummary::TABLE.'.score) AS score' + ) + ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(ProjectDailySummary::TABLE.'.day') + ->groupBy(ProjectDailySummary::TABLE.'.day') + ->findAll(); + } + + /** + * Get aggregated metrics for the project within a data range + * + * [ + * ['Date', 'Column1', 'Column2'], + * ['2014-11-16', 2, 5], + * ['2014-11-17', 20, 15], + * ] + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return array + */ + public function getAggregatedMetrics($project_id, $from, $to) + { + $columns = $this->board->getColumnsList($project_id); + $column_ids = array_keys($columns); + $metrics = array(array(e('Date')) + $columns); + $aggregates = array(); + + // Fetch metrics for the project + $records = $this->db->table(ProjectDailySummary::TABLE) + ->eq('project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->findAll(); + + // Aggregate by day + foreach ($records as $record) { + + if (! isset($aggregates[$record['day']])) { + $aggregates[$record['day']] = array($record['day']); + } + + $aggregates[$record['day']][$record['column_id']] = $record['total']; + } + + // Aggregate by row + foreach ($aggregates as $aggregate) { + + $row = array($aggregate[0]); + + foreach ($column_ids as $column_id) { + $row[] = (int) $aggregate[$column_id]; + } + + $metrics[] = $row; + } + + return $metrics; + } +} diff --git a/app/Model/ProjectDuplication.php b/app/Model/ProjectDuplication.php new file mode 100644 index 00000000..7e3407be --- /dev/null +++ b/app/Model/ProjectDuplication.php @@ -0,0 +1,108 @@ +<?php + +namespace Model; + +/** + * Project Duplication + * + * @package model + * @author Frederic Guillot + * @author Antonio Rabelo + */ +class ProjectDuplication extends Base +{ + /** + * Get a valid project name for the duplication + * + * @access public + * @param string $name Project name + * @param integer $max_length Max length allowed + * @return string + */ + public function getClonedProjectName($name, $max_length = 50) + { + $suffix = ' ('.t('Clone').')'; + + if (strlen($name.$suffix) > $max_length) { + $name = substr($name, 0, $max_length - strlen($suffix)); + } + + return $name.$suffix; + } + + /** + * Create a project from another one + * + * @param integer $project_id Project Id + * @return integer Cloned Project Id + */ + public function copy($project_id) + { + $project = $this->project->getById($project_id); + + $values = array( + 'name' => $this->getClonedProjectName($project['name']), + 'is_active' => true, + 'last_modified' => 0, + 'token' => '', + 'is_public' => 0, + 'is_private' => empty($project['is_private']) ? 0 : 1, + ); + + if (! $this->db->table(Project::TABLE)->save($values)) { + return 0; + } + + return $this->db->getConnection()->getLastId(); + } + + /** + * Clone a project with all settings + * + * @param integer $project_id Project Id + * @param array $part_selection Selection of optional project parts to duplicate. Possible options: 'swimlane', 'action', 'category', 'task' + * @return integer Cloned Project Id + */ + public function duplicate($project_id, $part_selection = array('category', 'action')) + { + $this->db->startTransaction(); + + // Get the cloned project Id + $clone_project_id = $this->copy($project_id); + + if (! $clone_project_id) { + $this->db->cancelTransaction(); + return false; + } + + // Clone Columns, Categories, Permissions and Actions + $optional_parts = array('swimlane', 'action', 'category'); + foreach (array('board', 'category', 'projectPermission', 'action', 'swimlane') as $model) { + + // Skip if optional part has not been selected + if (in_array($model, $optional_parts) && ! in_array($model, $part_selection)) { + continue; + } + + if (! $this->$model->duplicate($project_id, $clone_project_id)) { + $this->db->cancelTransaction(); + return false; + } + } + + $this->db->closeTransaction(); + + // Clone Tasks if in $part_selection + if (in_array('task', $part_selection)) { + $tasks = $this->taskFinder->getAll($project_id); + + foreach ($tasks as $task) { + if (! $this->taskDuplication->duplicateToProject($task['id'], $clone_project_id)) { + return false; + } + } + } + + return (int) $clone_project_id; + } +} diff --git a/app/Model/ProjectIntegration.php b/app/Model/ProjectIntegration.php new file mode 100644 index 00000000..98ff8d4c --- /dev/null +++ b/app/Model/ProjectIntegration.php @@ -0,0 +1,66 @@ +<?php + +namespace Model; + +/** + * Project integration + * + * @package model + * @author Frederic Guillot + */ +class ProjectIntegration extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_integrations'; + + /** + * Get all parameters for a project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getParameters($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->findOne() ?: array(); + } + + /** + * Save parameters for a project + * + * @access public + * @param integer $project_id + * @param array $values + * @return boolean + */ + public function saveParameters($project_id, array $values) + { + if ($this->db->table(self::TABLE)->eq('project_id', $project_id)->count() === 1) { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->update($values); + } + + return $this->db->table(self::TABLE)->insert($values + array('project_id' => $project_id)); + } + + /** + * Check if a project has the given parameter/value + * + * @access public + * @param integer $project_id + * @param string $option + * @param string $value + * @return boolean + */ + public function hasValue($project_id, $option, $value) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq($option, $value) + ->count() === 1; + } +} diff --git a/app/Model/ProjectPermission.php b/app/Model/ProjectPermission.php index fb9847b5..b0a09df4 100644 --- a/app/Model/ProjectPermission.php +++ b/app/Model/ProjectPermission.php @@ -27,11 +27,16 @@ class ProjectPermission extends Base * @param integer $project_id Project id * @param bool $prepend_unassigned Prepend the 'Unassigned' value * @param bool $prepend_everybody Prepend the 'Everbody' value + * @param bool $allow_single_user If there is only one user return only this user * @return array */ - public function getUsersList($project_id, $prepend_unassigned = true, $prepend_everybody = false) + public function getMemberList($project_id, $prepend_unassigned = true, $prepend_everybody = false, $allow_single_user = false) { - $allowed_users = $this->getAllowedUsers($project_id); + $allowed_users = $this->getMembers($project_id); + + if ($allow_single_user && count($allowed_users) === 1) { + return $allowed_users; + } if ($prepend_unassigned) { $allowed_users = array(t('Unassigned')) + $allowed_users; @@ -51,7 +56,7 @@ class ProjectPermission extends Base * @param integer $project_id Project id * @return array */ - public function getAllowedUsers($project_id) + public function getMembers($project_id) { if ($this->isEverybodyAllowed($project_id)) { return $this->user->getList(); @@ -81,6 +86,27 @@ class ProjectPermission extends Base } /** + * Get a list of owners for a project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getManagers($project_id) + { + $users = $this->db + ->table(self::TABLE) + ->join(User::TABLE, 'id', 'user_id') + ->eq('project_id', $project_id) + ->eq('is_owner', 1) + ->asc('username') + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name') + ->findAll(); + + return $this->user->prepareList($users); + } + + /** * Get allowed and not allowed users for a project * * @access public @@ -92,11 +118,13 @@ class ProjectPermission extends Base $users = array( 'allowed' => array(), 'not_allowed' => array(), + 'managers' => array(), ); $all_users = $this->user->getList(); - $users['allowed'] = $this->getAllowedUsers($project_id); + $users['allowed'] = $this->getMembers($project_id); + $users['managers'] = $this->getManagers($project_id); foreach ($all_users as $user_id => $username) { @@ -109,14 +137,14 @@ class ProjectPermission extends Base } /** - * Allow a specific user for a given project + * Add a new project member * * @access public * @param integer $project_id Project id * @param integer $user_id User id * @return bool */ - public function allowUser($project_id, $user_id) + public function addMember($project_id, $user_id) { return $this->db ->table(self::TABLE) @@ -124,14 +152,14 @@ class ProjectPermission extends Base } /** - * Revoke a specific user for a given project + * Remove a member * * @access public * @param integer $project_id Project id * @param integer $user_id User id * @return bool */ - public function revokeUser($project_id, $user_id) + public function revokeMember($project_id, $user_id) { return $this->db ->table(self::TABLE) @@ -141,61 +169,104 @@ class ProjectPermission extends Base } /** - * Check if a specific user is allowed to access to a given project + * Add a project manager * * @access public * @param integer $project_id Project id * @param integer $user_id User id * @return bool */ - public function isUserAllowed($project_id, $user_id) + public function addManager($project_id, $user_id) { - if ($this->user->isAdmin($user_id)) { - return true; - } + return $this->db + ->table(self::TABLE) + ->save(array('project_id' => $project_id, 'user_id' => $user_id, 'is_owner' => 1)); + } + + /** + * Change the role of a member + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @param integer $is_owner Is user owner of the project + * @return bool + */ + public function changeRole($project_id, $user_id, $is_owner) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('user_id', $user_id) + ->update(array('is_owner' => (int) $is_owner)); + } + /** + * Check if a specific user is member of a project + * + * @access public + * @param integer $project_id Project id + * @param integer $user_id User id + * @return bool + */ + public function isMember($project_id, $user_id) + { if ($this->isEverybodyAllowed($project_id)) { return true; } - return (bool) $this->db + return $this->db ->table(self::TABLE) ->eq('project_id', $project_id) ->eq('user_id', $user_id) - ->count(); - } + ->count() === 1; + } - /** - * Return true if everybody is allowed for the project + /** + * Check if a specific user is manager of a given project * * @access public * @param integer $project_id Project id + * @param integer $user_id User id * @return bool */ - public function isEverybodyAllowed($project_id) + public function isManager($project_id, $user_id) { - return (bool) $this->db - ->table(Project::TABLE) - ->eq('id', $project_id) - ->eq('is_everybody_allowed', 1) - ->count(); + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('user_id', $user_id) + ->eq('is_owner', 1) + ->count() === 1; } /** - * Check if a specific user is allowed to manage a project + * Check if a specific user is allowed to access to a given project * * @access public * @param integer $project_id Project id * @param integer $user_id User id * @return bool */ - public function adminAllowed($project_id, $user_id) + public function isUserAllowed($project_id, $user_id) { - if ($this->isUserAllowed($project_id, $user_id) && $this->project->isPrivate($project_id)) { - return true; - } + return $project_id === 0 || $this->user->isAdmin($user_id) || $this->isMember($project_id, $user_id); + } - return false; + /** + * Return true if everybody is allowed for the project + * + * @access public + * @param integer $project_id Project id + * @return bool + */ + public function isEverybodyAllowed($project_id) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $project_id) + ->eq('is_everybody_allowed', 1) + ->count() === 1; } /** @@ -204,12 +275,13 @@ class ProjectPermission extends Base * @access public * @param array $projects Project list: ['project_id' => 'project_name'] * @param integer $user_id User id + * @param string $filter Method name to apply * @return array */ - public function filterProjects(array $projects, $user_id) + public function filterProjects(array $projects, $user_id, $filter = 'isUserAllowed') { foreach ($projects as $project_id => $project_name) { - if (! $this->isUserAllowed($project_id, $user_id)) { + if (! $this->$filter($project_id, $user_id)) { unset($projects[$project_id]); } } @@ -218,7 +290,7 @@ class ProjectPermission extends Base } /** - * Return a list of projects for a given user + * Return a list of allowed active projects for a given user * * @access public * @param integer $user_id User id @@ -226,23 +298,117 @@ class ProjectPermission extends Base */ public function getAllowedProjects($user_id) { - return $this->filterProjects($this->project->getListByStatus(Project::ACTIVE), $user_id); + if ($this->user->isAdmin($user_id)) { + return $this->project->getListByStatus(Project::ACTIVE); + } + + return $this->getActiveMemberProjects($user_id); + } + + /** + * Return a list of projects where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getMemberProjects($user_id) + { + return $this->db + ->hashtable(Project::TABLE) + ->beginOr() + ->eq(self::TABLE.'.user_id', $user_id) + ->eq(Project::TABLE.'.is_everybody_allowed', 1) + ->closeOr() + ->join(self::TABLE, 'project_id', 'id') + ->getAll('projects.id', 'name'); + } + + /** + * Return a list of project ids where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getMemberProjectIds($user_id) + { + return $this->db + ->table(Project::TABLE) + ->beginOr() + ->eq(self::TABLE.'.user_id', $user_id) + ->eq(Project::TABLE.'.is_everybody_allowed', 1) + ->closeOr() + ->join(self::TABLE, 'project_id', 'id') + ->findAllByColumn('projects.id'); + } + + /** + * Return a list of active project ids where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getActiveMemberProjectIds($user_id) + { + return $this->db + ->table(Project::TABLE) + ->beginOr() + ->eq(self::TABLE.'.user_id', $user_id) + ->eq(Project::TABLE.'.is_everybody_allowed', 1) + ->closeOr() + ->eq(Project::TABLE.'.is_active', Project::ACTIVE) + ->join(self::TABLE, 'project_id', 'id') + ->findAllByColumn('projects.id'); + } + + /** + * Return a list of active projects where the user is member + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getActiveMemberProjects($user_id) + { + return $this->db + ->hashtable(Project::TABLE) + ->beginOr() + ->eq(self::TABLE.'.user_id', $user_id) + ->eq(Project::TABLE.'.is_everybody_allowed', 1) + ->closeOr() + ->eq(Project::TABLE.'.is_active', Project::ACTIVE) + ->join(self::TABLE, 'project_id', 'id') + ->getAll('projects.id', 'name'); } /** * Copy user access from a project to another one * - * @author Antonio Rabelo - * @param integer $project_from Project Template - * @return integer $project_to Project that receives the copy + * @param integer $project_src Project Template + * @return integer $project_dst Project that receives the copy * @return boolean */ - public function duplicate($project_from, $project_to) + public function duplicate($project_src, $project_dst) { - $users = $this->getAllowedUsers($project_from); - - foreach ($users as $user_id => $name) { - if (! $this->allowUser($project_to, $user_id)) { + $rows = $this->db + ->table(self::TABLE) + ->columns('project_id', 'user_id', 'is_owner') + ->eq('project_id', $project_src) + ->findAll(); + + foreach ($rows as $row) { + + $result = $this->db + ->table(self::TABLE) + ->save(array( + 'project_id' => $project_dst, + 'user_id' => $row['user_id'], + 'is_owner' => (int) $row['is_owner'], // (int) for postgres + )); + + if (! $result) { return false; } } @@ -264,6 +430,7 @@ class ProjectPermission extends Base new Validators\Integer('project_id', t('This value must be an integer')), new Validators\Required('user_id', t('The user id is required')), new Validators\Integer('user_id', t('This value must be an integer')), + new Validators\Integer('is_owner', t('This value must be an integer')), )); return array( diff --git a/app/Model/SubTask.php b/app/Model/SubTask.php deleted file mode 100644 index 886ad1f3..00000000 --- a/app/Model/SubTask.php +++ /dev/null @@ -1,277 +0,0 @@ -<?php - -namespace Model; - -use SimpleValidator\Validator; -use SimpleValidator\Validators; - -/** - * Subtask model - * - * @package model - * @author Frederic Guillot - */ -class SubTask extends Base -{ - /** - * SQL table name - * - * @var string - */ - const TABLE = 'task_has_subtasks'; - - /** - * Task "done" status - * - * @var integer - */ - const STATUS_DONE = 2; - - /** - * Task "in progress" status - * - * @var integer - */ - const STATUS_INPROGRESS = 1; - - /** - * Task "todo" status - * - * @var integer - */ - const STATUS_TODO = 0; - - /** - * Events - * - * @var string - */ - const EVENT_UPDATE = 'subtask.update'; - const EVENT_CREATE = 'subtask.create'; - - /** - * Get available status - * - * @access public - * @return array - */ - public function getStatusList() - { - $status = array( - self::STATUS_TODO => t('Todo'), - self::STATUS_INPROGRESS => t('In progress'), - self::STATUS_DONE => t('Done'), - ); - - asort($status); - - return $status; - } - - /** - * Get all subtasks for a given task - * - * @access public - * @param integer $task_id Task id - * @return array - */ - public function getAll($task_id) - { - $status = $this->getStatusList(); - $subtasks = $this->db->table(self::TABLE) - ->eq('task_id', $task_id) - ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') - ->join(User::TABLE, 'id', 'user_id') - ->asc(self::TABLE.'.id') - ->findAll(); - - foreach ($subtasks as &$subtask) { - $subtask['status_name'] = $status[$subtask['status']]; - } - - return $subtasks; - } - - /** - * Get a subtask by the id - * - * @access public - * @param integer $subtask_id Subtask id - * @param bool $more Fetch more data - * @return array - */ - public function getById($subtask_id, $more = false) - { - if ($more) { - - $subtask = $this->db->table(self::TABLE) - ->eq(self::TABLE.'.id', $subtask_id) - ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') - ->join(User::TABLE, 'id', 'user_id') - ->findOne(); - - if ($subtask) { - $status = $this->getStatusList(); - $subtask['status_name'] = $status[$subtask['status']]; - } - - return $subtask; - } - - return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); - } - - /** - * Prepare data before insert/update - * - * @access public - * @param array $values Form values - */ - public function prepare(array &$values) - { - $this->removeFields($values, array('another_subtask')); - $this->resetFields($values, array('time_estimated', 'time_spent')); - } - - /** - * Create - * - * @access public - * @param array $values Form values - * @return bool - */ - public function create(array $values) - { - $this->prepare($values); - $result = $this->db->table(self::TABLE)->save($values); - - if ($result) { - $values['id'] = $this->db->getConnection()->getLastId(); - $this->event->trigger(self::EVENT_CREATE, $values); - } - - return $result; - } - - /** - * Update - * - * @access public - * @param array $values Form values - * @return bool - */ - public function update(array $values) - { - $this->prepare($values); - $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); - - if ($result) { - $this->event->trigger(self::EVENT_UPDATE, $values); - } - - return $result; - } - - /** - * Remove - * - * @access public - * @param integer $subtask_id Subtask id - * @return bool - */ - public function remove($subtask_id) - { - return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); - } - - /** - * Duplicate all subtasks to another task - * - * @access public - * @param integer $src_task_id Source task id - * @param integer $dst_task_id Destination task id - * @return bool - */ - public function duplicate($src_task_id, $dst_task_id) - { - $subtasks = $this->db->table(self::TABLE) - ->columns('title', 'time_estimated') - ->eq('task_id', $src_task_id) - ->findAll(); - - foreach ($subtasks as &$subtask) { - - $subtask['task_id'] = $dst_task_id; - $subtask['time_spent'] = 0; - - if (! $this->db->table(self::TABLE)->save($subtask)) { - return false; - } - } - - return true; - } - - /** - * Validate creation - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateCreation(array $values) - { - $rules = array( - new Validators\Required('task_id', t('The task id is required')), - new Validators\Required('title', t('The title is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Validate modification - * - * @access public - * @param array $values Form values - * @return array $valid, $errors [0] = Success or not, [1] = List of errors - */ - public function validateModification(array $values) - { - $rules = array( - new Validators\Required('id', t('The subtask id is required')), - new Validators\Required('task_id', t('The task id is required')), - ); - - $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); - - return array( - $v->execute(), - $v->getErrors() - ); - } - - /** - * Common validation rules - * - * @access private - * @return array - */ - private function commonValidationRules() - { - return array( - new Validators\Integer('id', t('The subtask id must be an integer')), - new Validators\Integer('task_id', t('The task id must be an integer')), - new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100), - new Validators\Integer('user_id', t('The user id must be an integer')), - new Validators\Integer('status', t('The status must be an integer')), - new Validators\Numeric('time_estimated', t('The time must be a numeric value')), - new Validators\Numeric('time_spent', t('The time must be a numeric value')), - ); - } -} diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php new file mode 100644 index 00000000..65fa0f0c --- /dev/null +++ b/app/Model/Subtask.php @@ -0,0 +1,497 @@ +<?php + +namespace Model; + +use Event\SubtaskEvent; +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Subtask model + * + * @package model + * @author Frederic Guillot + */ +class Subtask extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'subtasks'; + + /** + * Task "done" status + * + * @var integer + */ + const STATUS_DONE = 2; + + /** + * Task "in progress" status + * + * @var integer + */ + const STATUS_INPROGRESS = 1; + + /** + * Task "todo" status + * + * @var integer + */ + const STATUS_TODO = 0; + + /** + * Events + * + * @var string + */ + const EVENT_UPDATE = 'subtask.update'; + const EVENT_CREATE = 'subtask.create'; + + /** + * Get available status + * + * @access public + * @return array + */ + public function getStatusList() + { + return array( + self::STATUS_TODO => t('Todo'), + self::STATUS_INPROGRESS => t('In progress'), + self::STATUS_DONE => t('Done'), + ); + } + + /** + * Add subtask status status to the resultset + * + * @access public + * @param array $subtasks Subtasks + * @return array + */ + public function addStatusName(array $subtasks) + { + $status = $this->getStatusList(); + + foreach ($subtasks as &$subtask) { + $subtask['status_name'] = $status[$subtask['status']]; + } + + return $subtasks; + } + + /** + * Get the query to fetch subtasks assigned to a user + * + * @access public + * @param integer $user_id User id + * @param array $status List of status + * @return \PicoDb\Table + */ + public function getUserQuery($user_id, array $status) + { + return $this->db->table(Subtask::TABLE) + ->columns( + Subtask::TABLE.'.*', + Task::TABLE.'.project_id', + Task::TABLE.'.color_id', + Task::TABLE.'.title AS task_name', + Project::TABLE.'.name AS project_name' + ) + ->eq('user_id', $user_id) + ->eq(Project::TABLE.'.is_active', Project::ACTIVE) + ->in(Subtask::TABLE.'.status', $status) + ->join(Task::TABLE, 'id', 'task_id') + ->join(Project::TABLE, 'id', 'project_id', Task::TABLE) + ->filter(array($this, 'addStatusName')); + } + + /** + * Get all subtasks for a given task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAll($task_id) + { + return $this->db + ->table(self::TABLE) + ->eq('task_id', $task_id) + ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') + ->join(User::TABLE, 'id', 'user_id') + ->asc(self::TABLE.'.position') + ->filter(array($this, 'addStatusName')) + ->findAll(); + } + + /** + * Get a subtask by the id + * + * @access public + * @param integer $subtask_id Subtask id + * @param bool $more Fetch more data + * @return array + */ + public function getById($subtask_id, $more = false) + { + if ($more) { + + return $this->db + ->table(self::TABLE) + ->eq(self::TABLE.'.id', $subtask_id) + ->columns(self::TABLE.'.*', User::TABLE.'.username', User::TABLE.'.name') + ->join(User::TABLE, 'id', 'user_id') + ->filter(array($this, 'addStatusName')) + ->findOne(); + } + + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne(); + } + + /** + * Prepare data before insert/update + * + * @access public + * @param array $values Form values + */ + public function prepare(array &$values) + { + $this->removeFields($values, array('another_subtask')); + $this->resetFields($values, array('time_estimated', 'time_spent')); + } + + /** + * Get the position of the last column for a given project + * + * @access public + * @param integer $task_id Task id + * @return integer + */ + public function getLastPosition($task_id) + { + return (int) $this->db + ->table(self::TABLE) + ->eq('task_id', $task_id) + ->desc('position') + ->findOneColumn('position'); + } + + /** + * Create a new subtask + * + * @access public + * @param array $values Form values + * @return bool|integer + */ + public function create(array $values) + { + $this->prepare($values); + $values['position'] = $this->getLastPosition($values['task_id']) + 1; + + $subtask_id = $this->persist(self::TABLE, $values); + + if ($subtask_id) { + $this->container['dispatcher']->dispatch( + self::EVENT_CREATE, + new SubtaskEvent(array('id' => $subtask_id) + $values) + ); + } + + return $subtask_id; + } + + /** + * Update + * + * @access public + * @param array $values Form values + * @return bool + */ + public function update(array $values) + { + $this->prepare($values); + $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values); + + if ($result) { + + $this->container['dispatcher']->dispatch( + self::EVENT_UPDATE, + new SubtaskEvent($values) + ); + } + + return $result; + } + + /** + * Get subtasks with consecutive positions + * + * If you remove a subtask, the positions are not anymore consecutives + * + * @access public + * @param integer $task_id + * @return array + */ + public function getNormalizedPositions($task_id) + { + $subtasks = $this->db->hashtable(self::TABLE)->eq('task_id', $task_id)->asc('position')->getAll('id', 'position'); + $position = 1; + + foreach ($subtasks as $subtask_id => $subtask_position) { + $subtasks[$subtask_id] = $position++; + } + + return $subtasks; + } + + /** + * Save the new positions for a set of subtasks + * + * @access public + * @param array $subtasks Hashmap of column_id/column_position + * @return boolean + */ + public function savePositions(array $subtasks) + { + return $this->db->transaction(function ($db) use ($subtasks) { + + foreach ($subtasks as $subtask_id => $position) { + if (! $db->table(Subtask::TABLE)->eq('id', $subtask_id)->update(array('position' => $position))) { + return false; + } + } + }); + } + + /** + * Move a subtask down, increment the position value + * + * @access public + * @param integer $task_id + * @param integer $subtask_id + * @return boolean + */ + public function moveDown($task_id, $subtask_id) + { + $subtasks = $this->getNormalizedPositions($task_id); + $positions = array_flip($subtasks); + + if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] < count($subtasks)) { + + $position = ++$subtasks[$subtask_id]; + $subtasks[$positions[$position]]--; + + return $this->savePositions($subtasks); + } + + return false; + } + + /** + * Move a subtask up, decrement the position value + * + * @access public + * @param integer $task_id + * @param integer $subtask_id + * @return boolean + */ + public function moveUp($task_id, $subtask_id) + { + $subtasks = $this->getNormalizedPositions($task_id); + $positions = array_flip($subtasks); + + if (isset($subtasks[$subtask_id]) && $subtasks[$subtask_id] > 1) { + + $position = --$subtasks[$subtask_id]; + $subtasks[$positions[$position]]++; + + return $this->savePositions($subtasks); + } + + return false; + } + + /** + * Change the status of subtask + * + * Todo -> In progress -> Done -> Todo -> etc... + * + * @access public + * @param integer $subtask_id + * @return bool + */ + public function toggleStatus($subtask_id) + { + $subtask = $this->getById($subtask_id); + + $values = array( + 'id' => $subtask['id'], + 'status' => ($subtask['status'] + 1) % 3, + 'task_id' => $subtask['task_id'], + ); + + return $this->update($values); + } + + /** + * Get the subtask in progress for this user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getSubtaskInProgress($user_id) + { + return $this->db->table(self::TABLE) + ->eq('status', self::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->findOne(); + } + + /** + * Return true if the user have a subtask in progress + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function hasSubtaskInProgress($user_id) + { + return $this->config->get('subtask_restriction') == 1 && + $this->db->table(self::TABLE) + ->eq('status', self::STATUS_INPROGRESS) + ->eq('user_id', $user_id) + ->count() === 1; + } + + /** + * Remove + * + * @access public + * @param integer $subtask_id Subtask id + * @return bool + */ + public function remove($subtask_id) + { + return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove(); + } + + /** + * Duplicate all subtasks to another task + * + * @access public + * @param integer $src_task_id Source task id + * @param integer $dst_task_id Destination task id + * @return bool + */ + public function duplicate($src_task_id, $dst_task_id) + { + return $this->db->transaction(function ($db) use ($src_task_id, $dst_task_id) { + + $subtasks = $db->table(Subtask::TABLE) + ->columns('title', 'time_estimated', 'position') + ->eq('task_id', $src_task_id) + ->asc('position') + ->findAll(); + + foreach ($subtasks as &$subtask) { + + $subtask['task_id'] = $dst_task_id; + + if (! $db->table(Subtask::TABLE)->save($subtask)) { + return false; + } + } + }); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $rules = array( + new Validators\Required('task_id', t('The task id is required')), + new Validators\Required('title', t('The title is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The subtask id is required')), + new Validators\Required('task_id', t('The task id is required')), + new Validators\Required('title', t('The title is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate API modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateApiModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The subtask id is required')), + new Validators\Required('task_id', t('The task id is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('The subtask id must be an integer')), + new Validators\Integer('task_id', t('The task id must be an integer')), + new Validators\MaxLength('title', t('The maximum length is %d characters', 255), 255), + new Validators\Integer('user_id', t('The user id must be an integer')), + new Validators\Integer('status', t('The status must be an integer')), + new Validators\Numeric('time_estimated', t('The time must be a numeric value')), + new Validators\Numeric('time_spent', t('The time must be a numeric value')), + ); + } +} diff --git a/app/Model/SubtaskExport.php b/app/Model/SubtaskExport.php new file mode 100644 index 00000000..23dcc019 --- /dev/null +++ b/app/Model/SubtaskExport.php @@ -0,0 +1,119 @@ +<?php + +namespace Model; + +/** + * Subtask Export + * + * @package model + * @author Frederic Guillot + */ +class SubtaskExport extends Base +{ + /** + * Subtask statuses + * + * @access private + * @var array + */ + private $subtask_status = array(); + + /** + * Fetch subtasks and return the prepared CSV + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function export($project_id, $from, $to) + { + $this->subtask_status = $this->subtask->getStatusList(); + $subtasks = $this->getSubtasks($project_id, $from, $to); + $results = array($this->getColumns()); + + foreach ($subtasks as $subtask) { + $results[] = $this->format($subtask); + } + + return $results; + } + + /** + * Get column titles + * + * @access public + * @return string[] + */ + public function getColumns() + { + return array( + e('Subtask Id'), + e('Title'), + e('Status'), + e('Assignee'), + e('Time estimated'), + e('Time spent'), + e('Task Id'), + e('Task Title'), + ); + } + + /** + * Format the output of a subtask array + * + * @access public + * @param array $subtask Subtask properties + * @return array + */ + public function format(array $subtask) + { + $values = array(); + $values[] = $subtask['id']; + $values[] = $subtask['title']; + $values[] = $this->subtask_status[$subtask['status']]; + $values[] = $subtask['assignee_name'] ?: $subtask['assignee_username']; + $values[] = $subtask['time_estimated']; + $values[] = $subtask['time_spent']; + $values[] = $subtask['task_id']; + $values[] = $subtask['task_title']; + + return $values; + } + + /** + * Get all subtasks for a given project + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function getSubtasks($project_id, $from, $to) + { + if (! is_numeric($from)) { + $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); + } + + if (! is_numeric($to)) { + $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + } + + return $this->db->table(Subtask::TABLE) + ->eq('project_id', $project_id) + ->columns( + Subtask::TABLE.'.*', + User::TABLE.'.username AS assignee_username', + User::TABLE.'.name AS assignee_name', + Task::TABLE.'.title AS task_title' + ) + ->gte('date_creation', $from) + ->lte('date_creation', $to) + ->join(Task::TABLE, 'id', 'task_id') + ->join(User::TABLE, 'id', 'user_id') + ->asc(Subtask::TABLE.'.id') + ->findAll(); + } +} diff --git a/app/Model/SubtaskForecast.php b/app/Model/SubtaskForecast.php new file mode 100644 index 00000000..263aa27a --- /dev/null +++ b/app/Model/SubtaskForecast.php @@ -0,0 +1,124 @@ +<?php + +namespace Model; + +use DateTime; +use DateInterval; + +/** + * Subtask Forecast + * + * @package model + * @author Frederic Guillot + */ +class SubtaskForecast extends Base +{ + /** + * Get not completed subtasks with an estimate sorted by postition + * + * @access public + * @param integer $user_id + * @return array + */ + public function getSubtasks($user_id) + { + return $this->db + ->table(Subtask::TABLE) + ->columns(Subtask::TABLE.'.id', Task::TABLE.'.project_id', Subtask::TABLE.'.task_id', Subtask::TABLE.'.title', Subtask::TABLE.'.time_estimated') + ->join(Task::TABLE, 'id', 'task_id') + ->asc(Task::TABLE.'.position') + ->asc(Subtask::TABLE.'.position') + ->gt(Subtask::TABLE.'.time_estimated', 0) + ->eq(Subtask::TABLE.'.status', Subtask::STATUS_TODO) + ->eq(Subtask::TABLE.'.user_id', $user_id) + ->findAll(); + } + + /** + * Get the start date for the forecast + * + * @access public + * @param integer $user_id + * @return array + */ + public function getStartDate($user_id) + { + $subtask = $this->db->table(Subtask::TABLE) + ->columns(Subtask::TABLE.'.time_estimated', SubtaskTimeTracking::TABLE.'.start') + ->eq(SubtaskTimeTracking::TABLE.'.user_id', $user_id) + ->eq(SubtaskTimeTracking::TABLE.'.end', 0) + ->status('status', Subtask::STATUS_INPROGRESS) + ->join(SubtaskTimeTracking::TABLE, 'subtask_id', 'id') + ->findOne(); + + if ($subtask && $subtask['time_estimated'] && $subtask['start']) { + return date('Y-m-d H:i', $subtask['start'] + $subtask['time_estimated'] * 3600); + } + + return date('Y-m-d H:i'); + } + + /** + * Get all calendar events according to the user timetable and the subtasks estimates + * + * @access public + * @param integer $user_id + * @param string $end End date of the calendar + * @return array + */ + public function getCalendarEvents($user_id, $end) + { + $events = array(); + $start_date = new DateTime($this->getStartDate($user_id)); + $timetable = $this->timetable->calculate($user_id, $start_date, new DateTime($end)); + $subtasks = $this->getSubtasks($user_id); + $total = count($subtasks); + $offset = 0; + + foreach ($timetable as $slot) { + + $interval = $this->dateParser->getHours($slot[0], $slot[1]); + $start = $slot[0]->getTimestamp(); + + if ($slot[0] < $start_date) { + + if (! $this->dateParser->withinDateRange($start_date, $slot[0], $slot[1])) { + continue; + } + + $interval = $this->dateParser->getHours(new DateTime, $slot[1]); + $start = time(); + } + + while ($offset < $total) { + + $event = array( + 'id' => $subtasks[$offset]['id'].'-'.$subtasks[$offset]['task_id'].'-'.$offset, + 'subtask_id' => $subtasks[$offset]['id'], + 'title' => t('#%d', $subtasks[$offset]['task_id']).' '.$subtasks[$offset]['title'], + 'url' => $this->helper->url->to('task', 'show', array('task_id' => $subtasks[$offset]['task_id'], 'project_id' => $subtasks[$offset]['project_id'])), + 'editable' => false, + 'start' => date('Y-m-d\TH:i:s', $start), + ); + + if ($subtasks[$offset]['time_estimated'] <= $interval) { + + $start += $subtasks[$offset]['time_estimated'] * 3600; + $interval -= $subtasks[$offset]['time_estimated']; + $offset++; + + $event['end'] = date('Y-m-d\TH:i:s', $start); + $events[] = $event; + } + else { + $subtasks[$offset]['time_estimated'] -= $interval; + $event['end'] = $slot[1]->format('Y-m-d\TH:i:s'); + $events[] = $event; + break; + } + } + } + + return $events; + } +} diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php new file mode 100644 index 00000000..93a698b6 --- /dev/null +++ b/app/Model/SubtaskTimeTracking.php @@ -0,0 +1,340 @@ +<?php + +namespace Model; + +use DateTime; + +/** + * Subtask timesheet + * + * @package model + * @author Frederic Guillot + */ +class SubtaskTimeTracking extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'subtask_time_tracking'; + + /** + * Get query for user timesheet (pagination) + * + * @access public + * @param integer $user_id User id + * @return \PicoDb\Table + */ + public function getUserQuery($user_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.subtask_id', + self::TABLE.'.end', + self::TABLE.'.start', + self::TABLE.'.time_spent', + Subtask::TABLE.'.task_id', + Subtask::TABLE.'.title AS subtask_title', + Task::TABLE.'.title AS task_title', + Task::TABLE.'.project_id', + Task::TABLE.'.color_id' + ) + ->join(Subtask::TABLE, 'id', 'subtask_id') + ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->eq(self::TABLE.'.user_id', $user_id); + } + + /** + * Get query for task timesheet (pagination) + * + * @access public + * @param integer $task_id Task id + * @return \PicoDb\Table + */ + public function getTaskQuery($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.subtask_id', + self::TABLE.'.end', + self::TABLE.'.start', + self::TABLE.'.time_spent', + self::TABLE.'.user_id', + Subtask::TABLE.'.task_id', + Subtask::TABLE.'.title AS subtask_title', + Task::TABLE.'.project_id', + User::TABLE.'.username', + User::TABLE.'.name AS user_fullname' + ) + ->join(Subtask::TABLE, 'id', 'subtask_id') + ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->join(User::TABLE, 'id', 'user_id', self::TABLE) + ->eq(Task::TABLE.'.id', $task_id); + } + + /** + * Get query for project timesheet (pagination) + * + * @access public + * @param integer $project_id Project id + * @return \PicoDb\Table + */ + public function getProjectQuery($project_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.subtask_id', + self::TABLE.'.end', + self::TABLE.'.start', + self::TABLE.'.time_spent', + self::TABLE.'.user_id', + Subtask::TABLE.'.task_id', + Subtask::TABLE.'.title AS subtask_title', + Task::TABLE.'.project_id', + Task::TABLE.'.color_id', + User::TABLE.'.username', + User::TABLE.'.name AS user_fullname' + ) + ->join(Subtask::TABLE, 'id', 'subtask_id') + ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE) + ->join(User::TABLE, 'id', 'user_id', self::TABLE) + ->eq(Task::TABLE.'.project_id', $project_id) + ->asc(self::TABLE.'.id'); + } + + /** + * Get all recorded time slots for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getUserTimesheet($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->findAll(); + } + + /** + * Get user calendar events + * + * @access public + * @param integer $user_id + * @param integer $start + * @param integer $end + * @return array + */ + public function getUserCalendarEvents($user_id, $start, $end) + { + $result = $this->getUserQuery($user_id) + ->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + 'start', + 'end' + )) + ->findAll(); + + $result = $this->timetable->calculateEventsIntersect($user_id, $result, $start, $end); + + return $this->toCalendarEvents($result); + } + + /** + * Get project calendar events + * + * @access public + * @param integer $project_id + * @param integer $start + * @param integer $end + * @return array + */ + public function getProjectCalendarEvents($project_id, $start, $end) + { + $result = $this + ->getProjectQuery($project_id) + ->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + 'start', + 'end' + )) + ->findAll(); + + return $this->toCalendarEvents($result); + } + + /** + * Convert a record set to calendar events + * + * @access private + * @param array $rows + * @return array + */ + private function toCalendarEvents(array $rows) + { + $events = array(); + + foreach ($rows as $row) { + + $user = isset($row['username']) ? ' ('.($row['user_fullname'] ?: $row['username']).')' : ''; + + $events[] = array( + 'id' => $row['id'], + 'subtask_id' => $row['subtask_id'], + 'title' => t('#%d', $row['task_id']).' '.$row['subtask_title'].$user, + 'start' => date('Y-m-d\TH:i:s', $row['start']), + 'end' => date('Y-m-d\TH:i:s', $row['end'] ?: time()), + 'backgroundColor' => $this->color->getBackgroundColor($row['color_id']), + 'borderColor' => $this->color->getBorderColor($row['color_id']), + 'textColor' => 'black', + 'url' => $this->helper->url->to('task', 'show', array('task_id' => $row['task_id'], 'project_id' => $row['project_id'])), + 'editable' => false, + ); + } + + return $events; + } + + /** + * Log start time + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @return boolean + */ + public function logStartTime($subtask_id, $user_id) + { + return $this->db + ->table(self::TABLE) + ->insert(array('subtask_id' => $subtask_id, 'user_id' => $user_id, 'start' => time())); + } + + /** + * Log end time + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @return boolean + */ + public function logEndTime($subtask_id, $user_id) + { + $time_spent = $this->getTimeSpent($subtask_id, $user_id); + + if ($time_spent > 0) { + $this->updateSubtaskTimeSpent($subtask_id, $time_spent); + } + + return $this->db + ->table(self::TABLE) + ->eq('subtask_id', $subtask_id) + ->eq('user_id', $user_id) + ->eq('end', 0) + ->update(array( + 'end' => time(), + 'time_spent' => $time_spent, + )); + } + + /** + * Calculate the time spent when the clock is stopped + * + * @access public + * @param integer $subtask_id + * @param integer $user_id + * @return float + */ + public function getTimeSpent($subtask_id, $user_id) + { + $start_time = $this->db + ->table(self::TABLE) + ->eq('subtask_id', $subtask_id) + ->eq('user_id', $user_id) + ->eq('end', 0) + ->findOneColumn('start'); + + if ($start_time) { + + $start = new DateTime; + $start->setTimestamp($start_time); + + return $this->timetable->calculateEffectiveDuration($user_id, $start, new DateTime); + } + + return 0; + } + + /** + * Update subtask time spent + * + * @access public + * @param integer $subtask_id + * @param float $time_spent + * @return bool + */ + public function updateSubtaskTimeSpent($subtask_id, $time_spent) + { + $subtask = $this->subtask->getById($subtask_id); + + // Fire the event subtask.update + return $this->subtask->update(array( + 'id' => $subtask['id'], + 'time_spent' => $subtask['time_spent'] + $time_spent, + 'task_id' => $subtask['task_id'], + )); + } + + /** + * Update task time tracking based on subtasks time tracking + * + * @access public + * @param integer $task_id Task id + * @return bool + */ + public function updateTaskTimeTracking($task_id) + { + $result = $this->calculateSubtaskTime($task_id); + + if (empty($result['total_spent']) && empty($result['total_estimated'])) { + return true; + } + + return $this->db + ->table(Task::TABLE) + ->eq('id', $task_id) + ->update(array( + 'time_spent' => $result['total_spent'], + 'time_estimated' => $result['total_estimated'], + )); + } + + /** + * Sum time spent and time estimated for all subtasks + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function calculateSubtaskTime($task_id) + { + return $this->db + ->table(Subtask::TABLE) + ->eq('task_id', $task_id) + ->columns( + 'SUM(time_spent) AS total_spent', + 'SUM(time_estimated) AS total_estimated' + ) + ->findOne(); + } +} diff --git a/app/Model/Swimlane.php b/app/Model/Swimlane.php new file mode 100644 index 00000000..3b78a406 --- /dev/null +++ b/app/Model/Swimlane.php @@ -0,0 +1,561 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Swimlanes + * + * @package model + * @author Frederic Guillot + */ +class Swimlane extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'swimlanes'; + + /** + * Value for active swimlanes + * + * @var integer + */ + const ACTIVE = 1; + + /** + * Value for inactive swimlanes + * + * @var integer + */ + const INACTIVE = 0; + + /** + * Get a swimlane by the id + * + * @access public + * @param integer $swimlane_id Swimlane id + * @return array + */ + public function getById($swimlane_id) + { + return $this->db->table(self::TABLE)->eq('id', $swimlane_id)->findOne(); + } + + /** + * Get the swimlane name by the id + * + * @access public + * @param integer $swimlane_id Swimlane id + * @return string + */ + public function getNameById($swimlane_id) + { + return $this->db->table(self::TABLE)->eq('id', $swimlane_id)->findOneColumn('name') ?: ''; + } + + /** + * Get a swimlane id by the project and the name + * + * @access public + * @param integer $project_id Project id + * @param string $name Name + * @return integer + */ + public function getIdByName($project_id, $name) + { + return (int) $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('name', $name) + ->findOneColumn('id'); + } + + /** + * Get a swimlane by the project and the name + * + * @access public + * @param integer $project_id Project id + * @param string $name Swimlane name + * @return array + */ + public function getByName($project_id, $name) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('name', $name) + ->findOne(); + } + + /** + * Get default swimlane properties + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getDefault($project_id) + { + $result = $this->db->table(Project::TABLE) + ->eq('id', $project_id) + ->columns('id', 'default_swimlane', 'show_default_swimlane') + ->findOne(); + + if ($result['default_swimlane'] === 'Default swimlane') { + $result['default_swimlane'] = t($result['default_swimlane']); + } + + return $result; + } + + /** + * Get all swimlanes for a given project + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getAll($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->orderBy('position', 'asc') + ->findAll(); + } + + /** + * Get the list of swimlanes by status + * + * @access public + * @param integer $project_id Project id + * @param integer $status Status + * @return array + */ + public function getAllByStatus($project_id, $status = self::ACTIVE) + { + $query = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', $status); + + if ($status == self::ACTIVE) { + $query->asc('position'); + } + else { + $query->asc('name'); + } + + return $query->findAll(); + } + + /** + * Get active swimlanes + * + * @access public + * @param integer $project_id Project id + * @return array + */ + public function getSwimlanes($project_id) + { + $swimlanes = $this->db->table(self::TABLE) + ->columns('id', 'name') + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->orderBy('position', 'asc') + ->findAll(); + + $default_swimlane = $this->db->table(Project::TABLE) + ->eq('id', $project_id) + ->eq('show_default_swimlane', 1) + ->findOneColumn('default_swimlane'); + + if ($default_swimlane) { + + if ($default_swimlane === 'Default swimlane') { + $default_swimlane = t($default_swimlane); + } + + array_unshift($swimlanes, array('id' => 0, 'name' => $default_swimlane)); + } + + return $swimlanes; + } + + /** + * Get list of all swimlanes + * + * @access public + * @param integer $project_id Project id + * @param boolean $prepend Prepend default value + * @param boolean $only_active Return only active swimlanes + * @return array + */ + public function getList($project_id, $prepend = false, $only_active = false) + { + $swimlanes = array(); + $default = $this->db->table(Project::TABLE)->eq('id', $project_id)->eq('show_default_swimlane', 1)->findOneColumn('default_swimlane'); + + if ($prepend) { + $swimlanes[-1] = t('All swimlanes'); + } + + if (! empty($default)) { + $swimlanes[0] = $default === 'Default swimlane' ? t($default) : $default; + } + + return $swimlanes + $this->db->hashtable(self::TABLE) + ->eq('project_id', $project_id) + ->in('is_active', $only_active ? array(self::ACTIVE) : array(self::ACTIVE, self::INACTIVE)) + ->orderBy('position', 'asc') + ->getAll('id', 'name'); + } + + /** + * Add a new swimlane + * + * @access public + * @param integer $project_id + * @param string $name + * @return integer|boolean + */ + public function create($project_id, $name) + { + return $this->persist(self::TABLE, array( + 'project_id' => $project_id, + 'name' => $name, + 'position' => $this->getLastPosition($project_id), + )); + } + + /** + * Rename a swimlane + * + * @access public + * @param integer $swimlane_id Swimlane id + * @param string $name Swimlane name + * @return bool + */ + public function rename($swimlane_id, $name) + { + return $this->db->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array('name' => $name)); + } + + /** + * Update the default swimlane + * + * @access public + * @param array $values Form values + * @return bool + */ + public function updateDefault(array $values) + { + return $this->db + ->table(Project::TABLE) + ->eq('id', $values['id']) + ->update(array( + 'default_swimlane' => $values['default_swimlane'], + 'show_default_swimlane' => $values['show_default_swimlane'], + )); + } + + /** + * Get the last position of a swimlane + * + * @access public + * @param integer $project_id + * @return integer + */ + public function getLastPosition($project_id) + { + return $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->count() + 1; + } + + /** + * Disable a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function disable($project_id, $swimlane_id) + { + $result = $this->db + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::INACTIVE, + 'position' => 0, + )); + + if ($result) { + // Re-order positions + $this->updatePositions($project_id); + } + + return $result; + } + + /** + * Enable a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function enable($project_id, $swimlane_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array( + 'is_active' => self::ACTIVE, + 'position' => $this->getLastPosition($project_id), + )); + } + + /** + * Remove a swimlane + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return bool + */ + public function remove($project_id, $swimlane_id) + { + $this->db->startTransaction(); + + // Tasks should not be assigned anymore to this swimlane + $this->db->table(Task::TABLE)->eq('swimlane_id', $swimlane_id)->update(array('swimlane_id' => 0)); + + if (! $this->db->table(self::TABLE)->eq('id', $swimlane_id)->remove()) { + $this->db->cancelTransaction(); + return false; + } + + // Re-order positions + $this->updatePositions($project_id); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Update swimlane positions after disabling or removing a swimlane + * + * @access public + * @param integer $project_id Project id + * @return boolean + */ + public function updatePositions($project_id) + { + $position = 0; + $swimlanes = $this->db->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', 1) + ->asc('position') + ->findAllByColumn('id'); + + if (! $swimlanes) { + return false; + } + + foreach ($swimlanes as $swimlane_id) { + $this->db->table(self::TABLE) + ->eq('id', $swimlane_id) + ->update(array('position' => ++$position)); + } + + return true; + } + + /** + * Move a swimlane down, increment the position value + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + public function moveDown($project_id, $swimlane_id) + { + $swimlanes = $this->db->hashtable(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->asc('position') + ->getAll('id', 'position'); + + $positions = array_flip($swimlanes); + + if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] < count($swimlanes)) { + + $position = ++$swimlanes[$swimlane_id]; + $swimlanes[$positions[$position]]--; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * Move a swimlane up, decrement the position value + * + * @access public + * @param integer $project_id Project id + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + public function moveUp($project_id, $swimlane_id) + { + $swimlanes = $this->db->hashtable(self::TABLE) + ->eq('project_id', $project_id) + ->eq('is_active', self::ACTIVE) + ->asc('position') + ->getAll('id', 'position'); + + $positions = array_flip($swimlanes); + + if (isset($swimlanes[$swimlane_id]) && $swimlanes[$swimlane_id] > 1) { + + $position = --$swimlanes[$swimlane_id]; + $swimlanes[$positions[$position]]++; + + $this->db->startTransaction(); + $this->db->table(self::TABLE)->eq('id', $swimlane_id)->update(array('position' => $position)); + $this->db->table(self::TABLE)->eq('id', $positions[$position])->update(array('position' => $swimlanes[$positions[$position]])); + $this->db->closeTransaction(); + + return true; + } + + return false; + } + + /** + * Duplicate Swimlane to project + * + * @access public + * @param integer $project_from Project Template + * @param integer $project_to Project that receives the copy + * @return integer|boolean + */ + + public function duplicate($project_from, $project_to) + { + $swimlanes = $this->getAll($project_from); + + foreach ($swimlanes as $swimlane) { + + unset($swimlane['id']); + $swimlane['project_id'] = $project_to; + + if (! $this->db->table(self::TABLE)->save($swimlane)) { + return false; + } + } + + $default_swimlane = $this->getDefault($project_from); + $default_swimlane['id'] = $project_to; + + $this->updateDefault($default_swimlane); + + return true; + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $rules = array( + new Validators\Required('project_id', t('The project id is required')), + new Validators\Required('name', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('name', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate default swimlane modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateDefaultModification(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + new Validators\Required('default_swimlane', t('The name is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Integer('id', t('The id must be an integer')), + new Validators\Integer('project_id', t('The project id must be an integer')), + new Validators\MaxLength('name', t('The maximum length is %d characters', 50), 50) + ); + } +} diff --git a/app/Model/Task.php b/app/Model/Task.php index a0090641..71d973a4 100644 --- a/app/Model/Task.php +++ b/app/Model/Task.php @@ -30,212 +30,52 @@ class Task extends Base * * @var string */ + const EVENT_MOVE_PROJECT = 'task.move.project'; const EVENT_MOVE_COLUMN = 'task.move.column'; const EVENT_MOVE_POSITION = 'task.move.position'; + const EVENT_MOVE_SWIMLANE = 'task.move.swimlane'; const EVENT_UPDATE = 'task.update'; const EVENT_CREATE = 'task.create'; const EVENT_CLOSE = 'task.close'; const EVENT_OPEN = 'task.open'; const EVENT_CREATE_UPDATE = 'task.create_update'; const EVENT_ASSIGNEE_CHANGE = 'task.assignee_change'; + const EVENT_OVERDUE = 'task.overdue'; /** - * Prepare data before task creation or modification + * Recurrence: status * - * @access public - * @param array $values Form values - */ - public function prepare(array &$values) - { - $this->dateParser->convert($values, array('date_due', 'date_started')); - $this->removeFields($values, array('another_task', 'id')); - $this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); - $this->convertIntegerFields($values, array('is_active')); - } - - /** - * Prepare data before task creation - * - * @access public - * @param array $values Form values - */ - public function prepareCreation(array &$values) - { - $this->prepare($values); - - if (empty($values['column_id'])) { - $values['column_id'] = $this->board->getFirstColumn($values['project_id']); - } - - if (empty($values['color_id'])) { - $colors = $this->color->getList(); - $values['color_id'] = key($colors); - } - - $values['date_creation'] = time(); - $values['date_modification'] = $values['date_creation']; - $values['position'] = $this->taskFinder->countByColumnId($values['project_id'], $values['column_id']) + 1; - } - - /** - * Prepare data before task modification - * - * @access public - * @param array $values Form values - */ - public function prepareModification(array &$values) - { - $this->prepare($values); - $values['date_modification'] = time(); - } - - /** - * Create a task - * - * @access public - * @param array $values Form values - * @return boolean|integer - */ - public function create(array $values) - { - $this->db->startTransaction(); - - $this->prepareCreation($values); - - if (! $this->db->table(self::TABLE)->save($values)) { - $this->db->cancelTransaction(); - return false; - } - - $task_id = $this->db->getConnection()->getLastId(); - - $this->db->closeTransaction(); - - // Trigger events - $this->event->trigger(self::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values); - $this->event->trigger(self::EVENT_CREATE, array('task_id' => $task_id) + $values); - - return $task_id; - } - - /** - * Update a task - * - * @access public - * @param array $values Form values - * @param boolean $trigger_Events Trigger events - * @return boolean + * @var integer */ - public function update(array $values, $trigger_events = true) - { - // Fetch original task - $original_task = $this->taskFinder->getById($values['id']); - - if (! $original_task) { - return false; - } - - // Prepare data - $updated_task = $values; - $this->prepareModification($updated_task); - - $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task); - - if ($result && $trigger_events) { - $this->triggerUpdateEvents($original_task, $updated_task); - } - - return true; - } + const RECURRING_STATUS_NONE = 0; + const RECURRING_STATUS_PENDING = 1; + const RECURRING_STATUS_PROCESSED = 2; /** - * Trigger events for task modification + * Recurrence: trigger * - * @access public - * @param array $original_task Original task data - * @param array $updated_task Updated task data + * @var integer */ - public function triggerUpdateEvents(array $original_task, array $updated_task) - { - $events = array(); - - if (isset($updated_task['owner_id']) && $original_task['owner_id'] != $updated_task['owner_id']) { - $events[] = self::EVENT_ASSIGNEE_CHANGE; - } - else if (isset($updated_task['column_id']) && $original_task['column_id'] != $updated_task['column_id']) { - $events[] = self::EVENT_MOVE_COLUMN; - } - else if (isset($updated_task['position']) && $original_task['position'] != $updated_task['position']) { - $events[] = self::EVENT_MOVE_POSITION; - } - else { - $events[] = self::EVENT_CREATE_UPDATE; - $events[] = self::EVENT_UPDATE; - } - - $event_data = array_merge($original_task, $updated_task); - $event_data['task_id'] = $original_task['id']; - - foreach ($events as $event) { - $this->event->trigger($event, $event_data); - } - } + const RECURRING_TRIGGER_FIRST_COLUMN = 0; + const RECURRING_TRIGGER_LAST_COLUMN = 1; + const RECURRING_TRIGGER_CLOSE = 2; /** - * Mark a task closed + * Recurrence: timeframe * - * @access public - * @param integer $task_id Task id - * @return boolean + * @var integer */ - public function close($task_id) - { - if (! $this->taskFinder->exists($task_id)) { - return false; - } - - $result = $this->db - ->table(self::TABLE) - ->eq('id', $task_id) - ->update(array( - 'is_active' => 0, - 'date_completed' => time() - )); - - if ($result) { - $this->event->trigger(self::EVENT_CLOSE, array('task_id' => $task_id) + $this->taskFinder->getById($task_id)); - } - - return $result; - } + const RECURRING_TIMEFRAME_DAYS = 0; + const RECURRING_TIMEFRAME_MONTHS = 1; + const RECURRING_TIMEFRAME_YEARS = 2; /** - * Mark a task open + * Recurrence: base date used to calculate new due date * - * @access public - * @param integer $task_id Task id - * @return boolean + * @var integer */ - public function open($task_id) - { - if (! $this->taskFinder->exists($task_id)) { - return false; - } - - $result = $this->db - ->table(self::TABLE) - ->eq('id', $task_id) - ->update(array( - 'is_active' => 1, - 'date_completed' => 0 - )); - - if ($result) { - $this->event->trigger(self::EVENT_OPEN, array('task_id' => $task_id) + $this->taskFinder->getById($task_id)); - } - - return $result; - } + const RECURRING_BASEDATE_DUEDATE = 0; + const RECURRING_BASEDATE_TRIGGERDATE = 1; /** * Remove a task @@ -256,242 +96,78 @@ class Task extends Base } /** - * Move a task to another column or to another position - * - * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param integer $column_id Column id - * @param integer $position Position (must be >= 1) - * @return boolean - */ - public function movePosition($project_id, $task_id, $column_id, $position) - { - // The position can't be lower than 1 - if ($position < 1) { - return false; - } - - $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id'); - $columns = array(); - - // Prepare the columns - foreach ($board as $board_column_id) { - - $columns[$board_column_id] = $this->db->table(self::TABLE) - ->eq('is_active', 1) - ->eq('project_id', $project_id) - ->eq('column_id', $board_column_id) - ->neq('id', $task_id) - ->asc('position') - ->findAllByColumn('id'); - } - - // The column must exists - if (! isset($columns[$column_id])) { - return false; - } - - // We put our task to the new position - array_splice($columns[$column_id], $position - 1, 0, $task_id); // print_r($columns); - - // We save the new positions for all tasks - return $this->savePositions($task_id, $columns); - } - - /** - * Save task positions + * Get a the task id from a text * - * @access private - * @param integer $moved_task_id Id of the moved task - * @param array $columns Sorted tasks - * @return boolean - */ - private function savePositions($moved_task_id, array $columns) - { - $this->db->startTransaction(); - - foreach ($columns as $column_id => $column) { - - $position = 1; - - foreach ($column as $task_id) { - - if ($task_id == $moved_task_id) { - - // Events will be triggered only for that task - $result = $this->update(array( - 'id' => $task_id, - 'position' => $position, - 'column_id' => $column_id - )); - } - else { - $result = $this->db->table(self::TABLE)->eq('id', $task_id)->update(array( - 'position' => $position, - 'column_id' => $column_id - )); - } - - $position++; - - if (! $result) { - $this->db->cancelTransaction(); - return false; - } - } - } - - $this->db->closeTransaction(); - - return true; - } - - /** - * Move a task to another project + * Example: "Fix bug #1234" will return 1234 * * @access public - * @param integer $project_id Project id - * @param array $task Task data - * @return boolean + * @param string $message Text + * @return integer */ - public function moveToAnotherProject($project_id, array $task) + public function getTaskIdFromText($message) { - $values = array(); - - // Clear values (categories are different for each project) - $values['category_id'] = 0; - $values['owner_id'] = 0; - - // Check if the assigned user is allowed for the new project - if ($task['owner_id'] && $this->projectPermission->isUserAllowed($project_id, $task['owner_id'])) { - $values['owner_id'] = $task['owner_id']; - } - - // We use the first column of the new project - $values['column_id'] = $this->board->getFirstColumn($project_id); - $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1; - $values['project_id'] = $project_id; - - // The task will be open (close event binding) - $values['is_active'] = 1; - - if ($this->db->table(self::TABLE)->eq('id', $task['id'])->update($values)) { - return $task['id']; + if (preg_match('!#(\d+)!i', $message, $matches) && isset($matches[1])) { + return $matches[1]; } - return false; + return 0; } /** - * Generic method to duplicate a task + * Return the list user selectable recurrence status * * @access public - * @param array $task Task data - * @param array $override Task properties to override - * @return integer|boolean + * @return array */ - public function copy(array $task, array $override = array()) + public function getRecurrenceStatusList() { - // Values to override - if (! empty($override)) { - $task = $override + $task; - } - - $this->db->startTransaction(); - - // Assign new values - $values = array(); - $values['title'] = $task['title']; - $values['description'] = $task['description']; - $values['date_creation'] = time(); - $values['date_modification'] = $values['date_creation']; - $values['date_due'] = $task['date_due']; - $values['color_id'] = $task['color_id']; - $values['project_id'] = $task['project_id']; - $values['column_id'] = $task['column_id']; - $values['owner_id'] = 0; - $values['creator_id'] = $task['creator_id']; - $values['position'] = $this->taskFinder->countByColumnId($values['project_id'], $values['column_id']) + 1; - $values['score'] = $task['score']; - $values['category_id'] = 0; - - // Check if the assigned user is allowed for the new project - if ($task['owner_id'] && $this->projectPermission->isUserAllowed($values['project_id'], $task['owner_id'])) { - $values['owner_id'] = $task['owner_id']; - } - - // Check if the category exists - if ($task['category_id'] && $this->category->exists($task['category_id'], $task['project_id'])) { - $values['category_id'] = $task['category_id']; - } - - // Save task - if (! $this->db->table(Task::TABLE)->save($values)) { - $this->db->cancelTransaction(); - return false; - } - - $task_id = $this->db->getConnection()->getLastId(); - - // Duplicate subtasks - if (! $this->subTask->duplicate($task['id'], $task_id)) { - $this->db->cancelTransaction(); - return false; - } - - $this->db->closeTransaction(); - - // Trigger events - $this->event->trigger(Task::EVENT_CREATE_UPDATE, array('task_id' => $task_id) + $values); - $this->event->trigger(Task::EVENT_CREATE, array('task_id' => $task_id) + $values); - - return $task_id; + return array ( + Task::RECURRING_STATUS_NONE => t('No'), + Task::RECURRING_STATUS_PENDING => t('Yes'), + ); } /** - * Duplicate a task to the same project + * Return the list recurrence triggers * * @access public - * @param array $task Task data - * @return integer|boolean + * @return array */ - public function duplicateToSameProject($task) + public function getRecurrenceTriggerList() { - return $this->copy($task); + return array ( + Task::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'), + Task::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'), + Task::RECURRING_TRIGGER_CLOSE => t('When task is closed'), + ); } /** - * Duplicate a task to another project (always copy to the first column) + * Return the list options to calculate recurrence due date * * @access public - * @param integer $project_id Destination project id - * @param array $task Task data - * @return integer|boolean + * @return array */ - public function duplicateToAnotherProject($project_id, array $task) + public function getRecurrenceBasedateList() { - return $this->copy($task, array( - 'project_id' => $project_id, - 'column_id' => $this->board->getFirstColumn($project_id), - )); + return array ( + Task::RECURRING_BASEDATE_DUEDATE => t('Existing due date'), + Task::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'), + ); } /** - * Get a the task id from a text - * - * Example: "Fix bug #1234" will return 1234 + * Return the list recurrence timeframes * * @access public - * @param string $message Text - * @return integer + * @return array */ - public function getTaskIdFromText($message) + public function getRecurrenceTimeframeList() { - if (preg_match('!#(\d+)!i', $message, $matches) && isset($matches[1])) { - return $matches[1]; - } - - return 0; + return array ( + Task::RECURRING_TIMEFRAME_DAYS => t('Day(s)'), + Task::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'), + Task::RECURRING_TIMEFRAME_YEARS => t('Year(s)'), + ); } } diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php new file mode 100644 index 00000000..893cbc43 --- /dev/null +++ b/app/Model/TaskCreation.php @@ -0,0 +1,82 @@ +<?php + +namespace Model; + +use Event\TaskEvent; + +/** + * Task Creation + * + * @package model + * @author Frederic Guillot + */ +class TaskCreation extends Base +{ + /** + * Create a task + * + * @access public + * @param array $values Form values + * @return integer + */ + public function create(array $values) + { + if (! $this->project->exists($values['project_id'])) { + return 0; + } + + $this->prepare($values); + $task_id = $this->persist(Task::TABLE, $values); + + if ($task_id !== false) { + $this->fireEvents($task_id, $values); + } + + return (int) $task_id; + } + + /** + * Prepare data + * + * @access public + * @param array $values Form values + */ + public function prepare(array &$values) + { + $this->dateParser->convert($values, array('date_due', 'date_started')); + $this->removeFields($values, array('another_task')); + $this->resetFields($values, array('owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); + + if (empty($values['column_id'])) { + $values['column_id'] = $this->board->getFirstColumn($values['project_id']); + } + + if (empty($values['color_id'])) { + $values['color_id'] = $this->color->getDefaultColor(); + } + + if (empty($values['title'])) { + $values['title'] = t('Untitled'); + } + + $values['swimlane_id'] = empty($values['swimlane_id']) ? 0 : $values['swimlane_id']; + $values['date_creation'] = time(); + $values['date_modification'] = $values['date_creation']; + $values['date_moved'] = $values['date_creation']; + $values['position'] = $this->taskFinder->countByColumnAndSwimlaneId($values['project_id'], $values['column_id'], $values['swimlane_id']) + 1; + } + + /** + * Fire events + * + * @access private + * @param integer $task_id Task id + * @param array $values Form values + */ + private function fireEvents($task_id, array $values) + { + $values['task_id'] = $task_id; + $this->container['dispatcher']->dispatch(Task::EVENT_CREATE_UPDATE, new TaskEvent($values)); + $this->container['dispatcher']->dispatch(Task::EVENT_CREATE, new TaskEvent($values)); + } +} diff --git a/app/Model/TaskDuplication.php b/app/Model/TaskDuplication.php new file mode 100755 index 00000000..afcac4c7 --- /dev/null +++ b/app/Model/TaskDuplication.php @@ -0,0 +1,248 @@ +<?php + +namespace Model; + +use DateTime; +use DateInterval; +use Event\TaskEvent; + +/** + * Task Duplication + * + * @package model + * @author Frederic Guillot + */ +class TaskDuplication extends Base +{ + /** + * Fields to copy when duplicating a task + * + * @access private + * @var array + */ + private $fields_to_duplicate = array( + 'title', + 'description', + 'date_due', + 'color_id', + 'project_id', + 'column_id', + 'owner_id', + 'score', + 'category_id', + 'time_estimated', + 'swimlane_id', + 'recurrence_status', + 'recurrence_trigger', + 'recurrence_factor', + 'recurrence_timeframe', + 'recurrence_basedate', + ); + + /** + * Duplicate a task to the same project + * + * @access public + * @param integer $task_id Task id + * @return boolean|integer Duplicated task id + */ + public function duplicate($task_id) + { + return $this->save($task_id, $this->copyFields($task_id)); + } + + /** + * Duplicate recurring task + * + * @access public + * @param integer $task_id Task id + * @return boolean|integer Recurrence task id + */ + public function duplicateRecurringTask($task_id) + { + $values = $this->copyFields($task_id); + + if ($values['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { + + $values['recurrence_parent'] = $task_id; + $values['column_id'] = $this->board->getFirstColumn($values['project_id']); + $this->calculateRecurringTaskDueDate($values); + + $recurring_task_id = $this->save($task_id, $values); + + if ($recurring_task_id > 0) { + + $parent_update = $this->db + ->table(Task::TABLE) + ->eq('id', $task_id) + ->update(array( + 'recurrence_status' => Task::RECURRING_STATUS_PROCESSED, + 'recurrence_child' => $recurring_task_id, + )); + + if ($parent_update) { + return $recurring_task_id; + } + } + } + + return false; + } + + /** + * Duplicate a task to another project + * + * @access public + * @param integer $task_id Task id + * @param integer $project_id Project id + * @return boolean|integer Duplicated task id + */ + public function duplicateToProject($task_id, $project_id) + { + $values = $this->copyFields($task_id); + $values['project_id'] = $project_id; + $values['column_id'] = $this->board->getFirstColumn($project_id); + + $this->checkDestinationProjectValues($values); + + return $this->save($task_id, $values); + } + + /** + * Move a task to another project + * + * @access public + * @param integer $task_id Task id + * @param integer $project_id Project id + * @return boolean + */ + public function moveToProject($task_id, $project_id) + { + $task = $this->taskFinder->getById($task_id); + + $values = array(); + $values['is_active'] = 1; + $values['project_id'] = $project_id; + $values['column_id'] = $this->board->getFirstColumn($project_id); + $values['position'] = $this->taskFinder->countByColumnId($project_id, $values['column_id']) + 1; + $values['owner_id'] = $task['owner_id']; + $values['category_id'] = $task['category_id']; + $values['swimlane_id'] = $task['swimlane_id']; + + $this->checkDestinationProjectValues($values); + + if ($this->db->table(Task::TABLE)->eq('id', $task['id'])->update($values)) { + $this->container['dispatcher']->dispatch( + Task::EVENT_MOVE_PROJECT, + new TaskEvent(array_merge($task, $values, array('task_id' => $task['id']))) + ); + } + + return true; + } + + /** + * Check if the assignee and the category are available in the destination project + * + * @access private + * @param array $values + */ + private function checkDestinationProjectValues(&$values) + { + // Check if the assigned user is allowed for the destination project + if ($values['owner_id'] > 0 && ! $this->projectPermission->isUserAllowed($values['project_id'], $values['owner_id'])) { + $values['owner_id'] = 0; + } + + // Check if the category exists for the destination project + if ($values['category_id'] > 0) { + $values['category_id'] = $this->category->getIdByName( + $values['project_id'], + $this->category->getNameById($values['category_id']) + ); + } + + // Check if the swimlane exists for the destination project + if ($values['swimlane_id'] > 0) { + $values['swimlane_id'] = $this->swimlane->getIdByName( + $values['project_id'], + $this->swimlane->getNameById($values['swimlane_id']) + ); + } + } + + /** + * Calculate new due date for new recurrence task + * + * @access public + * @param array $values Task fields + */ + public function calculateRecurringTaskDueDate(array &$values) + { + if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) { + + if ($values['recurrence_basedate'] == Task::RECURRING_BASEDATE_TRIGGERDATE) { + $values['date_due'] = time(); + } + + $factor = abs($values['recurrence_factor']); + $subtract = $values['recurrence_factor'] < 0; + + switch ($values['recurrence_timeframe']) { + case Task::RECURRING_TIMEFRAME_MONTHS: + $interval = 'P' . $factor . 'M'; + break; + case Task::RECURRING_TIMEFRAME_YEARS: + $interval = 'P' . $factor . 'Y'; + break; + default: + $interval = 'P' . $factor . 'D'; + } + + $date_due = new DateTime(); + $date_due->setTimestamp($values['date_due']); + + $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval)); + + $values['date_due'] = $date_due->getTimestamp(); + } + } + + /** + * Duplicate fields for the new task + * + * @access private + * @param integer $task_id Task id + * @return array + */ + private function copyFields($task_id) + { + $task = $this->taskFinder->getById($task_id); + $values = array(); + + foreach ($this->fields_to_duplicate as $field) { + $values[$field] = $task[$field]; + } + + return $values; + } + + /** + * Create the new task and duplicate subtasks + * + * @access private + * @param integer $task_id Task id + * @param array $values Form values + * @return boolean|integer + */ + private function save($task_id, array $values) + { + $new_task_id = $this->taskCreation->create($values); + + if ($new_task_id) { + $this->subtask->duplicate($task_id, $new_task_id); + } + + return $new_task_id; + } +} diff --git a/app/Model/TaskExport.php b/app/Model/TaskExport.php index b929823e..90aa1964 100644 --- a/app/Model/TaskExport.php +++ b/app/Model/TaskExport.php @@ -24,10 +24,11 @@ class TaskExport extends Base public function export($project_id, $from, $to) { $tasks = $this->getTasks($project_id, $from, $to); + $swimlanes = $this->swimlane->getList($project_id); $results = array($this->getColumns()); foreach ($tasks as &$task) { - $results[] = array_values($this->format($task)); + $results[] = array_values($this->format($task, $swimlanes)); } return $results; @@ -50,6 +51,7 @@ class TaskExport extends Base projects.name AS project_name, tasks.is_active, project_has_categories.name AS category_name, + tasks.swimlane_id, columns.title AS column_title, tasks.position, tasks.color_id, @@ -71,14 +73,15 @@ class TaskExport extends Base LEFT JOIN columns ON columns.id = tasks.column_id LEFT JOIN projects ON projects.id = tasks.project_id WHERE tasks.date_creation >= ? AND tasks.date_creation <= ? AND tasks.project_id = ? + ORDER BY tasks.id ASC '; if (! is_numeric($from)) { - $from = $this->dateParser->resetDateToMidnight($this->dateParser->getTimestamp($from)); + $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); } if (! is_numeric($to)) { - $to = $this->dateParser->resetDateToMidnight(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); } $rq = $this->db->execute($sql, array($from, $to, $project_id)); @@ -89,15 +92,18 @@ class TaskExport extends Base * Format the output of a task array * * @access public - * @param array $task Task properties + * @param array $task Task properties + * @param array $swimlanes List of swimlanes * @return array */ - public function format(array &$task) + public function format(array &$task, array &$swimlanes) { $colors = $this->color->getList(); $task['is_active'] = $task['is_active'] == Task::STATUS_OPEN ? e('Open') : e('Closed'); $task['color_id'] = $colors[$task['color_id']]; + $task['score'] = $task['score'] ?: 0; + $task['swimlane_id'] = isset($swimlanes[$task['swimlane_id']]) ? $swimlanes[$task['swimlane_id']] : '?'; $this->dateParser->format($task, array('date_due', 'date_modification', 'date_creation', 'date_started', 'date_completed'), 'Y-m-d'); @@ -108,7 +114,7 @@ class TaskExport extends Base * Get column titles * * @access public - * @return array + * @return string[] */ public function getColumns() { @@ -117,6 +123,7 @@ class TaskExport extends Base e('Project'), e('Status'), e('Category'), + e('Swimlane'), e('Column'), e('Position'), e('Color'), diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php new file mode 100644 index 00000000..134f67cd --- /dev/null +++ b/app/Model/TaskFilter.php @@ -0,0 +1,467 @@ +<?php + +namespace Model; + +use DateTime; +use Eluceo\iCal\Component\Calendar; +use Eluceo\iCal\Component\Event; +use Eluceo\iCal\Property\Event\Attendees; + +/** + * Task Filter + * + * @package model + * @author Frederic Guillot + */ +class TaskFilter extends Base +{ + /** + * Query + * + * @access public + * @var \PicoDb\Table + */ + public $query; + + /** + * Create a new query + * + * @access public + * @return TaskFilter + */ + public function create() + { + $this->query = $this->db->table(Task::TABLE); + $this->query->left(User::TABLE, 'ua', 'id', Task::TABLE, 'owner_id'); + $this->query->left(User::TABLE, 'uc', 'id', Task::TABLE, 'creator_id'); + + $this->query->columns( + Task::TABLE.'.*', + 'ua.email AS assignee_email', + 'ua.username AS assignee_username', + 'uc.email AS creator_email', + 'uc.username AS creator_username' + ); + + return $this; + } + + /** + * Clone the filter + * + * @access public + * @return TaskFilter + */ + public function copy() + { + $filter = clone($this); + $filter->query = clone($this->query); + return $filter; + } + + /** + * Exclude a list of task_id + * + * @access public + * @param array $task_ids + * @return TaskFilter + */ + public function excludeTasks(array $task_ids) + { + $this->query->notin(Task::TABLE.'.id', $task_ids); + return $this; + } + + /** + * Filter by id + * + * @access public + * @param integer $task_id + * @return TaskFilter + */ + public function filterById($task_id) + { + if ($task_id > 0) { + $this->query->eq(Task::TABLE.'.id', $task_id); + } + + return $this; + } + + /** + * Filter by title + * + * @access public + * @param string $title + * @return TaskFilter + */ + public function filterByTitle($title) + { + $this->query->ilike('title', '%'.$title.'%'); + return $this; + } + + /** + * Filter by a list of project id + * + * @access public + * @param array $project_ids + * @return TaskFilter + */ + public function filterByProjects(array $project_ids) + { + $this->query->in('project_id', $project_ids); + return $this; + } + + /** + * Filter by project id + * + * @access public + * @param integer $project_id + * @return TaskFilter + */ + public function filterByProject($project_id) + { + if ($project_id > 0) { + $this->query->eq('project_id', $project_id); + } + + return $this; + } + + /** + * Filter by category id + * + * @access public + * @param integer $category_id + * @return TaskFilter + */ + public function filterByCategory($category_id) + { + if ($category_id >= 0) { + $this->query->eq('category_id', $category_id); + } + + return $this; + } + + /** + * Filter by assignee + * + * @access public + * @param integer $owner_id + * @return TaskFilter + */ + public function filterByOwner($owner_id) + { + if ($owner_id >= 0) { + $this->query->eq('owner_id', $owner_id); + } + + return $this; + } + + /** + * Filter by color + * + * @access public + * @param string $color_id + * @return TaskFilter + */ + public function filterByColor($color_id) + { + if ($color_id !== '') { + $this->query->eq('color_id', $color_id); + } + + return $this; + } + + /** + * Filter by column + * + * @access public + * @param integer $column_id + * @return TaskFilter + */ + public function filterByColumn($column_id) + { + if ($column_id >= 0) { + $this->query->eq('column_id', $column_id); + } + + return $this; + } + + /** + * Filter by swimlane + * + * @access public + * @param integer $swimlane_id + * @return TaskFilter + */ + public function filterBySwimlane($swimlane_id) + { + if ($swimlane_id >= 0) { + $this->query->eq('swimlane_id', $swimlane_id); + } + + return $this; + } + + /** + * Filter by status + * + * @access public + * @param integer $is_active + * @return TaskFilter + */ + public function filterByStatus($is_active) + { + if ($is_active >= 0) { + $this->query->eq('is_active', $is_active); + } + + return $this; + } + + /** + * Filter by due date (range) + * + * @access public + * @param string $start + * @param string $end + * @return TaskFilter + */ + public function filterByDueDateRange($start, $end) + { + $this->query->gte('date_due', $this->dateParser->getTimestampFromIsoFormat($start)); + $this->query->lte('date_due', $this->dateParser->getTimestampFromIsoFormat($end)); + + return $this; + } + + /** + * Filter by start date (range) + * + * @access public + * @param string $start + * @param strings $end + * @return TaskFilter + */ + public function filterByStartDateRange($start, $end) + { + $this->query->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + 'date_started', + 'date_completed' + )); + + return $this; + } + + /** + * Filter by creation date + * + * @access public + * @param string $start + * @param string $end + * @return TaskFilter + */ + public function filterByCreationDateRange($start, $end) + { + $this->query->addCondition($this->getCalendarCondition( + $this->dateParser->getTimestampFromIsoFormat($start), + $this->dateParser->getTimestampFromIsoFormat($end), + 'date_creation', + 'date_completed' + )); + + return $this; + } + + /** + * Get all results of the filter + * + * @access public + * @return array + */ + public function findAll() + { + return $this->query->findAll(); + } + + /** + * Format the results to the ajax autocompletion + * + * @access public + * @return array + */ + public function toAutoCompletion() + { + return $this->query->columns('id', 'title')->filter(function(array $results) { + + foreach ($results as &$result) { + $result['value'] = $result['title']; + $result['label'] = '#'.$result['id'].' - '.$result['title']; + } + + return $results; + + })->findAll(); + } + + /** + * Transform results to calendar events + * + * @access public + * @param string $start_column Column name for the start date + * @param string $end_column Column name for the end date + * @return array + */ + public function toDateTimeCalendarEvents($start_column, $end_column) + { + $events = array(); + + foreach ($this->query->findAll() as $task) { + + $events[] = array_merge( + $this->getTaskCalendarProperties($task), + array( + 'start' => date('Y-m-d\TH:i:s', $task[$start_column]), + 'end' => date('Y-m-d\TH:i:s', $task[$end_column] ?: time()), + 'editable' => false, + ) + ); + } + + return $events; + } + + /** + * Transform results to all day calendar events + * + * @access public + * @param string $column Column name for the date + * @return array + */ + public function toAllDayCalendarEvents($column = 'date_due') + { + $events = array(); + + foreach ($this->query->findAll() as $task) { + + $events[] = array_merge( + $this->getTaskCalendarProperties($task), + array( + 'start' => date('Y-m-d', $task[$column]), + 'end' => date('Y-m-d', $task[$column]), + 'allday' => true, + ) + ); + } + + return $events; + } + + /** + * Transform results to ical events + * + * @access public + * @param string $start_column Column name for the start date + * @param string $end_column Column name for the end date + * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object + * @return Eluceo\iCal\Component\Calendar + */ + public function addDateTimeIcalEvents($start_column, $end_column, Calendar $vCalendar = null) + { + if ($vCalendar === null) { + $vCalendar = new Calendar('Kanboard'); + } + + foreach ($this->query->findAll() as $task) { + + $start = new DateTime; + $start->setTimestamp($task[$start_column]); + + $end = new DateTime; + $end->setTimestamp($task[$end_column] ?: time()); + + $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$start_column.'-'.$end_column); + $vEvent->setDtStart($start); + $vEvent->setDtEnd($end); + + $vCalendar->addComponent($vEvent); + } + + return $vCalendar; + } + + /** + * Transform results to all day ical events + * + * @access public + * @param string $column Column name for the date + * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object + * @return Eluceo\iCal\Component\Calendar + */ + public function addAllDayIcalEvents($column = 'date_due', Calendar $vCalendar = null) + { + if ($vCalendar === null) { + $vCalendar = new Calendar('Kanboard'); + } + + foreach ($this->query->findAll() as $task) { + + $date = new DateTime; + $date->setTimestamp($task[$column]); + + $vEvent = $this->getTaskIcalEvent($task, 'task-#'.$task['id'].'-'.$column); + $vEvent->setDtStart($date); + $vEvent->setDtEnd($date); + $vEvent->setNoTime(true); + + $vCalendar->addComponent($vEvent); + } + + return $vCalendar; + } + + /** + * Get common events for task ical events + * + * @access protected + * @param array $task + * @param string $uid + * @return Eluceo\iCal\Component\Event + */ + protected function getTaskIcalEvent(array &$task, $uid) + { + $dateCreation = new DateTime; + $dateCreation->setTimestamp($task['date_creation']); + + $dateModif = new DateTime; + $dateModif->setTimestamp($task['date_modification']); + + $vEvent = new Event($uid); + $vEvent->setCreated($dateCreation); + $vEvent->setModified($dateModif); + $vEvent->setUseTimezone(true); + $vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']); + $vEvent->setUrl($this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); + + if (! empty($task['creator_id'])) { + $vEvent->setOrganizer('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); + } + + if (! empty($task['owner_id'])) { + $attendees = new Attendees; + $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); + $vEvent->setAttendees($attendees); + } + + return $vEvent; + } +} diff --git a/app/Model/TaskFinder.php b/app/Model/TaskFinder.php index 56795152..5a1d33c6 100644 --- a/app/Model/TaskFinder.php +++ b/app/Model/TaskFinder.php @@ -13,20 +13,78 @@ use PDO; class TaskFinder extends Base { /** - * Common request to fetch a list of tasks + * Get query for closed tasks * - * @access private + * @access public + * @param integer $project_id Project id + * @return \PicoDb\Table + */ + public function getClosedTaskQuery($project_id) + { + return $this->getExtendedQuery() + ->eq('project_id', $project_id) + ->eq('is_active', Task::STATUS_CLOSED); + } + + /** + * Get query for task search + * + * @access public + * @param integer $project_id Project id + * @param string $search Search terms * @return \PicoDb\Table */ - private function prepareRequestList() + public function getSearchQuery($project_id, $search) + { + return $this->getExtendedQuery() + ->eq('project_id', $project_id) + ->ilike('title', '%'.$search.'%'); + } + + /** + * Get query for assigned user tasks + * + * @access public + * @param integer $user_id User id + * @return \PicoDb\Table + */ + public function getUserQuery($user_id) + { + return $this->db + ->table(Task::TABLE) + ->columns( + 'tasks.id', + 'tasks.title', + 'tasks.date_due', + 'tasks.date_creation', + 'tasks.project_id', + 'tasks.color_id', + 'tasks.time_spent', + 'tasks.time_estimated', + 'projects.name AS project_name' + ) + ->join(Project::TABLE, 'id', 'project_id') + ->eq(Task::TABLE.'.owner_id', $user_id) + ->eq(Task::TABLE.'.is_active', Task::STATUS_OPEN) + ->eq(Project::TABLE.'.is_active', Project::ACTIVE); + } + + /** + * Extended query + * + * @access public + * @return \PicoDb\Table + */ + public function getExtendedQuery() { return $this->db ->table(Task::TABLE) ->columns( - '(SELECT count(*) FROM comments WHERE task_id=tasks.id) AS nb_comments', - '(SELECT count(*) FROM task_has_files WHERE task_id=tasks.id) AS nb_files', - '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id) AS nb_subtasks', - '(SELECT count(*) FROM task_has_subtasks WHERE task_id=tasks.id AND status=2) AS nb_completed_subtasks', + '(SELECT count(*) FROM '.Comment::TABLE.' WHERE task_id=tasks.id) AS nb_comments', + '(SELECT count(*) FROM '.File::TABLE.' WHERE task_id=tasks.id) AS nb_files', + '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id) AS nb_subtasks', + '(SELECT count(*) FROM '.Subtask::TABLE.' WHERE '.Subtask::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks', + '(SELECT count(*) FROM '.TaskLink::TABLE.' WHERE '.TaskLink::TABLE.'.task_id = tasks.id) AS nb_links', 'tasks.id', 'tasks.reference', 'tasks.title', @@ -38,12 +96,21 @@ class TaskFinder extends Base 'tasks.color_id', 'tasks.project_id', 'tasks.column_id', + 'tasks.swimlane_id', 'tasks.owner_id', 'tasks.creator_id', 'tasks.position', 'tasks.is_active', 'tasks.score', 'tasks.category_id', + 'tasks.date_moved', + 'tasks.recurrence_status', + 'tasks.recurrence_trigger', + 'tasks.recurrence_factor', + 'tasks.recurrence_timeframe', + 'tasks.recurrence_basedate', + 'tasks.recurrence_parent', + 'tasks.recurrence_child', 'users.username AS assignee_username', 'users.name AS assignee_name' ) @@ -51,94 +118,26 @@ class TaskFinder extends Base } /** - * Task search with pagination - * - * @access public - * @param integer $project_id Project id - * @param string $search Search terms - * @param integer $offset Offset - * @param integer $limit Limit - * @param string $column Sorting column - * @param string $direction Sorting direction - * @return array - */ - public function search($project_id, $search, $offset = 0, $limit = 25, $column = 'tasks.id', $direction = 'DESC') - { - return $this->prepareRequestList() - ->eq('project_id', $project_id) - ->like('title', '%'.$search.'%') - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); - } - - /** - * Get all completed tasks with pagination - * - * @access public - * @param integer $project_id Project id - * @param integer $offset Offset - * @param integer $limit Limit - * @param string $column Sorting column - * @param string $direction Sorting direction - * @return array - */ - public function getClosedTasks($project_id, $offset = 0, $limit = 25, $column = 'tasks.date_completed', $direction = 'DESC') - { - return $this->prepareRequestList() - ->eq('project_id', $project_id) - ->eq('is_active', Task::STATUS_CLOSED) - ->offset($offset) - ->limit($limit) - ->orderBy($column, $direction) - ->findAll(); - } - - /** * Get all tasks shown on the board (sorted by position) * * @access public - * @param integer $project_id Project id + * @param integer $project_id Project id + * @param integer $column_id Column id + * @param integer $swimlane_id Swimlane id * @return array */ - public function getTasksOnBoard($project_id) + public function getTasksByColumnAndSwimlane($project_id, $column_id, $swimlane_id = 0) { - return $this->prepareRequestList() + return $this->getExtendedQuery() ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('swimlane_id', $swimlane_id) ->eq('is_active', Task::STATUS_OPEN) ->asc('tasks.position') ->findAll(); } /** - * Get all open tasks for a given user - * - * @access public - * @param integer $user_id User id - * @return array - */ - public function getAllTasksByUser($user_id) - { - return $this->db - ->table(Task::TABLE) - ->columns( - 'tasks.id', - 'tasks.title', - 'tasks.date_due', - 'tasks.date_creation', - 'tasks.project_id', - 'tasks.color_id', - 'projects.name AS project_name' - ) - ->join(Project::TABLE, 'id', 'project_id') - ->eq('tasks.owner_id', $user_id) - ->eq('tasks.is_active', Task::STATUS_OPEN) - ->asc('tasks.id') - ->findAll(); - } - - /** * Get all tasks for a given project and status * * @access public @@ -169,6 +168,8 @@ class TaskFinder extends Base Task::TABLE.'.title', Task::TABLE.'.date_due', Task::TABLE.'.project_id', + Task::TABLE.'.creator_id', + Task::TABLE.'.owner_id', Project::TABLE.'.name AS project_name', User::TABLE.'.username AS assignee_username', User::TABLE.'.name AS assignee_name' @@ -185,6 +186,18 @@ class TaskFinder extends Base } /** + * Get project id for a given task + * + * @access public + * @param integer $task_id Task id + * @return integer + */ + public function getProjectId($task_id) + { + return (int) $this->db->table(Task::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0; + } + + /** * Fetch a task by the id * * @access public @@ -200,12 +213,13 @@ class TaskFinder extends Base * Fetch a task by the reference (external id) * * @access public + * @param integer $project_id Project id * @param string $reference Task reference * @return array */ - public function getByReference($reference) + public function getByReference($project_id, $reference) { - return $this->db->table(Task::TABLE)->eq('reference', $reference)->findOne(); + return $this->db->table(Task::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne(); } /** @@ -239,6 +253,15 @@ class TaskFinder extends Base tasks.is_active, tasks.score, tasks.category_id, + tasks.swimlane_id, + tasks.date_moved, + tasks.recurrence_status, + tasks.recurrence_trigger, + tasks.recurrence_factor, + tasks.recurrence_timeframe, + tasks.recurrence_basedate, + tasks.recurrence_parent, + tasks.recurrence_child, project_has_categories.name AS category_name, projects.name AS project_name, columns.title AS column_title, @@ -282,33 +305,36 @@ class TaskFinder extends Base * @access public * @param integer $project_id Project id * @param integer $column_id Column id - * @param array $status List of status id * @return integer */ - public function countByColumnId($project_id, $column_id, array $status = array(Task::STATUS_OPEN)) + public function countByColumnId($project_id, $column_id) { return $this->db ->table(Task::TABLE) ->eq('project_id', $project_id) ->eq('column_id', $column_id) - ->in('is_active', $status) + ->in('is_active', 1) ->count(); } /** - * Count the number of tasks for a custom search + * Count the number of tasks for a given column and swimlane * * @access public - * @param integer $project_id Project id - * @param string $search Search terms + * @param integer $project_id Project id + * @param integer $column_id Column id + * @param integer $swimlane_id Swimlane id * @return integer */ - public function countSearch($project_id, $search) + public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id) { - return $this->db->table(Task::TABLE) - ->eq('project_id', $project_id) - ->like('title', '%'.$search.'%') - ->count(); + return $this->db + ->table(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('swimlane_id', $swimlane_id) + ->in('is_active', 1) + ->count(); } /** diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php new file mode 100644 index 00000000..251460c9 --- /dev/null +++ b/app/Model/TaskLink.php @@ -0,0 +1,273 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; +use PicoDb\Table; + +/** + * TaskLink model + * + * @package model + * @author Olivier Maridat + * @author Frederic Guillot + */ +class TaskLink extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'task_has_links'; + + /** + * Get a task link + * + * @access public + * @param integer $task_link_id Task link id + * @return array + */ + public function getById($task_link_id) + { + return $this->db->table(self::TABLE)->eq('id', $task_link_id)->findOne(); + } + + /** + * Get the opposite task link (use the unique index task_has_links_unique) + * + * @access public + * @param array $task_link + * @return array + */ + public function getOppositeTaskLink(array $task_link) + { + $opposite_link_id = $this->link->getOppositeLinkId($task_link['link_id']); + + return $this->db->table(self::TABLE) + ->eq('opposite_task_id', $task_link['task_id']) + ->eq('task_id', $task_link['opposite_task_id']) + ->eq('link_id', $opposite_link_id) + ->findOne(); + } + + /** + * Get all links attached to a task + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAll($task_id) + { + return $this->db + ->table(self::TABLE) + ->columns( + self::TABLE.'.id', + self::TABLE.'.opposite_task_id AS task_id', + Link::TABLE.'.label', + Task::TABLE.'.title', + Task::TABLE.'.is_active', + Task::TABLE.'.project_id', + Task::TABLE.'.time_spent AS task_time_spent', + Task::TABLE.'.time_estimated AS task_time_estimated', + Task::TABLE.'.owner_id AS task_assignee_id', + User::TABLE.'.username AS task_assignee_username', + User::TABLE.'.name AS task_assignee_name', + Board::TABLE.'.title AS column_title' + ) + ->eq(self::TABLE.'.task_id', $task_id) + ->join(Link::TABLE, 'id', 'link_id') + ->join(Task::TABLE, 'id', 'opposite_task_id') + ->join(Board::TABLE, 'id', 'column_id', Task::TABLE) + ->join(User::TABLE, 'id', 'owner_id', Task::TABLE) + ->orderBy(Link::TABLE.'.id ASC, '.Board::TABLE.'.position ASC, '.Task::TABLE.'.is_active DESC, '.Task::TABLE.'.id', Table::SORT_ASC) + ->findAll(); + } + + /** + * Get all links attached to a task grouped by label + * + * @access public + * @param integer $task_id Task id + * @return array + */ + public function getAllGroupedByLabel($task_id) + { + $links = $this->getAll($task_id); + $result = array(); + + foreach ($links as $link) { + + if (! isset($result[$link['label']])) { + $result[$link['label']] = array(); + } + + $result[$link['label']][] = $link; + } + + return $result; + } + + /** + * Create a new link + * + * @access public + * @param integer $task_id Task id + * @param integer $opposite_task_id Opposite task id + * @param integer $link_id Link id + * @return integer Task link id + */ + public function create($task_id, $opposite_task_id, $link_id) + { + $this->db->startTransaction(); + + // Get opposite link + $opposite_link_id = $this->link->getOppositeLinkId($link_id); + + // Create the original task link + $this->db->table(self::TABLE)->insert(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + + $task_link_id = $this->db->getConnection()->getLastId(); + + // Create the opposite task link + $this->db->table(self::TABLE)->insert(array( + 'task_id' => $opposite_task_id, + 'opposite_task_id' => $task_id, + 'link_id' => $opposite_link_id, + )); + + $this->db->closeTransaction(); + + return (int) $task_link_id; + } + + /** + * Update a task link + * + * @access public + * @param integer $task_link_id Task link id + * @param integer $task_id Task id + * @param integer $opposite_task_id Opposite task id + * @param integer $link_id Link id + * @return boolean + */ + public function update($task_link_id, $task_id, $opposite_task_id, $link_id) + { + $this->db->startTransaction(); + + // Get original task link + $task_link = $this->getById($task_link_id); + + // Find opposite task link + $opposite_task_link = $this->getOppositeTaskLink($task_link); + + // Get opposite link + $opposite_link_id = $this->link->getOppositeLinkId($link_id); + + // Update the original task link + $rs1 = $this->db->table(self::TABLE)->eq('id', $task_link_id)->update(array( + 'task_id' => $task_id, + 'opposite_task_id' => $opposite_task_id, + 'link_id' => $link_id, + )); + + // Update the opposite link + $rs2 = $this->db->table(self::TABLE)->eq('id', $opposite_task_link['id'])->update(array( + 'task_id' => $opposite_task_id, + 'opposite_task_id' => $task_id, + 'link_id' => $opposite_link_id, + )); + + $this->db->closeTransaction(); + + return $rs1 && $rs2; + } + + /** + * Remove a link between two tasks + * + * @access public + * @param integer $task_link_id + * @return boolean + */ + public function remove($task_link_id) + { + $this->db->startTransaction(); + + $link = $this->getById($task_link_id); + $link_id = $this->link->getOppositeLinkId($link['link_id']); + + $this->db->table(self::TABLE)->eq('id', $task_link_id)->remove(); + + $this->db + ->table(self::TABLE) + ->eq('opposite_task_id', $link['task_id']) + ->eq('task_id', $link['opposite_task_id']) + ->eq('link_id', $link_id)->remove(); + + $this->db->closeTransaction(); + + return true; + } + + /** + * Common validation rules + * + * @access private + * @return array + */ + private function commonValidationRules() + { + return array( + new Validators\Required('task_id', t('Field required')), + new Validators\Required('opposite_task_id', t('Field required')), + new Validators\Required('link_id', t('Field required')), + new Validators\NotEquals('opposite_task_id', 'task_id', t('A task cannot be linked to itself')), + new Validators\Exists('opposite_task_id', t('This linked task id doesn\'t exists'), $this->db->getConnection(), Task::TABLE, 'id') + ); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, $this->commonValidationRules()); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + /** + * Validate modification + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateModification(array $values) + { + $rules = array( + new Validators\Required('id', t('Field required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php new file mode 100644 index 00000000..677fcd60 --- /dev/null +++ b/app/Model/TaskModification.php @@ -0,0 +1,74 @@ +<?php + +namespace Model; + +use Event\TaskEvent; + +/** + * Task Modification + * + * @package model + * @author Frederic Guillot + */ +class TaskModification extends Base +{ + /** + * Update a task + * + * @access public + * @param array $values + * @return boolean + */ + public function update(array $values) + { + $original_task = $this->taskFinder->getById($values['id']); + + $this->prepare($values); + $result = $this->db->table(Task::TABLE)->eq('id', $original_task['id'])->update($values); + + if ($result) { + $this->fireEvents($original_task, $values); + } + + return $result; + } + + /** + * Fire events + * + * @access public + * @param array $task + * @param array $new_values + */ + public function fireEvents(array $task, array $new_values) + { + $event_data = array_merge($task, $new_values, array('task_id' => $task['id'])); + + if (isset($new_values['owner_id']) && $task['owner_id'] != $new_values['owner_id']) { + $events = array(Task::EVENT_ASSIGNEE_CHANGE); + } + else { + $events = array(Task::EVENT_CREATE_UPDATE, Task::EVENT_UPDATE); + } + + foreach ($events as $event) { + $this->container['dispatcher']->dispatch($event, new TaskEvent($event_data)); + } + } + + /** + * Prepare data before task modification + * + * @access public + * @param array $values Form values + */ + public function prepare(array &$values) + { + $this->dateParser->convert($values, array('date_due', 'date_started')); + $this->removeFields($values, array('another_task', 'id')); + $this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); + $this->convertIntegerFields($values, array('is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); + + $values['date_modification'] = time(); + } +} diff --git a/app/Model/TaskPermission.php b/app/Model/TaskPermission.php index 2ab154f4..e2420e10 100644 --- a/app/Model/TaskPermission.php +++ b/app/Model/TaskPermission.php @@ -20,10 +20,10 @@ class TaskPermission extends Base */ public function canRemoveTask(array $task) { - if ($this->acl->isAdminUser()) { + if ($this->userSession->isAdmin() || $this->projectPermission->isManager($task['project_id'], $this->userSession->getId())) { return true; } - else if (isset($task['creator_id']) && $task['creator_id'] == $this->acl->getUserId()) { + else if (isset($task['creator_id']) && $task['creator_id'] == $this->userSession->getId()) { return true; } diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php new file mode 100644 index 00000000..0c4beb2d --- /dev/null +++ b/app/Model/TaskPosition.php @@ -0,0 +1,185 @@ +<?php + +namespace Model; + +use Event\TaskEvent; + +/** + * Task Position + * + * @package model + * @author Frederic Guillot + */ +class TaskPosition extends Base +{ + /** + * Move a task to another column or to another position + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id + * @param boolean $fire_events Fire events + * @return boolean + */ + public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true) + { + $original_task = $this->taskFinder->getById($task_id); + + $result = $this->calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id); + + if ($result) { + + if ($original_task['swimlane_id'] != $swimlane_id) { + $this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['swimlane_id']); + } + + if ($fire_events) { + $this->fireEvents($original_task, $column_id, $position, $swimlane_id); + } + } + + return $result; + } + + /** + * Calculate the new position of all tasks + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id + * @return array|boolean + */ + public function calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id = 0) + { + // The position can't be lower than 1 + if ($position < 1) { + return false; + } + + $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id'); + $columns = array(); + + // For each column fetch all tasks ordered by position + foreach ($board as $board_column_id) { + + $columns[$board_column_id] = $this->db->table(Task::TABLE) + ->eq('is_active', 1) + ->eq('swimlane_id', $swimlane_id) + ->eq('project_id', $project_id) + ->eq('column_id', $board_column_id) + ->neq('id', $task_id) + ->asc('position') + ->asc('id') // Fix Postgresql unit test + ->findAllByColumn('id'); + } + + // The column must exists + if (! isset($columns[$column_id])) { + return false; + } + + // We put our task to the new position + if ($task_id) { + array_splice($columns[$column_id], $position - 1, 0, $task_id); + } + + return $columns; + } + + /** + * Save task positions + * + * @access private + * @param array $columns Sorted tasks + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + private function savePositions(array $columns, $swimlane_id) + { + return $this->db->transaction(function ($db) use ($columns, $swimlane_id) { + + foreach ($columns as $column_id => $column) { + + $position = 1; + + foreach ($column as $task_id) { + + $result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array( + 'position' => $position, + 'column_id' => $column_id, + 'swimlane_id' => $swimlane_id, + )); + + if (! $result) { + return false; + } + + $position++; + } + } + }); + } + + /** + * Fire events + * + * @access private + * @param array $task + * @param integer $new_column_id + * @param integer $new_position + * @param integer $new_swimlane_id + */ + private function fireEvents(array $task, $new_column_id, $new_position, $new_swimlane_id) + { + $event_data = array( + 'task_id' => $task['id'], + 'project_id' => $task['project_id'], + 'position' => $new_position, + 'column_id' => $new_column_id, + 'swimlane_id' => $new_swimlane_id, + 'src_column_id' => $task['column_id'], + 'dst_column_id' => $new_column_id, + 'date_moved' => $task['date_moved'], + 'recurrence_status' => $task['recurrence_status'], + 'recurrence_trigger' => $task['recurrence_trigger'], + ); + + if ($task['swimlane_id'] != $new_swimlane_id) { + $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_SWIMLANE, new TaskEvent($event_data)); + } + else if ($task['column_id'] != $new_column_id) { + $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_COLUMN, new TaskEvent($event_data)); + } + else if ($task['position'] != $new_position) { + $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); + } + } + + /** + * Calculate the new position of all tasks + * + * @access private + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param integer $column_id Column id + * @param integer $position Position (must be >= 1) + * @param integer $swimlane_id Swimlane id + * @return boolean + */ + private function calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id) + { + $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id); + + if ($positions === false || ! $this->savePositions($positions, $swimlane_id)) { + return false; + } + + return true; + } +} diff --git a/app/Model/TaskStatus.php b/app/Model/TaskStatus.php new file mode 100644 index 00000000..30a65e1e --- /dev/null +++ b/app/Model/TaskStatus.php @@ -0,0 +1,131 @@ +<?php + +namespace Model; + +use Event\TaskEvent; + +/** + * Task Status + * + * @package model + * @author Frederic Guillot + */ +class TaskStatus extends Base +{ + /** + * Return the list of statuses + * + * @access public + * @param boolean $prepend Prepend default value + * @return array + */ + public function getList($prepend = false) + { + $listing = $prepend ? array(-1 => t('All status')) : array(); + + return $listing + array( + Task::STATUS_OPEN => t('Open'), + Task::STATUS_CLOSED => t('Closed'), + ); + } + + /** + * Return true if the task is closed + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function isClosed($task_id) + { + return $this->checkStatus($task_id, Task::STATUS_CLOSED); + } + + /** + * Return true if the task is open + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function isOpen($task_id) + { + return $this->checkStatus($task_id, Task::STATUS_OPEN); + } + + /** + * Mark a task closed + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function close($task_id) + { + return $this->changeStatus($task_id, Task::STATUS_CLOSED, time(), Task::EVENT_CLOSE); + } + + /** + * Mark a task open + * + * @access public + * @param integer $task_id Task id + * @return boolean + */ + public function open($task_id) + { + return $this->changeStatus($task_id, Task::STATUS_OPEN, 0, Task::EVENT_OPEN); + } + + /** + * Common method to change the status of task + * + * @access private + * @param integer $task_id Task id + * @param integer $status Task status + * @param integer $date_completed Timestamp + * @param string $event Event name + * @return boolean + */ + private function changeStatus($task_id, $status, $date_completed, $event) + { + if (! $this->taskFinder->exists($task_id)) { + return false; + } + + $result = $this->db + ->table(Task::TABLE) + ->eq('id', $task_id) + ->update(array( + 'is_active' => $status, + 'date_completed' => $date_completed, + 'date_modification' => time(), + )); + + if ($result) { + $this->container['dispatcher']->dispatch( + $event, + new TaskEvent(array('task_id' => $task_id) + $this->taskFinder->getById($task_id)) + ); + } + + return $result; + } + + /** + * Check the status of task + * + * @access private + * @param integer $task_id Task id + * @param integer $status Task status + * @return boolean + */ + private function checkStatus($task_id, $status) + { + return $this->db + ->table(Task::TABLE) + ->eq('id', $task_id) + ->eq('is_active', $status) + ->count() === 1; + } +} diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php index 816008cf..ec1383ad 100644 --- a/app/Model/TaskValidator.php +++ b/app/Model/TaskValidator.php @@ -29,6 +29,14 @@ class TaskValidator extends Base new Validators\Integer('creator_id', t('This value must be an integer')), new Validators\Integer('score', t('This value must be an integer')), new Validators\Integer('category_id', t('This value must be an integer')), + new Validators\Integer('swimlane_id', t('This value must be an integer')), + new Validators\Integer('recurrence_child', t('This value must be an integer')), + new Validators\Integer('recurrence_parent', t('This value must be an integer')), + new Validators\Integer('recurrence_factor', t('This value must be an integer')), + new Validators\Integer('recurrence_timeframe', t('This value must be an integer')), + new Validators\Integer('recurrence_basedate', t('This value must be an integer')), + new Validators\Integer('recurrence_trigger', t('This value must be an integer')), + new Validators\Integer('recurrence_status', t('This value must be an integer')), new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()), new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateFormats()), @@ -70,7 +78,6 @@ class TaskValidator extends Base { $rules = array( new Validators\Required('id', t('The id is required')), - new Validators\Required('description', t('The description is required')), ); $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); @@ -82,6 +89,28 @@ class TaskValidator extends Base } /** + * Validate edit recurrence + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateEditRecurrence(array $values) + { + $rules = array( + new Validators\Required('id', t('The id is required')), + ); + + $v = new Validator($values, array_merge($rules, $this->commonValidationRules())); + + return array( + $v->execute(), + $v->getErrors() + ); + } + + + /** * Validate task modification (form) * * @access public diff --git a/app/Model/TimeTracking.php b/app/Model/TimeTracking.php deleted file mode 100644 index 4ddddf12..00000000 --- a/app/Model/TimeTracking.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -namespace Model; - -/** - * Time tracking model - * - * @package model - * @author Frederic Guillot - */ -class TimeTracking extends Base -{ - /** - * Calculate time metrics for a task - * - * Use subtasks time metrics if not empty otherwise return task time metrics - * - * @access public - * @param array $task Task properties - * @param array $subtasks Subtasks list - * @return array - */ - public function getTaskTimesheet(array $task, array $subtasks) - { - $timesheet = array( - 'time_spent' => 0, - 'time_estimated' => 0, - 'time_remaining' => 0, - ); - - foreach ($subtasks as &$subtask) { - $timesheet['time_estimated'] += $subtask['time_estimated']; - $timesheet['time_spent'] += $subtask['time_spent']; - } - - if ($timesheet['time_estimated'] == 0 && $timesheet['time_spent'] == 0) { - $timesheet['time_estimated'] = $task['time_estimated']; - $timesheet['time_spent'] = $task['time_spent']; - } - - $timesheet['time_remaining'] = $timesheet['time_estimated'] - $timesheet['time_spent']; - - return $timesheet; - } -} diff --git a/app/Model/Timetable.php b/app/Model/Timetable.php new file mode 100644 index 00000000..6ddf826b --- /dev/null +++ b/app/Model/Timetable.php @@ -0,0 +1,356 @@ +<?php + +namespace Model; + +use DateTime; +use DateInterval; + +/** + * Timetable + * + * @package model + * @author Frederic Guillot + */ +class Timetable extends Base +{ + /** + * User time slots + * + * @access private + * @var array + */ + private $day; + private $week; + private $overtime; + private $timeoff; + + /** + * Get a set of events by using the intersection between the timetable and the time tracking data + * + * @access public + * @param integer $user_id + * @param array $events Time tracking data + * @param string $start ISO8601 date + * @param string $end ISO8601 date + * @return array + */ + public function calculateEventsIntersect($user_id, array $events, $start, $end) + { + $start_dt = new DateTime($start); + $start_dt->setTime(0, 0); + + $end_dt = new DateTime($end); + $end_dt->setTime(23, 59); + + $timetable = $this->calculate($user_id, $start_dt, $end_dt); + + // The user has no timetable + if (empty($this->week)) { + return $events; + } + + $results = array(); + + foreach ($events as $event) { + $results = array_merge($results, $this->calculateEventIntersect($event, $timetable)); + } + + return $results; + } + + /** + * Get a serie of events based on the timetable and the provided event + * + * @access public + * @param array $event + * @param array $timetable + * @return array + */ + public function calculateEventIntersect(array $event, array $timetable) + { + $events = array(); + + foreach ($timetable as $slot) { + + $start_ts = $slot[0]->getTimestamp(); + $end_ts = $slot[1]->getTimestamp(); + + if ($start_ts > $event['end']) { + break; + } + + if ($event['start'] <= $start_ts) { + $event['start'] = $start_ts; + } + + if ($event['start'] >= $start_ts && $event['start'] <= $end_ts) { + + if ($event['end'] >= $end_ts) { + $events[] = array_merge($event, array('end' => $end_ts)); + } + else { + $events[] = $event; + break; + } + } + } + + return $events; + } + + /** + * Calculate effective worked hours by taking into consideration the timetable + * + * @access public + * @param integer $user_id + * @param \DateTime $start + * @param \DateTime $end + * @return float + */ + public function calculateEffectiveDuration($user_id, DateTime $start, DateTime $end) + { + $end_timetable = clone($end); + $end_timetable->setTime(23, 59); + + $timetable = $this->calculate($user_id, $start, $end_timetable); + $found_start = false; + $hours = 0; + + // The user has no timetable + if (empty($this->week)) { + return $this->dateParser->getHours($start, $end); + } + + foreach ($timetable as $slot) { + + $isStartSlot = $this->dateParser->withinDateRange($start, $slot[0], $slot[1]); + $isEndSlot = $this->dateParser->withinDateRange($end, $slot[0], $slot[1]); + + // Start and end are within the same time slot + if ($isStartSlot && $isEndSlot) { + return $this->dateParser->getHours($start, $end); + } + + // We found the start slot + if (! $found_start && $isStartSlot) { + $found_start = true; + $hours = $this->dateParser->getHours($start, $slot[1]); + } + else if ($found_start) { + + // We found the end slot + if ($isEndSlot) { + $hours += $this->dateParser->getHours($slot[0], $end); + break; + } + else { + + // Sum hours of the intermediate time slots + $hours += $this->dateParser->getHours($slot[0], $slot[1]); + } + } + } + + // The start date was not found in regular hours so we get the nearest time slot + if (! empty($timetable) && ! $found_start) { + $slot = $this->findClosestTimeSlot($start, $timetable); + + if ($start < $slot[0]) { + return $this->calculateEffectiveDuration($user_id, $slot[0], $end); + } + } + + return $hours; + } + + /** + * Find the nearest time slot + * + * @access public + * @param DateTime $date + * @param array $timetable + * @return array + */ + public function findClosestTimeSlot(DateTime $date, array $timetable) + { + $values = array(); + + foreach ($timetable as $slot) { + $t1 = abs($slot[0]->getTimestamp() - $date->getTimestamp()); + $t2 = abs($slot[1]->getTimestamp() - $date->getTimestamp()); + + $values[] = min($t1, $t2); + } + + asort($values); + return $timetable[key($values)]; + } + + /** + * Get the timetable for a user for a given date range + * + * @access public + * @param integer $user_id + * @param \DateTime $start + * @param \DateTime $end + * @return array + */ + public function calculate($user_id, DateTime $start, DateTime $end) + { + $timetable = array(); + + $this->day = $this->timetableDay->getByUser($user_id); + $this->week = $this->timetableWeek->getByUser($user_id); + $this->overtime = $this->timetableExtra->getByUserAndDate($user_id, $start->format('Y-m-d'), $end->format('Y-m-d')); + $this->timeoff = $this->timetableOff->getByUserAndDate($user_id, $start->format('Y-m-d'), $end->format('Y-m-d')); + + for ($today = clone($start); $today <= $end; $today->add(new DateInterval('P1D'))) { + $week_day = $today->format('N'); + $timetable = array_merge($timetable, $this->getWeekSlots($today, $week_day)); + $timetable = array_merge($timetable, $this->getOvertimeSlots($today, $week_day)); + } + + return $timetable; + } + + /** + * Return worked time slots for the given day + * + * @access public + * @param \DateTime $today + * @param string $week_day + * @return array + */ + public function getWeekSlots(DateTime $today, $week_day) + { + $slots = array(); + $dayoff = $this->getDayOff($today); + + if (! empty($dayoff) && $dayoff['all_day'] == 1) { + return array(); + } + + foreach ($this->week as $slot) { + if ($week_day == $slot['day']) { + $slots = array_merge($slots, $this->getDayWorkSlots($slot, $dayoff, $today)); + } + } + + return $slots; + } + + /** + * Get the overtime time slots for the given day + * + * @access public + * @param \DateTime $today + * @param string $week_day + * @return array + */ + public function getOvertimeSlots(DateTime $today, $week_day) + { + $slots = array(); + + foreach ($this->overtime as $slot) { + + $day = new DateTime($slot['date']); + + if ($week_day == $day->format('N')) { + + if ($slot['all_day'] == 1) { + $slots = array_merge($slots, $this->getDaySlots($today)); + } + else { + $slots[] = $this->getTimeSlot($slot, $day); + } + } + } + + return $slots; + } + + /** + * Get worked time slots and remove time off + * + * @access public + * @param array $slot + * @param array $dayoff + * @param \DateTime $today + * @return array + */ + public function getDayWorkSlots(array $slot, array $dayoff, DateTime $today) + { + $slots = array(); + + if (! empty($dayoff) && $dayoff['start'] < $slot['end']) { + + if ($dayoff['start'] > $slot['start']) { + $slots[] = $this->getTimeSlot(array('end' => $dayoff['start']) + $slot, $today); + } + + if ($dayoff['end'] < $slot['end']) { + $slots[] = $this->getTimeSlot(array('start' => $dayoff['end']) + $slot, $today); + } + } + else { + $slots[] = $this->getTimeSlot($slot, $today); + } + + return $slots; + } + + /** + * Get regular day work time slots + * + * @access public + * @param \DateTime $today + * @return array + */ + public function getDaySlots(DateTime $today) + { + $slots = array(); + + foreach ($this->day as $day) { + $slots[] = $this->getTimeSlot($day, $today); + } + + return $slots; + } + + /** + * Get the start and end time slot for a given day + * + * @access public + * @param array $slot + * @param \DateTime $today + * @return array + */ + public function getTimeSlot(array $slot, DateTime $today) + { + $date = $today->format('Y-m-d'); + + return array( + new DateTime($date.' '.$slot['start']), + new DateTime($date.' '.$slot['end']), + ); + } + + /** + * Return day off time slot + * + * @access public + * @param \DateTime $today + * @return array + */ + public function getDayOff(DateTime $today) + { + foreach ($this->timeoff as $day) { + + if ($day['date'] === $today->format('Y-m-d')) { + return $day; + } + } + + return array(); + } +} diff --git a/app/Model/TimetableDay.php b/app/Model/TimetableDay.php new file mode 100644 index 00000000..0c7bf20b --- /dev/null +++ b/app/Model/TimetableDay.php @@ -0,0 +1,87 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Timetable Workweek + * + * @package model + * @author Frederic Guillot + */ +class TimetableDay extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'timetable_day'; + + /** + * Get the timetable for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getByUser($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('start')->findAll(); + } + + /** + * Add a new time slot in the database + * + * @access public + * @param integer $user_id User id + * @param string $start Start hour (24h format) + * @param string $end End hour (24h format) + * @return boolean|integer + */ + public function create($user_id, $start, $end) + { + $values = array( + 'user_id' => $user_id, + 'start' => $start, + 'end' => $end, + ); + + return $this->persist(self::TABLE, $values); + } + + /** + * Remove a specific time slot + * + * @access public + * @param integer $slot_id + * @return boolean + */ + public function remove($slot_id) + { + return $this->db->table(self::TABLE)->eq('id', $slot_id)->remove(); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('user_id', t('Field required')), + new Validators\Required('start', t('Field required')), + new Validators\Required('end', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/TimetableExtra.php b/app/Model/TimetableExtra.php new file mode 100644 index 00000000..48db662d --- /dev/null +++ b/app/Model/TimetableExtra.php @@ -0,0 +1,22 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Timetable over-time + * + * @package model + * @author Frederic Guillot + */ +class TimetableExtra extends TimetableOff +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'timetable_extra'; +} diff --git a/app/Model/TimetableOff.php b/app/Model/TimetableOff.php new file mode 100644 index 00000000..e4fe32d2 --- /dev/null +++ b/app/Model/TimetableOff.php @@ -0,0 +1,125 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Timetable time off + * + * @package model + * @author Frederic Guillot + */ +class TimetableOff extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'timetable_off'; + + /** + * Get query to fetch everything (pagination) + * + * @access public + * @param integer $user_id User id + * @return \PicoDb\Table + */ + public function getUserQuery($user_id) + { + return $this->db->table(static::TABLE)->eq('user_id', $user_id); + } + + /** + * Get the timetable for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getByUser($user_id) + { + return $this->db->table(static::TABLE)->eq('user_id', $user_id)->desc('date')->asc('start')->findAll(); + } + + /** + * Get the timetable for a given user + * + * @access public + * @param integer $user_id User id + * @param string $start_date + * @param string $end_date + * @return array + */ + public function getByUserAndDate($user_id, $start_date, $end_date) + { + return $this->db->table(static::TABLE) + ->eq('user_id', $user_id) + ->gte('date', $start_date) + ->lte('date', $end_date) + ->desc('date') + ->asc('start') + ->findAll(); + } + + /** + * Add a new time slot in the database + * + * @access public + * @param integer $user_id User id + * @param string $date Day (ISO8601 format) + * @param boolean $all_day All day flag + * @param float $start Start hour (24h format) + * @param float $end End hour (24h format) + * @param string $comment + * @return boolean|integer + */ + public function create($user_id, $date, $all_day, $start = '', $end = '', $comment = '') + { + $values = array( + 'user_id' => $user_id, + 'date' => $date, + 'all_day' => (int) $all_day, // Postgres fix + 'start' => $all_day ? '' : $start, + 'end' => $all_day ? '' : $end, + 'comment' => $comment, + ); + + return $this->persist(static::TABLE, $values); + } + + /** + * Remove a specific time slot + * + * @access public + * @param integer $slot_id + * @return boolean + */ + public function remove($slot_id) + { + return $this->db->table(static::TABLE)->eq('id', $slot_id)->remove(); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('user_id', t('Field required')), + new Validators\Required('date', t('Field required')), + new Validators\Numeric('all_day', t('This value must be numeric')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/TimetableWeek.php b/app/Model/TimetableWeek.php new file mode 100644 index 00000000..b22b3b7e --- /dev/null +++ b/app/Model/TimetableWeek.php @@ -0,0 +1,91 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Timetable Workweek + * + * @package model + * @author Frederic Guillot + */ +class TimetableWeek extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'timetable_week'; + + /** + * Get the timetable for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getByUser($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('day')->asc('start')->findAll(); + } + + /** + * Add a new time slot in the database + * + * @access public + * @param integer $user_id User id + * @param string $day Day of the week (ISO-8601) + * @param string $start Start hour (24h format) + * @param string $end End hour (24h format) + * @return boolean|integer + */ + public function create($user_id, $day, $start, $end) + { + $values = array( + 'user_id' => $user_id, + 'day' => $day, + 'start' => $start, + 'end' => $end, + ); + + return $this->persist(self::TABLE, $values); + } + + /** + * Remove a specific time slot + * + * @access public + * @param integer $slot_id + * @return boolean + */ + public function remove($slot_id) + { + return $this->db->table(self::TABLE)->eq('id', $slot_id)->remove(); + } + + /** + * Validate creation + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validateCreation(array $values) + { + $v = new Validator($values, array( + new Validators\Required('user_id', t('Field required')), + new Validators\Required('day', t('Field required')), + new Validators\Numeric('day', t('This value must be numeric')), + new Validators\Required('start', t('Field required')), + new Validators\Required('end', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/Transition.php b/app/Model/Transition.php new file mode 100644 index 00000000..cb759e4a --- /dev/null +++ b/app/Model/Transition.php @@ -0,0 +1,170 @@ +<?php + +namespace Model; + +/** + * Transition model + * + * @package model + * @author Frederic Guillot + */ +class Transition extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'transitions'; + + /** + * Save transition event + * + * @access public + * @param integer $user_id + * @param array $task + * @return boolean + */ + public function save($user_id, array $task) + { + return $this->db->table(self::TABLE)->insert(array( + 'user_id' => $user_id, + 'project_id' => $task['project_id'], + 'task_id' => $task['task_id'], + 'src_column_id' => $task['src_column_id'], + 'dst_column_id' => $task['dst_column_id'], + 'date' => time(), + 'time_spent' => time() - $task['date_moved'] + )); + } + + /** + * Get all transitions by task + * + * @access public + * @param integer $task_id + * @return array + */ + public function getAllByTask($task_id) + { + return $this->db->table(self::TABLE) + ->columns( + 'src.title as src_column', + 'dst.title as dst_column', + User::TABLE.'.name', + User::TABLE.'.username', + self::TABLE.'.user_id', + self::TABLE.'.date', + self::TABLE.'.time_spent' + ) + ->eq('task_id', $task_id) + ->desc('date') + ->join(User::TABLE, 'id', 'user_id') + ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->findAll(); + } + + /** + * Get all transitions by project + * + * @access public + * @param integer $project_id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function getAllByProjectAndDate($project_id, $from, $to) + { + if (! is_numeric($from)) { + $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); + } + + if (! is_numeric($to)) { + $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + } + + return $this->db->table(self::TABLE) + ->columns( + Task::TABLE.'.id', + Task::TABLE.'.title', + 'src.title as src_column', + 'dst.title as dst_column', + User::TABLE.'.name', + User::TABLE.'.username', + self::TABLE.'.user_id', + self::TABLE.'.date', + self::TABLE.'.time_spent' + ) + ->gte('date', $from) + ->lte('date', $to) + ->eq(self::TABLE.'.project_id', $project_id) + ->desc('date') + ->join(Task::TABLE, 'id', 'task_id') + ->join(User::TABLE, 'id', 'user_id') + ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->findAll(); + } + + /** + * Get project export + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function export($project_id, $from, $to) + { + $results = array($this->getColumns()); + $transitions = $this->getAllByProjectAndDate($project_id, $from, $to); + + foreach ($transitions as $transition) { + $results[] = $this->format($transition); + } + + return $results; + } + + /** + * Get column titles + * + * @access public + * @return string[] + */ + public function getColumns() + { + return array( + e('Id'), + e('Task Title'), + e('Source column'), + e('Destination column'), + e('Executer'), + e('Date'), + e('Time spent'), + ); + } + + /** + * Format the output of a transition array + * + * @access public + * @param array $transition + * @return array + */ + public function format(array $transition) + { + $values = array(); + $values[] = $transition['id']; + $values[] = $transition['title']; + $values[] = $transition['src_column']; + $values[] = $transition['dst_column']; + $values[] = $transition['name'] ?: $transition['username']; + $values[] = date('Y-m-d H:i', $transition['date']); + $values[] = round($transition['time_spent'] / 3600, 2); + + return $values; + } +} diff --git a/app/Model/User.php b/app/Model/User.php index 9544f3c9..83cf065b 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -5,6 +5,7 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; use Core\Session; +use Core\Security; /** * User model @@ -29,54 +30,68 @@ class User extends Base const EVERYBODY_ID = -1; /** - * Return true is the given user id is administrator + * Return true if the user exists * * @access public - * @param integer $user_id User id + * @param integer $user_id User id * @return boolean */ - public function isAdmin($user_id) + public function exists($user_id) { - $result = $this->db - ->table(User::TABLE) - ->eq('id', $user_id) - ->eq('is_admin', 1) - ->count(); - - return $result > 0; + return $this->db->table(self::TABLE)->eq('id', $user_id)->count() === 1; } /** - * Get the default project from the session + * Get query to fetch all users * * @access public - * @return integer + * @return \PicoDb\Table */ - public function getFavoriteProjectId() + public function getQuery() { - return isset($_SESSION['user']['default_project_id']) ? $_SESSION['user']['default_project_id'] : 0; + return $this->db + ->table(self::TABLE) + ->columns( + 'id', + 'username', + 'name', + 'email', + 'is_admin', + 'default_project_id', + 'is_ldap_user', + 'notifications_enabled', + 'google_id', + 'github_id', + 'twofactor_activated' + ); } /** - * Get the last seen project from the session + * Return the full name * - * @access public - * @return integer + * @param array $user User properties + * @return string */ - public function getLastSeenProjectId() + public function getFullname(array $user) { - return empty($_SESSION['user']['last_show_project_id']) ? 0 : $_SESSION['user']['last_show_project_id']; + return $user['name'] ?: $user['username']; } /** - * Set the last seen project from the session + * Return true is the given user id is administrator * * @access public - * @@param integer $project_id Project id + * @param integer $user_id User id + * @return boolean */ - public function storeLastSeenProjectId($project_id) + public function isAdmin($user_id) { - $_SESSION['user']['last_show_project_id'] = (int) $project_id; + return $this->userSession->isAdmin() || // Avoid SQL query if connected + $this->db + ->table(User::TABLE) + ->eq('id', $user_id) + ->eq('is_admin', 1) + ->count() === 1; } /** @@ -96,10 +111,14 @@ class User extends Base * * @access public * @param string $google_id Google unique id - * @return array + * @return array|boolean */ public function getByGoogleId($google_id) { + if (empty($google_id)) { + return false; + } + return $this->db->table(self::TABLE)->eq('google_id', $google_id)->findOne(); } @@ -108,10 +127,14 @@ class User extends Base * * @access public * @param string $github_id GitHub user id - * @return array + * @return array|boolean */ public function getByGitHubId($github_id) { + if (empty($github_id)) { + return false; + } + return $this->db->table(self::TABLE)->eq('github_id', $github_id)->findOne(); } @@ -128,6 +151,38 @@ class User extends Base } /** + * Get a specific user by the email address + * + * @access public + * @param string $email Email + * @return array|boolean + */ + public function getByEmail($email) + { + if (empty($email)) { + return false; + } + + return $this->db->table(self::TABLE)->eq('email', $email)->findOne(); + } + + /** + * Fetch user by using the token + * + * @access public + * @param string $token Token + * @return array|boolean + */ + public function getByToken($token) + { + if (empty($token)) { + return false; + } + + return $this->db->table(self::TABLE)->eq('token', $token)->findOne(); + } + + /** * Get all users * * @access public @@ -135,11 +190,18 @@ class User extends Base */ public function getAll() { - return $this->db - ->table(self::TABLE) - ->asc('username') - ->columns('id', 'username', 'name', 'email', 'is_admin', 'default_project_id', 'is_ldap_user', 'notifications_enabled', 'google_id', 'github_id') - ->findAll(); + return $this->getQuery()->asc('username')->findAll(); + } + + /** + * Get the number of users + * + * @access public + * @return integer + */ + public function count() + { + return $this->db->table(self::TABLE)->count(); } /** @@ -201,12 +263,12 @@ class User extends Base * * @access public * @param array $values Form values - * @return boolean + * @return boolean|integer */ public function create(array $values) { $this->prepare($values); - return $this->db->table(self::TABLE)->save($values); + return $this->persist(self::TABLE, $values); } /** @@ -222,8 +284,8 @@ class User extends Base $result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values); // If the user is connected refresh his session - if (Session::isOpen() && $_SESSION['user']['id'] == $values['id']) { - $this->updateSession(); + if (Session::isOpen() && $this->userSession->getId() == $values['id']) { + $this->userSession->refresh(); } return $result; @@ -238,39 +300,59 @@ class User extends Base */ public function remove($user_id) { - $this->db->startTransaction(); + return $this->db->transaction(function ($db) use ($user_id) { - // All tasks assigned to this user will be unassigned - $this->db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0)); - $result = $this->db->table(self::TABLE)->eq('id', $user_id)->remove(); + // All assigned tasks are now unassigned + if (! $db->table(Task::TABLE)->eq('owner_id', $user_id)->update(array('owner_id' => 0))) { + return false; + } - $this->db->closeTransaction(); + // All private projects are removed + $project_ids = $db->table(Project::TABLE) + ->eq('is_private', 1) + ->eq(ProjectPermission::TABLE.'.user_id', $user_id) + ->join(ProjectPermission::TABLE, 'project_id', 'id') + ->findAllByColumn(Project::TABLE.'.id'); - return $result; + if (! empty($project_ids)) { + $db->table(Project::TABLE)->in('id', $project_ids)->remove(); + } + + // Finally remove the user + if (! $db->table(User::TABLE)->eq('id', $user_id)->remove()) { + return false; + } + }); } /** - * Update user session information + * Enable public access for a user * * @access public - * @param array $user User data + * @param integer $user_id User id + * @return bool */ - public function updateSession(array $user = array()) + public function enablePublicAccess($user_id) { - if (empty($user)) { - $user = $this->getById($_SESSION['user']['id']); - } - - if (isset($user['password'])) { - unset($user['password']); - } - - $user['id'] = (int) $user['id']; - $user['default_project_id'] = (int) $user['default_project_id']; - $user['is_admin'] = (bool) $user['is_admin']; - $user['is_ldap_user'] = (bool) $user['is_ldap_user']; + return $this->db + ->table(self::TABLE) + ->eq('id', $user_id) + ->save(array('token' => Security::generateToken())); + } - $_SESSION['user'] = $user; + /** + * Disable public access for a user + * + * @access public + * @param integer $user_id User id + * @return bool + */ + public function disablePublicAccess($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $user_id) + ->save(array('token' => '')); } /** @@ -389,7 +471,7 @@ class User extends Base if ($v->execute()) { // Check password - if ($this->authentication->authenticate($_SESSION['user']['username'], $values['current_password'])) { + if ($this->authentication->authenticate($this->session['user']['username'], $values['current_password'])) { return array(true, array()); } else { diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php new file mode 100644 index 00000000..6703a1bc --- /dev/null +++ b/app/Model/UserSession.php @@ -0,0 +1,131 @@ +<?php + +namespace Model; + +use Core\Translator; + +/** + * User Session + * + * @package model + * @author Frederic Guillot + */ +class UserSession extends Base +{ + /** + * Update user session information + * + * @access public + * @param array $user User data + */ + public function refresh(array $user = array()) + { + if (empty($user)) { + $user = $this->user->getById($this->userSession->getId()); + } + + if (isset($user['password'])) { + unset($user['password']); + } + + if (isset($user['twofactor_secret'])) { + unset($user['twofactor_secret']); + } + + $user['id'] = (int) $user['id']; + $user['default_project_id'] = (int) $user['default_project_id']; + $user['is_admin'] = (bool) $user['is_admin']; + $user['is_ldap_user'] = (bool) $user['is_ldap_user']; + $user['twofactor_activated'] = (bool) $user['twofactor_activated']; + + $this->session['user'] = $user; + } + + /** + * Return true if the user has validated the 2FA key + * + * @access public + * @return bool + */ + public function check2FA() + { + return isset($this->session['2fa_validated']) && $this->session['2fa_validated'] === true; + } + + /** + * Return true if the user has 2FA enabled + * + * @access public + * @return bool + */ + public function has2FA() + { + return isset($this->session['user']['twofactor_activated']) && $this->session['user']['twofactor_activated'] === true; + } + + /** + * Return true if the logged user is admin + * + * @access public + * @return bool + */ + public function isAdmin() + { + return isset($this->session['user']['is_admin']) && $this->session['user']['is_admin'] === true; + } + + /** + * Get the connected user id + * + * @access public + * @return integer + */ + public function getId() + { + return isset($this->session['user']['id']) ? (int) $this->session['user']['id'] : 0; + } + + /** + * Check is the user is connected + * + * @access public + * @return bool + */ + public function isLogged() + { + return ! empty($this->session['user']); + } + + /** + * Get the last seen project from the session + * + * @access public + * @return integer + */ + public function getLastSeenProjectId() + { + return empty($this->session['last_show_project_id']) ? 0 : $this->session['last_show_project_id']; + } + + /** + * Get the default project from the session + * + * @access public + * @return integer + */ + public function getFavoriteProjectId() + { + return isset($this->session['user']['default_project_id']) ? $this->session['user']['default_project_id'] : 0; + } + + /** + * Set the last seen project from the session + * + * @access public + * @param integer $project_id Project id + */ + public function storeLastSeenProjectId($project_id) + { + $this->session['last_show_project_id'] = (int) $project_id; + } +} diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php index b84728cf..e3af37f7 100644 --- a/app/Model/Webhook.php +++ b/app/Model/Webhook.php @@ -2,8 +2,6 @@ namespace Model; -use Event\WebhookListener; - /** * Webhook model * @@ -13,139 +11,26 @@ use Event\WebhookListener; class Webhook extends Base { /** - * HTTP connection timeout in seconds - * - * @var integer - */ - const HTTP_TIMEOUT = 1; - - /** - * Number of maximum redirections for the HTTP client - * - * @var integer - */ - const HTTP_MAX_REDIRECTS = 3; - - /** - * HTTP client user agent - * - * @var string - */ - const HTTP_USER_AGENT = 'Kanboard Webhook'; - - /** - * URL to call for task creation - * - * @access private - * @var string - */ - private $url_task_creation = ''; - - /** - * URL to call for task modification - * - * @access private - * @var string - */ - private $url_task_modification = ''; - - /** - * Webook token - * - * @access private - * @var string - */ - private $token = ''; - - /** - * Attach events - * - * @access public - */ - public function attachEvents() - { - $this->url_task_creation = $this->config->get('webhook_url_task_creation'); - $this->url_task_modification = $this->config->get('webhook_url_task_modification'); - $this->token = $this->config->get('webhook_token'); - - if ($this->url_task_creation) { - $this->attachCreateEvents(); - } - - if ($this->url_task_modification) { - $this->attachUpdateEvents(); - } - } - - /** - * Attach events for task modification - * - * @access public - */ - public function attachUpdateEvents() - { - $events = array( - Task::EVENT_UPDATE, - Task::EVENT_CLOSE, - Task::EVENT_OPEN, - Task::EVENT_MOVE_COLUMN, - Task::EVENT_MOVE_POSITION, - Task::EVENT_ASSIGNEE_CHANGE, - ); - - $listener = new WebhookListener($this->registry); - $listener->setUrl($this->url_task_modification); - - foreach ($events as $event_name) { - $this->event->attach($event_name, $listener); - } - } - - /** - * Attach events for task creation - * - * @access public - */ - public function attachCreateEvents() - { - $listener = new WebhookListener($this->registry); - $listener->setUrl($this->url_task_creation); - - $this->event->attach(Task::EVENT_CREATE, $listener); - } - - /** * Call the external URL * * @access public - * @param string $url URL to call - * @param array $task Task data + * @param array $values Event payload */ - public function notify($url, array $task) + public function notify(array $values) { - $headers = array( - 'Connection: close', - 'User-Agent: '.self::HTTP_USER_AGENT, - ); + $url = $this->config->get('webhook_url'); + $token = $this->config->get('webhook_token'); - $context = stream_context_create(array( - 'http' => array( - 'method' => 'POST', - 'protocol_version' => 1.1, - 'timeout' => self::HTTP_TIMEOUT, - 'max_redirects' => self::HTTP_MAX_REDIRECTS, - 'header' => implode("\r\n", $headers), - 'content' => json_encode($task) - ) - )); + if (! empty($url)) { - if (strpos($url, '?') !== false) { - $url .= '&token='.$this->token; - } - else { - $url .= '?token='.$this->token; - } + if (strpos($url, '?') !== false) { + $url .= '&token='.$token; + } + else { + $url .= '?token='.$token; + } - @file_get_contents($url, false, $context); + return $this->httpClient->postJson($url, $values); + } } } diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 4f74f761..bcb365bd 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -4,8 +4,487 @@ namespace Schema; use PDO; use Core\Security; +use Model\Link; -const VERSION = 34; +const VERSION = 73; + +function version_73($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_filter INT DEFAULT 4"); +} + +function version_72($pdo) +{ + $pdo->exec('ALTER TABLE files MODIFY name VARCHAR(255)'); +} + +function version_71($pdo) +{ + $rq = $pdo->prepare('INSERT INTO `settings` VALUES (?, ?)'); + $rq->execute(array('webhook_url', '')); + + $pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_creation'"); + $pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_modification'"); +} + +function version_70($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN token VARCHAR(255) DEFAULT ''"); +} + +function version_69($pdo) +{ + $rq = $pdo->prepare("SELECT `value` FROM `settings` WHERE `option`='subtask_forecast'"); + $rq->execute(); + $result = $rq->fetch(PDO::FETCH_ASSOC); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0)); + $rq->execute(array('calendar_user_subtasks_time_tracking', 0)); + $rq->execute(array('calendar_user_tasks', 'date_started')); + $rq->execute(array('calendar_project_tasks', 'date_started')); + + $pdo->exec("DELETE FROM `settings` WHERE `option`='subtask_forecast'"); +} + +function version_68($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_jabber', '0')); + $rq->execute(array('integration_jabber_server', '')); + $rq->execute(array('integration_jabber_domain', '')); + $rq->execute(array('integration_jabber_username', '')); + $rq->execute(array('integration_jabber_password', '')); + $rq->execute(array('integration_jabber_nickname', 'kanboard')); + $rq->execute(array('integration_jabber_room', '')); + + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber INTEGER DEFAULT '0'"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_server VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_domain VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_username VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_password VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_nickname VARCHAR(255) DEFAULT 'kanboard'"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_room VARCHAR(255) DEFAULT ''"); +} + +function version_67($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER'); +} + +function version_66($pdo) +{ + $pdo->exec("ALTER TABLE projects ADD COLUMN identifier VARCHAR(50) DEFAULT ''"); +} + +function version_65($pdo) +{ + $pdo->exec(" + CREATE TABLE project_integrations ( + `id` INT NOT NULL AUTO_INCREMENT, + `project_id` INT NOT NULL UNIQUE, + `hipchat` TINYINT(1) DEFAULT 0, + `hipchat_api_url` VARCHAR(255) DEFAULT 'https://api.hipchat.com', + `hipchat_room_id` VARCHAR(255), + `hipchat_room_token` VARCHAR(255), + `slack` TINYINT(1) DEFAULT 0, + `slack_webhook_url` VARCHAR(255), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} + +function version_64($pdo) +{ + $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INT NOT NULL DEFAULT 0'); +} + +function version_63($pdo) +{ + $pdo->exec('ALTER TABLE project_has_categories ADD COLUMN description TEXT'); +} + +function version_62($pdo) +{ + $pdo->exec('ALTER TABLE files ADD COLUMN `date` INT NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE files ADD COLUMN `user_id` INT NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE files ADD COLUMN `size` INT NOT NULL DEFAULT 0'); +} + +function version_61($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated TINYINT(1) DEFAULT 0'); + $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret CHAR(16)'); +} + +function version_60($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_gravatar', '0')); +} + +function version_59($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} + +function version_58($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} + +function version_57($pdo) +{ + $pdo->exec('CREATE TABLE currencies (`currency` CHAR(3) NOT NULL UNIQUE, `rate` FLOAT DEFAULT 0) ENGINE=InnoDB CHARSET=utf8'); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_currency', 'USD')); +} + +function version_56($pdo) +{ + $pdo->exec('CREATE TABLE transitions ( + `id` INT NOT NULL AUTO_INCREMENT, + `user_id` INT NOT NULL, + `project_id` INT NOT NULL, + `task_id` INT NOT NULL, + `src_column_id` INT NOT NULL, + `dst_column_id` INT NOT NULL, + `date` INT NOT NULL, + `time_spent` INT DEFAULT 0, + FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8'); + + $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)"); + $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)"); + $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)"); +} + +function version_55($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_forecast', '0')); +} + +function version_54($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_stylesheet', '')); +} + +function version_53($pdo) +{ + $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent FLOAT DEFAULT 0"); +} + +function version_52($pdo) +{ + $pdo->exec('CREATE TABLE budget_lines ( + `id` INT NOT NULL AUTO_INCREMENT, + `project_id` INT NOT NULL, + `amount` FLOAT NOT NULL, + `date` VARCHAR(10) NOT NULL, + `comment` TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8'); +} + +function version_51($pdo) +{ + $pdo->exec('CREATE TABLE timetable_day ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT NOT NULL, + start VARCHAR(5) NOT NULL, + end VARCHAR(5) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8'); + + $pdo->exec('CREATE TABLE timetable_week ( + id INT NOT NULL AUTO_INCREMENT, + user_id INTEGER NOT NULL, + day INT NOT NULL, + start VARCHAR(5) NOT NULL, + end VARCHAR(5) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8'); + + $pdo->exec('CREATE TABLE timetable_off ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT NOT NULL, + date VARCHAR(10) NOT NULL, + all_day TINYINT(1) DEFAULT 0, + start VARCHAR(5) DEFAULT 0, + end VARCHAR(5) DEFAULT 0, + comment TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8'); + + $pdo->exec('CREATE TABLE timetable_extra ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT NOT NULL, + date VARCHAR(10) NOT NULL, + all_day TINYINT(1) DEFAULT 0, + start VARCHAR(5) DEFAULT 0, + end VARCHAR(5) DEFAULT 0, + comment TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8'); +} + +function version_50($pdo) +{ + $pdo->exec("CREATE TABLE hourly_rates ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT NOT NULL, + rate FLOAT DEFAULT 0, + date_effective INTEGER NOT NULL, + currency CHAR(3) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8"); +} + +function version_49($pdo) +{ + $pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1'); + + $task_id = 0; + $position = 1; + $urq = $pdo->prepare('UPDATE subtasks SET position=? WHERE id=?'); + + $rq = $pdo->prepare('SELECT * FROM subtasks ORDER BY task_id, id ASC'); + $rq->execute(); + + foreach ($rq->fetchAll(PDO::FETCH_ASSOC) as $subtask) { + + if ($task_id != $subtask['task_id']) { + $position = 1; + $task_id = $subtask['task_id']; + } + + $urq->execute(array($position, $subtask['id'])); + $position++; + } +} + +function version_48($pdo) +{ + $pdo->exec('RENAME TABLE task_has_files TO files'); + $pdo->exec('RENAME TABLE task_has_subtasks TO subtasks'); +} + +function version_47($pdo) +{ + $pdo->exec('ALTER TABLE projects ADD COLUMN description TEXT'); +} + +function version_46($pdo) +{ + $pdo->exec("CREATE TABLE links ( + id INT NOT NULL AUTO_INCREMENT, + label VARCHAR(255) NOT NULL, + opposite_id INT DEFAULT 0, + PRIMARY KEY(id), + UNIQUE(label) + ) ENGINE=InnoDB CHARSET=utf8"); + + $pdo->exec("CREATE TABLE task_has_links ( + id INT NOT NULL AUTO_INCREMENT, + link_id INT NOT NULL, + task_id INT NOT NULL, + opposite_task_id INT NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} + +function version_45($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0'); + + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. + * We take max project_activities.date_creation where event_name in task.create','task.move.column + * since creation date is always less than task moves + */ + $pdo->exec("UPDATE tasks + SET date_moved = ( + SELECT md + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + WHERE id = src.task_id + ) + WHERE (date_moved IS NULL OR date_moved = 0) AND id IN ( + SELECT task_id + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + )"); + + // If there is no activities for some tasks use the date_creation + $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0"); +} + +function version_44($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN disable_login_form TINYINT(1) DEFAULT 0'); +} + +function version_43($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_restriction', '0')); + $rq->execute(array('subtask_time_tracking', '0')); + + $pdo->exec(" + CREATE TABLE subtask_time_tracking ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT NOT NULL, + subtask_id INT NOT NULL, + start INT DEFAULT 0, + end INT DEFAULT 0, + PRIMARY KEY(id), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); +} + +function version_42($pdo) +{ + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); +} + +function version_41($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN timezone VARCHAR(50)'); + $pdo->exec('ALTER TABLE users ADD COLUMN language CHAR(5)'); +} + +function version_40($pdo) +{ + // Avoid some full table scans + $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)'); + $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)'); + $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)'); + $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)'); + $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)'); + $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)'); + $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)'); + $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)'); + + // Set the ownership for all private projects + $rq = $pdo->prepare('SELECT id FROM projects WHERE is_private=1'); + $rq->execute(); + $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0); + + $rq = $pdo->prepare('UPDATE project_has_users SET is_owner=1 WHERE project_id=?'); + + foreach ($project_ids as $project_id) { + $rq->execute(array($project_id)); + } +} + +function version_39($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('project_categories', '')); +} + +function version_38($pdo) +{ + $pdo->exec(" + CREATE TABLE swimlanes ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(200) NOT NULL, + position INT DEFAULT 1, + is_active INT DEFAULT 1, + project_id INT, + PRIMARY KEY(id), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE (name, project_id) + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INT DEFAULT 0'); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INT DEFAULT 1"); +} + +function version_37($pdo) +{ + $pdo->exec("ALTER TABLE project_has_users ADD COLUMN is_owner TINYINT(1) DEFAULT '0'"); +} + +function version_36($pdo) +{ + $pdo->exec('ALTER TABLE tasks MODIFY title VARCHAR(255) NOT NULL'); +} + +function version_35($pdo) +{ + $pdo->exec(" + CREATE TABLE project_daily_summaries ( + id INT NOT NULL AUTO_INCREMENT, + day CHAR(10) NOT NULL, + project_id INT NOT NULL, + column_id INT NOT NULL, + total INT NOT NULL DEFAULT 0, + PRIMARY KEY(id), + FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)'); +} function version_34($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index f301f3e8..65a9c9bf 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -4,8 +4,469 @@ namespace Schema; use PDO; use Core\Security; +use Model\Link; -const VERSION = 15; +const VERSION = 53; + +function version_53($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_filter INTEGER DEFAULT 4"); +} + +function version_52($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('webhook_url', '')); + + $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'"); + $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'"); +} + +function version_51($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN token VARCHAR(255) DEFAULT ''"); +} + +function version_50($pdo) +{ + $rq = $pdo->prepare("SELECT value FROM settings WHERE option='subtask_forecast'"); + $rq->execute(); + $result = $rq->fetch(PDO::FETCH_ASSOC); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0)); + $rq->execute(array('calendar_user_subtasks_time_tracking', 0)); + $rq->execute(array('calendar_user_tasks', 'date_started')); + $rq->execute(array('calendar_project_tasks', 'date_started')); + + $pdo->exec("DELETE FROM settings WHERE option='subtask_forecast'"); +} + +function version_49($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_jabber', '0')); + $rq->execute(array('integration_jabber_server', '')); + $rq->execute(array('integration_jabber_domain', '')); + $rq->execute(array('integration_jabber_username', '')); + $rq->execute(array('integration_jabber_password', '')); + $rq->execute(array('integration_jabber_nickname', 'kanboard')); + $rq->execute(array('integration_jabber_room', '')); + + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber INTEGER DEFAULT '0'"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_server VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_domain VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_username VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_password VARCHAR(255) DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_nickname VARCHAR(255) DEFAULT 'kanboard'"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_room VARCHAR(255) DEFAULT ''"); +} + +function version_48($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER'); +} + +function version_47($pdo) +{ + $pdo->exec("ALTER TABLE projects ADD COLUMN identifier VARCHAR(50) DEFAULT ''"); +} + +function version_46($pdo) +{ + $pdo->exec(" + CREATE TABLE project_integrations ( + id SERIAL PRIMARY KEY, + project_id INTEGER NOT NULL UNIQUE, + hipchat BOOLEAN DEFAULT '0', + hipchat_api_url VARCHAR(255) DEFAULT 'https://api.hipchat.com', + hipchat_room_id VARCHAR(255), + hipchat_room_token VARCHAR(255), + slack BOOLEAN DEFAULT '0', + slack_webhook_url VARCHAR(255), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); +} + +function version_45($pdo) +{ + $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0'); +} + +function version_44($pdo) +{ + $pdo->exec('ALTER TABLE project_has_categories ADD COLUMN description TEXT'); +} + +function version_43($pdo) +{ + $pdo->exec('ALTER TABLE files ADD COLUMN "date" INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE files ADD COLUMN "user_id" INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE files ADD COLUMN "size" INTEGER NOT NULL DEFAULT 0'); +} + +function version_42($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated BOOLEAN DEFAULT \'0\''); + $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret CHAR(16)'); +} + +function version_41($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_gravatar', '0')); +} + +function version_40($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} + +function version_39($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} + +function version_38($pdo) +{ + $pdo->exec('CREATE TABLE currencies ("currency" CHAR(3) NOT NULL UNIQUE, "rate" REAL DEFAULT 0)'); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_currency', 'USD')); +} + +function version_37($pdo) +{ + $pdo->exec('CREATE TABLE transitions ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "project_id" INTEGER NOT NULL, + "task_id" INTEGER NOT NULL, + "src_column_id" INTEGER NOT NULL, + "dst_column_id" INTEGER NOT NULL, + "date" INTEGER NOT NULL, + "time_spent" INTEGER DEFAULT 0, + FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + )'); + + $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)"); + $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)"); + $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)"); +} + +function version_36($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_forecast', '0')); +} + +function version_35($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_stylesheet', '')); +} + +function version_34($pdo) +{ + $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0"); +} + +function version_33($pdo) +{ + $pdo->exec('CREATE TABLE budget_lines ( + "id" SERIAL PRIMARY KEY, + "project_id" INTEGER NOT NULL, + "amount" REAL NOT NULL, + "date" VARCHAR(10) NOT NULL, + "comment" TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )'); +} + +function version_32($pdo) +{ + $pdo->exec('CREATE TABLE timetable_day ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "start" VARCHAR(5) NOT NULL, + "end" VARCHAR(5) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); + + $pdo->exec('CREATE TABLE timetable_week ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "day" INTEGER NOT NULL, + "start" VARCHAR(5) NOT NULL, + "end" VARCHAR(5) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); + + $pdo->exec('CREATE TABLE timetable_off ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "date" VARCHAR(10) NOT NULL, + "all_day" BOOLEAN DEFAULT \'0\', + "start" VARCHAR(5) DEFAULT 0, + "end" VARCHAR(5) DEFAULT 0, + "comment" TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); + + $pdo->exec('CREATE TABLE timetable_extra ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "date" VARCHAR(10) NOT NULL, + "all_day" BOOLEAN DEFAULT \'0\', + "start" VARCHAR(5) DEFAULT 0, + "end" VARCHAR(5) DEFAULT 0, + "comment" TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); +} + +function version_31($pdo) +{ + $pdo->exec("CREATE TABLE hourly_rates ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + rate REAL DEFAULT 0, + date_effective INTEGER NOT NULL, + currency CHAR(3) NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )"); +} + +function version_30($pdo) +{ + $pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1'); + + $task_id = 0; + $position = 1; + $urq = $pdo->prepare('UPDATE subtasks SET position=? WHERE id=?'); + + $rq = $pdo->prepare('SELECT * FROM subtasks ORDER BY task_id, id ASC'); + $rq->execute(); + + foreach ($rq->fetchAll(PDO::FETCH_ASSOC) as $subtask) { + + if ($task_id != $subtask['task_id']) { + $position = 1; + $task_id = $subtask['task_id']; + } + + $urq->execute(array($position, $subtask['id'])); + $position++; + } +} + +function version_29($pdo) +{ + $pdo->exec('ALTER TABLE task_has_files RENAME TO files'); + $pdo->exec('ALTER TABLE task_has_subtasks RENAME TO subtasks'); +} + +function version_28($pdo) +{ + $pdo->exec('ALTER TABLE projects ADD COLUMN description TEXT'); +} + +function version_27($pdo) +{ + $pdo->exec('CREATE TABLE links ( + "id" SERIAL PRIMARY KEY, + "label" VARCHAR(255) NOT NULL, + "opposite_id" INTEGER DEFAULT 0, + UNIQUE("label") + )'); + + $pdo->exec("CREATE TABLE task_has_links ( + id SERIAL PRIMARY KEY, + link_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + opposite_task_id INTEGER NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE + )"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} + +function version_26($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INT DEFAULT 0'); + + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. + * We take max project_activities.date_creation where event_name in task.create','task.move.column + * since creation date is always less than task moves + */ + $pdo->exec("UPDATE tasks + SET date_moved = ( + SELECT md + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + WHERE id = src.task_id + ) + WHERE (date_moved IS NULL OR date_moved = 0) AND id IN ( + SELECT task_id + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + )"); + + // If there is no activities for some tasks use the date_creation + $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0"); +} + +function version_25($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN disable_login_form BOOLEAN DEFAULT '0'"); +} + +function version_24($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_restriction', '0')); + $rq->execute(array('subtask_time_tracking', '0')); + + $pdo->exec(' + CREATE TABLE subtask_time_tracking ( + id SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "subtask_id" INTEGER NOT NULL, + "start" INTEGER DEFAULT 0, + "end" INTEGER DEFAULT 0, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE + ) + '); +} + +function version_23($pdo) +{ + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); +} + +function version_22($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN timezone VARCHAR(50)'); + $pdo->exec('ALTER TABLE users ADD COLUMN language CHAR(5)'); +} + +function version_21($pdo) +{ + // Avoid some full table scans + $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)'); + $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)'); + $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)'); + $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)'); + $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)'); + $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)'); + $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)'); + $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)'); + + // Set the ownership for all private projects + $rq = $pdo->prepare("SELECT id FROM projects WHERE is_private='1'"); + $rq->execute(); + $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0); + + $rq = $pdo->prepare("UPDATE project_has_users SET is_owner='1' WHERE project_id=?"); + + foreach ($project_ids as $project_id) { + $rq->execute(array($project_id)); + } +} + +function version_20($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('project_categories', '')); +} + +function version_19($pdo) +{ + $pdo->exec(" + CREATE TABLE swimlanes ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + position INTEGER DEFAULT 1, + is_active BOOLEAN DEFAULT '1', + project_id INTEGER, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE (name, project_id) + ) + "); + + $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0'); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane VARCHAR(200) DEFAULT 'Default swimlane'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane BOOLEAN DEFAULT '1'"); +} + +function version_18($pdo) +{ + $pdo->exec("ALTER TABLE project_has_users ADD COLUMN is_owner BOOLEAN DEFAULT '0'"); +} + +function version_17($pdo) +{ + $pdo->exec('ALTER TABLE tasks ALTER COLUMN title SET NOT NULL'); +} + +function version_16($pdo) +{ + $pdo->exec(" + CREATE TABLE project_daily_summaries ( + id SERIAL PRIMARY KEY, + day CHAR(10) NOT NULL, + project_id INTEGER NOT NULL, + column_id INTEGER NOT NULL, + total INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); + + $pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)'); +} function version_15($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 8571d924..ceb3028c 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -4,8 +4,466 @@ namespace Schema; use Core\Security; use PDO; +use Model\Link; -const VERSION = 34; +const VERSION = 71; + +function version_71($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN notifications_filter INTEGER DEFAULT 4"); +} + +function version_70($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('webhook_url', '')); + + $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'"); + $pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'"); +} + +function version_69($pdo) +{ + $pdo->exec("ALTER TABLE users ADD COLUMN token TEXT DEFAULT ''"); +} + +function version_68($pdo) +{ + $rq = $pdo->prepare("SELECT value FROM settings WHERE option='subtask_forecast'"); + $rq->execute(); + $result = $rq->fetch(PDO::FETCH_ASSOC); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('calendar_user_subtasks_forecast', isset($result['subtask_forecast']) && $result['subtask_forecast'] == 1 ? 1 : 0)); + $rq->execute(array('calendar_user_subtasks_time_tracking', 0)); + $rq->execute(array('calendar_user_tasks', 'date_started')); + $rq->execute(array('calendar_project_tasks', 'date_started')); + + $pdo->exec("DELETE FROM settings WHERE option='subtask_forecast'"); +} + +function version_67($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_jabber', '0')); + $rq->execute(array('integration_jabber_server', '')); + $rq->execute(array('integration_jabber_domain', '')); + $rq->execute(array('integration_jabber_username', '')); + $rq->execute(array('integration_jabber_password', '')); + $rq->execute(array('integration_jabber_nickname', 'kanboard')); + $rq->execute(array('integration_jabber_room', '')); + + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber INTEGER DEFAULT '0'"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_server TEXT DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_domain TEXT DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_username TEXT DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_password TEXT DEFAULT ''"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_nickname TEXT DEFAULT 'kanboard'"); + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN jabber_room TEXT DEFAULT ''"); +} + +function version_66($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_status INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_trigger INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_factor INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_timeframe INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_basedate INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_parent INTEGER'); + $pdo->exec('ALTER TABLE tasks ADD COLUMN recurrence_child INTEGER'); +} + +function version_65($pdo) +{ + $pdo->exec("ALTER TABLE projects ADD COLUMN identifier TEXT DEFAULT ''"); +} + +function version_64($pdo) +{ + $pdo->exec(" + CREATE TABLE project_integrations ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL UNIQUE, + hipchat INTEGER DEFAULT 0, + hipchat_api_url TEXT DEFAULT 'https://api.hipchat.com', + hipchat_room_id TEXT, + hipchat_room_token TEXT, + slack INTEGER DEFAULT 0, + slack_webhook_url TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); +} + +function version_63($pdo) +{ + $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0'); +} + +function version_62($pdo) +{ + $pdo->exec('ALTER TABLE project_has_categories ADD COLUMN description TEXT'); +} + +function version_61($pdo) +{ + $pdo->exec('ALTER TABLE files ADD COLUMN "date" INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE files ADD COLUMN "user_id" INTEGER NOT NULL DEFAULT 0'); + $pdo->exec('ALTER TABLE files ADD COLUMN "size" INTEGER NOT NULL DEFAULT 0'); +} + +function version_60($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated INTEGER DEFAULT 0'); + $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret TEXT'); +} + +function version_59($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_gravatar', '0')); +} + +function version_58($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} + +function version_57($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} + +function version_56($pdo) +{ + $pdo->exec('CREATE TABLE currencies ("currency" TEXT NOT NULL UNIQUE, "rate" REAL DEFAULT 0)'); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_currency', 'USD')); +} + +function version_55($pdo) +{ + $pdo->exec('CREATE TABLE transitions ( + "id" INTEGER PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "project_id" INTEGER NOT NULL, + "task_id" INTEGER NOT NULL, + "src_column_id" INTEGER NOT NULL, + "dst_column_id" INTEGER NOT NULL, + "date" INTEGER NOT NULL, + "time_spent" INTEGER DEFAULT 0, + FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + )'); + + $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)"); + $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)"); + $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)"); +} + +function version_54($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_forecast', '0')); +} + +function version_53($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_stylesheet', '')); +} + +function version_52($pdo) +{ + $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0"); +} + +function version_51($pdo) +{ + $pdo->exec('CREATE TABLE budget_lines ( + "id" INTEGER PRIMARY KEY, + "project_id" INTEGER NOT NULL, + "amount" REAL NOT NULL, + "date" TEXT NOT NULL, + "comment" TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + )'); +} + +function version_50($pdo) +{ + $pdo->exec('CREATE TABLE timetable_day ( + "id" INTEGER PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "start" TEXT NOT NULL, + "end" TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); + + $pdo->exec('CREATE TABLE timetable_week ( + "id" INTEGER PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "day" INTEGER NOT NULL, + "start" TEXT NOT NULL, + "end" TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); + + $pdo->exec('CREATE TABLE timetable_off ( + "id" INTEGER PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "date" TEXT NOT NULL, + "all_day" INTEGER DEFAULT 0, + "start" TEXT DEFAULT 0, + "end" TEXT DEFAULT 0, + "comment" TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); + + $pdo->exec('CREATE TABLE timetable_extra ( + "id" INTEGER PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "date" TEXT NOT NULL, + "all_day" INTEGER DEFAULT 0, + "start" TEXT DEFAULT 0, + "end" TEXT DEFAULT 0, + "comment" TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )'); +} + +function version_49($pdo) +{ + $pdo->exec("CREATE TABLE hourly_rates ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + rate REAL DEFAULT 0, + date_effective INTEGER NOT NULL, + currency TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )"); +} + +function version_48($pdo) +{ + $pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1'); + + // Migrate all subtasks position + + $task_id = 0; + $position = 1; + $urq = $pdo->prepare('UPDATE subtasks SET position=? WHERE id=?'); + + $rq = $pdo->prepare('SELECT * FROM subtasks ORDER BY task_id, id ASC'); + $rq->execute(); + + foreach ($rq->fetchAll(PDO::FETCH_ASSOC) as $subtask) { + + if ($task_id != $subtask['task_id']) { + $position = 1; + $task_id = $subtask['task_id']; + } + + $urq->execute(array($position, $subtask['id'])); + $position++; + } +} + +function version_47($pdo) +{ + $pdo->exec('ALTER TABLE task_has_files RENAME TO files'); + $pdo->exec('ALTER TABLE task_has_subtasks RENAME TO subtasks'); +} + +function version_46($pdo) +{ + $pdo->exec('ALTER TABLE projects ADD COLUMN description TEXT'); +} + +function version_45($pdo) +{ + $pdo->exec("CREATE TABLE links ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + opposite_id INTEGER DEFAULT 0, + UNIQUE(label) + )"); + + $pdo->exec("CREATE TABLE task_has_links ( + id INTEGER PRIMARY KEY, + link_id INTEGER NOT NULL, + task_id INTEGER NOT NULL, + opposite_task_id INTEGER NOT NULL, + FOREIGN KEY(link_id) REFERENCES links(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + FOREIGN KEY(opposite_task_id) REFERENCES tasks(id) ON DELETE CASCADE + )"); + + $pdo->exec("CREATE INDEX task_has_links_task_index ON task_has_links(task_id)"); + $pdo->exec("CREATE UNIQUE INDEX task_has_links_unique ON task_has_links(link_id, task_id, opposite_task_id)"); + + $rq = $pdo->prepare('INSERT INTO links (label, opposite_id) VALUES (?, ?)'); + $rq->execute(array('relates to', 0)); + $rq->execute(array('blocks', 3)); + $rq->execute(array('is blocked by', 2)); + $rq->execute(array('duplicates', 5)); + $rq->execute(array('is duplicated by', 4)); + $rq->execute(array('is a child of', 7)); + $rq->execute(array('is a parent of', 6)); + $rq->execute(array('targets milestone', 9)); + $rq->execute(array('is a milestone of', 8)); + $rq->execute(array('fixes', 11)); + $rq->execute(array('is fixed by', 10)); +} + +function version_44($pdo) +{ + $pdo->exec('ALTER TABLE tasks ADD COLUMN date_moved INTEGER DEFAULT 0'); + + /* Update tasks.date_moved from project_activities table if tasks.date_moved = null or 0. + * We take max project_activities.date_creation where event_name in task.create','task.move.column + * since creation date is always less than task moves + */ + $pdo->exec("UPDATE tasks + SET date_moved = ( + SELECT md + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + WHERE id = src.task_id + ) + WHERE (date_moved IS NULL OR date_moved = 0) AND id IN ( + SELECT task_id + FROM ( + SELECT task_id, max(date_creation) md + FROM project_activities + WHERE event_name IN ('task.create', 'task.move.column') + GROUP BY task_id + ) src + )"); + + // If there is no activities for some tasks use the date_creation + $pdo->exec("UPDATE tasks SET date_moved = date_creation WHERE date_moved IS NULL OR date_moved = 0"); +} + +function version_43($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN disable_login_form INTEGER DEFAULT 0'); +} + +function version_42($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_restriction', '0')); + $rq->execute(array('subtask_time_tracking', '0')); + + $pdo->exec(" + CREATE TABLE subtask_time_tracking ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + subtask_id INTEGER NOT NULL, + start INTEGER DEFAULT 0, + end INTEGER DEFAULT 0, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(subtask_id) REFERENCES task_has_subtasks(id) ON DELETE CASCADE + ) + "); +} + +function version_41($pdo) +{ + $pdo->exec('ALTER TABLE columns ADD COLUMN description TEXT'); +} + +function version_40($pdo) +{ + $pdo->exec('ALTER TABLE users ADD COLUMN timezone TEXT'); + $pdo->exec('ALTER TABLE users ADD COLUMN language TEXT'); +} + +function version_39($pdo) +{ + // Avoid some full table scans + $pdo->exec('CREATE INDEX users_admin_idx ON users(is_admin)'); + $pdo->exec('CREATE INDEX columns_project_idx ON columns(project_id)'); + $pdo->exec('CREATE INDEX tasks_project_idx ON tasks(project_id)'); + $pdo->exec('CREATE INDEX swimlanes_project_idx ON swimlanes(project_id)'); + $pdo->exec('CREATE INDEX categories_project_idx ON project_has_categories(project_id)'); + $pdo->exec('CREATE INDEX subtasks_task_idx ON task_has_subtasks(task_id)'); + $pdo->exec('CREATE INDEX files_task_idx ON task_has_files(task_id)'); + $pdo->exec('CREATE INDEX comments_task_idx ON comments(task_id)'); + + // Set the ownership for all private projects + $rq = $pdo->prepare('SELECT id FROM projects WHERE is_private=1'); + $rq->execute(); + $project_ids = $rq->fetchAll(PDO::FETCH_COLUMN, 0); + + $rq = $pdo->prepare('UPDATE project_has_users SET is_owner=1 WHERE project_id=?'); + + foreach ($project_ids as $project_id) { + $rq->execute(array($project_id)); + } +} + +function version_38($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('project_categories', '')); +} + +function version_37($pdo) +{ + $pdo->exec(" + CREATE TABLE swimlanes ( + id INTEGER PRIMARY KEY, + name TEXT, + position INTEGER DEFAULT 1, + is_active INTEGER DEFAULT 1, + project_id INTEGER, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + UNIQUE (name, project_id) + ) + "); + + $pdo->exec('ALTER TABLE tasks ADD COLUMN swimlane_id INTEGER DEFAULT 0'); + $pdo->exec("ALTER TABLE projects ADD COLUMN default_swimlane TEXT DEFAULT 'Default swimlane'"); + $pdo->exec("ALTER TABLE projects ADD COLUMN show_default_swimlane INTEGER DEFAULT 1"); +} + +function version_36($pdo) +{ + $pdo->exec('ALTER TABLE project_has_users ADD COLUMN is_owner INTEGER DEFAULT "0"'); +} + +function version_35($pdo) +{ + $pdo->exec(" + CREATE TABLE project_daily_summaries ( + id INTEGER PRIMARY KEY, + day TEXT NOT NULL, + project_id INTEGER NOT NULL, + column_id INTEGER NOT NULL, + total INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); + + $pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)'); +} function version_34($pdo) { @@ -440,7 +898,7 @@ function version_1($pdo) $pdo->exec(" CREATE TABLE tasks ( id INTEGER PRIMARY KEY, - title TEXT, + title TEXT NOCASE NOT NULL, description TEXT, date_creation INTEGER, color_id TEXT, diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php new file mode 100644 index 00000000..28884b5a --- /dev/null +++ b/app/ServiceProvider/ClassProvider.php @@ -0,0 +1,107 @@ +<?php + +namespace ServiceProvider; + +use Core\Paginator; +use Model\Config; +use Model\Project; +use Model\Webhook; +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +class ClassProvider implements ServiceProviderInterface +{ + private $classes = array( + 'Model' => array( + 'Acl', + 'Action', + 'Authentication', + 'Board', + 'Budget', + 'Category', + 'Color', + 'Comment', + 'Config', + 'Currency', + 'DateParser', + 'File', + 'HourlyRate', + 'LastLogin', + 'Link', + 'Notification', + 'Project', + 'ProjectActivity', + 'ProjectAnalytic', + 'ProjectDuplication', + 'ProjectDailySummary', + 'ProjectIntegration', + 'ProjectPermission', + 'Subtask', + 'SubtaskExport', + 'SubtaskForecast', + 'SubtaskTimeTracking', + 'Swimlane', + 'Task', + 'TaskCreation', + 'TaskDuplication', + 'TaskExport', + 'TaskFinder', + 'TaskFilter', + 'TaskLink', + 'TaskModification', + 'TaskPermission', + 'TaskPosition', + 'TaskStatus', + 'TaskValidator', + 'Timetable', + 'TimetableDay', + 'TimetableExtra', + 'TimetableWeek', + 'TimetableOff', + 'Transition', + 'User', + 'UserSession', + 'Webhook', + ), + 'Core' => array( + 'EmailClient', + 'Helper', + 'HttpClient', + 'MemoryCache', + 'Request', + 'Session', + 'Template', + ), + 'Integration' => array( + 'BitbucketWebhook', + 'GithubWebhook', + 'GitlabWebhook', + 'HipchatWebhook', + 'Jabber', + 'Mailgun', + 'Postmark', + 'SendgridWebhook', + 'SlackWebhook', + 'Smtp', + ) + ); + + public function register(Container $container) + { + foreach ($this->classes as $namespace => $classes) { + + foreach ($classes as $name) { + + $class = '\\'.$namespace.'\\'.$name; + + $container[lcfirst($name)] = function ($c) use ($class) { + return new $class($c); + }; + } + } + + $container['paginator'] = $container->factory(function ($c) { + return new Paginator($c); + }); + } +} diff --git a/app/ServiceProvider/DatabaseProvider.php b/app/ServiceProvider/DatabaseProvider.php new file mode 100644 index 00000000..e6a75a4e --- /dev/null +++ b/app/ServiceProvider/DatabaseProvider.php @@ -0,0 +1,108 @@ +<?php + +namespace ServiceProvider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use PicoDb\Database; + +class DatabaseProvider implements ServiceProviderInterface +{ + public function register(Container $container) + { + $container['db'] = $this->getInstance(); + $container['db']->stopwatch = DEBUG; + $container['db']->log_queries = DEBUG; + } + + /** + * Setup the database driver and execute schema migration + * + * @access public + * @return \PicoDb\Database + */ + public function getInstance() + { + switch (DB_DRIVER) { + case 'sqlite': + $db = $this->getSqliteInstance(); + break; + + case 'mysql': + $db = $this->getMysqlInstance(); + break; + + case 'postgres': + $db = $this->getPostgresInstance(); + break; + + default: + die('Database driver not supported'); + } + + if ($db->schema()->check(\Schema\VERSION)) { + return $db; + } + else { + $errors = $db->getLogMessages(); + die('Unable to migrate database schema: <br/><br/><strong>'.(isset($errors[0]) ? $errors[0] : 'Unknown error').'</strong>'); + } + } + + /** + * Setup the Sqlite database driver + * + * @access private + * @return \PicoDb\Database + */ + private function getSqliteInstance() + { + require_once __DIR__.'/../Schema/Sqlite.php'; + + return new Database(array( + 'driver' => 'sqlite', + 'filename' => DB_FILENAME + )); + } + + /** + * Setup the Mysql database driver + * + * @access private + * @return \PicoDb\Database + */ + private function getMysqlInstance() + { + require_once __DIR__.'/../Schema/Mysql.php'; + + return new Database(array( + 'driver' => 'mysql', + 'hostname' => DB_HOSTNAME, + 'username' => DB_USERNAME, + 'password' => DB_PASSWORD, + 'database' => DB_NAME, + 'charset' => 'utf8', + 'port' => DB_PORT, + )); + } + + /** + * Setup the Postgres database driver + * + * @access private + * @return \PicoDb\Database + */ + private function getPostgresInstance() + { + require_once __DIR__.'/../Schema/Postgres.php'; + + return new Database(array( + 'driver' => 'postgres', + 'hostname' => DB_HOSTNAME, + 'username' => DB_USERNAME, + 'password' => DB_PASSWORD, + 'database' => DB_NAME, + 'port' => DB_PORT, + )); + } +} diff --git a/app/ServiceProvider/EventDispatcherProvider.php b/app/ServiceProvider/EventDispatcherProvider.php new file mode 100644 index 00000000..f566ede8 --- /dev/null +++ b/app/ServiceProvider/EventDispatcherProvider.php @@ -0,0 +1,40 @@ +<?php + +namespace ServiceProvider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Subscriber\AuthSubscriber; +use Subscriber\BootstrapSubscriber; +use Subscriber\NotificationSubscriber; +use Subscriber\ProjectActivitySubscriber; +use Subscriber\ProjectDailySummarySubscriber; +use Subscriber\ProjectModificationDateSubscriber; +use Subscriber\WebhookSubscriber; +use Subscriber\SubtaskTimesheetSubscriber; +use Subscriber\TaskMovedDateSubscriber; +use Subscriber\TransitionSubscriber; +use Subscriber\RecurringTaskSubscriber; + +class EventDispatcherProvider implements ServiceProviderInterface +{ + public function register(Container $container) + { + $container['dispatcher'] = new EventDispatcher; + $container['dispatcher']->addSubscriber(new BootstrapSubscriber($container)); + $container['dispatcher']->addSubscriber(new AuthSubscriber($container)); + $container['dispatcher']->addSubscriber(new ProjectActivitySubscriber($container)); + $container['dispatcher']->addSubscriber(new ProjectDailySummarySubscriber($container)); + $container['dispatcher']->addSubscriber(new ProjectModificationDateSubscriber($container)); + $container['dispatcher']->addSubscriber(new WebhookSubscriber($container)); + $container['dispatcher']->addSubscriber(new NotificationSubscriber($container)); + $container['dispatcher']->addSubscriber(new SubtaskTimesheetSubscriber($container)); + $container['dispatcher']->addSubscriber(new TaskMovedDateSubscriber($container)); + $container['dispatcher']->addSubscriber(new TransitionSubscriber($container)); + $container['dispatcher']->addSubscriber(new RecurringTaskSubscriber($container)); + + // Automatic actions + $container['action']->attachEvents(); + } +} diff --git a/app/ServiceProvider/LoggingProvider.php b/app/ServiceProvider/LoggingProvider.php new file mode 100644 index 00000000..dd79d654 --- /dev/null +++ b/app/ServiceProvider/LoggingProvider.php @@ -0,0 +1,28 @@ +<?php + +namespace ServiceProvider; + +use Psr\Log\LogLevel; +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use SimpleLogger\Logger; +use SimpleLogger\Syslog; +use SimpleLogger\File; + +class LoggingProvider implements ServiceProviderInterface +{ + public function register(Container $container) + { + $syslog = new Syslog('kanboard'); + $syslog->setLevel(LogLevel::ERROR); + + $logger = new Logger; + $logger->setLogger($syslog); + + if (DEBUG) { + $logger->setLogger(new File(DEBUG_FILE)); + } + + $container['logger'] = $logger; + } +} diff --git a/app/Subscriber/AuthSubscriber.php b/app/Subscriber/AuthSubscriber.php new file mode 100644 index 00000000..b814057f --- /dev/null +++ b/app/Subscriber/AuthSubscriber.php @@ -0,0 +1,27 @@ +<?php + +namespace Subscriber; + +use Core\Request; +use Event\AuthEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class AuthSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + 'auth.success' => array('onSuccess', 0), + ); + } + + public function onSuccess(AuthEvent $event) + { + $this->lastLogin->create( + $event->getAuthType(), + $event->getUserId(), + Request::getIpAddress(), + Request::getUserAgent() + ); + } +} diff --git a/app/Subscriber/BootstrapSubscriber.php b/app/Subscriber/BootstrapSubscriber.php new file mode 100644 index 00000000..793ba3e7 --- /dev/null +++ b/app/Subscriber/BootstrapSubscriber.php @@ -0,0 +1,23 @@ +<?php + +namespace Subscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class BootstrapSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + 'session.bootstrap' => array('setup', 0), + 'api.bootstrap' => array('setup', 0), + 'console.bootstrap' => array('setup', 0), + ); + } + + public function setup() + { + $this->config->setupTranslations(); + $this->config->setupTimezone(); + } +} diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php new file mode 100644 index 00000000..41fd6aef --- /dev/null +++ b/app/Subscriber/NotificationSubscriber.php @@ -0,0 +1,61 @@ +<?php + +namespace Subscriber; + +use Event\GenericEvent; +use Model\Task; +use Model\Comment; +use Model\Subtask; +use Model\File; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class NotificationSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_CREATE => array('execute', 0), + Task::EVENT_UPDATE => array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_MOVE_POSITION => array('execute', 0), + Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), + Subtask::EVENT_CREATE => array('execute', 0), + Subtask::EVENT_UPDATE => array('execute', 0), + Comment::EVENT_CREATE => array('execute', 0), + Comment::EVENT_UPDATE => array('execute', 0), + File::EVENT_CREATE => array('execute', 0), + ); + } + + public function execute(GenericEvent $event, $event_name) + { + $this->notification->sendNotifications($event_name, $this->getEventData($event)); + } + + public function getEventData(GenericEvent $event) + { + $values = array(); + + switch (get_class($event)) { + case 'Event\TaskEvent': + $values['task'] = $this->taskFinder->getDetails($event['task_id']); + break; + case 'Event\SubtaskEvent': + $values['subtask'] = $this->subtask->getById($event['id'], true); + $values['task'] = $this->taskFinder->getDetails($values['subtask']['task_id']); + break; + case 'Event\FileEvent': + $values['file'] = $event->getAll(); + $values['task'] = $this->taskFinder->getDetails($values['file']['task_id']); + break; + case 'Event\CommentEvent': + $values['comment'] = $this->comment->getById($event['id']); + $values['task'] = $this->taskFinder->getDetails($values['comment']['task_id']); + break; + } + + return $values; + } +} diff --git a/app/Subscriber/ProjectActivitySubscriber.php b/app/Subscriber/ProjectActivitySubscriber.php new file mode 100644 index 00000000..31f771f8 --- /dev/null +++ b/app/Subscriber/ProjectActivitySubscriber.php @@ -0,0 +1,73 @@ +<?php + +namespace Subscriber; + +use Event\GenericEvent; +use Model\Task; +use Model\Comment; +use Model\Subtask; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class ProjectActivitySubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), + Task::EVENT_UPDATE => array('execute', 0), + Task::EVENT_CREATE => array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_MOVE_POSITION => array('execute', 0), + Comment::EVENT_UPDATE => array('execute', 0), + Comment::EVENT_CREATE => array('execute', 0), + Subtask::EVENT_UPDATE => array('execute', 0), + Subtask::EVENT_CREATE => array('execute', 0), + ); + } + + public function execute(GenericEvent $event, $event_name) + { + // Executed only when someone is logged + if ($this->userSession->isLogged() && isset($event['task_id'])) { + + $values = $this->getValues($event); + + $this->projectActivity->createEvent( + $values['task']['project_id'], + $values['task']['id'], + $this->userSession->getId(), + $event_name, + $values + ); + + // Send notifications to third-party services + foreach (array('slackWebhook', 'hipchatWebhook', 'jabber') as $model) { + $this->$model->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); + } + } + } + + private function getValues(GenericEvent $event) + { + $values = array(); + $values['task'] = $this->taskFinder->getDetails($event['task_id']); + + switch (get_class($event)) { + case 'Event\SubtaskEvent': + $values['subtask'] = $this->subtask->getById($event['id'], true); + break; + case 'Event\CommentEvent': + $values['comment'] = $this->comment->getById($event['id']); + break; + } + + return $values; + } +} diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php new file mode 100644 index 00000000..9e4f15b0 --- /dev/null +++ b/app/Subscriber/ProjectDailySummarySubscriber.php @@ -0,0 +1,28 @@ +<?php + +namespace Subscriber; + +use Event\TaskEvent; +use Model\Task; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class ProjectDailySummarySubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_CREATE => array('execute', 0), + Task::EVENT_UPDATE => array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + ); + } + + public function execute(TaskEvent $event) + { + if (isset($event['project_id'])) { + $this->projectDailySummary->updateTotals($event['project_id'], date('Y-m-d')); + } + } +} diff --git a/app/Subscriber/ProjectModificationDateSubscriber.php b/app/Subscriber/ProjectModificationDateSubscriber.php new file mode 100644 index 00000000..2c01173b --- /dev/null +++ b/app/Subscriber/ProjectModificationDateSubscriber.php @@ -0,0 +1,31 @@ +<?php + +namespace Subscriber; + +use Event\GenericEvent; +use Model\Task; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class ProjectModificationDateSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_CREATE_UPDATE => array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_SWIMLANE => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_MOVE_POSITION => array('execute', 0), + Task::EVENT_MOVE_PROJECT => array('execute', 0), + Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), + ); + } + + public function execute(GenericEvent $event) + { + if (isset($event['project_id'])) { + $this->project->updateModificationDate($event['project_id']); + } + } +} diff --git a/app/Subscriber/RecurringTaskSubscriber.php b/app/Subscriber/RecurringTaskSubscriber.php new file mode 100644 index 00000000..68d704f0 --- /dev/null +++ b/app/Subscriber/RecurringTaskSubscriber.php @@ -0,0 +1,38 @@ +<?php + +namespace Subscriber; + +use Event\TaskEvent; +use Model\Task; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class RecurringTaskSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_MOVE_COLUMN => array('onMove', 0), + Task::EVENT_CLOSE => array('onClose', 0), + ); + } + + public function onMove(TaskEvent $event) + { + if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING) { + + if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_FIRST_COLUMN && $this->board->getFirstColumn($event['project_id']) == $event['src_column_id']) { + $this->taskDuplication->duplicateRecurringTask($event['task_id']); + } + else if ($event['recurrence_trigger'] == Task::RECURRING_TRIGGER_LAST_COLUMN && $this->board->getLastColumn($event['project_id']) == $event['dst_column_id']) { + $this->taskDuplication->duplicateRecurringTask($event['task_id']); + } + } + } + + public function onClose(TaskEvent $event) + { + if ($event['recurrence_status'] == Task::RECURRING_STATUS_PENDING && $event['recurrence_trigger'] == Task::RECURRING_TRIGGER_CLOSE) { + $this->taskDuplication->duplicateRecurringTask($event['task_id']); + } + } +} diff --git a/app/Subscriber/SubtaskTimesheetSubscriber.php b/app/Subscriber/SubtaskTimesheetSubscriber.php new file mode 100644 index 00000000..fdaf442f --- /dev/null +++ b/app/Subscriber/SubtaskTimesheetSubscriber.php @@ -0,0 +1,47 @@ +<?php + +namespace Subscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Model\Subtask; +use Event\SubtaskEvent; + +class SubtaskTimesheetSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Subtask::EVENT_CREATE => array('updateTaskTime', 0), + Subtask::EVENT_UPDATE => array( + array('logStartEnd', 10), + array('updateTaskTime', 0), + ) + ); + } + + public function updateTaskTime(SubtaskEvent $event) + { + if (isset($event['task_id'])) { + $this->subtaskTimeTracking->updateTaskTimeTracking($event['task_id']); + } + } + + public function logStartEnd(SubtaskEvent $event) + { + if ($this->config->get('subtask_time_tracking') == 1 && isset($event['status'])) { + + $subtask = $this->subtask->getById($event['id']); + + if (empty($subtask['user_id'])) { + return false; + } + + if ($subtask['status'] == Subtask::STATUS_INPROGRESS) { + return $this->subtaskTimeTracking->logStartTime($subtask['id'], $subtask['user_id']); + } + else { + return $this->subtaskTimeTracking->logEndTime($subtask['id'], $subtask['user_id']); + } + } + } +} diff --git a/app/Subscriber/TaskMovedDateSubscriber.php b/app/Subscriber/TaskMovedDateSubscriber.php new file mode 100644 index 00000000..eb04d62c --- /dev/null +++ b/app/Subscriber/TaskMovedDateSubscriber.php @@ -0,0 +1,24 @@ +<?php + +namespace Subscriber; + +use Event\TaskEvent; +use Model\Task; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class TaskMovedDateSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_MOVE_COLUMN => array('execute', 0), + ); + } + + public function execute(TaskEvent $event) + { + if (isset($event['task_id'])) { + $this->container['db']->table(Task::TABLE)->eq('id', $event['task_id'])->update(array('date_moved' => time())); + } + } +} diff --git a/app/Subscriber/TransitionSubscriber.php b/app/Subscriber/TransitionSubscriber.php new file mode 100644 index 00000000..5804dab7 --- /dev/null +++ b/app/Subscriber/TransitionSubscriber.php @@ -0,0 +1,26 @@ +<?php + +namespace Subscriber; + +use Event\TaskEvent; +use Model\Task; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class TransitionSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_MOVE_COLUMN => array('execute', 0), + ); + } + + public function execute(TaskEvent $event) + { + $user_id = $this->userSession->getId(); + + if (! empty($user_id)) { + $this->transition->save($user_id, $event->getAll()); + } + } +}
\ No newline at end of file diff --git a/app/Subscriber/WebhookSubscriber.php b/app/Subscriber/WebhookSubscriber.php new file mode 100644 index 00000000..5176a7ff --- /dev/null +++ b/app/Subscriber/WebhookSubscriber.php @@ -0,0 +1,45 @@ +<?php + +namespace Subscriber; + +use Event\CommentEvent; +use Event\GenericEvent; +use Event\TaskEvent; +use Model\Comment; +use Model\Task; +use Model\File; +use Model\Subtask; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class WebhookSubscriber extends \Core\Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_CREATE => array('execute', 0), + Task::EVENT_UPDATE => array('execute', 0), + Task::EVENT_CLOSE => array('execute', 0), + Task::EVENT_OPEN => array('execute', 0), + Task::EVENT_MOVE_COLUMN => array('execute', 0), + Task::EVENT_MOVE_POSITION => array('execute', 0), + Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0), + Task::EVENT_MOVE_PROJECT => array('execute', 0), + Task::EVENT_MOVE_SWIMLANE => array('execute', 0), + Comment::EVENT_CREATE => array('execute', 0), + Comment::EVENT_UPDATE => array('execute', 0), + File::EVENT_CREATE => array('execute', 0), + Subtask::EVENT_CREATE => array('execute', 0), + Subtask::EVENT_UPDATE => array('execute', 0), + ); + } + + public function execute(GenericEvent $event, $event_name) + { + $payload = array( + 'event_name' => $event_name, + 'event_data' => $event->getAll(), + ); + + $this->webhook->notify($payload); + } +} diff --git a/app/Template/action/event.php b/app/Template/action/event.php new file mode 100644 index 00000000..7f968a97 --- /dev/null +++ b/app/Template/action/event.php @@ -0,0 +1,25 @@ +<div class="page-header"> + <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> +</div> + +<h3><?= t('Choose an event') ?></h3> +<form method="post" action="<?= $this->url->href('action', 'params', array('project_id' => $project['id'])) ?>"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('project_id', $values) ?> + <?= $this->form->hidden('action_name', $values) ?> + + <?= $this->form->label(t('Event'), 'event_name') ?> + <?= $this->form->select('event_name', $events, $values) ?><br/> + + <div class="form-help"> + <?= t('When the selected event occurs execute the corresponding action.') ?> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/action/index.php b/app/Template/action/index.php new file mode 100644 index 00000000..9e98554c --- /dev/null +++ b/app/Template/action/index.php @@ -0,0 +1,64 @@ +<div class="page-header"> + <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> +</div> + +<?php if (! empty($actions)): ?> + +<h3><?= t('Defined actions') ?></h3> +<table> + <tr> + <th><?= t('Event name') ?></th> + <th><?= t('Action name') ?></th> + <th><?= t('Action parameters') ?></th> + <th><?= t('Action') ?></th> + </tr> + + <?php foreach ($actions as $action): ?> + <tr> + <td><?= $this->text->in($action['event_name'], $available_events) ?></td> + <td><?= $this->text->in($action['action_name'], $available_actions) ?></td> + <td> + <ul> + <?php foreach ($action['params'] as $param): ?> + <li> + <?= $this->text->in($param['name'], $available_params) ?> = + <strong> + <?php if ($this->text->contains($param['name'], 'column_id')): ?> + <?= $this->text->in($param['value'], $columns_list) ?> + <?php elseif ($this->text->contains($param['name'], 'user_id')): ?> + <?= $this->text->in($param['value'], $users_list) ?> + <?php elseif ($this->text->contains($param['name'], 'project_id')): ?> + <?= $this->text->in($param['value'], $projects_list) ?> + <?php elseif ($this->text->contains($param['name'], 'color_id')): ?> + <?= $this->text->in($param['value'], $colors_list) ?> + <?php elseif ($this->text->contains($param['name'], 'category_id')): ?> + <?= $this->text->in($param['value'], $categories_list) ?> + <?php elseif ($this->text->contains($param['name'], 'label')): ?> + <?= $this->e($param['value']) ?> + <?php endif ?> + </strong> + </li> + <?php endforeach ?> + </ul> + </td> + <td> + <?= $this->url->link(t('Remove'), 'action', 'confirm', array('project_id' => $project['id'], 'action_id' => $action['id'])) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<?php endif ?> + +<h3><?= t('Add an action') ?></h3> +<form method="post" action="<?= $this->url->href('action', 'event', array('project_id' => $project['id'])) ?>" class="listing"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Action'), 'action_name') ?> + <?= $this->form->select('action_name', $available_actions, $values) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/action/params.php b/app/Template/action/params.php new file mode 100644 index 00000000..685cbcc5 --- /dev/null +++ b/app/Template/action/params.php @@ -0,0 +1,43 @@ +<div class="page-header"> + <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> +</div> + +<h3><?= t('Define action parameters') ?></h3> +<form method="post" action="<?= $this->url->href('action', 'create', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('project_id', $values) ?> + <?= $this->form->hidden('event_name', $values) ?> + <?= $this->form->hidden('action_name', $values) ?> + + <?php foreach ($action_params as $param_name => $param_desc): ?> + + <?php if ($this->text->contains($param_name, 'column_id')): ?> + <?= $this->form->label($param_desc, $param_name) ?> + <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?><br/> + <?php elseif ($this->text->contains($param_name, 'user_id')): ?> + <?= $this->form->label($param_desc, $param_name) ?> + <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?><br/> + <?php elseif ($this->text->contains($param_name, 'project_id')): ?> + <?= $this->form->label($param_desc, $param_name) ?> + <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?><br/> + <?php elseif ($this->text->contains($param_name, 'color_id')): ?> + <?= $this->form->label($param_desc, $param_name) ?> + <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?><br/> + <?php elseif ($this->text->contains($param_name, 'category_id')): ?> + <?= $this->form->label($param_desc, $param_name) ?> + <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?><br/> + <?php elseif ($this->text->contains($param_name, 'label')): ?> + <?= $this->form->label($param_desc, $param_name) ?> + <?= $this->form->text('params['.$param_name.']', $values) ?> + <?php endif ?> + + <?php endforeach ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/action/remove.php b/app/Template/action/remove.php new file mode 100644 index 00000000..c8d4dfe4 --- /dev/null +++ b/app/Template/action/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove an automatic action') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this action: "%s"?', $this->text->in($action['event_name'], $available_events).'/'.$this->text->in($action['action_name'], $available_actions)) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'action', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/analytic/burndown.php b/app/Template/analytic/burndown.php new file mode 100644 index 00000000..839573be --- /dev/null +++ b/app/Template/analytic/burndown.php @@ -0,0 +1,34 @@ +<div class="page-header"> + <h2><?= t('Burndown chart') ?></h2> +</div> + +<?php if (! $display_graph): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> + <section id="analytic-burndown"> + <div id="chart" data-url="<?= $this->url->href('analytic', 'burndown', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div> + </section> +<?php endif ?> + +<hr/> + +<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'burndown', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <div class="form-inline-group"> + <?= $this->form->label(t('Start Date'), 'from') ?> + <?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <?= $this->form->label(t('End Date'), 'to') ?> + <?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form> + +<p class="alert alert-info"><?= t('This chart show the task complexity over the time (Work Remaining).') ?></p> diff --git a/app/Template/analytic/cfd.php b/app/Template/analytic/cfd.php new file mode 100644 index 00000000..26696b31 --- /dev/null +++ b/app/Template/analytic/cfd.php @@ -0,0 +1,32 @@ +<div class="page-header"> + <h2><?= t('Cumulative flow diagram') ?></h2> +</div> + +<?php if (! $display_graph): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> + <section id="analytic-cfd"> + <div id="chart" data-url="<?= $this->url->href('analytic', 'cfd', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div> + </section> +<?php endif ?> + +<hr/> + +<form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'cfd', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <div class="form-inline-group"> + <?= $this->form->label(t('Start Date'), 'from') ?> + <?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <?= $this->form->label(t('End Date'), 'to') ?> + <?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form> diff --git a/app/Template/analytic/layout.php b/app/Template/analytic/layout.php new file mode 100644 index 00000000..de8d0de9 --- /dev/null +++ b/app/Template/analytic/layout.php @@ -0,0 +1,35 @@ +<?= $this->asset->js('assets/js/vendor/d3.v3.4.8.min.js') ?> +<?= $this->asset->js('assets/js/vendor/dimple.v2.1.2.min.js') ?> + +<section id="main"> + <div class="page-header"> + <ul> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <i class="fa fa-table fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> + </li> + <li> + <i class="fa fa-folder fa-fw"></i> + <?= $this->url->link(t('All projects'), 'project', 'index') ?> + </li> + </ul> + </div> + <section class="sidebar-container" id="analytic-section"> + + <?= $this->render('analytic/sidebar', array('project' => $project)) ?> + + <div class="sidebar-content"> + <?= $content_for_sublayout ?> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php new file mode 100644 index 00000000..2d1a7c96 --- /dev/null +++ b/app/Template/analytic/sidebar.php @@ -0,0 +1,17 @@ +<div class="sidebar"> + <h2><?= t('Reportings') ?></h2> + <ul> + <li> + <?= $this->url->link(t('Task distribution'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('User repartition'), 'analytic', 'users', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?> + </li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/analytic/tasks.php b/app/Template/analytic/tasks.php new file mode 100644 index 00000000..faa4bacc --- /dev/null +++ b/app/Template/analytic/tasks.php @@ -0,0 +1,34 @@ +<div class="page-header"> + <h2><?= t('Task distribution') ?></h2> +</div> + +<?php if (empty($metrics)): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> + <section id="analytic-task-repartition"> + + <div id="chart" data-url="<?= $this->url->href('analytic', 'tasks', array('project_id' => $project['id'])) ?>"></div> + + <table> + <tr> + <th><?= t('Column') ?></th> + <th><?= t('Number of tasks') ?></th> + <th><?= t('Percentage') ?></th> + </tr> + <?php foreach ($metrics as $metric): ?> + <tr> + <td> + <?= $this->e($metric['column_title']) ?> + </td> + <td> + <?= $metric['nb_tasks'] ?> + </td> + <td> + <?= n($metric['percentage']) ?>% + </td> + </tr> + <?php endforeach ?> + </table> + + </section> +<?php endif ?> diff --git a/app/Template/analytic/users.php b/app/Template/analytic/users.php new file mode 100644 index 00000000..982ef206 --- /dev/null +++ b/app/Template/analytic/users.php @@ -0,0 +1,34 @@ +<div class="page-header"> + <h2><?= t('User repartition') ?></h2> +</div> + +<?php if (empty($metrics)): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> + <section id="analytic-user-repartition"> + + <div id="chart" data-url="<?= $this->url->href('analytic', 'users', array('project_id' => $project['id'])) ?>"></div> + + <table> + <tr> + <th><?= t('User') ?></th> + <th><?= t('Number of tasks') ?></th> + <th><?= t('Percentage') ?></th> + </tr> + <?php foreach ($metrics as $metric): ?> + <tr> + <td> + <?= $this->e($metric['user']) ?> + </td> + <td> + <?= $metric['nb_tasks'] ?> + </td> + <td> + <?= n($metric['percentage']) ?>% + </td> + </tr> + <?php endforeach ?> + </table> + + </section> +<?php endif ?> diff --git a/app/Template/app/dashboard.php b/app/Template/app/dashboard.php new file mode 100644 index 00000000..faf49ef5 --- /dev/null +++ b/app/Template/app/dashboard.php @@ -0,0 +1,60 @@ +<section id="main"> + <div class="page-header page-header-mobile"> + <ul> + <?php if ($this->user->isAdmin()): ?> + <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li> + <?php endif ?> + <li><i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?></li> + <li><i class="fa fa-folder fa-fw"></i><?= $this->url->link(t('Project management'), 'project', 'index') ?></li> + <?php if ($this->user->isAdmin()): ?> + <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('User management'), 'user', 'index') ?></li> + <li><i class="fa fa-cog fa-fw"></i><?= $this->url->link(t('Settings'), 'config', 'index') ?></li> + <?php endif ?> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Change dashboard view') ?></a> + <ul> + <li> + <a href="#" class="dashboard-toggle" data-toggle="projects"><?= t('Show/hide projects') ?></a> + </li> + <li> + <a href="#" class="dashboard-toggle" data-toggle="tasks"><?= t('Show/hide tasks') ?></a> + </li> + <li> + <a href="#" class="dashboard-toggle" data-toggle="subtasks"><?= t('Show/hide subtasks') ?></a> + </li> + <li> + <a href="#" class="dashboard-toggle" data-toggle="calendar"><?= t('Show/hide calendar') ?></a> + </li> + <li> + <a href="#" class="dashboard-toggle" data-toggle="activities"><?= t('Show/hide activities') ?></a> + </li> + </ul> + </span> + </span> + </li> + </ul> + </div> + <section id="dashboard"> + <div class="dashboard-left-column"> + <div id="dashboard-projects"><?= $this->render('app/projects', array('paginator' => $project_paginator)) ?></div> + <div id="dashboard-tasks"><?= $this->render('app/tasks', array('paginator' => $task_paginator)) ?></div> + <div id="dashboard-subtasks"><?= $this->render('app/subtasks', array('paginator' => $subtask_paginator)) ?></div> + </div> + <div class="dashboard-right-column"> + <div id="dashboard-calendar"> + <div id="user-calendar" + data-check-url="<?= $this->url->href('calendar', 'user') ?>" + data-user-id="<?= $user_id ?>" + data-save-url="<?= $this->url->href('calendar', 'save') ?>" + > + </div> + </div> + <div id="dashboard-activities"> + <h2><?= t('Activity stream') ?></h2> + <?= $this->render('event/events', array('events' => $events)) ?> + </div> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Templates/app_forbidden.php b/app/Template/app/forbidden.php index 0c035404..96e76115 100644 --- a/app/Templates/app_forbidden.php +++ b/app/Template/app/forbidden.php @@ -1,8 +1,4 @@ <section id="main"> - <div class="page-header"> - <h2><?= t('Forbidden') ?></h2> - </div> - <p class="alert alert-error"> <?= t('Access Forbidden') ?> </p> diff --git a/app/Template/app/notfound.php b/app/Template/app/notfound.php new file mode 100644 index 00000000..0419902c --- /dev/null +++ b/app/Template/app/notfound.php @@ -0,0 +1,5 @@ +<section id="main"> + <p class="alert alert-error"> + <?= t('Sorry, I didn\'t find this information in my database!') ?> + </p> +</section>
\ No newline at end of file diff --git a/app/Template/app/projects.php b/app/Template/app/projects.php new file mode 100644 index 00000000..90e6e67d --- /dev/null +++ b/app/Template/app/projects.php @@ -0,0 +1,41 @@ +<h2><?= t('My projects') ?></h2> +<?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('Your are not member of any project.') ?></p> +<?php else: ?> + <table class="table-fixed"> + <tr> + <th class="column-8"><?= $paginator->order('Id', 'id') ?></th> + <th class="column-20"><?= $paginator->order(t('Project'), 'name') ?></th> + <th><?= t('Columns') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $project): ?> + <tr> + <td> + <?= $this->url->link('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?> + </td> + <td> + <?php if ($this->user->isManager($project['id'])): ?> + <?= $this->url->link('<i class="fa fa-cog"></i>', 'project', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Settings')) ?> + <?php endif ?> + + <?= $this->url->link('<i class="fa fa-calendar"></i>', 'calendar', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Calendar')) ?> + + <?= $this->url->link($this->e($project['name']), 'board', 'show', array('project_id' => $project['id'])) ?> + <?php if (! empty($project['description'])): ?> + <span class="column-tooltip" title='<?= $this->e($this->text->markdown($project['description'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?php endif ?> + </td> + <td class="dashboard-project-stats"> + <?php foreach ($project['columns'] as $column): ?> + <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong> + <span><?= $this->e($column['title']) ?></span> + <?php endforeach ?> + </td> + </tr> + <?php endforeach ?> + </table> + + <?= $paginator ?> +<?php endif ?> diff --git a/app/Template/app/subtasks.php b/app/Template/app/subtasks.php new file mode 100644 index 00000000..5afb71b0 --- /dev/null +++ b/app/Template/app/subtasks.php @@ -0,0 +1,41 @@ +<h2><?= t('My subtasks') ?></h2> +<?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('There is nothing assigned to you.') ?></p> +<?php else: ?> + <table class="table-fixed"> + <tr> + <th class="column-10"><?= $paginator->order('Id', 'tasks.id') ?></th> + <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> + <th><?= $paginator->order(t('Task'), 'task_name') ?></th> + <th><?= $paginator->order(t('Subtask'), 'title') ?></th> + <th class="column-20"><?= t('Time tracking') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $subtask): ?> + <tr> + <td class="task-table color-<?= $subtask['color_id'] ?>"> + <?= $this->url->link('#'.$subtask['task_id'], 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> + </td> + <td> + <?= $this->url->link($this->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?> + </td> + <td> + <?= $this->url->link($this->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> + </td> + <td> + <?= $this->subtask->toggleStatus($subtask, 'dashboard') ?> + </td> + <td> + <?php if (! empty($subtask['time_spent'])): ?> + <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($subtask['time_estimated'])): ?> + <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </td> + </tr> + <?php endforeach ?> + </table> + + <?= $paginator ?> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/app/tasks.php b/app/Template/app/tasks.php new file mode 100644 index 00000000..f05c63ef --- /dev/null +++ b/app/Template/app/tasks.php @@ -0,0 +1,41 @@ +<h2><?= t('My tasks') ?></h2> +<?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('There is nothing assigned to you.') ?></p> +<?php else: ?> + <table class="table-fixed"> + <tr> + <th class="column-8"><?= $paginator->order('Id', 'tasks.id') ?></th> + <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> + <th><?= $paginator->order(t('Task'), 'title') ?></th> + <th class="column-20"><?= t('Time tracking') ?></th> + <th class="column-20"><?= $paginator->order(t('Due date'), 'date_due') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $task): ?> + <tr> + <td class="task-table color-<?= $task['color_id'] ?>"> + <?= $this->url->link('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </td> + <td> + <?= $this->url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + </td> + <td> + <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </td> + <td> + <?php if (! empty($task['time_spent'])): ?> + <strong><?= $this->e($task['time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($task['time_estimated'])): ?> + <strong><?= $this->e($task['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </td> + <td> + <?= dt('%B %e, %Y', $task['date_due']) ?> + </td> + </tr> + <?php endforeach ?> + </table> + + <?= $paginator ?> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php new file mode 100644 index 00000000..8801a512 --- /dev/null +++ b/app/Template/auth/index.php @@ -0,0 +1,32 @@ +<div class="form-login"> + + <?php if (isset($errors['login'])): ?> + <p class="alert alert-error"><?= $this->e($errors['login']) ?></p> + <?php endif ?> + + <form method="post" action="<?= $this->url->href('auth', 'check', array('redirect_query' => $redirect_query)) ?>"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Username'), 'username') ?> + <?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?><br/> + + <?= $this->form->label(t('Password'), 'password') ?> + <?= $this->form->password('password', $values, $errors, array('required')) ?> + + <?= $this->form->checkbox('remember_me', t('Remember Me'), 1) ?><br/> + + <?php if (GOOGLE_AUTH): ?> + <?= $this->url->link(t('Login with my Google Account'), 'user', 'google') ?> + <?php endif ?> + + <?php if (GITHUB_AUTH): ?> + <?= $this->url->link(t('Login with my GitHub Account'), 'user', 'gitHub') ?> + <?php endif ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/> + </div> + </form> + +</div>
\ No newline at end of file diff --git a/app/Template/board/assignee.php b/app/Template/board/assignee.php new file mode 100644 index 00000000..4af19cf7 --- /dev/null +++ b/app/Template/board/assignee.php @@ -0,0 +1,21 @@ +<section id="main"> + <section> + <h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3> + <form method="post" action="<?= $this->url->href('board', 'updateAssignee', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Assignee'), 'owner_id') ?> + <?= $this->form->select('owner_id', $users_list, $values, array(), array('autofocus')) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/board/category.php b/app/Template/board/category.php new file mode 100644 index 00000000..b38758d3 --- /dev/null +++ b/app/Template/board/category.php @@ -0,0 +1,22 @@ +<section id="main"> + <section> + <h3><?= t('Change category for the task "%s"', $values['title']) ?></h3> + <form method="post" action="<?= $this->url->href('board', 'updateCategory', array('task_id' => $values['id'], 'project_id' => $values['project_id'])) ?>"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Category'), 'category_id') ?> + <?= $this->form->select('category_id', $categories_list, $values) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> + </div> + </form> + </section> + +</section>
\ No newline at end of file diff --git a/app/Template/board/comments.php b/app/Template/board/comments.php new file mode 100644 index 00000000..75816af6 --- /dev/null +++ b/app/Template/board/comments.php @@ -0,0 +1,13 @@ +<section> + <?php foreach ($comments as $comment): ?> + <p class="comment-title"> + <span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%b %e, %Y, %k:%M %p', $comment['date']) ?></span> + </p> + + <div class="comment-inner"> + <div class="markdown"> + <?= $this->text->markdown($comment['comment']) ?> + </div> + </div> + <?php endforeach ?> +</section> diff --git a/app/Template/board/description.php b/app/Template/board/description.php new file mode 100644 index 00000000..7e0e3430 --- /dev/null +++ b/app/Template/board/description.php @@ -0,0 +1,5 @@ +<section class="tooltip-large"> +<div class="markdown"> + <?= $this->text->markdown($task['description']) ?> +</div> +</section>
\ No newline at end of file diff --git a/app/Template/board/files.php b/app/Template/board/files.php new file mode 100644 index 00000000..81136659 --- /dev/null +++ b/app/Template/board/files.php @@ -0,0 +1,31 @@ +<section> + <table> + <?php if (! empty($images)): ?> + <?php foreach ($images as $file): ?> + <tr> + <td class="column-70"> + <i class="fa fa-file-image-o fa-fw"></i> + <?= $this->e($file['name']) ?> + </td> + <td> + <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + <i class="fa fa-eye"></i> <?= $this->url->link(t('open'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + </td> + </tr> + <?php endforeach ?> + <?php endif ?> + <?php if (! empty($files)): ?> + <?php foreach ($files as $file): ?> + <tr> + <td> + <i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i> + <?= $this->e($file['name']) ?> + </td> + <td> + <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + </td> + </tr> + <?php endforeach ?> + <?php endif ?> + </table> +</section> diff --git a/app/Template/board/filters.php b/app/Template/board/filters.php new file mode 100644 index 00000000..bf2adfac --- /dev/null +++ b/app/Template/board/filters.php @@ -0,0 +1,43 @@ +<div class="page-header"> + <ul class="board-filters"> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <li> + <span class="filter-collapse"> + <i class="fa fa-compress fa-fw"></i> <a href="#" class="filter-collapse-link"><?= t('Collapse tasks') ?></a> + </span> + <span class="filter-expand" style="display: none"> + <i class="fa fa-expand fa-fw"></i> <a href="#" class="filter-expand-link"><?= t('Expand tasks') ?></a> + </span> + </li> + <li> + <span class="filter-compact"> + <i class="fa fa-th fa-fw"></i> <a href="#" class="filter-toggle-scrolling"><?= t('Compact view') ?></a> + </span> + <span class="filter-wide" style="display: none"> + <i class="fa fa-arrows-h fa-fw"></i> <a href="#" class="filter-toggle-scrolling"><?= t('Horizontal scrolling') ?></a> + </span> + </li> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <?= $this->form->select('user_id', $users, array(), array(), array('data-placeholder="'.t('Filter by user').'"', 'data-notfound="'.t('No results match:').'"'), 'apply-filters chosen-select') ?> + </li> + <li> + <?= $this->form->select('category_id', $categories, array(), array(), array('data-placeholder="'.t('Filter by category').'"', 'data-notfound="'.t('No results match:').'"'), 'apply-filters chosen-select') ?> + </li> + <li> + <select id="more-filters" multiple data-placeholder="<?= t('More filters') ?>" data-notfound="<?= t('No results match:') ?>" class="apply-filters hide-mobile"> + <option value=""></option> + <option value="filter-due-date"><?= t('Filter by due date') ?></option> + <option value="filter-recent"><?= t('Filter recently updated') ?></option> + </select> + </li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/board/index.php b/app/Template/board/index.php new file mode 100644 index 00000000..f87e0077 --- /dev/null +++ b/app/Template/board/index.php @@ -0,0 +1,18 @@ +<section id="main"> + + <?= $this->render('board/filters', array( + 'categories' => $categories_listing, + 'users' => $users, + 'project' => $project, + )) ?> + + <?= $this->render('board/show', array( + 'project' => $project, + 'swimlanes' => $swimlanes, + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'board_private_refresh_interval' => $board_private_refresh_interval, + 'board_highlight_period' => $board_highlight_period, + )) ?> + +</section> diff --git a/app/Template/board/public.php b/app/Template/board/public.php new file mode 100644 index 00000000..9e5360ce --- /dev/null +++ b/app/Template/board/public.php @@ -0,0 +1,13 @@ +<section id="main" class="public-board"> + + <?= $this->render('board/show', array( + 'project' => $project, + 'swimlanes' => $swimlanes, + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'board_private_refresh_interval' => $board_private_refresh_interval, + 'board_highlight_period' => $board_highlight_period, + 'not_editable' => true, + )) ?> + +</section>
\ No newline at end of file diff --git a/app/Template/board/show.php b/app/Template/board/show.php new file mode 100644 index 00000000..3b10d82c --- /dev/null +++ b/app/Template/board/show.php @@ -0,0 +1,32 @@ +<div id="board-container"> + <?php if (isset($not_editable)): ?> + <table id="board" class="board-project-<?= $project['id'] ?>"> + <?php else: ?> + <table id="board" + class="board-project-<?= $project['id'] ?>" + data-project-id="<?= $project['id'] ?>" + data-check-interval="<?= $board_private_refresh_interval ?>" + data-save-url="<?= $this->url->href('board', 'save', array('project_id' => $project['id'])) ?>" + data-check-url="<?= $this->url->href('board', 'check', array('project_id' => $project['id'], 'timestamp' => time())) ?>" + data-task-creation-url="<?= $this->url->href('task', 'create', array('project_id' => $project['id'])) ?>" + > + <?php endif ?> + + <?php foreach ($swimlanes as $swimlane): ?> + <?php if (empty($swimlane['columns'])): ?> + <p class="alert alert-error"><?= t('There is no column in your project!') ?></p> + <?php break ?> + <?php else: ?> + <?= $this->render('board/swimlane', array( + 'project' => $project, + 'swimlane' => $swimlane, + 'board_highlight_period' => $board_highlight_period, + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'hide_swimlane' => count($swimlanes) === 1, + 'not_editable' => isset($not_editable), + )) ?> + <?php endif ?> + <?php endforeach ?> + </table> +</div>
\ No newline at end of file diff --git a/app/Template/board/subtasks.php b/app/Template/board/subtasks.php new file mode 100644 index 00000000..950da925 --- /dev/null +++ b/app/Template/board/subtasks.php @@ -0,0 +1,7 @@ +<section id="tooltip-subtasks"> +<?php foreach ($subtasks as $subtask): ?> + <?= $this->subtask->toggleStatus($subtask, 'board') ?> + <?= $this->e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?> + <br/> +<?php endforeach ?> +</section> diff --git a/app/Template/board/swimlane.php b/app/Template/board/swimlane.php new file mode 100644 index 00000000..201ee2fc --- /dev/null +++ b/app/Template/board/swimlane.php @@ -0,0 +1,85 @@ +<tr id="swimlane-<?= $swimlane['id'] ?>"> + <?php if (! $hide_swimlane): ?> + <th> + <?php if (! $not_editable && $swimlane['nb_tasks'] > 0): ?> + <a href="#" class="board-swimlane-toggle" data-swimlane-id="<?= $swimlane['id'] ?>"> + <i class="fa fa-minus-circle hide-icon-swimlane-<?= $swimlane['id'] ?>"></i> + <i class="fa fa-plus-circle show-icon-swimlane-<?= $swimlane['id'] ?>" style="display: none"></i> + </a> + <span class="board-swimlane-toggle-title show-icon-swimlane-<?= $swimlane['id'] ?>"><?= $this->e($swimlane['name']) ?></span> + <?php endif ?> + </th> + <?php endif ?> + + <?php foreach ($swimlane['columns'] as $column): ?> + <th class="board-column"> + <?php if (! $not_editable): ?> + <div class="board-add-icon"> + <?= $this->url->link('+', 'task', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'task-board-popover', t('Add a new task')) ?> + </div> + <?php endif ?> + + <?= $this->e($column['title']) ?> + + <?php if (! $not_editable && ! empty($column['description'])): ?> + <span class="column-tooltip pull-right" title='<?= $this->e($this->text->markdown($column['description'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?php endif ?> + + <?php if (! empty($column['score'])): ?> + <span class="column-score pull-right" title="<?= t('Score') ?>"> + <?= $column['score'] ?> + </span> + <?php endif ?> + + <?php if ($column['task_limit']): ?> + <span title="<?= t('Task limit') ?>" class="task-limit"> + (<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>/<?= $this->e($column['task_limit']) ?>) + </span> + <?php else: ?> + <span title="<?= t('Task count') ?>" class="task-count"> + (<span id="task-number-column-<?= $column['id'] ?>"><?= $column['nb_tasks'] ?></span>) + </span> + <?php endif ?> + </th> + <?php endforeach ?> +</tr> +<tr class="swimlane-row-<?= $swimlane['id'] ?>"> + + <?php if (! $hide_swimlane): ?> + <th class="board-swimlane-title"> + <?= $this->e($swimlane['name']) ?> + + <span title="<?= t('Task count') ?>" class="task-count"> + (<span><?= $swimlane['nb_tasks'] ?></span>) + </span> + </th> + <?php endif ?> + + <?php foreach ($swimlane['columns'] as $column): ?> + + <?php if ($not_editable): ?> + <td> + <?php else: ?> + <td + id="column-<?= $column['id'] ?>" + class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>" + data-column-id="<?= $column['id'] ?>" + data-swimlane-id="<?= $swimlane['id'] ?>" + data-task-limit="<?= $column['task_limit'] ?>"> + <?php endif ?> + + <?php foreach ($column['tasks'] as $task): ?> + <?= $this->render($not_editable ? 'board/task_public' : 'board/task_private', array( + 'project' => $project, + 'task' => $task, + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'board_highlight_period' => $board_highlight_period, + 'not_editable' => $not_editable, + )) ?> + <?php endforeach ?> + </td> + <?php endforeach ?> +</tr>
\ No newline at end of file diff --git a/app/Template/board/task_footer.php b/app/Template/board/task_footer.php new file mode 100644 index 00000000..5945d5af --- /dev/null +++ b/app/Template/board/task_footer.php @@ -0,0 +1,61 @@ +<?php if (! empty($task['category_id'])): ?> +<div class="task-board-category-container"> + <span class="task-board-category"> + <?php if ($not_editable): ?> + <?= $this->text->in($task['category_id'], $categories_listing) ?> + <?php else: ?> + <?= $this->url->link( + $this->text->in($task['category_id'], $categories_listing), + 'board', + 'changeCategory', + array('task_id' => $task['id'], 'project_id' => $task['project_id']), + false, + 'task-board-popover' . (isset($categories_description[$task['category_id']]) ? ' column-tooltip' : ''), + isset($categories_description[$task['category_id']]) ? $this->text->markdown($categories_description[$task['category_id']]) : t('Change category') + ) ?> + <?php endif ?> + </span> +</div> +<?php endif ?> + +<div class="task-board-icons"> + <?php if (! empty($task['date_due'])): ?> + <span class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>"> + <i class="fa fa-calendar"></i> <?= dt('%b %e', $task['date_due']) ?> + </span> + <?php endif ?> + + <?php if ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PENDING): ?> + <span title="<?= t('Recurrence') ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90"></i></span> + <?php endif ?> + + <?php if ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PROCESSED): ?> + <span title="<?= t('Recurrence') ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-refresh fa-rotate-90 fa-inverse"></i></span> + <?php endif ?> + + <?php if (! empty($task['nb_links'])): ?> + <span title="<?= t('Links') ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'tasklinks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-code-fork"></i> <?= $task['nb_links'] ?></span> + <?php endif ?> + + <?php if (! empty($task['nb_subtasks'])): ?> + <span title="<?= t('Sub-Tasks') ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'subtasks', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-bars"></i> <?= round($task['nb_completed_subtasks']/$task['nb_subtasks']*100, 0).'%' ?></span> + <?php endif ?> + + <?php if (! empty($task['nb_files'])): ?> + <span title="<?= t('Attachments') ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'attachments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-paperclip"></i> <?= $task['nb_files'] ?></span> + <?php endif ?> + + <?php if (! empty($task['nb_comments'])): ?> + <span title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'comments', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><i class="fa fa-comment-o"></i> <?= $task['nb_comments'] ?></span> + <?php endif ?> + + <?php if (! empty($task['description'])): ?> + <span title="<?= t('Description') ?>" class="task-board-tooltip" data-href="<?= $this->url->href('board', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"> + <i class="fa fa-file-text-o"></i> + </span> + <?php endif ?> + + <?php if ($task['score']): ?> + <span class="task-score"><?= $this->e($task['score']) ?></span> + <?php endif ?> +</div> diff --git a/app/Template/board/task_menu.php b/app/Template/board/task_menu.php new file mode 100644 index 00000000..97c0f8dc --- /dev/null +++ b/app/Template/board/task_menu.php @@ -0,0 +1,16 @@ +<span class="dropdown"> + <span> + <a href="#" class="dropdown-menu"><?= '#'.$task['id'] ?></a> + <ul> + <li><i class="fa fa-user"></i> <?= $this->url->link(t('Change assignee'), 'board', 'changeAssignee', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-tag"></i> <?= $this->url->link(t('Change category'), 'board', 'changeCategory', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-align-left"></i> <?= $this->url->link(t('Change description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-pencil-square-o"></i> <?= $this->url->link(t('Edit this task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-comment-o"></i> <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-code-fork"></i> <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-camera"></i> <?= $this->url->link(t('Add a screenshot'), 'board', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-refresh fa-rotate-90"></i> <?= $this->url->link(t('Edit recurrence'), 'task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-popover') ?></li> + <li><i class="fa fa-close"></i> <?= $this->url->link(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => 'board'), false, 'task-board-popover') ?></li> + </ul> + </span> +</span> diff --git a/app/Template/board/task_private.php b/app/Template/board/task_private.php new file mode 100644 index 00000000..088f47bc --- /dev/null +++ b/app/Template/board/task_private.php @@ -0,0 +1,50 @@ +<div class="task-board draggable-item color-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>" + data-task-id="<?= $task['id'] ?>" + data-owner-id="<?= $task['owner_id'] ?>" + data-category-id="<?= $task['category_id'] ?>" + data-due-date="<?= $task['date_due'] ?>" + data-task-url="<?= $this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"> + + <?= $this->render('board/task_menu', array('task' => $task)) ?> + + <div class="task-board-collapsed" style="display: none"> + <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title') ?> + </div> + + <div class="task-board-expanded"> + + <?php if ($task['reference']): ?> + <span class="task-board-reference" title="<?= t('Reference') ?>"> + (<?= $task['reference'] ?>) + </span> + <?php endif ?> + + <span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>"> + <?= $this->url->link( + (! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')), + 'board', + 'changeAssignee', + array('task_id' => $task['id'], 'project_id' => $task['project_id']), + false, + 'task-board-popover', + t('Change assignee') + ) ?> + </span> + + <div class="task-board-days"> + <span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->task->age($task['date_creation']) ?></span> + <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->task->age($task['date_moved']) ?></span> + </div> + + <div class="task-board-title"> + <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + </div> + + <?= $this->render('board/task_footer', array( + 'task' => $task, + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'not_editable' => $not_editable, + )) ?> + </div> +</div> diff --git a/app/Template/board/task_public.php b/app/Template/board/task_public.php new file mode 100644 index 00000000..9ac6e570 --- /dev/null +++ b/app/Template/board/task_public.php @@ -0,0 +1,31 @@ +<div class="task-board color-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>"> + + <?= $this->url->link('#'.$task['id'], 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> + + <?php if ($task['reference']): ?> + <span class="task-board-reference" title="<?= t('Reference') ?>"> + (<?= $task['reference'] ?>) + </span> + <?php endif ?> + + - + + <span class="task-board-user"> + <?php if (! empty($task['owner_id'])): ?> + <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?> + <?php else: ?> + <span class="task-board-nobody"><?= t('Nobody assigned') ?></span> + <?php endif ?> + </span> + + <div class="task-board-title"> + <?= $this->url->link($this->e($task['title']), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token'])) ?> + </div> + + <?= $this->render('board/task_footer', array( + 'task' => $task, + 'categories_listing' => $categories_listing, + 'categories_description' => $categories_description, + 'not_editable' => $not_editable, + )) ?> +</div>
\ No newline at end of file diff --git a/app/Template/board/tasklinks.php b/app/Template/board/tasklinks.php new file mode 100644 index 00000000..25aa91aa --- /dev/null +++ b/app/Template/board/tasklinks.php @@ -0,0 +1,18 @@ +<div class="tooltip-tasklinks"> + <ul> + <?php foreach($links as $link): ?> + <li> + <strong><?= t($link['label']) ?></strong> + <?= $this->url->link( + $this->e('#'.$link['task_id'].' - '.$link['title']), + 'task', 'show', array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + <?php if (! empty($link['task_assignee_username'])): ?> + [<?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?>] + <?php endif ?> + </li> + <?php endforeach ?> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/budget/breakdown.php b/app/Template/budget/breakdown.php new file mode 100644 index 00000000..92561188 --- /dev/null +++ b/app/Template/budget/breakdown.php @@ -0,0 +1,30 @@ +<div class="page-header"> + <h2><?= t('Cost breakdown') ?></h2> +</div> + +<?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('There is nothing to show.') ?></p> +<?php else: ?> + <table class="table-fixed"> + <tr> + <th class="column-20"><?= $paginator->order(t('Task'), 'task_title') ?></th> + <th class="column-25"><?= $paginator->order(t('Subtask'), 'subtask_title') ?></th> + <th class="column-20"><?= $paginator->order(t('User'), 'username') ?></th> + <th class="column-10"><?= t('Cost') ?></th> + <th class="column-10"><?= $paginator->order(t('Time spent'), \Model\SubtaskTimeTracking::TABLE.'.time_spent') ?></th> + <th class="column-15"><?= $paginator->order(t('Date'), 'start') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $record): ?> + <tr> + <td><?= $this->url->link($this->e($record['task_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td> + <td><?= $this->url->link($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td> + <td><?= $this->url->link($this->e($record['name'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?></td> + <td><?= n($record['cost']) ?></td> + <td><?= n($record['time_spent']).' '.t('hours') ?></td> + <td><?= dt('%B %e, %Y', $record['start']) ?></td> + </tr> + <?php endforeach ?> + </table> + + <?= $paginator ?> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/budget/create.php b/app/Template/budget/create.php new file mode 100644 index 00000000..a563796d --- /dev/null +++ b/app/Template/budget/create.php @@ -0,0 +1,47 @@ +<div class="page-header"> + <h2><?= t('Budget lines') ?></h2> +</div> + +<?php if (! empty($lines)): ?> +<table class="table-fixed table-stripped"> + <tr> + <th class="column-20"><?= t('Budget line') ?></th> + <th class="column-20"><?= t('Date') ?></th> + <th><?= t('Comment') ?></th> + <th><?= t('Action') ?></th> + </tr> + <?php foreach ($lines as $line): ?> + <tr> + <td><?= n($line['amount']) ?></td> + <td><?= dt('%B %e, %Y', strtotime($line['date'])) ?></td> + <td><?= $this->e($line['comment']) ?></td> + <td> + <?= $this->url->link(t('Remove'), 'budget', 'confirm', array('project_id' => $project['id'], 'budget_id' => $line['id'])) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<h3><?= t('New budget line') ?></h3> +<?php endif ?> + +<form method="post" action="<?= $this->url->href('budget', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Amount'), 'amount') ?> + <?= $this->form->text('amount', $values, $errors, array('required'), 'form-numeric') ?> + + <?= $this->form->label(t('Date'), 'date') ?> + <?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?> + + <?= $this->form->label(t('Comment'), 'comment') ?> + <?= $this->form->text('comment', $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/budget/index.php b/app/Template/budget/index.php new file mode 100644 index 00000000..4fe8ac69 --- /dev/null +++ b/app/Template/budget/index.php @@ -0,0 +1,33 @@ +<?= $this->asset->js('assets/js/vendor/d3.v3.4.8.min.js') ?> +<?= $this->asset->js('assets/js/vendor/dimple.v2.1.2.min.js') ?> + +<div class="page-header"> + <h2><?= t('Budget overview') ?></h2> +</div> + +<?php if (! empty($daily_budget)): ?> +<div id="budget-chart"> + <div id="chart" + data-serie='<?= json_encode($daily_budget) ?>' + data-labels='<?= json_encode(array('in' => t('Budget line'), 'out' => t('Expenses'), 'left' => t('Remaining'), 'value' => t('Amount'), 'date' => t('Date'), 'type' => t('Type'))) ?>'></div> +</div> +<hr/> +<table class="table-fixed table-stripped"> + <tr> + <th><?= t('Date') ?></td> + <th><?= t('Budget line') ?></td> + <th><?= t('Expenses') ?></td> + <th><?= t('Remaining') ?></td> + </tr> + <?php foreach ($daily_budget as $line): ?> + <tr> + <td><?= dt('%B %e, %Y', strtotime($line['date'])) ?></td> + <td><?= n($line['in']) ?></td> + <td><?= n($line['out']) ?></td> + <td><?= n($line['left']) ?></td> + </tr> + <?php endforeach ?> +</table> +<?php else: ?> + <p class="alert"><?= t('There is not enough data to show something.') ?></p> +<?php endif ?> diff --git a/app/Template/budget/remove.php b/app/Template/budget/remove.php new file mode 100644 index 00000000..a5b906a1 --- /dev/null +++ b/app/Template/budget/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove budget line') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this budget line?') ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'budget', 'remove', array('project_id' => $project['id'], 'budget_id' => $budget_id), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'budget', 'create', array('project_id' => $project['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/budget/sidebar.php b/app/Template/budget/sidebar.php new file mode 100644 index 00000000..7740cf00 --- /dev/null +++ b/app/Template/budget/sidebar.php @@ -0,0 +1,14 @@ +<div class="sidebar"> + <h2><?= t('Budget') ?></h2> + <ul> + <li> + <?= $this->url->link(t('Budget overview'), 'budget', 'index', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Budget lines'), 'budget', 'create', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Cost breakdown'), 'budget', 'breakdown', array('project_id' => $project['id'])) ?> + </li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/calendar/show.php b/app/Template/calendar/show.php new file mode 100644 index 00000000..cf2a20ec --- /dev/null +++ b/app/Template/calendar/show.php @@ -0,0 +1,46 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <i class="fa fa-table fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> + </li> + <li> + <i class="fa fa-folder fa-fw"></i> + <?= $this->url->link(t('All projects'), 'project', 'index') ?> + </li> + </ul> + </div> + <section class="sidebar-container"> + + <?= $this->render('calendar/sidebar', array( + 'project' => $project, + 'users_list' => $users_list, + 'categories_list' => $categories_list, + 'columns_list' => $columns_list, + 'swimlanes_list' => $swimlanes_list, + 'colors_list' => $colors_list, + 'status_list' => $status_list + )) ?> + + <div class="sidebar-content"> + <div id="calendar" + data-project-id="<?= $project['id'] ?>" + data-save-url="<?= $this->url->href('calendar', 'save') ?>" + data-check-url="<?= $this->url->href('calendar', 'project', array('project_id' => $project['id'])) ?>" + data-check-interval="<?= $check_interval ?>" + > + </div> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/calendar/sidebar.php b/app/Template/calendar/sidebar.php new file mode 100644 index 00000000..6c4fb5b0 --- /dev/null +++ b/app/Template/calendar/sidebar.php @@ -0,0 +1,40 @@ +<div class="sidebar"> + <ul class="no-bullet"> + <li> + <?= t('Filter by user') ?> + </li> + <li> + <?= $this->form->select('owner_id', $users_list, array(), array(), array(), 'calendar-filter') ?> + </li> + <li> + <?= t('Filter by category') ?> + </li> + <li> + <?= $this->form->select('category_id', $categories_list, array(), array(), array(), 'calendar-filter') ?> + </li> + <li> + <?= t('Filter by column') ?> + </li> + <li> + <?= $this->form->select('column_id', $columns_list, array(), array(), array(), 'calendar-filter') ?> + </li> + <li> + <?= t('Filter by swimlane') ?> + </li> + <li> + <?= $this->form->select('swimlane_id', $swimlanes_list, array(), array(), array(), 'calendar-filter') ?> + </li> + <li> + <?= t('Filter by color') ?> + </li> + <li> + <?= $this->form->select('color_id', $colors_list, array(), array(), array(), 'calendar-filter') ?> + </li> + <li> + <?= t('Filter by status') ?> + </li> + <li> + <?= $this->form->select('is_active', $status_list, array(), array(), array(), 'calendar-filter') ?> + </li> + </ul> +</div> diff --git a/app/Template/category/edit.php b/app/Template/category/edit.php new file mode 100644 index 00000000..7d40fe65 --- /dev/null +++ b/app/Template/category/edit.php @@ -0,0 +1,38 @@ +<div class="page-header"> + <h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('category', 'update', array('project_id' => $project['id'], 'category_id' => $values['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Category Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + </div> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/category/index.php b/app/Template/category/index.php new file mode 100644 index 00000000..dba537d0 --- /dev/null +++ b/app/Template/category/index.php @@ -0,0 +1,42 @@ +<?php if (! empty($categories)): ?> +<div class="page-header"> + <h2><?= t('Categories') ?></h2> +</div> +<table> + <tr> + <th><?= t('Category Name') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($categories as $category_id => $category_name): ?> + <tr> + <td><?= $this->e($category_name) ?></td> + <td> + <ul> + <li> + <?= $this->url->link(t('Edit'), 'category', 'edit', array('project_id' => $project['id'], 'category_id' => $category_id)) ?> + </li> + <li> + <?= $this->url->link(t('Remove'), 'category', 'confirm', array('project_id' => $project['id'], 'category_id' => $category_id)) ?> + </li> + </ul> + </td> + </tr> + <?php endforeach ?> +</table> +<?php endif ?> + +<div class="page-header"> + <h2><?= t('Add a new category') ?></h2> +</div> +<form method="post" action="<?= $this->url->href('category', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Category Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Templates/category_remove.php b/app/Template/category/remove.php index cfc23e07..ce589785 100644 --- a/app/Templates/category_remove.php +++ b/app/Template/category/remove.php @@ -9,8 +9,9 @@ </p> <div class="form-actions"> - <a href="?controller=category&action=remove&project_id=<?= $project['id'] ?>&category_id=<?= $category['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=category&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> + <?= $this->url->link(t('Yes'), 'category', 'remove', array('project_id' => $project['id'], 'category_id' => $category['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'category', 'index', array('project_id' => $project['id'])) ?> </div> </div> </section>
\ No newline at end of file diff --git a/app/Template/column/edit.php b/app/Template/column/edit.php new file mode 100644 index 00000000..4d9848a9 --- /dev/null +++ b/app/Template/column/edit.php @@ -0,0 +1,42 @@ +<div class="page-header"> + <h2><?= t('Edit column "%s"', $column['title']) ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('column', 'update', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <?= $this->form->label(t('Task limit'), 'task_limit') ?> + <?= $this->form->number('task_limit', $values, $errors) ?> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + </div> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/column/index.php b/app/Template/column/index.php new file mode 100644 index 00000000..18e7f284 --- /dev/null +++ b/app/Template/column/index.php @@ -0,0 +1,89 @@ +<div class="page-header"> + <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2> +</div> + +<?php if (! empty($columns)): ?> + + <?php $first_position = $columns[0]['position']; ?> + <?php $last_position = $columns[count($columns) - 1]['position']; ?> + + <h3><?= t('Change columns') ?></h3> + <table> + <tr> + <th><?= t('Column title') ?></th> + <th><?= t('Task limit') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($columns as $column): ?> + <tr> + <td class="column-60"><?= $this->e($column['title']) ?> + <?php if (! empty($column['description'])): ?> + <span class="column-tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?php endif ?> + </td> + <td class="column-10"><?= $this->e($column['task_limit']) ?></td> + <td class="column-30"> + <ul> + <li> + <?= $this->url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> + </li> + <?php if ($column['position'] != $first_position): ?> + <li> + <?= $this->url->link(t('Move Up'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?> + </li> + <?php endif ?> + <?php if ($column['position'] != $last_position): ?> + <li> + <?= $this->url->link(t('Move Down'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?> + </li> + <?php endif ?> + <li> + <?= $this->url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> + </li> + </ul> + </td> + </tr> + <?php endforeach ?> + </table> + +<?php endif ?> + +<h3><?= t('Add a new column') ?></h3> +<form method="post" action="<?= $this->url->href('column', 'create', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('required', 'maxlength="50"')) ?> + + <?= $this->form->label(t('Task limit'), 'task_limit') ?> + <?= $this->form->number('task_limit', $values, $errors) ?> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + </div> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Templates/board_remove.php b/app/Template/column/remove.php index 4529063b..28d0928f 100644 --- a/app/Templates/board_remove.php +++ b/app/Template/column/remove.php @@ -9,7 +9,7 @@ </p> <div class="form-actions"> - <?= Helper\a(t('Yes'), 'board', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> - <?= t('or') ?> <?= Helper\a(t('cancel'), 'board', 'edit', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('Yes'), 'column', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id'])) ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/comment/create.php b/app/Template/comment/create.php new file mode 100644 index 00000000..8c66d9a4 --- /dev/null +++ b/app/Template/comment/create.php @@ -0,0 +1,40 @@ +<div class="page-header"> + <h2><?= t('Add a comment') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('comment', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" autocomplete="off" class="form-comment"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('task_id', $values) ?> + <?= $this->form->hidden('user_id', $values) ?> + + <div class="form-tabs"> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + <div class="write-area"> + <?= $this->form->textarea('comment', $values, $errors, array(! isset($skip_cancel) ? 'autofocus' : '', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + </div> + + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?php if (! isset($skip_cancel)): ?> + <?= t('or') ?> + <?php if (isset($ajax)): ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> + <?php else: ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?php endif ?> + <?php endif ?> + </div> +</form> diff --git a/app/Template/comment/edit.php b/app/Template/comment/edit.php new file mode 100644 index 00000000..d67aa387 --- /dev/null +++ b/app/Template/comment/edit.php @@ -0,0 +1,36 @@ +<div class="page-header"> + <h2><?= t('Edit a comment') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('comment', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('task_id', $values) ?> + <?= $this->form->hidden('user_id', $values) ?> + + <div class="form-tabs"> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + <div class="write-area"> + <?= $this->form->textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + </div> + + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</form> diff --git a/app/Template/comment/forbidden.php b/app/Template/comment/forbidden.php new file mode 100644 index 00000000..1e306d45 --- /dev/null +++ b/app/Template/comment/forbidden.php @@ -0,0 +1,7 @@ +<div class="page-header"> + <h2><?= t('Forbidden') ?></h2> +</div> + +<p class="alert alert-error"> + <?= t('Only administrators or the creator of the comment can access to this page.') ?> +</p>
\ No newline at end of file diff --git a/app/Template/comment/remove.php b/app/Template/comment/remove.php new file mode 100644 index 00000000..afc3346f --- /dev/null +++ b/app/Template/comment/remove.php @@ -0,0 +1,17 @@ +<div class="page-header"> + <h2><?= t('Remove a comment') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this comment?') ?> + </p> + + <?= $this->render('comment/show', array('comment' => $comment, 'task' => $task, 'preview' => true)) ?> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'comment', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/comment/show.php b/app/Template/comment/show.php new file mode 100644 index 00000000..35394ccb --- /dev/null +++ b/app/Template/comment/show.php @@ -0,0 +1,52 @@ +<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>"> + + <p class="comment-title"> + <?php if (! empty($comment['email'])): ?> + <?= $this->user->avatar($comment['email'], $comment['name'] ?: $comment['username']) ?> + <?php endif ?> + <span class="comment-username"><?= $this->e($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date']) ?></span> + </p> + <div class="comment-inner"> + + <?php if (! isset($preview)): ?> + <ul class="comment-actions"> + <li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li> + <?php if ((! isset($not_editable) || ! $not_editable) && ($this->user->isAdmin() || $this->user->isCurrentUser($comment['user_id']))): ?> + <li> + <?= $this->url->link(t('remove'), 'comment', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?> + </li> + <li> + <?= $this->url->link(t('edit'), 'comment', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'comment_id' => $comment['id'])) ?> + </li> + <?php endif ?> + </ul> + <?php endif ?> + + <div class="markdown"> + <?php if (isset($is_public) && $is_public): ?> + <?= $this->text->markdown( + $comment['comment'], + array( + 'controller' => 'task', + 'action' => 'readonly', + 'params' => array( + 'token' => $project['token'] + ) + ) + ) ?> + <?php else: ?> + <?= $this->text->markdown( + $comment['comment'], + array( + 'controller' => 'task', + 'action' => 'show', + 'params' => array( + 'project_id' => $task['project_id'] + ) + ) + ) ?> + <?php endif ?> + </div> + + </div> +</div>
\ No newline at end of file diff --git a/app/Template/config/about.php b/app/Template/config/about.php new file mode 100644 index 00000000..a7098c1b --- /dev/null +++ b/app/Template/config/about.php @@ -0,0 +1,57 @@ +<div class="page-header"> + <h2><?= t('About') ?></h2> +</div> +<div class="listing"> + <ul> + <li> + <?= t('Official website:') ?> + <a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a> + </li> + <li> + <?= t('Application version:') ?> + <strong><?= APP_VERSION ?></strong> + </li> + </ul> +</div> +<div class="page-header"> + <h2><?= t('Database') ?></h2> +</div> +<div class="listing"> + <ul> + <li> + <?= t('Database driver:') ?> + <strong><?= $this->e(DB_DRIVER) ?></strong> + </li> + <?php if (DB_DRIVER === 'sqlite'): ?> + <li> + <?= t('Database size:') ?> + <strong><?= $this->text->bytes($db_size) ?></strong> + </li> + <li> + <?= $this->url->link(t('Download the database'), 'config', 'downloadDb', array(), true) ?> + <?= t('(Gzip compressed Sqlite file)') ?> + </li> + <li> + <?= $this->url->link(t('Optimize the database'), 'config', 'optimizeDb', array(), true) ?> + <?= t('(VACUUM command)') ?> + </li> + <?php endif ?> + </ul> +</div> +<div class="page-header"> + <h2><?= t('Keyboard shortcuts') ?></h2> +</div> +<div class="listing"> + <h3><?= t('Board view') ?></h3> + <ul> + <li><?= t('New task') ?> = <strong>n</strong></li> + <li><?= t('Expand/collapse tasks') ?> = <strong>s</strong></li> + <li><?= t('Compact/wide view') ?> = <strong>c</strong></li> + </ul> + <h3><?= t('Application') ?></h3> + <ul> + <li><?= t('Open board switcher') ?> = <strong>b</strong></li> + <li><?= t('Close dialog box') ?> = <strong>ESC</strong></li> + <li><?= t('Submit a form') ?> = <strong>CTRL+ENTER</strong> <?= t('or') ?> <strong>⌘+ENTER</strong></li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/config/api.php b/app/Template/config/api.php new file mode 100644 index 00000000..489f1968 --- /dev/null +++ b/app/Template/config/api.php @@ -0,0 +1,18 @@ +<div class="page-header"> + <h2><?= t('API') ?></h2> +</div> +<section class="listing"> + <ul> + <li> + <?= t('API token:') ?> + <strong><?= $this->e($values['api_token']) ?></strong> + </li> + <li> + <?= t('API endpoint:') ?> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().'jsonrpc.php' ?>"> + </li> + <li> + <?= $this->url->link(t('Reset token'), 'config', 'token', array('type' => 'api'), true) ?> + </li> + </ul> +</section>
\ No newline at end of file diff --git a/app/Template/config/application.php b/app/Template/config/application.php new file mode 100644 index 00000000..7d4c811d --- /dev/null +++ b/app/Template/config/application.php @@ -0,0 +1,30 @@ +<div class="page-header"> + <h2><?= t('Application settings') ?></h2> +</div> +<section> +<form method="post" action="<?= $this->url->href('config', 'application') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Application URL'), 'application_url') ?> + <?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?><br/> + <p class="form-help"><?= t('Example: http://example.kanboard.net/ (used by email notifications)') ?></p> + + <?= $this->form->label(t('Language'), 'application_language') ?> + <?= $this->form->select('application_language', $languages, $values, $errors) ?><br/> + + <?= $this->form->label(t('Timezone'), 'application_timezone') ?> + <?= $this->form->select('application_timezone', $timezones, $values, $errors) ?><br/> + + <?= $this->form->label(t('Date format'), 'application_date_format') ?> + <?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?><br/> + <p class="form-help"><?= t('ISO format is always accepted, example: "%s" and "%s"', date('Y-m-d'), date('Y_m_d')) ?></p> + + <?= $this->form->label(t('Custom Stylesheet'), 'application_stylesheet') ?> + <?= $this->form->textarea('application_stylesheet', $values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> +</section>
\ No newline at end of file diff --git a/app/Template/config/board.php b/app/Template/config/board.php new file mode 100644 index 00000000..19a4bcd7 --- /dev/null +++ b/app/Template/config/board.php @@ -0,0 +1,25 @@ +<div class="page-header"> + <h2><?= t('Board settings') ?></h2> +</div> +<section> +<form method="post" action="<?= $this->url->href('config', 'board') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Task highlight period'), 'board_highlight_period') ?> + <?= $this->form->number('board_highlight_period', $values, $errors) ?><br/> + <p class="form-help"><?= t('Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)') ?></p> + + <?= $this->form->label(t('Refresh interval for public board'), 'board_public_refresh_interval') ?> + <?= $this->form->number('board_public_refresh_interval', $values, $errors) ?><br/> + <p class="form-help"><?= t('Frequency in second (60 seconds by default)') ?></p> + + <?= $this->form->label(t('Refresh interval for private board'), 'board_private_refresh_interval') ?> + <?= $this->form->number('board_private_refresh_interval', $values, $errors) ?><br/> + <p class="form-help"><?= t('Frequency in second (0 to disable this feature, 10 seconds by default)') ?></p> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> +</section>
\ No newline at end of file diff --git a/app/Template/config/calendar.php b/app/Template/config/calendar.php new file mode 100644 index 00000000..1cc985c8 --- /dev/null +++ b/app/Template/config/calendar.php @@ -0,0 +1,33 @@ +<div class="page-header"> + <h2><?= t('Calendar settings') ?></h2> +</div> +<section> +<form method="post" action="<?= $this->url->href('config', 'calendar') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <h3><?= t('Project calendar view') ?></h3> + <div class="listing"> + <?= $this->form->radios('calendar_project_tasks', array( + 'date_creation' => t('Show tasks based on the creation date'), + 'date_started' => t('Show tasks based on the start date'), + ), $values) ?> + </div> + + <h3><?= t('User calendar view') ?></h3> + <div class="listing"> + <?= $this->form->radios('calendar_user_tasks', array( + 'date_creation' => t('Show tasks based on the creation date'), + 'date_started' => t('Show tasks based on the start date'), + ), $values) ?> + + <h4><?= t('Subtasks time tracking') ?></h4> + <?= $this->form->checkbox('calendar_user_subtasks_time_tracking', t('Show subtasks based on the time tracking'), 1, $values['calendar_user_subtasks_time_tracking'] == 1) ?> + <?= $this->form->checkbox('calendar_user_subtasks_forecast', t('Show subtask estimates (forecast of future work)'), 1, $values['calendar_user_subtasks_forecast'] == 1) ?> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> +</section>
\ No newline at end of file diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php new file mode 100644 index 00000000..a1299806 --- /dev/null +++ b/app/Template/config/integrations.php @@ -0,0 +1,87 @@ +<div class="page-header"> + <h2><?= t('Integration with third-party services') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('config', 'integrations') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <h3><img src="assets/img/mailgun-icon.png"/> <?= t('Mailgun (incoming emails)') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'mailgun', array('token' => $values['webhook_token'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/mailgun" target="_blank"><?= t('Help on Mailgun integration') ?></a></p> + </div> + + <h3><img src="assets/img/sendgrid-icon.png"/> <?= t('Sendgrid (incoming emails)') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'sendgrid', array('token' => $values['webhook_token'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/sendgrid" target="_blank"><?= t('Help on Sendgrid integration') ?></a></p> + </div> + + <h3><img src="assets/img/postmark-icon.png"/> <?= t('Postmark (incoming emails)') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'postmark', array('token' => $values['webhook_token'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/postmark" target="_blank"><?= t('Help on Postmark integration') ?></a></p> + </div> + + <h3><img src="assets/img/gravatar-icon.png"/> <?= t('Gravatar') ?></h3> + <div class="listing"> + <?= $this->form->checkbox('integration_gravatar', t('Enable Gravatar images'), 1, $values['integration_gravatar'] == 1) ?> + </div> + + <h3><img src="assets/img/jabber-icon.png"/> <?= t('Jabber (XMPP)') ?></h3> + <div class="listing"> + <?= $this->form->checkbox('integration_jabber', t('Send notifications to Jabber'), 1, $values['integration_jabber'] == 1) ?> + + <?= $this->form->label(t('XMPP server address'), 'integration_jabber_server') ?> + <?= $this->form->text('integration_jabber_server', $values, $errors, array('placeholder="tcp://myserver:5222"')) ?> + <p class="form-help"><?= t('The server address must use this format: "tcp://hostname:5222"') ?></p> + + <?= $this->form->label(t('Jabber domain'), 'integration_jabber_domain') ?> + <?= $this->form->text('integration_jabber_domain', $values, $errors, array('placeholder="example.com"')) ?> + + <?= $this->form->label(t('Username'), 'integration_jabber_username') ?> + <?= $this->form->text('integration_jabber_username', $values, $errors) ?> + + <?= $this->form->label(t('Password'), 'integration_jabber_password') ?> + <?= $this->form->password('integration_jabber_password', $values, $errors) ?> + + <?= $this->form->label(t('Jabber nickname'), 'integration_jabber_nickname') ?> + <?= $this->form->text('integration_jabber_nickname', $values, $errors) ?> + + <?= $this->form->label(t('Multi-user chat room'), 'integration_jabber_room') ?> + <?= $this->form->text('integration_jabber_room', $values, $errors, array('placeholder="myroom@conference.example.com"')) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/jabber" target="_blank"><?= t('Help on Jabber integration') ?></a></p> + </div> + + <h3><img src="assets/img/hipchat-icon.png"/> <?= t('Hipchat') ?></h3> + <div class="listing"> + <?= $this->form->checkbox('integration_hipchat', t('Send notifications to Hipchat'), 1, $values['integration_hipchat'] == 1) ?> + + <?= $this->form->label(t('API URL'), 'integration_hipchat_api_url') ?> + <?= $this->form->text('integration_hipchat_api_url', $values, $errors) ?> + + <?= $this->form->label(t('Room API ID or name'), 'integration_hipchat_room_id') ?> + <?= $this->form->text('integration_hipchat_room_id', $values, $errors) ?> + + <?= $this->form->label(t('Room notification token'), 'integration_hipchat_room_token') ?> + <?= $this->form->text('integration_hipchat_room_token', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/hipchat" target="_blank"><?= t('Help on Hipchat integration') ?></a></p> + </div> + + <h3><i class="fa fa-slack fa-fw"></i> <?= t('Slack') ?></h3> + <div class="listing"> + <?= $this->form->checkbox('integration_slack_webhook', t('Send notifications to a Slack channel'), 1, $values['integration_slack_webhook'] == 1) ?> + + <?= $this->form->label(t('Webhook URL'), 'integration_slack_webhook_url') ?> + <?= $this->form->text('integration_slack_webhook_url', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/config/layout.php b/app/Template/config/layout.php new file mode 100644 index 00000000..028f138c --- /dev/null +++ b/app/Template/config/layout.php @@ -0,0 +1,10 @@ +<section id="main"> + <section class="sidebar-container" id="config-section"> + + <?= $this->render('config/sidebar') ?> + + <div class="sidebar-content"> + <?= $config_content_for_layout ?> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/config/project.php b/app/Template/config/project.php new file mode 100644 index 00000000..90dd9c8e --- /dev/null +++ b/app/Template/config/project.php @@ -0,0 +1,24 @@ +<div class="page-header"> + <h2><?= t('Project settings') ?></h2> +</div> +<section> +<form method="post" action="<?= $this->url->href('config', 'project') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Default columns for new projects (Comma-separated)'), 'board_columns') ?> + <?= $this->form->text('board_columns', $values, $errors) ?><br/> + <p class="form-help"><?= t('Default values are "%s"', $default_columns) ?></p> + + <?= $this->form->label(t('Default categories for new projects (Comma-separated)'), 'project_categories') ?> + <?= $this->form->text('project_categories', $values, $errors) ?><br/> + <p class="form-help"><?= t('Example: "Bug, Feature Request, Improvement"') ?></p> + + <?= $this->form->checkbox('subtask_restriction', t('Allow only one subtask in progress at the same time for a user'), 1, $values['subtask_restriction'] == 1) ?> + <?= $this->form->checkbox('subtask_time_tracking', t('Enable time tracking for subtasks'), 1, $values['subtask_time_tracking'] == 1) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> +</section>
\ No newline at end of file diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php new file mode 100644 index 00000000..7f946dee --- /dev/null +++ b/app/Template/config/sidebar.php @@ -0,0 +1,35 @@ +<div class="sidebar"> + <h2><?= t('Actions') ?></h2> + <ul> + <li> + <?= $this->url->link(t('About'), 'config', 'index') ?> + </li> + <li> + <?= $this->url->link(t('Application settings'), 'config', 'application') ?> + </li> + <li> + <?= $this->url->link(t('Project settings'), 'config', 'project') ?> + </li> + <li> + <?= $this->url->link(t('Board settings'), 'config', 'board') ?> + </li> + <li> + <?= $this->url->link(t('Calendar settings'), 'config', 'calendar') ?> + </li> + <li> + <?= $this->url->link(t('Link settings'), 'link', 'index') ?> + </li> + <li> + <?= $this->url->link(t('Currency rates'), 'currency', 'index') ?> + </li> + <li> + <?= $this->url->link(t('Integrations'), 'config', 'integrations') ?> + </li> + <li> + <?= $this->url->link(t('Webhooks'), 'config', 'webhook') ?> + </li> + <li> + <?= $this->url->link(t('API'), 'config', 'api') ?> + </li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/config/webhook.php b/app/Template/config/webhook.php new file mode 100644 index 00000000..73ca3598 --- /dev/null +++ b/app/Template/config/webhook.php @@ -0,0 +1,35 @@ +<div class="page-header"> + <h2><?= t('Webhook settings') ?></h2> +</div> +<section> +<form method="post" action="<?= $this->url->href('config', 'webhook') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Webhook URL'), 'webhook_url') ?> + <?= $this->form->text('webhook_url', $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> +</section> + +<div class="page-header"> + <h2><?= t('URL and token') ?></h2> +</div> +<section class="listing"> + <ul> + <li> + <?= t('Webhook token:') ?> + <strong><?= $this->e($values['webhook_token']) ?></strong> + </li> + <li> + <?= t('URL for task creation:') ?> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'task', array('token' => $values['webhook_token'])) ?>"> + </li> + <li> + <?= $this->url->link(t('Reset token'), 'config', 'token', array('type' => 'webhook'), true) ?> + </li> + </ul> +</section>
\ No newline at end of file diff --git a/app/Template/currency/index.php b/app/Template/currency/index.php new file mode 100644 index 00000000..f72c5700 --- /dev/null +++ b/app/Template/currency/index.php @@ -0,0 +1,56 @@ +<div class="page-header"> + <h2><?= t('Currency rates') ?></h2> +</div> + +<?php if (! empty($rates)): ?> + +<table class="table-stripped"> + <tr> + <th class="column-35"><?= t('Currency') ?></th> + <th><?= t('Rate') ?></th> + </tr> + <?php foreach ($rates as $rate): ?> + <tr> + <td> + <strong><?= $this->e($rate['currency']) ?></strong> + </td> + <td> + <?= n($rate['rate']) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<hr/> +<h3><?= t('Change reference currency') ?></h3> +<?php endif ?> +<form method="post" action="<?= $this->url->href('currency', 'reference') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Reference currency'), 'application_currency') ?> + <?= $this->form->select('application_currency', $currencies, $config_values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<hr/> +<h3><?= t('Add a new currency rate') ?></h3> +<form method="post" action="<?= $this->url->href('currency', 'create') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Currency'), 'currency') ?> + <?= $this->form->select('currency', $currencies, $values, $errors) ?><br/> + + <?= $this->form->label(t('Rate'), 'rate') ?> + <?= $this->form->text('rate', $values, $errors, array(), 'form-numeric') ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<p class="alert alert-info"><?= t('Currency rates are used to calculate project budget.') ?></p> diff --git a/app/Template/event/comment_create.php b/app/Template/event/comment_create.php new file mode 100644 index 00000000..462f15ca --- /dev/null +++ b/app/Template/event/comment_create.php @@ -0,0 +1,12 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s commented the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> +</p> +<div class="activity-description"> + <em><?= $this->e($task['title']) ?></em><br/> + <div class="markdown"><?= $this->text->markdown($comment['comment']) ?></div> +</div>
\ No newline at end of file diff --git a/app/Template/event/comment_update.php b/app/Template/event/comment_update.php new file mode 100644 index 00000000..0cb10bf6 --- /dev/null +++ b/app/Template/event/comment_update.php @@ -0,0 +1,11 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s updated a comment on the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> +</p> +<div class="activity-description"> + <em><?= $this->e($task['title']) ?></em><br/> +</div>
\ No newline at end of file diff --git a/app/Templates/project_events.php b/app/Template/event/events.php index 1b606414..2dc79871 100644 --- a/app/Templates/project_events.php +++ b/app/Template/event/events.php @@ -5,11 +5,11 @@ <?php foreach ($events as $event): ?> <div class="activity-event"> <p class="activity-datetime"> - <?php if (Helper\contains($event['event_name'], 'subtask')): ?> + <?php if ($this->text->contains($event['event_name'], 'subtask')): ?> <i class="fa fa-tasks"></i> - <?php elseif (Helper\contains($event['event_name'], 'task')): ?> + <?php elseif ($this->text->contains($event['event_name'], 'task')): ?> <i class="fa fa-newspaper-o"></i> - <?php elseif (Helper\contains($event['event_name'], 'comment')): ?> + <?php elseif ($this->text->contains($event['event_name'], 'comment')): ?> <i class="fa fa-comments-o"></i> <?php endif ?> <?= dt('%B %e, %Y at %k:%M %p', $event['date_creation']) ?> diff --git a/app/Templates/event_subtask_create.php b/app/Template/event/subtask_create.php index 664e9da2..ca23aa9c 100644 --- a/app/Templates/event_subtask_create.php +++ b/app/Template/event/subtask_create.php @@ -1,12 +1,17 @@ +<?= $this->user->avatar($email, $author) ?> + <p class="activity-title"> - <?= e('%s created a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> + <?= e('%s created a subtask for the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> </p> <div class="activity-description"> - <p><em><?= Helper\escape($task['title']) ?></em></p> + <p><em><?= $this->e($task['title']) ?></em></p> <ul> <li> - <?= Helper\escape($subtask['title']) ?> (<strong><?= Helper\escape($subtask['status_name']) ?></strong>) + <?= $this->e($subtask['title']) ?> (<strong><?= $this->e($subtask['status_name']) ?></strong>) </li> <li> <?php if ($subtask['username']): ?> diff --git a/app/Templates/event_subtask_update.php b/app/Template/event/subtask_update.php index 96a589dd..11a778de 100644 --- a/app/Templates/event_subtask_update.php +++ b/app/Template/event/subtask_update.php @@ -1,12 +1,17 @@ +<?= $this->user->avatar($email, $author) ?> + <p class="activity-title"> - <?= e('%s updated a subtask for the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> + <?= e('%s updated a subtask for the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> </p> <div class="activity-description"> - <p><em><?= Helper\escape($task['title']) ?></em></p> + <p><em><?= $this->e($task['title']) ?></em></p> <ul> <li> - <?= Helper\escape($subtask['title']) ?> (<strong><?= Helper\escape($subtask['status_name']) ?></strong>) + <?= $this->e($subtask['title']) ?> (<strong><?= $this->e($subtask['status_name']) ?></strong>) </li> <li> <?php if ($subtask['username']): ?> diff --git a/app/Template/event/task_assignee_change.php b/app/Template/event/task_assignee_change.php new file mode 100644 index 00000000..cdec8743 --- /dev/null +++ b/app/Template/event/task_assignee_change.php @@ -0,0 +1,18 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?php $assignee = $task['assignee_name'] ?: $task['assignee_username'] ?> + + <?php if (! empty($assignee)): ?> + <?= e('%s changed the assignee of the task %s to %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->e($assignee) + ) ?> + <?php else: ?> + <?= e('%s remove the assignee of the task %s', $this->e($author), $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> + <?php endif ?> +</p> +<p class="activity-description"> + <em><?= $this->e($task['title']) ?></em> +</p>
\ No newline at end of file diff --git a/app/Template/event/task_close.php b/app/Template/event/task_close.php new file mode 100644 index 00000000..3d8670a6 --- /dev/null +++ b/app/Template/event/task_close.php @@ -0,0 +1,11 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s closed the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> +</p> +<p class="activity-description"> + <em><?= $this->e($task['title']) ?></em> +</p>
\ No newline at end of file diff --git a/app/Template/event/task_create.php b/app/Template/event/task_create.php new file mode 100644 index 00000000..773f401c --- /dev/null +++ b/app/Template/event/task_create.php @@ -0,0 +1,11 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s created the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> +</p> +<p class="activity-description"> + <em><?= $this->e($task['title']) ?></em> +</p>
\ No newline at end of file diff --git a/app/Template/event/task_move_column.php b/app/Template/event/task_move_column.php new file mode 100644 index 00000000..ca482e46 --- /dev/null +++ b/app/Template/event/task_move_column.php @@ -0,0 +1,12 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s moved the task %s to the column "%s"', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->e($task['column_title']) + ) ?> +</p> +<p class="activity-description"> + <em><?= $this->e($task['title']) ?></em> +</p>
\ No newline at end of file diff --git a/app/Template/event/task_move_position.php b/app/Template/event/task_move_position.php new file mode 100644 index 00000000..dcdd3e1b --- /dev/null +++ b/app/Template/event/task_move_position.php @@ -0,0 +1,13 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s moved the task %s to the position #%d in the column "%s"', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $task['position'], + $this->e($task['column_title']) + ) ?> +</p> +<p class="activity-description"> + <em><?= $this->e($task['title']) ?></em> +</p>
\ No newline at end of file diff --git a/app/Template/event/task_open.php b/app/Template/event/task_open.php new file mode 100644 index 00000000..11fec64b --- /dev/null +++ b/app/Template/event/task_open.php @@ -0,0 +1,11 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s opened the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> +</p> +<p class="activity-description"> + <em><?= $this->e($task['title']) ?></em> +</p>
\ No newline at end of file diff --git a/app/Template/event/task_update.php b/app/Template/event/task_update.php new file mode 100644 index 00000000..7d036d43 --- /dev/null +++ b/app/Template/event/task_update.php @@ -0,0 +1,11 @@ +<?= $this->user->avatar($email, $author) ?> + +<p class="activity-title"> + <?= e('%s updated the task %s', + $this->e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> +</p> +<p class="activity-description"> + <em><?= $this->e($task['title']) ?></em> +</p>
\ No newline at end of file diff --git a/app/Template/export/sidebar.php b/app/Template/export/sidebar.php new file mode 100644 index 00000000..f93dcafb --- /dev/null +++ b/app/Template/export/sidebar.php @@ -0,0 +1,17 @@ +<div class="sidebar"> + <h2><?= t('Exports') ?></h2> + <ul> + <li> + <?= $this->url->link(t('Tasks'), 'export', 'tasks', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Subtasks'), 'export', 'subtasks', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Task transitions'), 'export', 'transitions', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Daily project summary'), 'export', 'summary', array('project_id' => $project['id'])) ?> + </li> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/export/subtasks.php b/app/Template/export/subtasks.php new file mode 100644 index 00000000..4aad2641 --- /dev/null +++ b/app/Template/export/subtasks.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2> + <?= t('Subtasks exportation for "%s"', $project['name']) ?> + </h2> +</div> + +<p class="alert alert-info"><?= t('This report contains all subtasks information for the given date range.') ?></p> + +<form method="get" action="?" autocomplete="off"> + + <?= $this->form->hidden('controller', $values) ?> + <?= $this->form->hidden('action', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Start Date'), 'from') ?> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + + <?= $this->form->label(t('End Date'), 'to') ?> + <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/export/summary.php b/app/Template/export/summary.php new file mode 100644 index 00000000..ffbd6ac2 --- /dev/null +++ b/app/Template/export/summary.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2> + <?= t('Daily project summary export for "%s"', $project['name']) ?> + </h2> +</div> + +<p class="alert alert-info"><?= t('This export contains the number of tasks per column grouped per day.') ?></p> + +<form method="get" action="?" autocomplete="off"> + + <?= $this->form->hidden('controller', $values) ?> + <?= $this->form->hidden('action', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Start Date'), 'from') ?> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + + <?= $this->form->label(t('End Date'), 'to') ?> + <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/export/tasks.php b/app/Template/export/tasks.php new file mode 100644 index 00000000..c74c8f98 --- /dev/null +++ b/app/Template/export/tasks.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2> + <?= t('Tasks exportation for "%s"', $project['name']) ?> + </h2> +</div> + +<p class="alert alert-info"><?= t('This report contains all tasks information for the given date range.') ?></p> + +<form method="get" action="?" autocomplete="off"> + + <?= $this->form->hidden('controller', $values) ?> + <?= $this->form->hidden('action', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Start Date'), 'from') ?> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + + <?= $this->form->label(t('End Date'), 'to') ?> + <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/export/transitions.php b/app/Template/export/transitions.php new file mode 100644 index 00000000..bf6ef249 --- /dev/null +++ b/app/Template/export/transitions.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2> + <?= t('Task transitions export') ?> + </h2> +</div> + +<p class="alert alert-info"><?= t('This report contains all column moves for each task with the date, the user and the time spent for each transition.') ?></p> + +<form method="get" action="?" autocomplete="off"> + + <?= $this->form->hidden('controller', $values) ?> + <?= $this->form->hidden('action', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Start Date'), 'from') ?> + <?= $this->form->text('from', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + + <?= $this->form->label(t('End Date'), 'to') ?> + <?= $this->form->text('to', $values, $errors, array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/file/new.php b/app/Template/file/new.php new file mode 100644 index 00000000..a1a59eae --- /dev/null +++ b/app/Template/file/new.php @@ -0,0 +1,14 @@ +<div class="page-header"> + <h2><?= t('Attach a document') ?></h2> +</div> + +<form action="<?= $this->url->href('file', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" enctype="multipart/form-data"> + <?= $this->form->csrf() ?> + <input type="file" name="files[]" multiple /> + <div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? $this->text->bytes($max_size) : $max_size ?></div> + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/file/open.php b/app/Template/file/open.php new file mode 100644 index 00000000..3df012b6 --- /dev/null +++ b/app/Template/file/open.php @@ -0,0 +1,6 @@ +<div class="page-header"> + <h2><?= $this->e($file['name']) ?></h2> + <div class="task-file-viewer"> + <img src="<?= $this->url->href('file', 'image', array('file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/file/remove.php b/app/Template/file/remove.php new file mode 100644 index 00000000..37f648eb --- /dev/null +++ b/app/Template/file/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a file') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this file: "%s"?', $this->e($file['name'])) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'file', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/file/screenshot.php b/app/Template/file/screenshot.php new file mode 100644 index 00000000..89d9324c --- /dev/null +++ b/app/Template/file/screenshot.php @@ -0,0 +1,17 @@ +<div class="page-header"> + <h2><?= t('Add a screenshot') ?></h2> +</div> + +<div id="screenshot-zone"> + <p id="screenshot-inner"><?= t('Take a screenshot and press CTRL+V or ⌘+V to paste here.') ?></p> +</div> + +<form action="<?= $this->url->href('file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'redirect' => $redirect)) ?>" method="post"> + <input type="hidden" name="screenshot"/> + <?= $this->form->csrf() ?> + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + </div> +</form> diff --git a/app/Template/file/show.php b/app/Template/file/show.php new file mode 100644 index 00000000..7d5dc96f --- /dev/null +++ b/app/Template/file/show.php @@ -0,0 +1,56 @@ +<?php if (! empty($files) || ! empty($images)): ?> +<div id="attachments" class="task-show-section"> + + <div class="page-header"> + <h2><?= t('Attachments') ?></h2> + </div> + <?php if (! empty($images)): ?> + <h3><?= t('Images') ?></h3> + <ul class="task-show-images"> + <?php foreach ($images as $file): ?> + <li> + <?php if (function_exists('imagecreatetruecolor')): ?> + <div class="img_container"> + <img src="<?= $this->url->href('file', 'thumbnail', array('width' => 250, 'height' => 100, 'file_id' => $file['id'], 'project_id' => $task['project_id'], 'task_id' => $file['task_id'])) ?>" alt="<?= $this->e($file['name']) ?>"/> + </div> + <?php endif ?> + <p> + <?= $this->e($file['name']) ?> + <span class="column-tooltip" title='<?= t('uploaded by: %s', $file['user_name'] ?: $file['username']).'<br>'.t('uploaded on: %s', dt('%B %e, %Y at %k:%M %p', $file['date'])).'<br>'.t('size: %s', $this->text->bytes($file['size'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + </p> + <span class="task-show-file-actions task-show-image-actions"> + <i class="fa fa-eye"></i> <?= $this->url->link(t('open'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + <i class="fa fa-trash"></i> <?= $this->url->link(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + </span> + </li> + <?php endforeach ?> + </ul> + <?php endif ?> + + <?php if (! empty($files)): ?> + <h3><?= t('Files') ?></h3> + <table class="task-show-file-table"> + <?php foreach ($files as $file): ?> + <tr> + <td><i class="fa <?= $this->file->icon($file['name']) ?> fa-fw"></i></td> + <td> + <?= $this->e($file['name']) ?> + <span class="column-tooltip" title='<?= t('uploaded by: %s', $file['user_name'] ?: $file['username']).'<br>'.t('uploaded on: %s', dt('%B %e, %Y at %k:%M %p', $file['date'])).'<br>'.t('size: %s', $this->text->bytes($file['size'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + </td> + <td> + <span class="task-show-file-actions"> + <i class="fa fa-trash"></i> <?= $this->url->link(t('remove'), 'file', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + <i class="fa fa-download"></i> <?= $this->url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + </span> + </td> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> +</div> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/hourlyrate/index.php b/app/Template/hourlyrate/index.php new file mode 100644 index 00000000..af305d07 --- /dev/null +++ b/app/Template/hourlyrate/index.php @@ -0,0 +1,46 @@ +<div class="page-header"> + <h2><?= t('Hourly rates') ?></h2> +</div> + +<?php if (! empty($rates)): ?> + +<table> + <tr> + <th><?= t('Hourly rate') ?></th> + <th><?= t('Currency') ?></th> + <th><?= t('Effective date') ?></th> + <th><?= t('Action') ?></th> + </tr> + <?php foreach ($rates as $rate): ?> + <tr> + <td><?= n($rate['rate']) ?></td> + <td><?= $rate['currency'] ?></td> + <td><?= dt('%b %e, %Y', $rate['date_effective']) ?></td> + <td> + <?= $this->url->link(t('Remove'), 'hourlyrate', 'confirm', array('user_id' => $user['id'], 'rate_id' => $rate['id'])) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<h3><?= t('Add new rate') ?></h3> +<?php endif ?> + +<form method="post" action="<?= $this->url->href('hourlyrate', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->hidden('user_id', $values) ?> + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Hourly rate'), 'rate') ?> + <?= $this->form->text('rate', $values, $errors, array('required'), 'form-numeric') ?> + + <?= $this->form->label(t('Currency'), 'currency') ?> + <?= $this->form->select('currency', $currencies_list, $values, $errors, array('required')) ?> + + <?= $this->form->label(t('Effective date'), 'date_effective') ?> + <?= $this->form->text('date_effective', $values, $errors, array('required'), 'form-date') ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> diff --git a/app/Template/hourlyrate/remove.php b/app/Template/hourlyrate/remove.php new file mode 100644 index 00000000..121436e4 --- /dev/null +++ b/app/Template/hourlyrate/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove hourly rate') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this hourly rate?') ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'hourlyrate', 'remove', array('user_id' => $user['id'], 'rate_id' => $rate_id), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'hourlyrate', 'index', array('user_id' => $user['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/layout.php b/app/Template/layout.php new file mode 100644 index 00000000..cf74c8ab --- /dev/null +++ b/app/Template/layout.php @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> + <meta name="mobile-web-app-capable" content="yes"> + <meta name="robots" content="noindex,nofollow"> + + <?php if (isset($board_public_refresh_interval)): ?> + <meta http-equiv="refresh" content="<?= $board_public_refresh_interval ?>"> + <?php endif ?> + + <?php if (! isset($not_editable)): ?> + <?= $this->asset->js('assets/js/app.js') ?> + <?php endif ?> + + <?= $this->asset->css($this->url->href('app', 'colors'), false, 'all') ?> + <?= $this->asset->css('assets/css/app.css') ?> + <?= $this->asset->css('assets/css/print.css', true, 'print') ?> + <?= $this->asset->customCss() ?> + + <link rel="icon" type="image/png" href="assets/img/favicon.png"> + <link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png"> + <link rel="apple-touch-icon" sizes="72x72" href="assets/img/touch-icon-ipad.png"> + <link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png"> + <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png"> + + <title><?= isset($title) ? $this->e($title) : 'Kanboard' ?></title> + </head> + <body data-status-url="<?= $this->url->href('app', 'status') ?>" + data-login-url="<?= $this->url->href('auth', 'login') ?>" + data-timezone="<?= $this->app->getTimezone() ?>" + data-js-lang="<?= $this->app->jsLang() ?>"> + + <?php if (isset($no_layout) && $no_layout): ?> + <?= $content_for_layout ?> + <?php else: ?> + <header> + <nav> + <h1><?= $this->url->link('K<span>B</span>', 'app', 'index', array(), false, 'logo', t('Dashboard')).' '.$this->text->truncate($this->e($title)) ?> + <?php if (! empty($description)): ?> + <span class="column-tooltip" title='<?= $this->e($this->text->markdown($description)) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?php endif ?> + </h1> + <ul> + <?php if (isset($board_selector) && ! empty($board_selector)): ?> + <li> + <select id="board-selector" data-notfound="<?= t('No results match:') ?>" data-placeholder="<?= t('Display another project') ?>" data-board-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>"> + <option value=""></option> + <?php foreach($board_selector as $board_id => $board_name): ?> + <option value="<?= $board_id ?>"><?= $this->e($board_name) ?></option> + <?php endforeach ?> + </select> + </li> + <?php endif ?> + <li> + <?= $this->url->link(t('Logout'), 'auth', 'logout') ?> + <span class="username hide-tablet">(<?= $this->user->getProfileLink() ?>)</span> + </li> + </ul> + </nav> + </header> + <section class="page"> + <?= $this->app->flashMessage() ?> + <?= $content_for_layout ?> + </section> + <?php endif ?> + </body> +</html> diff --git a/app/Template/link/create.php b/app/Template/link/create.php new file mode 100644 index 00000000..2b4ac62c --- /dev/null +++ b/app/Template/link/create.php @@ -0,0 +1,18 @@ +<div class="page-header"> + <h2><?= t('Add a new link') ?></h2> +</div> + +<form action="<?= $this->url->href('link', 'save') ?>" method="post" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Label'), 'label') ?> + <?= $this->form->text('label', $values, $errors, array('required')) ?> + + <?= $this->form->label(t('Opposite label'), 'opposite_label') ?> + <?= $this->form->text('opposite_label', $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/link/edit.php b/app/Template/link/edit.php new file mode 100644 index 00000000..516de464 --- /dev/null +++ b/app/Template/link/edit.php @@ -0,0 +1,21 @@ +<div class="page-header"> + <h2><?= t('Link modification') ?></h2> +</div> + +<form action="<?= $this->url->href('link', 'update', array('link_id' => $link['id'])) ?>" method="post" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + + <?= $this->form->label(t('Label'), 'label') ?> + <?= $this->form->text('label', $values, $errors, array('required')) ?> + + <?= $this->form->label(t('Opposite label'), 'opposite_id') ?> + <?= $this->form->select('opposite_id', $labels, $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'link', 'index') ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/link/index.php b/app/Template/link/index.php new file mode 100644 index 00000000..1475bd50 --- /dev/null +++ b/app/Template/link/index.php @@ -0,0 +1,33 @@ +<div class="page-header"> + <h2><?= t('Link labels') ?></h2> +</div> +<?php if (! empty($links)): ?> +<table> + <tr> + <th class="column-70"><?= t('Link labels') ?></th> + <th><?= t('Actions') ?></th> + </tr> + <?php foreach ($links as $link): ?> + <tr> + <td> + <strong><?= t($link['label']) ?></strong> + + <?php if (! empty($link['opposite_label'])): ?> + | <?= t($link['opposite_label']) ?> + <?php endif ?> + </td> + <td> + <ul> + <?= $this->url->link(t('Edit'), 'link', 'edit', array('link_id' => $link['id'])) ?> + <?= t('or') ?> + <?= $this->url->link(t('Remove'), 'link', 'confirm', array('link_id' => $link['id'])) ?> + </ul> + </td> + </tr> + <?php endforeach ?> +</table> +<?php else: ?> + <?= t('There is no link.') ?> +<?php endif ?> + +<?= $this->render('link/create', array('values' => $values, 'errors' => $errors)) ?>
\ No newline at end of file diff --git a/app/Template/link/remove.php b/app/Template/link/remove.php new file mode 100644 index 00000000..12ca14bb --- /dev/null +++ b/app/Template/link/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a link') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this link: "%s"?', $link['label']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'link', 'remove', array('link_id' => $link['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'link', 'index') ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/notification/comment_create.php b/app/Template/notification/comment_create.php new file mode 100644 index 00000000..747c4f43 --- /dev/null +++ b/app/Template/notification/comment_create.php @@ -0,0 +1,7 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('New comment posted by %s', $comment['name'] ?: $comment['username']) ?></h3> + +<?= $this->text->markdown($comment['comment']) ?> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/comment_update.php b/app/Template/notification/comment_update.php new file mode 100644 index 00000000..a15e5d6d --- /dev/null +++ b/app/Template/notification/comment_update.php @@ -0,0 +1,7 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Comment updated') ?></h3> + +<?= $this->text->markdown($comment['comment']) ?> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/file_create.php b/app/Template/notification/file_create.php new file mode 100644 index 00000000..63f7d1b8 --- /dev/null +++ b/app/Template/notification/file_create.php @@ -0,0 +1,5 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<p><?= t('New attachment added "%s"', $file['name']) ?></p> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/footer.php b/app/Template/notification/footer.php new file mode 100644 index 00000000..7041c43b --- /dev/null +++ b/app/Template/notification/footer.php @@ -0,0 +1,6 @@ +<hr/> +Kanboard + +<?php if (isset($application_url) && ! empty($application_url)): ?> + - <a href="<?= $application_url.$this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= t('view the task on Kanboard') ?></a>. +<?php endif ?> diff --git a/app/Template/notification/subtask_create.php b/app/Template/notification/subtask_create.php new file mode 100644 index 00000000..e1c62b73 --- /dev/null +++ b/app/Template/notification/subtask_create.php @@ -0,0 +1,17 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('New sub-task') ?></h3> + +<ul> + <li><?= t('Title:') ?> <?= $this->e($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= $this->e($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= $this->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> + <li> + <?= t('Time tracking:') ?> + <?php if (! empty($subtask['time_estimated'])): ?> + <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </li> +</ul> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/subtask_update.php b/app/Template/notification/subtask_update.php new file mode 100644 index 00000000..cfde9db6 --- /dev/null +++ b/app/Template/notification/subtask_update.php @@ -0,0 +1,21 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<h3><?= t('Sub-task updated') ?></h3> + +<ul> + <li><?= t('Title:') ?> <?= $this->e($subtask['title']) ?></li> + <li><?= t('Status:') ?> <?= $this->e($subtask['status_name']) ?></li> + <li><?= t('Assignee:') ?> <?= $this->e($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> + <li> + <?= t('Time tracking:') ?> + <?php if (! empty($subtask['time_spent'])): ?> + <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($subtask['time_estimated'])): ?> + <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </li> +</ul> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_task_assignee_change.php b/app/Template/notification/task_assignee_change.php index d23f769e..c9729ac9 100644 --- a/app/Templates/notification_task_assignee_change.php +++ b/app/Template/notification/task_assignee_change.php @@ -1,4 +1,4 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> @@ -14,7 +14,7 @@ <?php if (! empty($task['description'])): ?> <h2><?= t('Description') ?></h2> - <?= Helper\markdown($task['description']) ?: t('There is no description.') ?> + <?= $this->text->markdown($task['description']) ?: t('There is no description.') ?> <?php endif ?> -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_close.php b/app/Template/notification/task_close.php new file mode 100644 index 00000000..463223a0 --- /dev/null +++ b/app/Template/notification/task_close.php @@ -0,0 +1,5 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<p><?= t('The task #%d have been closed.', $task['id']) ?></p> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_task_update.php b/app/Template/notification/task_create.php index b3c07911..1d834d44 100644 --- a/app/Templates/notification_task_update.php +++ b/app/Template/notification/task_create.php @@ -1,4 +1,4 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> @@ -9,14 +9,14 @@ <strong><?= dt('Must be done before %B %e, %Y', $task['date_due']) ?></strong> </li> <?php endif ?> - <?php if ($task['creator_username']): ?> + <?php if (! empty($task['creator_username'])): ?> <li> <?= t('Created by %s', $task['creator_name'] ?: $task['creator_username']) ?> </li> <?php endif ?> <li> <strong> - <?php if ($task['assignee_username']): ?> + <?php if (! empty($task['assignee_username'])): ?> <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?> <?php else: ?> <?= t('There is nobody assigned') ?> @@ -25,19 +25,19 @@ </li> <li> <?= t('Column on the board:') ?> - <strong><?= Helper\escape($task['column_title']) ?></strong> + <strong><?= $this->e($task['column_title']) ?></strong> </li> - <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li> - <?php if ($task['category_name']): ?> + <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> + <?php if (! empty($task['category_name'])): ?> <li> - <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong> + <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong> </li> <?php endif ?> </ul> <?php if (! empty($task['description'])): ?> <h2><?= t('Description') ?></h2> - <?= Helper\markdown($task['description']) ?: t('There is no description.') ?> + <?= $this->text->markdown($task['description']) ?> <?php endif ?> -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_move_column.php b/app/Template/notification/task_move_column.php new file mode 100644 index 00000000..88ab8ab5 --- /dev/null +++ b/app/Template/notification/task_move_column.php @@ -0,0 +1,11 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<ul> + <li> + <?= t('Column on the board:') ?> + <strong><?= $this->e($task['column_title']) ?></strong> + </li> + <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> +</ul> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_move_position.php b/app/Template/notification/task_move_position.php new file mode 100644 index 00000000..88ab8ab5 --- /dev/null +++ b/app/Template/notification/task_move_position.php @@ -0,0 +1,11 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<ul> + <li> + <?= t('Column on the board:') ?> + <strong><?= $this->e($task['column_title']) ?></strong> + </li> + <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> +</ul> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_open.php b/app/Template/notification/task_open.php new file mode 100644 index 00000000..cb02a79c --- /dev/null +++ b/app/Template/notification/task_open.php @@ -0,0 +1,5 @@ +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> + +<p><?= t('The task #%d have been opened.', $task['id']) ?></p> + +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Template/notification/task_overdue.php b/app/Template/notification/task_overdue.php new file mode 100644 index 00000000..dc2659dc --- /dev/null +++ b/app/Template/notification/task_overdue.php @@ -0,0 +1,18 @@ +<h2><?= t('Overdue tasks for the project "%s"', $project_name) ?></h2> + +<ul> + <?php foreach ($tasks as $task): ?> + <li> + (<strong>#<?= $task['id'] ?></strong>) + <?php if ($application_url): ?> + <a href="<?= $application_url.$this->url->href('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"><?= $this->e($task['title']) ?></a> + <?php else: ?> + <?= $this->e($task['title']) ?> + <?php endif ?> + (<?= dt('%B %e, %Y', $task['date_due']) ?>) + <?php if ($task['assignee_username']): ?> + (<strong><?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></strong>) + <?php endif ?> + </li> + <?php endforeach ?> +</ul> diff --git a/app/Templates/notification_task_creation.php b/app/Template/notification/task_update.php index 1b555096..ffea49cd 100644 --- a/app/Templates/notification_task_creation.php +++ b/app/Template/notification/task_update.php @@ -1,4 +1,4 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> +<h2><?= $this->e($task['title']) ?> (#<?= $task['id'] ?>)</h2> <ul> <li> @@ -25,19 +25,19 @@ </li> <li> <?= t('Column on the board:') ?> - <strong><?= Helper\escape($task['column_title']) ?></strong> + <strong><?= $this->e($task['column_title']) ?></strong> </li> - <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li> + <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> <?php if ($task['category_name']): ?> <li> - <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong> + <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong> </li> <?php endif ?> </ul> <?php if (! empty($task['description'])): ?> <h2><?= t('Description') ?></h2> - <?= Helper\markdown($task['description']) ?> + <?= $this->text->markdown($task['description']) ?: t('There is no description.') ?> <?php endif ?> -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file +<?= $this->render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/project_disable.php b/app/Template/project/disable.php index 7a729fa3..ddfcdca2 100644 --- a/app/Templates/project_disable.php +++ b/app/Template/project/disable.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <?= Helper\a(t('Yes'), 'project', 'disable', array('project_id' => $project['id'], 'disable' => 'yes'), true, 'btn btn-red') ?> - <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('Yes'), 'project', 'disable', array('project_id' => $project['id'], 'disable' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/project/dropdown.php b/app/Template/project/dropdown.php new file mode 100644 index 00000000..2e2650a7 --- /dev/null +++ b/app/Template/project/dropdown.php @@ -0,0 +1,41 @@ +<li> + <i class="fa fa-search fa-fw"></i> + <?= $this->url->link(t('Search'), 'projectinfo', 'search', array('project_id' => $project['id'])) ?> +</li> +<li> + <i class="fa fa-check-square-o fa-fw"></i> + <?= $this->url->link(t('Completed tasks'), 'projectinfo', 'tasks', array('project_id' => $project['id'])) ?> +</li> +<li> + <i class="fa fa-dashboard fa-fw"></i> + <?= $this->url->link(t('Activity'), 'projectinfo', 'activity', array('project_id' => $project['id'])) ?> +</li> +<li> + <i class="fa fa-calendar fa-fw"></i> + <?= $this->url->link(t('Calendar'), 'calendar', 'show', array('project_id' => $project['id'])) ?> +</li> + +<?php if ($project['is_public']): ?> +<li> + <i class="fa fa-share-alt fa-fw"></i> <?= $this->url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?> +</li> +<?php endif ?> + +<?php if ($this->user->isManager($project['id'])): ?> +<li> + <i class="fa fa-line-chart fa-fw"></i> + <?= $this->url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?> +</li> +<li> + <i class="fa fa-pie-chart fa-fw"></i> + <?= $this->url->link(t('Budget'), 'budget', 'index', array('project_id' => $project['id'])) ?> +</li> +<li> + <i class="fa fa-download fa-fw"></i> + <?= $this->url->link(t('Exports'), 'export', 'tasks', array('project_id' => $project['id'])) ?> +</li> +<li> + <i class="fa fa-cog fa-fw"></i> + <?= $this->url->link(t('Settings'), 'project', 'show', array('project_id' => $project['id'])) ?> +</li> +<?php endif ?> diff --git a/app/Template/project/duplicate.php b/app/Template/project/duplicate.php new file mode 100644 index 00000000..8967c306 --- /dev/null +++ b/app/Template/project/duplicate.php @@ -0,0 +1,23 @@ +<div class="page-header"> + <h2><?= t('Clone this project') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Which parts of the project do you want to duplicate?') ?> + </p> + <form method="post" action="<?= $this->url->href('project', 'duplicate', array('project_id' => $project['id'], 'duplicate' => 'yes')) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->checkbox('category', t('Categories'), 1, true) ?> + <?= $this->form->checkbox('action', t('Actions'), 1, true) ?> + <?= $this->form->checkbox('swimlane', t('Swimlanes'), 1, false) ?> + <?= $this->form->checkbox('task', t('Tasks'), 1, false) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Duplicate') ?>" class="btn btn-red"/> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> + </div> + </form> +</div>
\ No newline at end of file diff --git a/app/Template/project/edit.php b/app/Template/project/edit.php new file mode 100644 index 00000000..794267f4 --- /dev/null +++ b/app/Template/project/edit.php @@ -0,0 +1,44 @@ +<div class="page-header"> + <h2><?= t('Edit project') ?></h2> +</div> +<form method="post" action="<?= $this->url->href('project', 'update', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('required', 'maxlength="50"')) ?> + + <?= $this->form->label(t('Identifier'), 'identifier') ?> + <?= $this->form->text('identifier', $values, $errors, array('maxlength="50"')) ?> + <p class="form-help"><?= t('The project identifier is an optional alphanumeric code used to identify your project.') ?></p> + + <?php if ($this->user->isAdmin()): ?> + <?= $this->form->checkbox('is_private', t('Private project'), 1, $project['is_private'] == 1) ?> + <?php endif ?> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + </div> + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> diff --git a/app/Templates/project_enable.php b/app/Template/project/enable.php index f2a1b0e7..c10d2f12 100644 --- a/app/Templates/project_enable.php +++ b/app/Template/project/enable.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <?= Helper\a(t('Yes'), 'project', 'enable', array('project_id' => $project['id'], 'enable' => 'yes'), true, 'btn btn-red') ?> - <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('Yes'), 'project', 'enable', array('project_id' => $project['id'], 'enable' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> </div> </div>
\ No newline at end of file diff --git a/app/Templates/project_feed.php b/app/Template/project/feed.php index 9d10ecb1..2062e801 100644 --- a/app/Templates/project_feed.php +++ b/app/Template/project/feed.php @@ -1,21 +1,21 @@ <?= '<?xml version="1.0" encoding="utf-8"?>' ?> <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"> <title><?= t('%s\'s activity', $project['name']) ?></title> - <link rel="alternate" type="text/html" href="<?= Helper\get_current_base_url() ?>"/> - <link rel="self" type="application/atom+xml" href="<?= Helper\get_current_base_url().Helper\u('project', 'feed', array('token' => $project['token'])) ?>"/> + <link rel="alternate" type="text/html" href="<?= $this->url->base() ?>"/> + <link rel="self" type="application/atom+xml" href="<?= $this->url->base().$this->url->href('project', 'feed', array('token' => $project['token'])) ?>"/> <updated><?= date(DATE_ATOM) ?></updated> - <id><?= Helper\get_current_base_url() ?></id> - <icon><?= Helper\get_current_base_url() ?>assets/img/favicon.png</icon> + <id><?= $this->url->base() ?></id> + <icon><?= $this->url->base() ?>assets/img/favicon.png</icon> <?php foreach ($events as $e): ?> <entry> <title type="text"><?= $e['event_title'] ?></title> - <link rel="alternate" href="<?= Helper\get_current_base_url().Helper\u('task', 'show', array('task_id' => $e['task_id'])) ?>"/> + <link rel="alternate" href="<?= $this->url->base().$this->url->href('task', 'show', array('task_id' => $e['task_id'])) ?>"/> <id><?= $e['id'].'-'.$e['event_name'].'-'.$e['task_id'].'-'.$e['date_creation'] ?></id> <published><?= date(DATE_ATOM, $e['date_creation']) ?></published> <updated><?= date(DATE_ATOM, $e['date_creation']) ?></updated> <author> - <name><?= Helper\escape($e['author']) ?></name> + <name><?= $this->e($e['author']) ?></name> </author> <content type="html"> <![CDATA[ diff --git a/app/Template/project/index.php b/app/Template/project/index.php new file mode 100644 index 00000000..1080968e --- /dev/null +++ b/app/Template/project/index.php @@ -0,0 +1,67 @@ +<section id="main"> + <div class="page-header"> + <ul> + <?php if ($this->user->isAdmin()): ?> + <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New project'), 'project', 'create') ?></li> + <?php endif ?> + <li><i class="fa fa-lock fa-fw"></i><?= $this->url->link(t('New private project'), 'project', 'create', array('private' => 1)) ?></li> + </ul> + </div> + <section> + <?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('No project') ?></p> + <?php else: ?> + <table class="table-fixed"> + <tr> + <th class="column-8"><?= $paginator->order(t('Id'), 'id') ?></th> + <th class="column-8"><?= $paginator->order(t('Status'), 'is_active') ?></th> + <th class="column-8"><?= $paginator->order(t('Identifier'), 'identifier') ?></th> + <th class="column-20"><?= $paginator->order(t('Project'), 'name') ?></th> + <th><?= t('Columns') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $project): ?> + <tr> + <td> + <?= $this->url->link('#'.$project['id'], 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link') ?> + </td> + <td> + <?php if ($project['is_active']): ?> + <?= t('Active') ?> + <?php else: ?> + <?= t('Inactive') ?> + <?php endif ?> + </td> + <td> + <?= $this->e($project['identifier']) ?> + </td> + <td> + <?= $this->url->link('<i class="fa fa-table"></i>', 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Board')) ?> + + <?php if ($project['is_public']): ?> + <i class="fa fa-share-alt fa-fw"></i> + <?php endif ?> + <?php if ($project['is_private']): ?> + <i class="fa fa-lock fa-fw"></i> + <?php endif ?> + + <?= $this->url->link($this->e($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?> + <?php if (! empty($project['description'])): ?> + <span class="column-tooltip" title='<?= $this->e($this->text->markdown($project['description'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?php endif ?> + </td> + <td class="dashboard-project-stats"> + <?php foreach ($project['columns'] as $column): ?> + <strong title="<?= t('Task count') ?>"><?= $column['nb_tasks'] ?></strong> + <span><?= $this->e($column['title']) ?></span> + <?php endforeach ?> + </td> + </tr> + <?php endforeach ?> + </table> + + <?= $paginator ?> + <?php endif ?> + </section> +</section> diff --git a/app/Template/project/integrations.php b/app/Template/project/integrations.php new file mode 100644 index 00000000..698e438c --- /dev/null +++ b/app/Template/project/integrations.php @@ -0,0 +1,95 @@ +<div class="page-header"> + <h2><?= t('Integration with third-party services') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('project', 'integration', array('project_id' => $project['id'])) ?>" autocomplete="off"> + <?= $this->form->csrf() ?> + + + <h3><i class="fa fa-github fa-fw"></i> <?= t('Github webhooks') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'github', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/github-webhooks" target="_blank"><?= t('Help on Github webhooks') ?></a></p> + </div> + + + <h3><img src="assets/img/gitlab-icon.png"/> <?= t('Gitlab webhooks') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'gitlab', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/gitlab-webhooks" target="_blank"><?= t('Help on Gitlab webhooks') ?></a></p> + </div> + + + <h3><i class="fa fa-bitbucket fa-fw"></i> <?= t('Bitbucket webhooks') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->base().$this->url->href('webhook', 'bitbucket', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/bitbucket-webhooks" target="_blank"><?= t('Help on Bitbucket webhooks') ?></a></p> + </div> + + + <h3><img src="assets/img/jabber-icon.png"/> <?= t('Jabber (XMPP)') ?></h3> + <div class="listing"> + <?= $this->form->checkbox('jabber', t('Send notifications to Jabber'), 1, isset($values['jabber']) && $values['jabber'] == 1) ?> + + <?= $this->form->label(t('XMPP server address'), 'jabber_server') ?> + <?= $this->form->text('jabber_server', $values, $errors, array('placeholder="tcp://myserver:5222"')) ?> + <p class="form-help"><?= t('The server address must use this format: "tcp://hostname:5222"') ?></p> + + <?= $this->form->label(t('Jabber domain'), 'jabber_domain') ?> + <?= $this->form->text('jabber_domain', $values, $errors, array('placeholder="example.com"')) ?> + + <?= $this->form->label(t('Username'), 'jabber_username') ?> + <?= $this->form->text('jabber_username', $values, $errors) ?> + + <?= $this->form->label(t('Password'), 'jabber_password') ?> + <?= $this->form->password('jabber_password', $values, $errors) ?> + + <?= $this->form->label(t('Jabber nickname'), 'jabber_nickname') ?> + <?= $this->form->text('jabber_nickname', $values, $errors) ?> + + <?= $this->form->label(t('Multi-user chat room'), 'jabber_room') ?> + <?= $this->form->text('jabber_room', $values, $errors, array('placeholder="myroom@conference.example.com"')) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/jabber" target="_blank"><?= t('Help on Jabber integration') ?></a></p> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </div> + + + <h3><img src="assets/img/hipchat-icon.png"/> <?= t('Hipchat') ?></h3> + <div class="listing"> + <?= $this->form->checkbox('hipchat', t('Send notifications to Hipchat'), 1, isset($values['hipchat']) && $values['hipchat'] == 1) ?> + + <?= $this->form->label(t('API URL'), 'hipchat_api_url') ?> + <?= $this->form->text('hipchat_api_url', $values, $errors) ?> + + <?= $this->form->label(t('Room API ID or name'), 'hipchat_room_id') ?> + <?= $this->form->text('hipchat_room_id', $values, $errors) ?> + + <?= $this->form->label(t('Room notification token'), 'hipchat_room_token') ?> + <?= $this->form->text('hipchat_room_token', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/hipchat" target="_blank"><?= t('Help on Hipchat integration') ?></a></p> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </div> + + + <h3><i class="fa fa-slack fa-fw"></i> <?= t('Slack') ?></h3> + <div class="listing"> + <?= $this->form->checkbox('slack', t('Send notifications to a Slack channel'), 1, isset($values['slack']) && $values['slack'] == 1) ?> + + <?= $this->form->label(t('Webhook URL'), 'slack_webhook_url') ?> + <?= $this->form->text('slack_webhook_url', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/project/layout.php b/app/Template/project/layout.php new file mode 100644 index 00000000..7bb3d478 --- /dev/null +++ b/app/Template/project/layout.php @@ -0,0 +1,32 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <i class="fa fa-table fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> + </li> + <li> + <i class="fa fa-folder fa-fw"></i> + <?= $this->url->link(t('All projects'), 'project', 'index') ?> + </li> + </ul> + </div> + <section class="sidebar-container"> + + <?= $this->render($sidebar_template, array('project' => $project)) ?> + + <div class="sidebar-content"> + <?= $project_content_for_layout ?> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/project/new.php b/app/Template/project/new.php new file mode 100644 index 00000000..8e4ccfec --- /dev/null +++ b/app/Template/project/new.php @@ -0,0 +1,24 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li><i class="fa fa-folder fa-fw"></i><?= $this->url->link(t('All projects'), 'project', 'index') ?></li> + </ul> + </div> + <form method="post" action="<?= $this->url->href('project', 'save') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('is_private', $values) ?> + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'index') ?> + </div> + </form> + <?php if (isset($is_private) && $is_private): ?> + <div class="alert alert-info"> + <p><?= t('There is no user management for private projects.') ?></p> + </div> + <?php endif ?> +</section>
\ No newline at end of file diff --git a/app/Templates/project_remove.php b/app/Template/project/remove.php index a98f94eb..fa43fc78 100644 --- a/app/Templates/project_remove.php +++ b/app/Template/project/remove.php @@ -8,7 +8,7 @@ </p> <div class="form-actions"> - <?= Helper\a(t('Yes'), 'project', 'remove', array('project_id' => $project['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> - <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> + <?= $this->url->link(t('Yes'), 'project', 'remove', array('project_id' => $project['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> </div> </div>
\ No newline at end of file diff --git a/app/Template/project/share.php b/app/Template/project/share.php new file mode 100644 index 00000000..a9146599 --- /dev/null +++ b/app/Template/project/share.php @@ -0,0 +1,19 @@ +<div class="page-header"> + <h2><?= t('Public access') ?></h2> +</div> + +<?php if ($project['is_public']): ?> + + <div class="listing"> + <ul class="no-bullet"> + <li><strong><i class="fa fa-share-alt"></i> <?= $this->url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?></strong></li> + <li><strong><i class="fa fa-rss-square"></i> <?= $this->url->link(t('RSS feed'), 'project', 'feed', array('token' => $project['token']), false, '', '', true) ?></strong></li> + <li><strong><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token']), false, '', '', true) ?></strong></li> + </ul> + </div> + + <?= $this->url->link(t('Disable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> + +<?php else: ?> + <?= $this->url->link(t('Enable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> +<?php endif ?> diff --git a/app/Template/project/show.php b/app/Template/project/show.php new file mode 100644 index 00000000..4869d8a4 --- /dev/null +++ b/app/Template/project/show.php @@ -0,0 +1,73 @@ +<div class="page-header"> + <h2><?= t('Summary') ?></h2> +</div> +<ul class="listing"> + <li><strong><?= $project['is_active'] ? t('Active') : t('Inactive') ?></strong></li> + + <?php if ($project['is_private']): ?> + <li><i class="fa fa-lock"></i> <?= t('This project is private') ?></li> + <?php endif ?> + + <?php if ($project['is_public']): ?> + <li><i class="fa fa-share-alt"></i> <?= $this->url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?></li> + <li><i class="fa fa-rss-square"></i> <?= $this->url->link(t('RSS feed'), 'project', 'feed', array('token' => $project['token']), false, '', '', true) ?></li> + <li><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li> + <?php else: ?> + <li><?= t('Public access disabled') ?></li> + <?php endif ?> + + <?php if ($project['last_modified']): ?> + <li><?= dt('Last modified on %B %e, %Y at %k:%M %p', $project['last_modified']) ?></li> + <?php endif ?> + + <?php if ($stats['nb_tasks'] > 0): ?> + + <?php if ($stats['nb_active_tasks'] > 0): ?> + <li><?= $this->url->link(t('%d tasks on the board', $stats['nb_active_tasks']), 'board', 'show', array('project_id' => $project['id'])) ?></li> + <?php endif ?> + + <?php if ($stats['nb_inactive_tasks'] > 0): ?> + <li><?= $this->url->link(t('%d closed tasks', $stats['nb_inactive_tasks']), 'project', 'tasks', array('project_id' => $project['id'])) ?></li> + <?php endif ?> + + <li><?= t('%d tasks in total', $stats['nb_tasks']) ?></li> + + <?php else: ?> + <li><?= t('No task for this project') ?></li> + <?php endif ?> +</ul> + +<div class="page-header"> + <h2><?= t('Board') ?></h2> +</div> +<table class="table-stripped"> + <tr> + <th class="column-60"><?= t('Column') ?></th> + <th class="column-20"><?= t('Task limit') ?></th> + <th class="column-20"><?= t('Active tasks') ?></th> + </tr> + <?php foreach ($stats['columns'] as $column): ?> + <tr> + <td> + <?= $this->e($column['title']) ?> + <?php if (! empty($column['description'])): ?> + <span class="column-tooltip" title='<?= $this->e($this->text->markdown($column['description'])) ?>'> + <i class="fa fa-info-circle"></i> + </span> + <?php endif ?> + </td> + <td><?= $column['task_limit'] ?: '∞' ?></td> + <td><?= $column['nb_active_tasks'] ?></td> + </tr> + <?php endforeach ?> +</table> + +<?php if (! empty($project['description'])): ?> + <div class="page-header"> + <h2><?= t('Description') ?></h2> + </div> + + <article class="markdown"> + <?= $this->text->markdown($project['description']) ?> + </article> +<?php endif ?> diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php new file mode 100644 index 00000000..a58c4604 --- /dev/null +++ b/app/Template/project/sidebar.php @@ -0,0 +1,52 @@ +<div class="sidebar"> + <h2><?= t('Actions') ?></h2> + <ul> + <li> + <?= $this->url->link(t('Summary'), 'project', 'show', array('project_id' => $project['id'])) ?> + </li> + + <?php if ($this->user->isManager($project['id'])): ?> + <li> + <?= $this->url->link(t('Public access'), 'project', 'share', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Integrations'), 'project', 'integration', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Edit project'), 'project', 'edit', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Edit board'), 'column', 'index', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Swimlanes'), 'swimlane', 'index', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Categories'), 'category', 'index', array('project_id' => $project['id'])) ?> + </li> + <?php if ($this->user->isAdmin() || $project['is_private'] == 0): ?> + <li> + <?= $this->url->link(t('Users'), 'project', 'users', array('project_id' => $project['id'])) ?> + </li> + <?php endif ?> + <li> + <?= $this->url->link(t('Automatic actions'), 'action', 'index', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Duplicate'), 'project', 'duplicate', array('project_id' => $project['id'])) ?> + </li> + <li> + <?php if ($project['is_active']): ?> + <?= $this->url->link(t('Disable'), 'project', 'disable', array('project_id' => $project['id']), true) ?> + <?php else: ?> + <?= $this->url->link(t('Enable'), 'project', 'enable', array('project_id' => $project['id']), true) ?> + <?php endif ?> + </li> + <?php if ($this->user->isAdmin()): ?> + <li> + <?= $this->url->link(t('Remove'), 'project', 'remove', array('project_id' => $project['id'])) ?> + </li> + <?php endif ?> + <?php endif ?> + </ul> +</div> diff --git a/app/Template/project/users.php b/app/Template/project/users.php new file mode 100644 index 00000000..d725a9e8 --- /dev/null +++ b/app/Template/project/users.php @@ -0,0 +1,81 @@ +<div class="page-header"> + <h2><?= t('List of authorized users') ?></h2> +</div> + +<?php if ($project['is_everybody_allowed']): ?> + <div class="alert"><?= t('Everybody have access to this project.') ?></div> +<?php else: ?> + + <?php if (empty($users['allowed'])): ?> + <div class="alert alert-error"><?= t('Nobody have access to this project.') ?></div> + <?php else: ?> + <table> + <tr> + <th><?= t('User') ?></th> + <th><?= t('Role for this project') ?></th> + <?php if ($project['is_private'] == 0): ?> + <th><?= t('Actions') ?></th> + <?php endif ?> + </tr> + <?php foreach ($users['allowed'] as $user_id => $username): ?> + <tr> + <td><?= $this->e($username) ?></td> + <td><?= isset($users['managers'][$user_id]) ? t('Project manager') : t('Project member') ?></td> + <?php if ($project['is_private'] == 0): ?> + <td> + <ul> + <li><?= $this->url->link(t('Revoke'), 'project', 'revoke', array('project_id' => $project['id'], 'user_id' => $user_id), true) ?></li> + <li> + <?php if (isset($users['managers'][$user_id])): ?> + <?= $this->url->link(t('Set project member'), 'project', 'role', array('project_id' => $project['id'], 'user_id' => $user_id, 'is_owner' => 0), true) ?> + <?php else: ?> + <?= $this->url->link(t('Set project manager'), 'project', 'role', array('project_id' => $project['id'], 'user_id' => $user_id, 'is_owner' => 1), true) ?> + <?php endif ?> + </li> + </ul> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + </table> + <?php endif ?> + + <?php if ($project['is_private'] == 0 && ! empty($users['not_allowed'])): ?> + <hr/> + <form method="post" action="<?= $this->url->href('project', 'allow', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('project_id', array('project_id' => $project['id'])) ?> + + <?= $this->form->label(t('User'), 'user_id') ?> + <?= $this->form->select('user_id', $users['not_allowed'], array(), array(), array('data-notfound="'.t('No results match:').'"'), 'chosen-select') ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Allow this user') ?>" class="btn btn-blue"/> + </div> + </form> + <?php endif ?> + +<?php endif ?> + +<?php if ($project['is_private'] == 0): ?> +<hr/> +<form method="post" action="<?= $this->url->href('project', 'allowEverybody', array('project_id' => $project['id'])) ?>"> + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', array('id' => $project['id'])) ?> + <?= $this->form->checkbox('is_everybody_allowed', t('Allow everybody to access to this project'), 1, $project['is_everybody_allowed']) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> +<?php endif ?> + +<div class="alert alert-info"> + <ul> + <li><?= t('A project manager can change the settings of the project and have more privileges than a standard user.') ?></li> + <li><?= t('Don\'t forget that administrators have access to everything.') ?></li> + </ul> +</div> diff --git a/app/Template/projectinfo/activity.php b/app/Template/projectinfo/activity.php new file mode 100644 index 00000000..528cdbee --- /dev/null +++ b/app/Template/projectinfo/activity.php @@ -0,0 +1,30 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <i class="fa fa-table fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> + </li> + <li> + <i class="fa fa-folder fa-fw"></i> + <?= $this->url->link(t('All projects'), 'project', 'index') ?> + </li> + <?php if ($project['is_public']): ?> + <li><i class="fa fa-rss-square fa-fw"></i><?= $this->url->link(t('RSS feed'), 'project', 'feed', array('token' => $project['token']), false, '', '', true) ?></li> + <li><i class="fa fa-calendar fa-fw"></i><?= $this->url->link(t('iCal feed'), 'ical', 'project', array('token' => $project['token'])) ?></li> + <?php endif ?> + </ul> + </div> + + <?= $this->render('event/events', array('events' => $events)) ?> +</section>
\ No newline at end of file diff --git a/app/Template/projectinfo/search.php b/app/Template/projectinfo/search.php new file mode 100644 index 00000000..4b7c8f70 --- /dev/null +++ b/app/Template/projectinfo/search.php @@ -0,0 +1,43 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <i class="fa fa-table fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> + </li> + <li> + <i class="fa fa-folder fa-fw"></i> + <?= $this->url->link(t('All projects'), 'project', 'index') ?> + </li> + </ul> + </div> + + <form method="get" action="?" autocomplete="off"> + <?= $this->form->hidden('controller', $values) ?> + <?= $this->form->hidden('action', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + <?= $this->form->text('search', $values, array(), array('autofocus', 'required', 'placeholder="'.t('Search').'"'), 'form-input-large') ?> + <input type="submit" value="<?= t('Search') ?>" class="btn btn-blue"/> + </form> + + <?php if (! empty($values['search']) && $paginator->isEmpty()): ?> + <p class="alert"><?= t('Nothing found.') ?></p> + <?php elseif (! $paginator->isEmpty()): ?> + <?= $this->render('task/table', array( + 'paginator' => $paginator, + 'categories' => $categories, + 'columns' => $columns, + )) ?> + <?php endif ?> + +</section>
\ No newline at end of file diff --git a/app/Template/projectinfo/tasks.php b/app/Template/projectinfo/tasks.php new file mode 100644 index 00000000..41884783 --- /dev/null +++ b/app/Template/projectinfo/tasks.php @@ -0,0 +1,33 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <span class="dropdown"> + <span> + <i class="fa fa-caret-down"></i> <a href="#" class="dropdown-menu"><?= t('Actions') ?></a> + <ul> + <?= $this->render('project/dropdown', array('project' => $project)) ?> + </ul> + </span> + </span> + </li> + <li> + <i class="fa fa-table fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?> + </li> + <li> + <i class="fa fa-folder fa-fw"></i> + <?= $this->url->link(t('All projects'), 'project', 'index') ?> + </li> + </ul> + </div> + <?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('There is no completed tasks at the moment.') ?></p> + <?php else: ?> + <?= $this->render('task/table', array( + 'paginator' => $paginator, + 'categories' => $categories, + 'columns' => $columns, + )) ?> + <?php endif ?> +</section>
\ No newline at end of file diff --git a/app/Template/subtask/create.php b/app/Template/subtask/create.php new file mode 100644 index 00000000..82e378f5 --- /dev/null +++ b/app/Template/subtask/create.php @@ -0,0 +1,27 @@ +<div class="page-header"> + <h2><?= t('Add a sub-task') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('task_id', $values) ?> + + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('required', 'autofocus', 'maxlength="255"')) ?><br/> + + <?= $this->form->label(t('Assignee'), 'user_id') ?> + <?= $this->form->select('user_id', $users_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Original estimate'), 'time_estimated') ?> + <?= $this->form->numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> + + <?= $this->form->checkbox('another_subtask', t('Create another sub-task'), 1, isset($values['another_subtask']) && $values['another_subtask'] == 1) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</form> diff --git a/app/Template/subtask/edit.php b/app/Template/subtask/edit.php new file mode 100644 index 00000000..2e583069 --- /dev/null +++ b/app/Template/subtask/edit.php @@ -0,0 +1,29 @@ +<div class="page-header"> + <h2><?= t('Edit a sub-task') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('subtask', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('task_id', $values) ?> + + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('required', 'autofocus', 'maxlength="255"')) ?><br/> + + <?= $this->form->label(t('Assignee'), 'user_id') ?> + <?= $this->form->select('user_id', $users_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Original estimate'), 'time_estimated') ?> + <?= $this->form->numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> + + <?= $this->form->label(t('Time spent'), 'time_spent') ?> + <?= $this->form->numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</form> diff --git a/app/Template/subtask/icons.php b/app/Template/subtask/icons.php new file mode 100644 index 00000000..1f31d51f --- /dev/null +++ b/app/Template/subtask/icons.php @@ -0,0 +1,7 @@ +<?php if ($subtask['status'] == 0): ?> + <i class="fa fa-square-o fa-fw"></i> +<?php elseif ($subtask['status'] == 1): ?> + <i class="fa fa-gears fa-fw"></i> +<?php else: ?> + <i class="fa fa-check-square-o fa-fw"></i> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/subtask/remove.php b/app/Template/subtask/remove.php new file mode 100644 index 00000000..65ade31d --- /dev/null +++ b/app/Template/subtask/remove.php @@ -0,0 +1,17 @@ +<div class="page-header"> + <h2><?= t('Remove a sub-task') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this sub-task?') ?> + </p> + + <p><strong><?= $this->e($subtask['title']) ?></strong></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'subtask', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/subtask/restriction_change_status.php b/app/Template/subtask/restriction_change_status.php new file mode 100644 index 00000000..88e91d82 --- /dev/null +++ b/app/Template/subtask/restriction_change_status.php @@ -0,0 +1,19 @@ +<div class="page-header"> + <h2><?= t('You already have one subtask in progress') ?></h2> +</div> + + <form action="<?= $this->url->href('subtask', 'changeRestrictionStatus', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?>" method="post"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('redirect', array('redirect' => $redirect)) ?> + + <p><?= t('Select the new status of the subtask: "%s"', $subtask_inprogress['title']) ?></p> + <?= $this->form->radios('status', $status_list) ?> + <?= $this->form->hidden('id', $subtask_inprogress) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-red"/> + <?= t('or') ?> + <a href="#" class="close-popover"><?= t('cancel') ?></a> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/subtask/show.php b/app/Template/subtask/show.php new file mode 100644 index 00000000..c9690f08 --- /dev/null +++ b/app/Template/subtask/show.php @@ -0,0 +1,80 @@ +<?php if (! empty($subtasks)): ?> + +<?php $first_position = $subtasks[0]['position']; ?> +<?php $last_position = $subtasks[count($subtasks) - 1]['position']; ?> + +<div id="subtasks" class="task-show-section"> + + <div class="page-header"> + <h2><?= t('Sub-Tasks') ?></h2> + </div> + + <table class="subtasks-table"> + <tr> + <th class="column-40"><?= t('Title') ?></th> + <th><?= t('Assignee') ?></th> + <th><?= t('Time tracking') ?></th> + <?php if (! isset($not_editable)): ?> + <th><?= t('Actions') ?></th> + <?php endif ?> + </tr> + <?php foreach ($subtasks as $subtask): ?> + <tr> + <td> + <?php if (! isset($not_editable)): ?> + <?= $this->subtask->toggleStatus($subtask, 'task') ?> + <?php else: ?> + <?= $this->render('subtask/icons', array('subtask' => $subtask)) . $this->e($subtask['title']) ?> + <?php endif ?> + </td> + <td> + <?php if (! empty($subtask['username'])): ?> + <?= $this->url->link($this->e($subtask['name'] ?: $subtask['username']), 'user', 'show', array('user_id' => $subtask['user_id'])) ?> + <?php endif ?> + </td> + <td> + <?php if (! empty($subtask['time_spent'])): ?> + <strong><?= $this->e($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($subtask['time_estimated'])): ?> + <strong><?= $this->e($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </td> + <?php if (! isset($not_editable)): ?> + <td> + <ul> + <?php if ($subtask['position'] != $first_position): ?> + <li> + <?= $this->url->link(t('Move Up'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'up'), true) ?> + </li> + <?php endif ?> + <?php if ($subtask['position'] != $last_position): ?> + <li> + <?= $this->url->link(t('Move Down'), 'subtask', 'movePosition', array('project_id' => $project['id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'], 'direction' => 'down'), true) ?> + </li> + <?php endif ?> + <li> + <?= $this->url->link(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'subtask_id' => $subtask['id'])) ?> + </li> + </ul> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + </table> + + <?php if (! isset($not_editable)): ?> + <form method="post" action="<?= $this->url->href('subtask', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> + <?= $this->form->text('title', array(), array(), array('required', 'placeholder="'.t('Type here to create a new sub-task').'"')) ?> + <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> + </form> + <?php endif ?> + +</div> +<?php endif ?> diff --git a/app/Template/swimlane/edit.php b/app/Template/swimlane/edit.php new file mode 100644 index 00000000..cc98b584 --- /dev/null +++ b/app/Template/swimlane/edit.php @@ -0,0 +1,18 @@ +<div class="page-header"> + <h2><?= t('Swimlane modification for the project "%s"', $project['name']) ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('swimlane', 'update', array('project_id' => $project['id'], 'swimlane_id' => $values['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/swimlane/index.php b/app/Template/swimlane/index.php new file mode 100644 index 00000000..daee6af5 --- /dev/null +++ b/app/Template/swimlane/index.php @@ -0,0 +1,47 @@ +<?php if (! empty($active_swimlanes)): ?> +<div class="page-header"> + <h2><?= t('Active swimlanes') ?></h2> +</div> +<?= $this->render('swimlane/table', array('swimlanes' => $active_swimlanes, 'project' => $project)) ?> +<?php endif ?> + +<div class="page-header"> + <h2><?= t('Add a new swimlane') ?></h2> +</div> +<form method="post" action="<?= $this->url->href('swimlane', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<div class="page-header"> + <h2><?= t('Change default swimlane') ?></h2> +</div> +<form method="post" action="<?= $this->url->href('swimlane', 'change', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $default_swimlane) ?> + + <?= $this->form->label(t('Rename'), 'default_swimlane') ?> + <?= $this->form->text('default_swimlane', $default_swimlane, array(), array('autofocus', 'required', 'maxlength="50"')) ?><br/> + + <?= $this->form->checkbox('show_default_swimlane', t('Show default swimlane'), 1, isset($default_swimlane['show_default_swimlane']) && $default_swimlane['show_default_swimlane'] == 1) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<?php if (! empty($inactive_swimlanes)): ?> +<div class="page-header"> + <h2><?= t('Inactive swimlanes') ?></h2> +</div> +<?= $this->render('swimlane/table', array('swimlanes' => $inactive_swimlanes, 'project' => $project, 'hide_position' => true)) ?> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/swimlane/remove.php b/app/Template/swimlane/remove.php new file mode 100644 index 00000000..1d7c2b7a --- /dev/null +++ b/app/Template/swimlane/remove.php @@ -0,0 +1,17 @@ +<section id="main"> + <div class="page-header"> + <h2><?= t('Remove a swimlane') ?></h2> + </div> + + <div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this swimlane: "%s"?', $swimlane['name']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'swimlane', 'remove', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'swimlane', 'index', array('project_id' => $project['id'])) ?> + </div> + </div> +</section>
\ No newline at end of file diff --git a/app/Template/swimlane/table.php b/app/Template/swimlane/table.php new file mode 100644 index 00000000..f38572a3 --- /dev/null +++ b/app/Template/swimlane/table.php @@ -0,0 +1,44 @@ +<table> + <tr> + <?php if (! isset($hide_position)): ?> + <th><?= t('Position') ?></th> + <?php endif ?> + <th class="column-60"><?= t('Name') ?></th> + <th class="column-35"><?= t('Actions') ?></th> + </tr> + <?php foreach ($swimlanes as $swimlane): ?> + <tr> + <?php if (! isset($hide_position)): ?> + <td>#<?= $swimlane['position'] ?></td> + <?php endif ?> + <td><?= $this->e($swimlane['name']) ?></td> + <td> + <ul> + <?php if ($swimlane['position'] != 0 && $swimlane['position'] != 1): ?> + <li> + <?= $this->url->link(t('Move Up'), 'swimlane', 'moveup', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + </li> + <?php endif ?> + <?php if ($swimlane['position'] != 0 && $swimlane['position'] != count($swimlanes)): ?> + <li> + <?= $this->url->link(t('Move Down'), 'swimlane', 'movedown', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + </li> + <?php endif ?> + <li> + <?= $this->url->link(t('Rename'), 'swimlane', 'edit', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id'])) ?> + </li> + <li> + <?php if ($swimlane['is_active']): ?> + <?= $this->url->link(t('Disable'), 'swimlane', 'disable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + <?php else: ?> + <?= $this->url->link(t('Enable'), 'swimlane', 'enable', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id']), true) ?> + <?php endif ?> + </li> + <li> + <?= $this->url->link(t('Remove'), 'swimlane', 'confirm', array('project_id' => $project['id'], 'swimlane_id' => $swimlane['id'])) ?> + </li> + </ul> + </td> + </tr> + <?php endforeach ?> +</table>
\ No newline at end of file diff --git a/app/Template/task/activity.php b/app/Template/task/activity.php new file mode 100644 index 00000000..cc4aad03 --- /dev/null +++ b/app/Template/task/activity.php @@ -0,0 +1,5 @@ +<div class="page-header"> + <h2><?= t('Activity stream') ?></h2> +</div> + +<?= $this->render('event/events', array('events' => $events)) ?>
\ No newline at end of file diff --git a/app/Template/task/close.php b/app/Template/task/close.php new file mode 100644 index 00000000..79150333 --- /dev/null +++ b/app/Template/task/close.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Close a task') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to close this task: "%s"?', $this->e($task['title'])) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/task_comments.php b/app/Template/task/comments.php index 5cfa99ce..a09862f9 100644 --- a/app/Templates/task_comments.php +++ b/app/Template/task/comments.php @@ -5,7 +5,7 @@ </div> <?php foreach ($comments as $comment): ?> - <?= Helper\template('comment_show', array( + <?= $this->render('comment/show', array( 'comment' => $comment, 'task' => $task, 'project' => $project, @@ -15,10 +15,10 @@ <?php endforeach ?> <?php if (! isset($not_editable)): ?> - <?= Helper\template('comment_create', array( + <?= $this->render('comment/create', array( 'skip_cancel' => true, 'values' => array( - 'user_id' => Helper\get_user_id(), + 'user_id' => $this->user->getId(), 'task_id' => $task['id'], ), 'errors' => array(), diff --git a/app/Templates/task_details.php b/app/Template/task/details.php index a4fdf6ce..f688585a 100644 --- a/app/Templates/task_details.php +++ b/app/Template/task/details.php @@ -1,7 +1,7 @@ -<div class="task-<?= $task['color_id'] ?> task-show-details"> - <h2><?= Helper\escape($task['title']) ?></h2> +<div class="color-<?= $task['color_id'] ?> task-show-details"> + <h2><?= $this->e('#'.$task['id'].' '.$task['title']) ?></h2> <?php if ($task['score']): ?> - <span class="task-score"><?= Helper\escape($task['score']) ?></span> + <span class="task-score"><?= $this->e($task['score']) ?></span> <?php endif ?> <ul> <?php if ($task['reference']): ?> @@ -58,13 +58,14 @@ </li> <li> <?= t('Column on the board:') ?> - <strong><?= Helper\escape($task['column_title']) ?></strong> - (<?= Helper\escape($task['project_name']) ?>) + <strong><?= $this->e($task['column_title']) ?></strong> + (<?= $this->e($task['project_name']) ?>) + <?= dt('since %B %e, %Y at %k:%M %p', $task['date_moved']) ?> </li> - <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li> + <li><?= t('Task position:').' '.$this->e($task['position']) ?></li> <?php if ($task['category_name']): ?> <li> - <?= t('Category:') ?> <strong><?= Helper\escape($task['category_name']) ?></strong> + <?= t('Category:') ?> <strong><?= $this->e($task['category_name']) ?></strong> </li> <?php endif ?> <li> @@ -76,7 +77,19 @@ </li> <?php if ($project['is_public']): ?> <li> - <a href="?controller=task&action=readonly&task_id=<?= $task['id'] ?>&token=<?= $project['token'] ?>" target="_blank"><?= t('Public link') ?></a> + <?= $this->url->link(t('Public link'), 'task', 'readonly', array('task_id' => $task['id'], 'token' => $project['token']), false, '', '', true) ?> + </li> + <?php endif ?> + + <?php if (! isset($not_editable) && $task['recurrence_status'] != \Model\Task::RECURRING_STATUS_NONE): ?> + <li> + <strong><?= t('Recurring information') ?></strong> + <?= $this->render('task/recurring_info', array( + 'task' => $task, + 'recurrence_trigger_list' => $recurrence_trigger_list, + 'recurrence_timeframe_list' => $recurrence_timeframe_list, + 'recurrence_basedate_list' => $recurrence_basedate_list, + )) ?> </li> <?php endif ?> </ul> diff --git a/app/Template/task/duplicate.php b/app/Template/task/duplicate.php new file mode 100644 index 00000000..e74d2906 --- /dev/null +++ b/app/Template/task/duplicate.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Duplicate a task') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to duplicate this task?') ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'task', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/task/duplicate_project.php b/app/Template/task/duplicate_project.php new file mode 100644 index 00000000..9a8e3c4a --- /dev/null +++ b/app/Template/task/duplicate_project.php @@ -0,0 +1,24 @@ +<div class="page-header"> + <h2><?= t('Duplicate the task to another project') ?></h2> +</div> + +<?php if (empty($projects_list)): ?> + <p class="alert"><?= t('No project') ?></p> +<?php else: ?> + + <form method="post" action="<?= $this->url->href('task', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->label(t('Project'), 'project_id') ?> + <?= $this->form->select('project_id', $projects_list, $values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> + </form> + +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/edit.php b/app/Template/task/edit.php new file mode 100644 index 00000000..2900b739 --- /dev/null +++ b/app/Template/task/edit.php @@ -0,0 +1,68 @@ +<div class="page-header"> + <h2><?= t('Edit a task') ?></h2> +</div> +<section id="task-section"> +<form method="post" action="<?= $this->url->href('task', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <div class="form-column"> + + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('required', 'maxlength="200"')) ?><br/> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"')) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + </div> + + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + </div> + + <div class="form-column"> + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Assignee'), 'owner_id') ?> + <?= $this->form->select('owner_id', $users_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Category'), 'category_id') ?> + <?= $this->form->select('category_id', $categories_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Color'), 'color_id') ?> + <?= $this->form->select('color_id', $colors_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Complexity'), 'score') ?> + <?= $this->form->number('score', $values, $errors) ?><br/> + + <?= $this->form->label(t('Due Date'), 'date_due') ?> + <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?php if ($ajax): ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?> + <?php else: ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?php endif ?> + </div> +</form> +</section> diff --git a/app/Template/task/edit_description.php b/app/Template/task/edit_description.php new file mode 100644 index 00000000..84f0cebd --- /dev/null +++ b/app/Template/task/edit_description.php @@ -0,0 +1,38 @@ +<div class="page-header"> + <h2><?= t('Edit the description') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + + <div class="form-tabs"> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors, array('autofocus', 'placeholder="'.t('Leave a description').'"'), 'description-textarea') ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + </div> + + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?php if ($ajax): ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> + <?php else: ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?php endif ?> + </div> +</form> diff --git a/app/Template/task/edit_recurrence.php b/app/Template/task/edit_recurrence.php new file mode 100644 index 00000000..c261e368 --- /dev/null +++ b/app/Template/task/edit_recurrence.php @@ -0,0 +1,52 @@ +<div class="page-header"> + <h2><?= t('Edit recurrence') ?></h2> +</div> + +<?php if ($task['recurrence_status'] != \Model\Task::RECURRING_STATUS_NONE): ?> +<div class="listing"> + <?= $this->render('task/recurring_info', array( + 'task' => $task, + 'recurrence_trigger_list' => $recurrence_trigger_list, + 'recurrence_timeframe_list' => $recurrence_timeframe_list, + 'recurrence_basedate_list' => $recurrence_basedate_list, + )) ?> +</div> +<?php endif ?> + +<?php if ($task['recurrence_status'] != \Model\Task::RECURRING_STATUS_PROCESSED): ?> + + <form method="post" action="<?= $this->url->href('task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => $ajax)) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Generate recurrent task'), 'recurrence_status') ?> + <?= $this->form->select('recurrence_status', $recurrence_status_list, $values, $errors) ?> + + <?= $this->form->label(t('Trigger to generate recurrent task'), 'recurrence_trigger') ?> + <?= $this->form->select('recurrence_trigger', $recurrence_trigger_list, $values, $errors) ?> + + <?= $this->form->label(t('Factor to calculate new due date'), 'recurrence_factor') ?> + <?= $this->form->number('recurrence_factor', $values, $errors) ?> + + <?= $this->form->label(t('Timeframe to calculate new due date'), 'recurrence_timeframe') ?> + <?= $this->form->select('recurrence_timeframe', $recurrence_timeframe_list, $values, $errors) ?> + + <?= $this->form->label(t('Base date to calculate new due date'), 'recurrence_basedate') ?> + <?= $this->form->select('recurrence_basedate', $recurrence_basedate_list, $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + + <?php if ($ajax): ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> + <?php else: ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?php endif ?> + </div> + </form> + +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php new file mode 100644 index 00000000..5a14fb39 --- /dev/null +++ b/app/Template/task/layout.php @@ -0,0 +1,28 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li> + <i class="fa fa-table fa-fw"></i> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, 'swimlane-'.$task['swimlane_id']) ?> + </li> + <?php if ($this->user->isManager($task['project_id'])): ?> + <li> + <i class="fa fa-cog fa-fw"></i> + <?= $this->url->link(t('Project settings'), 'project', 'show', array('project_id' => $task['project_id'])) ?> + </li> + <?php endif ?> + <li> + <i class="fa fa-calendar fa-fw"></i> + <?= $this->url->link(t('Project calendar'), 'calendar', 'show', array('project_id' => $task['project_id'])) ?> + </li> + </ul> + </div> + <section class="sidebar-container" id="task-section"> + + <?= $this->render('task/sidebar', array('task' => $task)) ?> + + <div class="sidebar-content"> + <?= $task_content_for_layout ?> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/task/move_project.php b/app/Template/task/move_project.php new file mode 100644 index 00000000..b0b33f81 --- /dev/null +++ b/app/Template/task/move_project.php @@ -0,0 +1,24 @@ +<div class="page-header"> + <h2><?= t('Move the task to another project') ?></h2> +</div> + +<?php if (empty($projects_list)): ?> + <p class="alert"><?= t('No project') ?></p> +<?php else: ?> + + <form method="post" action="<?= $this->url->href('task', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->label(t('Project'), 'project_id') ?> + <?= $this->form->select('project_id', $projects_list, $values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> + </form> + +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/new.php b/app/Template/task/new.php new file mode 100644 index 00000000..bd00d347 --- /dev/null +++ b/app/Template/task/new.php @@ -0,0 +1,84 @@ +<?php if (! $ajax): ?> +<div class="page-header"> + <ul> + <li><i class="fa fa-table fa-fw"></i><?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $values['project_id'])) ?></li> + </ul> +</div> +<?php else: ?> +<div class="page-header"> + <h2><?= t('New task') ?></h2> +</div> +<?php endif ?> + +<section id="task-section"> +<form method="post" action="<?= $this->url->href('task', 'save', array('project_id' => $values['project_id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <div class="form-column"> + <?= $this->form->label(t('Title'), 'title') ?> + <?= $this->form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"'), 'form-input-large') ?><br/> + + <?= $this->form->label(t('Description'), 'description') ?> + + <div class="form-tabs"> + <ul class="form-tabs-nav"> + <li class="form-tab form-tab-selected"> + <i class="fa fa-pencil-square-o fa-fw"></i><a id="markdown-write" href="#"><?= t('Write') ?></a> + </li> + <li class="form-tab"> + <a id="markdown-preview" href="#"><i class="fa fa-eye fa-fw"></i><?= t('Preview') ?></a> + </li> + </ul> + <div class="write-area"> + <?= $this->form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"')) ?> + </div> + <div class="preview-area"> + <div class="markdown"></div> + </div> + </div> + + <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> + + <?php if (! isset($duplicate)): ?> + <?= $this->form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> + <?php endif ?> + </div> + + <div class="form-column"> + <?= $this->form->hidden('project_id', $values) ?> + + <?= $this->form->label(t('Assignee'), 'owner_id') ?> + <?= $this->form->select('owner_id', $users_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Category'), 'category_id') ?> + <?= $this->form->select('category_id', $categories_list, $values, $errors) ?><br/> + + <?php if (! (count($swimlanes_list) === 1 && key($swimlanes_list) === 0)): ?> + <?= $this->form->label(t('Swimlane'), 'swimlane_id') ?> + <?= $this->form->select('swimlane_id', $swimlanes_list, $values, $errors) ?><br/> + <?php endif ?> + + <?= $this->form->label(t('Column'), 'column_id') ?> + <?= $this->form->select('column_id', $columns_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Color'), 'color_id') ?> + <?= $this->form->select('color_id', $colors_list, $values, $errors) ?><br/> + + <?= $this->form->label(t('Complexity'), 'score') ?> + <?= $this->form->number('score', $values, $errors) ?><br/> + + <?= $this->form->label(t('Original estimate'), 'time_estimated') ?> + <?= $this->form->numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> + + <?= $this->form->label(t('Due Date'), 'date_due') ?> + <?= $this->form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?><br/> + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> + </div> +</form> +</section> diff --git a/app/Template/task/open.php b/app/Template/task/open.php new file mode 100644 index 00000000..fbcc1111 --- /dev/null +++ b/app/Template/task/open.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Open a task') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to open this task: "%s"?', $this->e($task['title'])) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'task', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/task/public.php b/app/Template/task/public.php new file mode 100644 index 00000000..73116de9 --- /dev/null +++ b/app/Template/task/public.php @@ -0,0 +1,34 @@ +<section id="main" class="public-task"> + + <?= $this->render('task/details', array('task' => $task, 'project' => $project, 'not_editable' => true)) ?> + + <p class="pull-right"><?= $this->url->link(t('Back to the board'), 'board', 'readonly', array('token' => $project['token'])) ?></p> + + <?= $this->render('task/show_description', array( + 'task' => $task, + 'project' => $project, + 'is_public' => true + )) ?> + + <?= $this->render('tasklink/show', array( + 'task' => $task, + 'links' => $links, + 'project' => $project, + 'not_editable' => true + )) ?> + + <?= $this->render('subtask/show', array( + 'task' => $task, + 'subtasks' => $subtasks, + 'not_editable' => true + )) ?> + + <?= $this->render('task/comments', array( + 'task' => $task, + 'comments' => $comments, + 'project' => $project, + 'not_editable' => true, + 'is_public' => true, + )) ?> + +</section>
\ No newline at end of file diff --git a/app/Template/task/recurring_info.php b/app/Template/task/recurring_info.php new file mode 100644 index 00000000..ad64ae19 --- /dev/null +++ b/app/Template/task/recurring_info.php @@ -0,0 +1,37 @@ +<ul> + <?php if ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PENDING): ?> + <li><?= t('Recurrent task is scheduled to be generated') ?></li> + <?php elseif ($task['recurrence_status'] == \Model\Task::RECURRING_STATUS_PROCESSED): ?> + <li><?= t('Recurrent task has been generated:') ?> + <ul> + <li> + <?= t('Trigger to generate recurrent task: ') ?><strong><?= $this->e($recurrence_trigger_list[$task['recurrence_trigger']]) ?></strong> + </li> + <li> + <?= t('Factor to calculate new due date: ') ?><strong><?= $this->e($task['recurrence_factor']) ?></strong> + </li> + <li> + <?= t('Timeframe to calculate new due date: ') ?><strong><?= $this->e($recurrence_timeframe_list[$task['recurrence_timeframe']]) ?></strong> + </li> + <li> + <?= t('Base date to calculate new due date: ') ?><strong><?= $this->e($recurrence_basedate_list[$task['recurrence_basedate']]) ?></strong> + </li> + </ul> + </li> + <?php endif ?> + + <?php if ($task['recurrence_parent'] || $task['recurrence_child']): ?> + <?php if ($task['recurrence_parent']): ?> + <li> + <?= t('This task has been created by: ') ?> + <?= $this->url->link('#'.$task['recurrence_parent'], 'task', 'show', array('task_id' => $task['recurrence_parent'], 'project_id' => $task['project_id'])) ?> + </li> + <?php endif ?> + <?php if ($task['recurrence_child']): ?> + <li> + <?= t('This task has created this child task: ') ?> + <?= $this->url->link('#'.$task['recurrence_child'], 'task', 'show', array('task_id' => $task['recurrence_child'], 'project_id' => $task['project_id'])) ?> + </li> + <?php endif ?> + <?php endif ?> +</ul>
\ No newline at end of file diff --git a/app/Template/task/remove.php b/app/Template/task/remove.php new file mode 100644 index 00000000..2f6edc22 --- /dev/null +++ b/app/Template/task/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a task') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this task: "%s"?', $this->e($task['title'])) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/task/show.php b/app/Template/task/show.php new file mode 100644 index 00000000..54c124f6 --- /dev/null +++ b/app/Template/task/show.php @@ -0,0 +1,15 @@ +<?= $this->render('task/details', array( + 'task' => $task, + 'project' => $project, + 'recurrence_trigger_list' => $this->task->recurrenceTriggers(), + 'recurrence_timeframe_list' => $this->task->recurrenceTimeframes(), + 'recurrence_basedate_list' => $this->task->recurrenceBasedates(), +)) ?> + +<?= $this->render('task/time', array('task' => $task, 'values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?> +<?= $this->render('task/show_description', array('task' => $task)) ?> +<?= $this->render('tasklink/show', array('task' => $task, 'links' => $links, 'link_label_list' => $link_label_list)) ?> +<?= $this->render('subtask/show', array('task' => $task, 'subtasks' => $subtasks, 'project' => $project)) ?> +<?= $this->render('task/timesheet', array('task' => $task)) ?> +<?= $this->render('file/show', array('task' => $task, 'files' => $files, 'images' => $images)) ?> +<?= $this->render('task/comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?> diff --git a/app/Templates/task_show_description.php b/app/Template/task/show_description.php index 25312149..f823e7d6 100644 --- a/app/Templates/task_show_description.php +++ b/app/Template/task/show_description.php @@ -6,9 +6,18 @@ <article class="markdown task-show-description"> <?php if (! isset($is_public)): ?> - <?= Helper\markdown($task['description']) ?> + <?= $this->text->markdown( + $task['description'], + array( + 'controller' => 'task', + 'action' => 'show', + 'params' => array( + 'project_id' => $task['project_id'] + ) + ) + ) ?> <?php else: ?> - <?= Helper\markdown( + <?= $this->text->markdown( $task['description'], array( 'controller' => 'task', diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php new file mode 100644 index 00000000..bb137ac9 --- /dev/null +++ b/app/Template/task/sidebar.php @@ -0,0 +1,67 @@ +<div class="sidebar"> + <h2><?= t('Information') ?></h2> + <ul> + <li> + <?= $this->url->link(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Activity stream'), 'task', 'activites', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Transitions'), 'task', 'transitions', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?> + <li> + <?= $this->url->link(t('Time tracking'), 'task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <?php endif ?> + </ul> + <h2><?= t('Actions') ?></h2> + <ul> + <li> + <?= $this->url->link(t('Edit the task'), 'task', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Edit the description'), 'task', 'description', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Edit recurrence'), 'task', 'recurrence', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Add a sub-task'), 'subtask', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Add a link'), 'tasklink', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Add a comment'), 'comment', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Attach a document'), 'file', 'create', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Add a screenshot'), 'file', 'screenshot', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Duplicate'), 'task', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Duplicate to another project'), 'task', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?= $this->url->link(t('Move to another project'), 'task', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <li> + <?php if ($task['is_active'] == 1): ?> + <?= $this->url->link(t('Close this task'), 'task', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?php else: ?> + <?= $this->url->link(t('Open this task'), 'task', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?php endif ?> + </li> + <?php if ($this->task->canRemove($task)): ?> + <li> + <?= $this->url->link(t('Remove'), 'task', 'remove', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> + <?php endif ?> + </ul> +</div> diff --git a/app/Template/task/table.php b/app/Template/task/table.php new file mode 100644 index 00000000..d06bc7b7 --- /dev/null +++ b/app/Template/task/table.php @@ -0,0 +1,56 @@ +<table class="table-fixed table-small"> + <tr> + <th class="column-8"><?= $paginator->order(t('Id'), 'tasks.id') ?></th> + <th class="column-8"><?= $paginator->order(t('Column'), 'tasks.column_id') ?></th> + <th class="column-8"><?= $paginator->order(t('Category'), 'tasks.category_id') ?></th> + <th><?= $paginator->order(t('Title'), 'tasks.title') ?></th> + <th class="column-10"><?= $paginator->order(t('Assignee'), 'users.username') ?></th> + <th class="column-10"><?= $paginator->order(t('Due date'), 'tasks.date_due') ?></th> + <th class="column-10"><?= $paginator->order(t('Date created'), 'tasks.date_creation') ?></th> + <th class="column-10"><?= $paginator->order(t('Date completed'), 'tasks.date_completed') ?></th> + <th class="column-5"><?= $paginator->order(t('Status'), 'tasks.is_active') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $task): ?> + <tr> + <td class="task-table color-<?= $task['color_id'] ?>"> + <?= $this->url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + </td> + <td> + <?= $this->text->in($task['column_id'], $columns) ?> + </td> + <td> + <?= $this->text->in($task['category_id'], $categories, '') ?> + </td> + <td> + <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + </td> + <td> + <?php if ($task['assignee_username']): ?> + <?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?> + <?php else: ?> + <?= t('Unassigned') ?> + <?php endif ?> + </td> + <td> + <?= dt('%B %e, %Y', $task['date_due']) ?> + </td> + <td> + <?= dt('%B %e, %Y', $task['date_creation']) ?> + </td> + <td> + <?php if ($task['date_completed']): ?> + <?= dt('%B %e, %Y', $task['date_completed']) ?> + <?php endif ?> + </td> + <td> + <?php if ($task['is_active'] == \Model\Task::STATUS_OPEN): ?> + <?= t('Open') ?> + <?php else: ?> + <?= t('Closed') ?> + <?php endif ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<?= $paginator ?> diff --git a/app/Template/task/time.php b/app/Template/task/time.php new file mode 100644 index 00000000..6682a08d --- /dev/null +++ b/app/Template/task/time.php @@ -0,0 +1,15 @@ +<form method="post" action="<?= $this->url->href('task', 'time', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" class="form-inline task-time-form" autocomplete="off"> + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + + <?= $this->form->label(t('Start date'), 'date_started') ?> + <?= $this->form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + + <?= $this->form->label(t('Time estimated'), 'time_estimated') ?> + <?= $this->form->numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?> + + <?= $this->form->label(t('Time spent'), 'time_spent') ?> + <?= $this->form->numeric('time_spent', $values, array(), array('placeholder="'.t('hours').'"')) ?> + + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> +</form>
\ No newline at end of file diff --git a/app/Template/task/time_tracking.php b/app/Template/task/time_tracking.php new file mode 100644 index 00000000..441cb585 --- /dev/null +++ b/app/Template/task/time_tracking.php @@ -0,0 +1,27 @@ +<?= $this->render('task/timesheet', array('task' => $task)) ?> + +<h3><?= t('Subtask timesheet') ?></h3> +<?php if ($subtask_paginator->isEmpty()): ?> + <p class="alert"><?= t('There is nothing to show.') ?></p> +<?php else: ?> + <table class="table-fixed"> + <tr> + <th class="column-15"><?= $subtask_paginator->order(t('User'), 'username') ?></th> + <th><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th> + <th class="column-20"><?= $subtask_paginator->order(t('Start'), 'start') ?></th> + <th class="column-20"><?= $subtask_paginator->order(t('End'), 'end') ?></th> + <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), 'time_spent') ?></th> + </tr> + <?php foreach ($subtask_paginator->getCollection() as $record): ?> + <tr> + <td><?= $this->url->link($this->e($record['user_fullname'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?></td> + <td><?= t($record['subtask_title']) ?></td> + <td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td> + <td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td> + <td><?= n($record['time_spent']).' '.t('hours') ?></td> + </tr> + <?php endforeach ?> + </table> + + <?= $subtask_paginator ?> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/timesheet.php b/app/Template/task/timesheet.php new file mode 100644 index 00000000..0210be7e --- /dev/null +++ b/app/Template/task/timesheet.php @@ -0,0 +1,13 @@ +<?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?> + +<div class="page-header"> + <h2><?= t('Time tracking') ?></h2> +</div> + +<ul class="listing"> + <li><?= t('Estimate:') ?> <strong><?= $this->e($task['time_estimated']) ?></strong> <?= t('hours') ?></li> + <li><?= t('Spent:') ?> <strong><?= $this->e($task['time_spent']) ?></strong> <?= t('hours') ?></li> + <li><?= t('Remaining:') ?> <strong><?= $this->e($task['time_estimated'] - $task['time_spent']) ?></strong> <?= t('hours') ?></li> +</ul> + +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/task/transitions.php b/app/Template/task/transitions.php new file mode 100644 index 00000000..6455fd66 --- /dev/null +++ b/app/Template/task/transitions.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2><?= t('Transitions') ?></h2> +</div> + +<?php if (empty($transitions)): ?> + <p class="alert"><?= t('There is nothing to show.') ?></p> +<?php else: ?> + <table class="table-stripped"> + <tr> + <th><?= t('Date') ?></th> + <th><?= t('Source column') ?></th> + <th><?= t('Destination column') ?></th> + <th><?= t('Executer') ?></th> + <th><?= t('Time spent in the column') ?></th> + </tr> + <?php foreach ($transitions as $transition): ?> + <tr> + <td><?= dt('%B %e, %Y at %k:%M %p', $transition['date']) ?></td> + <td><?= $this->e($transition['src_column']) ?></td> + <td><?= $this->e($transition['dst_column']) ?></td> + <td><?= $this->url->link($this->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td> + <td><?= n(round($transition['time_spent'] / 3600, 2)).' '.t('hours') ?></td> + </tr> + <?php endforeach ?> + </table> +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/tasklink/create.php b/app/Template/tasklink/create.php new file mode 100644 index 00000000..749f2968 --- /dev/null +++ b/app/Template/tasklink/create.php @@ -0,0 +1,37 @@ +<div class="page-header"> + <h2><?= t('Add a new link') ?></h2> +</div> + +<form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'ajax' => isset($ajax))) ?>" method="post" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> + <?= $this->form->hidden('opposite_task_id', $values) ?> + + <?= $this->form->label(t('Label'), 'link_id') ?> + <?= $this->form->select('link_id', $labels, $values, $errors) ?> + + <?= $this->form->label(t('Task'), 'title') ?> + <?= $this->form->text( + 'title', + $values, + $errors, + array( + 'required', + 'placeholder="'.t('Start to type task title...').'"', + 'title="'.t('Start to type task title...').'"', + 'data-dst-field="opposite_task_id"', + 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', + ), + 'task-autocomplete') ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?php if (isset($ajax)): ?> + <?= $this->url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?> + <?php else: ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?php endif ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/tasklink/edit.php b/app/Template/tasklink/edit.php new file mode 100644 index 00000000..73b43277 --- /dev/null +++ b/app/Template/tasklink/edit.php @@ -0,0 +1,34 @@ +<div class="page-header"> + <h2><?= t('Edit link') ?></h2> +</div> + +<form action="<?= $this->url->href('tasklink', 'update', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'link_id' => $task_link['id'])) ?>" method="post" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('task_id', $values) ?> + <?= $this->form->hidden('opposite_task_id', $values) ?> + + <?= $this->form->label(t('Label'), 'link_id') ?> + <?= $this->form->select('link_id', $labels, $values, $errors) ?> + + <?= $this->form->label(t('Task'), 'title') ?> + <?= $this->form->text( + 'title', + $values, + $errors, + array( + 'required', + 'placeholder="'.t('Start to type task title...').'"', + 'title="'.t('Start to type task title...').'"', + 'data-dst-field="opposite_task_id"', + 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', + ), + 'task-autocomplete') ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/tasklink/remove.php b/app/Template/tasklink/remove.php new file mode 100644 index 00000000..262fb488 --- /dev/null +++ b/app/Template/tasklink/remove.php @@ -0,0 +1,15 @@ +<div class="page-header"> + <h2><?= t('Remove a link') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to remove this link with task #%d?', $link['opposite_task_id']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'tasklink', 'remove', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id']), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/tasklink/show.php b/app/Template/tasklink/show.php new file mode 100644 index 00000000..7125b11d --- /dev/null +++ b/app/Template/tasklink/show.php @@ -0,0 +1,104 @@ +<?php if (! empty($links)): ?> +<div class="page-header"> + <h2><?= t('Links') ?></h2> +</div> +<table id="links"> + <tr> + <th class="column-20"><?= t('Label') ?></th> + <th class="column-30"><?= t('Task') ?></th> + <th><?= t('Column') ?></th> + <th><?= t('Assignee') ?></th> + <?php if (! isset($not_editable)): ?> + <th><?= t('Action') ?></th> + <?php endif ?> + </tr> + <?php foreach ($links as $label => $grouped_links): ?> + <?php $hide_td = false ?> + <?php foreach ($grouped_links as $link): ?> + <tr> + <?php if (! $hide_td): ?> + <td rowspan="<?= count($grouped_links) ?>"><?= t('This task') ?> <strong><?= t($label) ?></strong></td> + <?php $hide_td = true ?> + <?php endif ?> + + <td> + <?php if (! isset($not_editable)): ?> + <?= $this->url->link( + $this->e('#'.$link['task_id'].' '.$link['title']), + 'task', + 'show', + array('task_id' => $link['task_id'], 'project_id' => $link['project_id']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + <?php else: ?> + <?= $this->url->link( + $this->e('#'.$link['task_id'].' '.$link['title']), + 'task', + 'readonly', + array('task_id' => $link['task_id'], 'token' => $project['token']), + false, + $link['is_active'] ? '' : 'task-link-closed' + ) ?> + <?php endif ?> + + <br/> + + <?php if (! empty($link['task_time_spent'])): ?> + <strong><?= $this->e($link['task_time_spent']).'h' ?></strong> <?= t('spent') ?> + <?php endif ?> + + <?php if (! empty($link['task_time_estimated'])): ?> + <strong><?= $this->e($link['task_time_estimated']).'h' ?></strong> <?= t('estimated') ?> + <?php endif ?> + </td> + <td><?= $this->e($link['column_title']) ?></td> + <td> + <?php if (! empty($link['task_assignee_username'])): ?> + <?php if (! isset($not_editable)): ?> + <?= $this->url->link($this->e($link['task_assignee_name'] ?: $link['task_assignee_username']), 'user', 'show', array('user_id' => $link['task_assignee_id'])) ?> + <?php else: ?> + <?= $this->e($link['task_assignee_name'] ?: $link['task_assignee_username']) ?> + <?php endif ?> + <?php endif ?> + </td> + <?php if (! isset($not_editable)): ?> + <td> + <ul> + <li><?= $this->url->link(t('Edit'), 'tasklink', 'edit', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li> + <li><?= $this->url->link(t('Remove'), 'tasklink', 'confirm', array('link_id' => $link['id'], 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?></li> + </ul> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + <?php endforeach ?> +</table> + +<?php if (! isset($not_editable) && isset($link_label_list)): ?> + <form action="<?= $this->url->href('tasklink', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" method="post" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->hidden('task_id', array('task_id' => $task['id'])) ?> + <?= $this->form->hidden('opposite_task_id', array()) ?> + + <?= $this->form->select('link_id', $link_label_list, array(), array()) ?> + + <?= $this->form->text( + 'title', + array(), + array(), + array( + 'required', + 'placeholder="'.t('Start to type task title...').'"', + 'title="'.t('Start to type task title...').'"', + 'data-dst-field="opposite_task_id"', + 'data-search-url="'.$this->url->href('app', 'autocomplete', array('exclude_task_id' => $task['id'])).'"', + ), + 'task-autocomplete') ?> + + <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> + </form> +<?php endif ?> + +<?php endif ?> diff --git a/app/Template/timetable/index.php b/app/Template/timetable/index.php new file mode 100644 index 00000000..7a63a2ec --- /dev/null +++ b/app/Template/timetable/index.php @@ -0,0 +1,44 @@ +<div class="page-header"> + <h2><?= t('Timetable') ?></h2> + <ul> + <li><?= $this->url->link(t('Day timetable'), 'timetableday', 'index', array('user_id' => $user['id'])) ?></li> + <li><?= $this->url->link(t('Week timetable'), 'timetableweek', 'index', array('user_id' => $user['id'])) ?></li> + <li><?= $this->url->link(t('Time off timetable'), 'timetableoff', 'index', array('user_id' => $user['id'])) ?></li> + <li><?= $this->url->link(t('Overtime timetable'), 'timetableextra', 'index', array('user_id' => $user['id'])) ?></li> + </ul> +</div> + +<form method="get" action="?" autocomplete="off" class="form-inline"> + + <?= $this->form->hidden('controller', $values) ?> + <?= $this->form->hidden('action', $values) ?> + <?= $this->form->hidden('user_id', $values) ?> + + <?= $this->form->label(t('From'), 'from') ?> + <?= $this->form->text('from', $values, array(), array(), 'form-date') ?> + + <?= $this->form->label(t('To'), 'to') ?> + <?= $this->form->text('to', $values, array(), array(), 'form-date') ?> + + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> +</form> + +<?php if (! empty($timetable)): ?> +<hr/> +<h3><?= t('Work timetable') ?></h3> +<table class="table-fixed table-stripped"> + <tr> + <th><?= t('Day') ?></th> + <th><?= t('Start') ?></th> + <th><?= t('End') ?></th> + </tr> + <?php foreach ($timetable as $slot): ?> + <tr> + <td><?= dt('%B %e, %Y', $slot[0]->getTimestamp()) ?></td> + <td><?= dt('%k:%M %p', $slot[0]->getTimestamp()) ?></td> + <td><?= dt('%k:%M %p', $slot[1]->getTimestamp()) ?></td> + </tr> + <?php endforeach ?> +</table> + +<?php endif ?>
\ No newline at end of file diff --git a/app/Template/timetable_day/index.php b/app/Template/timetable_day/index.php new file mode 100644 index 00000000..d2877816 --- /dev/null +++ b/app/Template/timetable_day/index.php @@ -0,0 +1,45 @@ +<div class="page-header"> + <h2><?= t('Day timetable') ?></h2> +</div> + +<?php if (! empty($timetable)): ?> + +<table class="table-fixed table-stripped"> + <tr> + <th><?= t('Start time') ?></th> + <th><?= t('End time') ?></th> + <th><?= t('Action') ?></th> + </tr> + <?php foreach ($timetable as $slot): ?> + <tr> + <td><?= $slot['start'] ?></td> + <td><?= $slot['end'] ?></td> + <td> + <?= $this->url->link(t('Remove'), 'timetableday', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<h3><?= t('Add new time slot') ?></h3> +<?php endif ?> + +<form method="post" action="<?= $this->url->href('timetableday', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->hidden('user_id', $values) ?> + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Start time'), 'start') ?> + <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + + <?= $this->form->label(t('End time'), 'end') ?> + <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<p class="alert alert-info"> + <?= t('This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.') ?> +</p>
\ No newline at end of file diff --git a/app/Template/timetable_day/remove.php b/app/Template/timetable_day/remove.php new file mode 100644 index 00000000..1b33b266 --- /dev/null +++ b/app/Template/timetable_day/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove time slot') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'timetableday', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'timetableday', 'index', array('user_id' => $user['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/timetable_extra/index.php b/app/Template/timetable_extra/index.php new file mode 100644 index 00000000..d3224ae6 --- /dev/null +++ b/app/Template/timetable_extra/index.php @@ -0,0 +1,56 @@ +<div class="page-header"> + <h2><?= t('Overtime timetable') ?></h2> +</div> + +<?php if (! $paginator->isEmpty()): ?> + +<table class="table-fixed table-stripped"> + <tr> + <th><?= $paginator->order(t('Day'), 'Day') ?></th> + <th><?= $paginator->order(t('All day'), 'all_day') ?></th> + <th><?= $paginator->order(t('Start time'), 'start') ?></th> + <th><?= $paginator->order(t('End time'), 'end') ?></th> + <th class="column-40"><?= t('Comment') ?></th> + <th><?= t('Action') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $slot): ?> + <tr> + <td><?= $slot['date'] ?></td> + <td><?= $slot['all_day'] == 1 ? t('Yes') : t('No') ?></td> + <td><?= $slot['start'] ?></td> + <td><?= $slot['end'] ?></td> + <td><?= $this->e($slot['comment']) ?></td> + <td> + <?= $this->url->link(t('Remove'), 'timetableextra', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<?= $paginator ?> + +<?php endif ?> + +<form method="post" action="<?= $this->url->href('timetableextra', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->hidden('user_id', $values) ?> + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Day'), 'date') ?> + <?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?> + + <?= $this->form->checkbox('all_day', t('All day'), 1) ?> + + <?= $this->form->label(t('Start time'), 'start') ?> + <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + + <?= $this->form->label(t('End time'), 'end') ?> + <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + + <?= $this->form->label(t('Comment'), 'comment') ?> + <?= $this->form->text('comment', $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/timetable_extra/remove.php b/app/Template/timetable_extra/remove.php new file mode 100644 index 00000000..fc907438 --- /dev/null +++ b/app/Template/timetable_extra/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove time slot') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'timetableextra', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'timetableextra', 'index', array('user_id' => $user['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/timetable_off/index.php b/app/Template/timetable_off/index.php new file mode 100644 index 00000000..75e02dbd --- /dev/null +++ b/app/Template/timetable_off/index.php @@ -0,0 +1,56 @@ +<div class="page-header"> + <h2><?= t('Time off timetable') ?></h2> +</div> + +<?php if (! $paginator->isEmpty()): ?> + +<table class="table-fixed table-stripped"> + <tr> + <th><?= $paginator->order(t('Day'), 'Day') ?></th> + <th><?= $paginator->order(t('All day'), 'all_day') ?></th> + <th><?= $paginator->order(t('Start time'), 'start') ?></th> + <th><?= $paginator->order(t('End time'), 'end') ?></th> + <th class="column-40"><?= t('Comment') ?></th> + <th><?= t('Action') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $slot): ?> + <tr> + <td><?= $slot['date'] ?></td> + <td><?= $slot['all_day'] == 1 ? t('Yes') : t('No') ?></td> + <td><?= $slot['start'] ?></td> + <td><?= $slot['end'] ?></td> + <td><?= $this->e($slot['comment']) ?></td> + <td> + <?= $this->url->link(t('Remove'), 'timetableoff', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<?= $paginator ?> + +<?php endif ?> + +<form method="post" action="<?= $this->url->href('timetableoff', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->hidden('user_id', $values) ?> + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Day'), 'date') ?> + <?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?> + + <?= $this->form->checkbox('all_day', t('All day'), 1) ?> + + <?= $this->form->label(t('Start time'), 'start') ?> + <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + + <?= $this->form->label(t('End time'), 'end') ?> + <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + + <?= $this->form->label(t('Comment'), 'comment') ?> + <?= $this->form->text('comment', $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/timetable_off/remove.php b/app/Template/timetable_off/remove.php new file mode 100644 index 00000000..621e191c --- /dev/null +++ b/app/Template/timetable_off/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove time slot') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'timetableoff', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'timetableoff', 'index', array('user_id' => $user['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/timetable_week/index.php b/app/Template/timetable_week/index.php new file mode 100644 index 00000000..552e9302 --- /dev/null +++ b/app/Template/timetable_week/index.php @@ -0,0 +1,46 @@ +<div class="page-header"> + <h2><?= t('Week timetable') ?></h2> +</div> + +<?php if (! empty($timetable)): ?> + +<table class="table-fixed table-stripped"> + <tr> + <th><?= t('Day') ?></th> + <th><?= t('Start time') ?></th> + <th><?= t('End time') ?></th> + <th><?= t('Action') ?></th> + </tr> + <?php foreach ($timetable as $slot): ?> + <tr> + <td><?= $this->datetime->getWeekDay($slot['day']) ?></td> + <td><?= $slot['start'] ?></td> + <td><?= $slot['end'] ?></td> + <td> + <?= $this->url->link(t('Remove'), 'timetableweek', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<h3><?= t('Add new time slot') ?></h3> +<?php endif ?> + +<form method="post" action="<?= $this->url->href('timetableweek', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->hidden('user_id', $values) ?> + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Day'), 'day') ?> + <?= $this->form->select('day', $this->datetime->getWeekDays(), $values, $errors) ?> + + <?= $this->form->label(t('Start time'), 'start') ?> + <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + + <?= $this->form->label(t('End time'), 'end') ?> + <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/timetable_week/remove.php b/app/Template/timetable_week/remove.php new file mode 100644 index 00000000..f5a10199 --- /dev/null +++ b/app/Template/timetable_week/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove time slot') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'timetableweek', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'timetableweek', 'index', array('user_id' => $user['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/twofactor/check.php b/app/Template/twofactor/check.php new file mode 100644 index 00000000..68a58a6c --- /dev/null +++ b/app/Template/twofactor/check.php @@ -0,0 +1,10 @@ +<form method="post" action="<?= $this->url->href('twofactor', 'check', array('user_id' => $this->user->getId())) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->label(t('Code'), 'code') ?> + <?= $this->form->text('code', array(), array(), array('placeholder="123456"', 'autofocus'), 'form-numeric') ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/twofactor/disable.php b/app/Template/twofactor/disable.php new file mode 100644 index 00000000..36be4ef9 --- /dev/null +++ b/app/Template/twofactor/disable.php @@ -0,0 +1,14 @@ +<div class="page-header"> + <h2><?= t('Disable two factor authentication') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"> + <?= t('Do you really want to disable the two factor authentication for this user: "%s"?', $user['name'] ?: $user['username']) ?> + </p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'twofactor', 'disable', array('user_id' => $user['id'], 'disable' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Template/twofactor/index.php b/app/Template/twofactor/index.php new file mode 100644 index 00000000..36b92653 --- /dev/null +++ b/app/Template/twofactor/index.php @@ -0,0 +1,37 @@ +<div class="page-header"> + <h2><?= t('Two factor authentication') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('twofactor', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->checkbox('twofactor_activated', t('Enable/disable two factor authentication'), 1, isset($user['twofactor_activated']) && $user['twofactor_activated'] == 1) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<?php if ($user['twofactor_activated'] == 1): ?> +<div class="listing"> + <p><?= t('Secret key: ') ?><strong><?= $this->e($user['twofactor_secret']) ?></strong> (base32)</p> + <p><br/><img src="<?= $qrcode_url ?>"/><br/><br/></p> + <p> + <?= t('This QR code contains the key URI: ') ?><strong><?= $this->e($key_url) ?></strong> + <br/><br/> + <?= t('Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).') ?> + </p> +</div> + +<h3><?= t('Test your device') ?></h3> +<form method="post" action="<?= $this->url->href('twofactor', 'test', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->label(t('Code'), 'code') ?> + <?= $this->form->text('code', array(), array(), array('placeholder="123456"'), 'form-numeric') ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/> + </div> +</form> +<?php endif ?> diff --git a/app/Template/user/calendar.php b/app/Template/user/calendar.php new file mode 100644 index 00000000..7ec12496 --- /dev/null +++ b/app/Template/user/calendar.php @@ -0,0 +1,6 @@ +<div id="user-calendar" + data-check-url="<?= $this->url->href('calendar', 'user') ?>" + data-user-id="<?= $user['id'] ?>" + data-save-url="<?= $this->url->href('calendar', 'save') ?>" +> +</div>
\ No newline at end of file diff --git a/app/Template/user/edit.php b/app/Template/user/edit.php new file mode 100644 index 00000000..e29dcfca --- /dev/null +++ b/app/Template/user/edit.php @@ -0,0 +1,42 @@ +<div class="page-header"> + <h2><?= t('Edit user') ?></h2> +</div> +<form method="post" action="<?= $this->url->href('user', 'edit', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->hidden('is_ldap_user', $values) ?> + + <?= $this->form->label(t('Username'), 'username') ?> + <?= $this->form->text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '', 'maxlength="50"')) ?><br/> + + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors) ?><br/> + + <?= $this->form->label(t('Email'), 'email') ?> + <?= $this->form->email('email', $values, $errors) ?><br/> + + <?= $this->form->label(t('Default project'), 'default_project_id') ?> + <?= $this->form->select('default_project_id', $projects, $values, $errors) ?><br/> + + <?= $this->form->label(t('Timezone'), 'timezone') ?> + <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/> + + <?= $this->form->label(t('Language'), 'language') ?> + <?= $this->form->select('language', $languages, $values, $errors) ?><br/> + + <div class="alert alert-error"> + <?= $this->form->checkbox('disable_login_form', t('Disable login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?><br/> + + <?php if ($this->user->isAdmin()): ?> + <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1) ?><br/> + <?php endif ?> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Templates/user_external.php b/app/Template/user/external.php index 676b2c73..df85ace7 100644 --- a/app/Templates/user_external.php +++ b/app/Template/user/external.php @@ -6,11 +6,11 @@ <h3><i class="fa fa-google"></i> <?= t('Google Account') ?></h3> <p class="listing"> - <?php if (Helper\is_current_user($user['id'])): ?> + <?php if ($this->user->isCurrentUser($user['id'])): ?> <?php if (empty($user['google_id'])): ?> - <a href="?controller=user&action=google<?= Helper\param_csrf() ?>"><?= t('Link my Google Account') ?></a> + <?= $this->url->link(t('Link my Google Account'), 'user', 'google', array(), true) ?> <?php else: ?> - <a href="?controller=user&action=unlinkGoogle<?= Helper\param_csrf() ?>"><?= t('Unlink my Google Account') ?></a> + <?= $this->url->link(t('Unlink my Google Account'), 'user', 'unlinkGoogle', array(), true) ?> <?php endif ?> <?php else: ?> <?= empty($user['google_id']) ? t('No account linked.') : t('Account linked.') ?> @@ -22,11 +22,11 @@ <h3><i class="fa fa-github"></i> <?= t('Github Account') ?></h3> <p class="listing"> - <?php if (Helper\is_current_user($user['id'])): ?> + <?php if ($this->user->isCurrentUser($user['id'])): ?> <?php if (empty($user['github_id'])): ?> - <a href="?controller=user&action=gitHub<?= Helper\param_csrf() ?>"><?= t('Link my GitHub Account') ?></a> + <?= $this->url->link(t('Link my GitHub Account'), 'user', 'github', array(), true) ?> <?php else: ?> - <a href="?controller=user&action=unlinkGitHub<?= Helper\param_csrf() ?>"><?= t('Unlink my GitHub Account') ?></a> + <?= $this->url->link(t('Unlink my GitHub Account'), 'user', 'unlinkGitHub', array(), true) ?> <?php endif ?> <?php else: ?> <?= empty($user['github_id']) ? t('No account linked.') : t('Account linked.') ?> diff --git a/app/Template/user/index.php b/app/Template/user/index.php new file mode 100644 index 00000000..6b4396b2 --- /dev/null +++ b/app/Template/user/index.php @@ -0,0 +1,76 @@ +<section id="main"> + <div class="page-header"> + <?php if ($this->user->isAdmin()): ?> + <ul> + <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New user'), 'user', 'create') ?></li> + </ul> + <?php endif ?> + </div> + <section> + <?php if ($paginator->isEmpty()): ?> + <p class="alert"><?= t('No user') ?></p> + <?php else: ?> + <table> + <tr> + <th><?= $paginator->order(t('Id'), 'id') ?></th> + <th><?= $paginator->order(t('Username'), 'username') ?></th> + <th><?= $paginator->order(t('Name'), 'name') ?></th> + <th><?= $paginator->order(t('Email'), 'email') ?></th> + <th><?= $paginator->order(t('Administrator'), 'is_admin') ?></th> + <th><?= $paginator->order(t('Two factor authentication'), 'twofactor_activated') ?></th> + <th><?= $paginator->order(t('Default project'), 'default_project_id') ?></th> + <th><?= $paginator->order(t('Notifications'), 'notifications_enabled') ?></th> + <th><?= t('External accounts') ?></th> + <th><?= $paginator->order(t('Account type'), 'is_ldap_user') ?></th> + </tr> + <?php foreach ($paginator->getCollection() as $user): ?> + <tr> + <td> + <?= $this->url->link('#'.$user['id'], 'user', 'show', array('user_id' => $user['id'])) ?> + </td> + <td> + <?= $this->url->link($this->e($user['username']), 'user', 'show', array('user_id' => $user['id'])) ?> + </td> + <td> + <?= $this->e($user['name']) ?> + </td> + <td> + <a href="mailto:<?= $this->e($user['email']) ?>"><?= $this->e($user['email']) ?></a> + </td> + <td> + <?= $user['is_admin'] ? t('Yes') : t('No') ?> + </td> + <td> + <?= $user['twofactor_activated'] ? t('Yes') : t('No') ?> + </td> + <td> + <?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? $this->e($projects[$user['default_project_id']]) : t('None'); ?> + </td> + <td> + <?php if ($user['notifications_enabled'] == 1): ?> + <?= t('Enabled') ?> + <?php else: ?> + <?= t('Disabled') ?> + <?php endif ?> + </td> + <td> + <ul class="no-bullet"> + <?php if ($user['google_id']): ?> + <li><i class="fa fa-google fa-fw"></i><?= t('Google account linked') ?></li> + <?php endif ?> + <?php if ($user['github_id']): ?> + <li><i class="fa fa-github fa-fw"></i><?= t('Github account linked') ?></li> + <?php endif ?> + </ul> + </td> + <td> + <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?> + </td> + </tr> + <?php endforeach ?> + </table> + + <?= $paginator ?> + <?php endif ?> + </section> +</section> diff --git a/app/Templates/user_last.php b/app/Template/user/last.php index 0b55b0d5..ab25f79b 100644 --- a/app/Templates/user_last.php +++ b/app/Template/user/last.php @@ -15,9 +15,9 @@ <?php foreach($last_logins as $login): ?> <tr> <td><?= dt('%B %e, %Y at %k:%M %p', $login['date_creation']) ?></td> - <td><?= Helper\escape($login['auth_type']) ?></td> - <td><?= Helper\escape($login['ip']) ?></td> - <td><?= Helper\escape(Helper\summary($login['user_agent'])) ?></td> + <td><?= $this->e($login['auth_type']) ?></td> + <td><?= $this->e($login['ip']) ?></td> + <td><?= $this->e($this->text->truncate($login['user_agent'])) ?></td> </tr> <?php endforeach ?> </table> diff --git a/app/Template/user/layout.php b/app/Template/user/layout.php new file mode 100644 index 00000000..e60ab77d --- /dev/null +++ b/app/Template/user/layout.php @@ -0,0 +1,18 @@ +<section id="main"> + <div class="page-header"> + <?php if ($this->user->isAdmin()): ?> + <ul> + <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li> + <li><i class="fa fa-plus fa-fw"></i><?= $this->url->link(t('New user'), 'user', 'create') ?></li> + </ul> + <?php endif ?> + </div> + <section class="sidebar-container" id="user-section"> + + <?= $this->render('user/sidebar', array('user' => $user)) ?> + + <div class="sidebar-content"> + <?= $user_content_for_layout ?> + </div> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/user/new.php b/app/Template/user/new.php new file mode 100644 index 00000000..ba7a3881 --- /dev/null +++ b/app/Template/user/new.php @@ -0,0 +1,45 @@ +<section id="main"> + <div class="page-header"> + <ul> + <li><i class="fa fa-user fa-fw"></i><?= $this->url->link(t('All users'), 'user', 'index') ?></li> + </ul> + </div> + <section> + <form method="post" action="<?= $this->url->href('user', 'save') ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <?= $this->form->label(t('Username'), 'username') ?> + <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?><br/> + + <?= $this->form->label(t('Name'), 'name') ?> + <?= $this->form->text('name', $values, $errors) ?><br/> + + <?= $this->form->label(t('Email'), 'email') ?> + <?= $this->form->email('email', $values, $errors) ?><br/> + + <?= $this->form->label(t('Password'), 'password') ?> + <?= $this->form->password('password', $values, $errors, array('required')) ?><br/> + + <?= $this->form->label(t('Confirmation'), 'confirmation') ?> + <?= $this->form->password('confirmation', $values, $errors, array('required')) ?><br/> + + <?= $this->form->label(t('Default project'), 'default_project_id') ?> + <?= $this->form->select('default_project_id', $projects, $values, $errors) ?><br/> + + <?= $this->form->label(t('Timezone'), 'timezone') ?> + <?= $this->form->select('timezone', $timezones, $values, $errors) ?><br/> + + <?= $this->form->label(t('Language'), 'language') ?> + <?= $this->form->select('language', $languages, $values, $errors) ?><br/> + + <?= $this->form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'index') ?> + </div> + </form> + </section> +</section>
\ No newline at end of file diff --git a/app/Template/user/notifications.php b/app/Template/user/notifications.php new file mode 100644 index 00000000..a425705d --- /dev/null +++ b/app/Template/user/notifications.php @@ -0,0 +1,38 @@ +<div class="page-header"> + <h2><?= t('Email notifications') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('user', 'notifications', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + <?= $this->form->checkbox('notifications_enabled', t('Enable email notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br> + + <hr> + + <?= t('I want to receive notifications for:') ?> + + <?= $this->form->radios('notifications_filter', array( + \Model\Notification::FILTER_NONE => t('All tasks'), + \Model\Notification::FILTER_ASSIGNEE => t('Only for tasks assigned to me'), + \Model\Notification::FILTER_CREATOR => t('Only for tasks created by me'), + \Model\Notification::FILTER_BOTH => t('Only for tasks created by me and assigned to me'), + ), $notifications) ?><br> + + <hr> + + <?php if (! empty($projects)): ?> + <p><?= t('I want to receive notifications only for those projects:') ?><br/><br/></p> + + <div class="form-checkbox-group"> + <?php foreach ($projects as $project_id => $project_name): ?> + <?= $this->form->checkbox('projects['.$project_id.']', $project_name, '1', isset($notifications['project_'.$project_id])) ?><br> + <?php endforeach ?> + </div> + <?php endif ?> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/user/password.php b/app/Template/user/password.php new file mode 100644 index 00000000..3ef28d33 --- /dev/null +++ b/app/Template/user/password.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2><?= t('Password modification') ?></h2> +</div> + +<form method="post" action="<?= $this->url->href('user', 'password', array('user_id' => $user['id'])) ?>" autocomplete="off"> + + <?= $this->form->hidden('id', $values) ?> + <?= $this->form->csrf() ?> + + <div class="alert alert-error"> + <?= $this->form->label(t('Current password for the user "%s"', $this->user->getFullname()), 'current_password') ?> + <?= $this->form->password('current_password', $values, $errors) ?><br/> + </div> + + <?= $this->form->label(t('New password for the user "%s"', $this->user->getFullname($user)), 'password') ?> + <?= $this->form->password('password', $values, $errors) ?><br/> + + <?= $this->form->label(t('Confirmation'), 'confirmation') ?> + <?= $this->form->password('confirmation', $values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + </div> +</form> diff --git a/app/Template/user/remove.php b/app/Template/user/remove.php new file mode 100644 index 00000000..810a3a3f --- /dev/null +++ b/app/Template/user/remove.php @@ -0,0 +1,13 @@ +<div class="page-header"> + <h2><?= t('Remove user') ?></h2> +</div> + +<div class="confirm"> + <p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', $user['name'] ?: $user['username']) ?></p> + + <div class="form-actions"> + <?= $this->url->link(t('Yes'), 'user', 'remove', array('user_id' => $user['id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + <?= t('or') ?> + <?= $this->url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> + </div> +</div>
\ No newline at end of file diff --git a/app/Templates/user_sessions.php b/app/Template/user/sessions.php index b647d726..a7504a7a 100644 --- a/app/Templates/user_sessions.php +++ b/app/Template/user/sessions.php @@ -17,9 +17,9 @@ <tr> <td><?= dt('%B %e, %Y at %k:%M %p', $session['date_creation']) ?></td> <td><?= dt('%B %e, %Y at %k:%M %p', $session['expiration']) ?></td> - <td><?= Helper\escape($session['ip']) ?></td> - <td><?= Helper\escape(Helper\summary($session['user_agent'])) ?></td> - <td><a href="?controller=user&action=removeSession&user_id=<?= $user['id'] ?>&id=<?= $session['id'].Helper\param_csrf() ?>"><?= t('Remove') ?></a></td> + <td><?= $this->e($session['ip']) ?></td> + <td><?= $this->e($this->text->truncate($session['user_agent'])) ?></td> + <td><?= $this->url->link(t('Remove'), 'user', 'removeSession', array('user_id' => $user['id'], 'id' => $session['id']), true) ?></td> </tr> <?php endforeach ?> </table> diff --git a/app/Template/user/share.php b/app/Template/user/share.php new file mode 100644 index 00000000..8f333a6b --- /dev/null +++ b/app/Template/user/share.php @@ -0,0 +1,17 @@ +<div class="page-header"> + <h2><?= t('Public access') ?></h2> +</div> + +<?php if (! empty($user['token'])): ?> + + <div class="listing"> + <ul class="no-bullet"> + <li><strong><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li> + </ul> + </div> + + <?= $this->url->link(t('Disable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> + +<?php else: ?> + <?= $this->url->link(t('Enable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> +<?php endif ?> diff --git a/app/Template/user/show.php b/app/Template/user/show.php new file mode 100644 index 00000000..5442e2e7 --- /dev/null +++ b/app/Template/user/show.php @@ -0,0 +1,39 @@ +<div class="page-header"> + <h2><?= t('Summary') ?></h2> +</div> +<ul class="listing"> + <li><?= t('Username:') ?> <strong><?= $this->e($user['username']) ?></strong></li> + <li><?= t('Name:') ?> <strong><?= $this->e($user['name']) ?: t('None') ?></strong></li> + <li><?= t('Email:') ?> <strong><?= $this->e($user['email']) ?: t('None') ?></strong></li> +</ul> + +<div class="page-header"> + <h2><?= t('Security') ?></h2> +</div> +<ul class="listing"> + <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : t('Regular user') ?></strong></li> + <li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li> + <li><?= $user['twofactor_activated'] == 1 ? t('Two factor authentication enabled') : t('Two factor authentication disabled') ?></li> +</ul> + +<div class="page-header"> + <h2><?= t('Preferences') ?></h2> +</div> +<ul class="listing"> + <li><?= t('Default project:') ?> <strong><?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? $this->e($projects[$user['default_project_id']]) : t('None') ?></strong></li> + <li><?= t('Timezone:') ?> <strong><?= $this->text->in($user['timezone'], $timezones) ?></strong></li> + <li><?= t('Language:') ?> <strong><?= $this->text->in($user['language'], $languages) ?></strong></li> + <li><?= t('Notifications:') ?> <strong><?= $user['notifications_enabled'] == 1 ? t('Enabled') : t('Disabled') ?></strong></li> +</ul> + +<?php if (! empty($user['token'])): ?> + <div class="page-header"> + <h2><?= t('Public access') ?></h2> + </div> + + <div class="listing"> + <ul class="no-bullet"> + <li><strong><i class="fa fa-calendar"></i> <?= $this->url->link(t('iCal feed'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li> + </ul> + </div> +<?php endif ?> diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php new file mode 100644 index 00000000..2c8e909a --- /dev/null +++ b/app/Template/user/sidebar.php @@ -0,0 +1,77 @@ +<div class="sidebar"> + <h2><?= t('Information') ?></h2> + <ul> + <li> + <?= $this->url->link(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?> + </li> + <?php if ($this->user->isAdmin()): ?> + <li> + <?= $this->url->link(t('User dashboard'), 'app', 'dashboard', array('user_id' => $user['id'])) ?> + </li> + <li> + <?= $this->url->link(t('User calendar'), 'user', 'calendar', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + <?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?> + <li> + <?= $this->url->link(t('Time tracking'), 'user', 'timesheet', array('user_id' => $user['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Last logins'), 'user', 'last', array('user_id' => $user['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + </ul> + + <h2><?= t('Actions') ?></h2> + <ul> + <?php if ($this->user->isAdmin() || $this->user->isCurrentUser($user['id'])): ?> + <li> + <?= $this->url->link(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?> + </li> + + <?php if ($user['is_ldap_user'] == 0): ?> + <li> + <?= $this->url->link(t('Change password'), 'user', 'password', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + + <?php if ($this->user->isCurrentUser($user['id'])): ?> + <li> + <?= $this->url->link(t('Two factor authentication'), 'twofactor', 'index', array('user_id' => $user['id'])) ?> + </li> + <?php elseif ($this->user->isAdmin() && $user['twofactor_activated'] == 1): ?> + <li> + <?= $this->url->link(t('Two factor authentication'), 'twofactor', 'disable', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + + <li> + <?= $this->url->link(t('Public access'), 'user', 'share', array('user_id' => $user['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Email notifications'), 'user', 'notifications', array('user_id' => $user['id'])) ?> + </li> + <li> + <?= $this->url->link(t('External accounts'), 'user', 'external', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + + <?php if ($this->user->isAdmin()): ?> + <li> + <?= $this->url->link(t('Hourly rates'), 'hourlyrate', 'index', array('user_id' => $user['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Manage timetable'), 'timetable', 'index', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + + <?php if ($this->user->isAdmin() && ! $this->user->isCurrentUser($user['id'])): ?> + <li> + <?= $this->url->link(t('Remove'), 'user', 'remove', array('user_id' => $user['id'])) ?> + </li> + <?php endif ?> + </ul> +</div>
\ No newline at end of file diff --git a/app/Template/user/timesheet.php b/app/Template/user/timesheet.php new file mode 100644 index 00000000..5c0d3af8 --- /dev/null +++ b/app/Template/user/timesheet.php @@ -0,0 +1,29 @@ +<div class="page-header"> + <h2><?= t('Time Tracking') ?></h2> +</div> + +<h3><?= t('Subtask timesheet') ?></h3> +<?php if ($subtask_paginator->isEmpty()): ?> + <p class="alert"><?= t('There is nothing to show.') ?></p> +<?php else: ?> + <table class="table-fixed"> + <tr> + <th class="column-25"><?= $subtask_paginator->order(t('Task'), 'task_title') ?></th> + <th class="column-25"><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th> + <th class="column-20"><?= $subtask_paginator->order(t('Start'), 'start') ?></th> + <th class="column-20"><?= $subtask_paginator->order(t('End'), 'end') ?></th> + <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), 'time_spent') ?></th> + </tr> + <?php foreach ($subtask_paginator->getCollection() as $record): ?> + <tr> + <td><?= $this->url->link($this->e($record['task_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td> + <td><?= $this->url->link($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td> + <td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td> + <td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td> + <td><?= n($record['time_spent']).' '.t('hours') ?></td> + </tr> + <?php endforeach ?> + </table> + + <?= $subtask_paginator ?> +<?php endif ?>
\ No newline at end of file diff --git a/app/Templates/action_event.php b/app/Templates/action_event.php deleted file mode 100644 index eee41780..00000000 --- a/app/Templates/action_event.php +++ /dev/null @@ -1,22 +0,0 @@ -<div class="page-header"> - <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> -</div> - -<h3><?= t('Choose an event') ?></h3> -<form method="post" action="?controller=action&action=params&project_id=<?= $project['id'] ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('project_id', $values) ?> - <?= Helper\form_hidden('action_name', $values) ?> - - <?= Helper\form_label(t('Event'), 'event_name') ?> - <?= Helper\form_select('event_name', $events, $values) ?><br/> - - <div class="form-help"> - <?= t('When the selected event occurs execute the corresponding action.') ?> - </div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> - <?= t('or') ?> <a href="?controller=action&action=index&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/action_index.php b/app/Templates/action_index.php deleted file mode 100644 index 30874591..00000000 --- a/app/Templates/action_index.php +++ /dev/null @@ -1,65 +0,0 @@ -<div class="page-header"> - <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> -</div> - -<?php if (! empty($actions)): ?> - -<h3><?= t('Defined actions') ?></h3> -<table> - <tr> - <th><?= t('Event name') ?></th> - <th><?= t('Action name') ?></th> - <th><?= t('Action parameters') ?></th> - <th><?= t('Action') ?></th> - </tr> - - <?php foreach ($actions as $action): ?> - <tr> - <td><?= Helper\in_list($action['event_name'], $available_events) ?></td> - <td><?= Helper\in_list($action['action_name'], $available_actions) ?></td> - <td> - <ul> - <?php foreach ($action['params'] as $param): ?> - <li> - <?= Helper\in_list($param['name'], $available_params) ?> = - <strong> - <?php if (Helper\contains($param['name'], 'column_id')): ?> - <?= Helper\in_list($param['value'], $columns_list) ?> - <?php elseif (Helper\contains($param['name'], 'user_id')): ?> - <?= Helper\in_list($param['value'], $users_list) ?> - <?php elseif (Helper\contains($param['name'], 'project_id')): ?> - <?= Helper\in_list($param['value'], $projects_list) ?> - <?php elseif (Helper\contains($param['name'], 'color_id')): ?> - <?= Helper\in_list($param['value'], $colors_list) ?> - <?php elseif (Helper\contains($param['name'], 'category_id')): ?> - <?= Helper\in_list($param['value'], $categories_list) ?> - <?php elseif (Helper\contains($param['name'], 'label')): ?> - <?= Helper\escape($param['value']) ?> - <?php endif ?> - </strong> - </li> - <?php endforeach ?> - </ul> - </td> - <td> - <a href="?controller=action&action=confirm&project_id=<?= $project['id'] ?>&action_id=<?= $action['id'] ?>"><?= t('Remove') ?></a> - </td> - </tr> - <?php endforeach ?> - -</table> - -<?php endif ?> - -<h3><?= t('Add an action') ?></h3> -<form method="post" action="?controller=action&action=event&project_id=<?= $project['id'] ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Action'), 'action_name') ?> - <?= Helper\form_select('action_name', $available_actions, $values) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Next step') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/action_params.php b/app/Templates/action_params.php deleted file mode 100644 index f647149b..00000000 --- a/app/Templates/action_params.php +++ /dev/null @@ -1,40 +0,0 @@ -<div class="page-header"> - <h2><?= t('Automatic actions for the project "%s"', $project['name']) ?></h2> -</div> - -<h3><?= t('Define action parameters') ?></h3> -<form method="post" action="?controller=action&action=create&project_id=<?= $project['id'] ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('project_id', $values) ?> - <?= Helper\form_hidden('event_name', $values) ?> - <?= Helper\form_hidden('action_name', $values) ?> - - <?php foreach ($action_params as $param_name => $param_desc): ?> - - <?php if (Helper\contains($param_name, 'column_id')): ?> - <?= Helper\form_label($param_desc, $param_name) ?> - <?= Helper\form_select('params['.$param_name.']', $columns_list, $values) ?><br/> - <?php elseif (Helper\contains($param_name, 'user_id')): ?> - <?= Helper\form_label($param_desc, $param_name) ?> - <?= Helper\form_select('params['.$param_name.']', $users_list, $values) ?><br/> - <?php elseif (Helper\contains($param_name, 'project_id')): ?> - <?= Helper\form_label($param_desc, $param_name) ?> - <?= Helper\form_select('params['.$param_name.']', $projects_list, $values) ?><br/> - <?php elseif (Helper\contains($param_name, 'color_id')): ?> - <?= Helper\form_label($param_desc, $param_name) ?> - <?= Helper\form_select('params['.$param_name.']', $colors_list, $values) ?><br/> - <?php elseif (Helper\contains($param_name, 'category_id')): ?> - <?= Helper\form_label($param_desc, $param_name) ?> - <?= Helper\form_select('params['.$param_name.']', $categories_list, $values) ?><br/> - <?php elseif (Helper\contains($param_name, 'label')): ?> - <?= Helper\form_label($param_desc, $param_name) ?> - <?= Helper\form_text('params['.$param_name.']', $values) ?> - <?php endif ?> - - <?php endforeach ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save this action') ?>" class="btn btn-blue"/> - <?= t('or') ?> <a href="?controller=action&action=index&project_id=<?= $project['id'] ?>"><?= t('cancel') ?></a> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/action_remove.php b/app/Templates/action_remove.php deleted file mode 100644 index 668067da..00000000 --- a/app/Templates/action_remove.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Remove an automatic action') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to remove this action: "%s"?', Helper\in_list($action['event_name'], $available_events).'/'.Helper\in_list($action['action_name'], $available_actions)) ?> - </p> - - <div class="form-actions"> - <?= Helper\a(t('Yes'), 'action', 'remove', array('project_id' => $project['id'], 'action_id' => $action['id']), true, 'btn btn-red') ?> - <?= t('or') ?> <?= Helper\a(t('cancel'), 'action', 'index', array('project_id' => $project['id'])) ?> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/app_index.php b/app/Templates/app_index.php deleted file mode 100644 index 91eecce4..00000000 --- a/app/Templates/app_index.php +++ /dev/null @@ -1,45 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Dashboard') ?></h2> - </div> - <section id="dashboard"> - <div class="dashboard-left-column"> - <h2><?= t('My tasks') ?></h2> - <?php if (empty($tasks)): ?> - <p class="alert"><?= t('There is nothing assigned to you.') ?></p> - <?php else: ?> - <table> - <tr> - <th> </th> - <th width="15%"><?= t('Project') ?></th> - <th width="40%"><?= t('Title') ?></th> - <th><?= t('Due date') ?></th> - <th><?= t('Date created') ?></th> - </tr> - <?php foreach ($tasks as $task): ?> - <tr> - <td class="task-table task-<?= $task['color_id'] ?>"> - <?= Helper\a('#'.$task['id'], 'task', 'show', array('task_id' => $task['id'])) ?> - </td> - <td> - <?= Helper\a(Helper\escape($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> - </td> - <td> - <?= Helper\a(Helper\escape($task['title']), 'task', 'show', array('task_id' => $task['id'])) ?> - </td> - <td> - <?= dt('%B %e, %Y', $task['date_due']) ?> - </td> - <td> - <?= dt('%B %e, %Y', $task['date_creation']) ?> - </td> - </tr> - <?php endforeach ?> - </table> - <?php endif ?> - </div> - <div class="dashboard-right-column"> - <h2><?= t('Activity stream') ?></h2> - <?= Helper\template('project_events', array('events' => $events)) ?> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/app_notfound.php b/app/Templates/app_notfound.php deleted file mode 100644 index 734d16a4..00000000 --- a/app/Templates/app_notfound.php +++ /dev/null @@ -1,9 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Page not found') ?></h2> - </div> - - <p class="alert alert-error"> - <?= t('Sorry, I didn\'t found this information in my database!') ?> - </p> -</section>
\ No newline at end of file diff --git a/app/Templates/board_assignee.php b/app/Templates/board_assignee.php deleted file mode 100644 index 41ede32b..00000000 --- a/app/Templates/board_assignee.php +++ /dev/null @@ -1,24 +0,0 @@ -<section id="main"> - - <div class="page-header board"> - <h2><?= t('Project "%s"', $current_project_name) ?></h2> - </div> - - <section> - <h3><?= t('Change assignee for the task "%s"', $values['title']) ?></h3> - <form method="post" action="?controller=board&action=updateAssignee" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Assignee'), 'owner_id') ?> - <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> <a href="?controller=board&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a> - </div> - </form> - </section> - -</section>
\ No newline at end of file diff --git a/app/Templates/board_category.php b/app/Templates/board_category.php deleted file mode 100644 index 36126a1d..00000000 --- a/app/Templates/board_category.php +++ /dev/null @@ -1,24 +0,0 @@ -<section id="main"> - - <div class="page-header board"> - <h2><?= t('Project "%s"', $current_project_name) ?></h2> - </div> - - <section> - <h3><?= t('Change category for the task "%s"', $values['title']) ?></h3> - <form method="post" action="?controller=board&action=updateCategory" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Category'), 'category_id') ?> - <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> <a href="?controller=board&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a> - </div> - </form> - </section> - -</section>
\ No newline at end of file diff --git a/app/Templates/board_edit.php b/app/Templates/board_edit.php deleted file mode 100644 index cfaebc50..00000000 --- a/app/Templates/board_edit.php +++ /dev/null @@ -1,58 +0,0 @@ -<div class="page-header"> - <h2><?= t('Edit the board for "%s"', $project['name']) ?></h2> -</div> -<section> - -<h3><?= t('Change columns') ?></h3> -<form method="post" action="?controller=board&action=update&project_id=<?= $project['id'] ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?php $i = 0; ?> - <table> - <tr> - <th><?= t('Position') ?></th> - <th><?= t('Column title') ?></th> - <th><?= t('Task limit') ?></th> - <th><?= t('Actions') ?></th> - </tr> - <?php foreach ($columns as $column): ?> - <tr> - <td><?= Helper\form_label(t('Column %d', ++$i), 'title['.$column['id'].']', array('title="column_id='.$column['id'].'"')) ?></td> - <td><?= Helper\form_text('title['.$column['id'].']', $values, $errors, array('required')) ?></td> - <td><?= Helper\form_number('task_limit['.$column['id'].']', $values, $errors, array('placeholder="'.t('limit').'"')) ?></td> - <td> - <ul> - <?php if ($column['position'] != 1): ?> - <li> - <?= Helper\a(t('Move Up'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?> - </li> - <?php endif ?> - <?php if ($column['position'] != count($columns)): ?> - <li> - <?= Helper\a(t('Move Down'), 'board', 'moveColumn', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?> - </li> - <?php endif ?> - <li> - <?= Helper\a(t('Remove'), 'board', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> - </li> - </ul> - </td> - </tr> - <?php endforeach ?> - </table> - - <div class="form-actions"> - <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/> - </div> -</form> - -<h3><?= t('Add a new column') ?></h3> -<form method="post" action="?controller=board&action=add&project_id=<?= $project['id'] ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('project_id', $values) ?> - <?= Helper\form_label(t('Title'), 'title') ?> - <?= Helper\form_text('title', $values, $errors, array('required')) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Add this column') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/board_index.php b/app/Templates/board_index.php deleted file mode 100644 index bff7dcc9..00000000 --- a/app/Templates/board_index.php +++ /dev/null @@ -1,38 +0,0 @@ -<section id="main"> - - <div class="page-header board"> - <h2> - <?= t('Project "%s"', $current_project_name) ?> - </h2> - </div> - - <div class="project-menu"> - <ul> - <li> - <span class="hide-tablet"><?= t('Filter by user') ?></span> - <?= Helper\form_select('user_id', $users, $filters) ?> - </li> - <li> - <span class="hide-tablet"><?= t('Filter by category') ?></span> - <?= Helper\form_select('category_id', $categories, $filters) ?> - </li> - <li><a href="#" id="filter-due-date"><?= t('Filter by due date') ?></a></li> - <li><a href="?controller=project&action=search&project_id=<?= $current_project_id ?>"><?= t('Search') ?></a></li> - <li><a href="?controller=project&action=tasks&project_id=<?= $current_project_id ?>"><?= t('Completed tasks') ?></a></li> - <li><a href="?controller=project&action=activity&project_id=<?= $current_project_id ?>"><?= t('Activity') ?></a></li> - </ul> - </div> - - <?php if (empty($board)): ?> - <p class="alert alert-error"><?= t('There is no column in your project!') ?></p> - <?php else: ?> - <?= Helper\template('board_show', array( - 'current_project_id' => $current_project_id, - 'board' => $board, - 'categories' => $categories, - 'board_private_refresh_interval' => $board_private_refresh_interval, - 'board_highlight_period' => $board_highlight_period, - )) ?> - <?php endif ?> - -</section> diff --git a/app/Templates/board_public.php b/app/Templates/board_public.php deleted file mode 100644 index 85c90cfa..00000000 --- a/app/Templates/board_public.php +++ /dev/null @@ -1,34 +0,0 @@ -<section id="main" class="public-board"> - - <?php if (empty($columns)): ?> - <p class="alert alert-error"><?= t('There is no column in your project!') ?></p> - <?php else: ?> - <table id="board"> - <tr> - <?php $column_with = round(100 / count($columns), 2); ?> - <?php foreach ($columns as $column): ?> - <th width="<?= $column_with ?>%"> - <?= Helper\escape($column['title']) ?> - <?php if ($column['task_limit']): ?> - <span title="<?= t('Task limit') ?>" class="task-limit">(<?= Helper\escape(count($column['tasks']).'/'.$column['task_limit']) ?>)</span> - <?php endif ?> - </th> - <?php endforeach ?> - </tr> - <tr> - <?php foreach ($columns as $column): ?> - <td class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"> - <?php foreach ($column['tasks'] as $task): ?> - <div class="task-board task-<?= $task['color_id'] ?>"> - - <?= Helper\template('board_task', array('task' => $task, 'categories' => $categories, 'not_editable' => true, 'project' => $project)) ?> - - </div> - <?php endforeach ?> - </td> - <?php endforeach ?> - </tr> - </table> - <?php endif ?> - -</section>
\ No newline at end of file diff --git a/app/Templates/board_show.php b/app/Templates/board_show.php deleted file mode 100644 index e8c3c1ba..00000000 --- a/app/Templates/board_show.php +++ /dev/null @@ -1,49 +0,0 @@ -<table id="board" data-project-id="<?= $current_project_id ?>" data-time="<?= time() ?>" data-check-interval="<?= $board_private_refresh_interval ?>" data-csrf-token=<?= \Core\Security::getCSRFToken() ?>> -<tr> - <?php $column_with = round(100 / count($board), 2); ?> - <?php foreach ($board as $column): ?> - <th width="<?= $column_with ?>%"> - <div class="board-add-icon"> - <a href="?controller=task&action=create&project_id=<?= $column['project_id'] ?>&column_id=<?= $column['id'] ?>" title="<?= t('Add a new task') ?>">+</a> - </div> - <?= Helper\escape($column['title']) ?> - <?php if ($column['task_limit']): ?> - <span title="<?= t('Task limit') ?>" class="task-limit"> - ( - <span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span> - / - <?= Helper\escape($column['task_limit']) ?> - ) - </span> - <?php else: ?> - <span title="<?= t('Task count') ?>" class="task-count"> - (<span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span>) - </span> - <?php endif ?> - </th> - <?php endforeach ?> -</tr> -<tr> - <?php foreach ($board as $column): ?> - <td - id="column-<?= $column['id'] ?>" - class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>" - data-column-id="<?= $column['id'] ?>" - data-task-limit="<?= $column['task_limit'] ?>" - > - <?php foreach ($column['tasks'] as $task): ?> - <div class="task-board draggable-item task-<?= $task['color_id'] ?> <?= $task['date_modification'] > time() - $board_highlight_period ? 'task-board-recent' : '' ?>" - data-task-id="<?= $task['id'] ?>" - data-owner-id="<?= $task['owner_id'] ?>" - data-category-id="<?= $task['category_id'] ?>" - data-due-date="<?= $task['date_due'] ?>" - title="<?= t('View this task') ?>"> - - <?= Helper\template('board_task', array('task' => $task, 'categories' => $categories)) ?> - - </div> - <?php endforeach ?> - </td> - <?php endforeach ?> -</tr> -</table> diff --git a/app/Templates/board_task.php b/app/Templates/board_task.php deleted file mode 100644 index ca854f37..00000000 --- a/app/Templates/board_task.php +++ /dev/null @@ -1,109 +0,0 @@ -<?php if (isset($not_editable)): ?> - - <a href="?controller=task&action=readonly&task_id=<?= $task['id'] ?>&token=<?= $project['token'] ?>">#<?= $task['id'] ?></a> - - <?php if ($task['reference']): ?> - <span class="task-board-reference" title="<?= t('Reference') ?>"> - (<?= $task['reference'] ?>) - </span> - <?php endif ?> - - - - - <span class="task-board-user"> - <?php if (! empty($task['owner_id'])): ?> - <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?> - <?php else: ?> - <span class="task-board-nobody"><?= t('Nobody assigned') ?></span> - <?php endif ?> - </span> - - <?php if ($task['score']): ?> - <span class="task-score"><?= Helper\escape($task['score']) ?></span> - <?php endif ?> - - <div class="task-board-title"> - <a href="?controller=task&action=readonly&task_id=<?= $task['id'] ?>&token=<?= $project['token'] ?>"> - <?= Helper\escape($task['title']) ?> - </a> - </div> - -<?php else: ?> - - <a class="task-edit-popover" href="?controller=task&action=edit&task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a> - - <?php if ($task['reference']): ?> - <span class="task-board-reference" title="<?= t('Reference') ?>"> - (<?= $task['reference'] ?>) - </span> - <?php endif ?> - - - - - <span class="task-board-user"> - <a class="assignee-popover" href="?controller=board&action=changeAssignee&task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>"> - <?php if (! empty($task['owner_id'])): ?> - <?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></a> - <?php else: ?> - <?= t('Nobody assigned') ?> - <?php endif ?> - </a> - </span> - - <?php if ($task['score']): ?> - <span class="task-score"><?= Helper\escape($task['score']) ?></span> - <?php endif ?> - - <div class="task-board-title"> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> - </div> - -<?php endif ?> - - -<?php if ($task['category_id']): ?> -<div class="task-board-category-container"> - <span class="task-board-category"> - <a class="category-popover" href="?controller=board&action=changeCategory&task_id=<?= $task['id'] ?>" title="<?= t('Change category') ?>"> - <?= Helper\in_list($task['category_id'], $categories) ?> - </a> - </span> -</div> -<?php endif ?> - - -<?php if (! empty($task['date_due']) || ! empty($task['nb_files']) || ! empty($task['nb_comments']) || ! empty($task['description']) || ! empty($task['nb_subtasks'])): ?> -<div class="task-board-footer"> - - <?php if (! empty($task['date_due'])): ?> - <div class="task-board-date <?= time() > $task['date_due'] ? 'task-board-date-overdue' : '' ?>"> - <?= dt('%B %e, %Y', $task['date_due']) ?> - </div> - <?php endif ?> - - <div class="task-board-icons"> - - <?php if (! empty($task['nb_subtasks'])): ?> - <span title="<?= t('Sub-Tasks') ?>"><?= $task['nb_completed_subtasks'].'/'.$task['nb_subtasks'] ?> <i class="fa fa-bars"></i></span> - <?php endif ?> - - <?php if (! empty($task['nb_files'])): ?> - <span title="<?= t('Attachments') ?>"><?= $task['nb_files'] ?> <i class="fa fa-paperclip"></i></span> - <?php endif ?> - - <?php if (! empty($task['nb_comments'])): ?> - <span title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"><?= $task['nb_comments'] ?> <i class="fa fa-comment-o"></i></span> - <?php endif ?> - - <?php if (! empty($task['description'])): ?> - <span title="<?= t('Description') ?>"> - <?php if (! isset($not_editable)): ?> - <a class="task-description-popover" href="?controller=task&action=description&task_id=<?= $task['id'] ?>"><i class="fa fa-file-text-o" data-href="?controller=task&action=description&task_id=<?= $task['id'] ?>"></i></a> - <?php else: ?> - <i class="fa fa-file-text-o"></i> - <?php endif ?> - </span> - <?php endif ?> - </div> -</div> -<?php endif ?> diff --git a/app/Templates/category_edit.php b/app/Templates/category_edit.php deleted file mode 100644 index 278d7e12..00000000 --- a/app/Templates/category_edit.php +++ /dev/null @@ -1,16 +0,0 @@ -<div class="page-header"> - <h2><?= t('Category modification for the project "%s"', $project['name']) ?></h2> -</div> - -<form method="post" action="?controller=category&action=update&project_id=<?= $project['id'] ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Category Name'), 'name') ?> - <?= Helper\form_text('name', $values, $errors, array('autofocus required')) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/category_index.php b/app/Templates/category_index.php deleted file mode 100644 index 4635406e..00000000 --- a/app/Templates/category_index.php +++ /dev/null @@ -1,41 +0,0 @@ -<div class="page-header"> - <h2><?= t('Categories') ?></h2> -</div> - -<?php if (! empty($categories)): ?> -<table> - <tr> - <th><?= t('Category Name') ?></th> - <th><?= t('Actions') ?></th> - </tr> - <?php foreach ($categories as $category_id => $category_name): ?> - <tr> - <td><?= Helper\escape($category_name) ?></td> - <td> - <ul> - <li> - <a href="?controller=category&action=edit&project_id=<?= $project['id'] ?>&category_id=<?= $category_id ?>"><?= t('Edit') ?></a> - </li> - <li> - <a href="?controller=category&action=confirm&project_id=<?= $project['id'] ?>&category_id=<?= $category_id ?>"><?= t('Remove') ?></a> - </li> - </ul> - </td> - </tr> - <?php endforeach ?> -</table> -<?php endif ?> - -<h3><?= t('Add a new category') ?></h3> -<form method="post" action="?controller=category&action=save&project_id=<?= $project['id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Category Name'), 'name') ?> - <?= Helper\form_text('name', $values, $errors, array('autofocus required')) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/comment_create.php b/app/Templates/comment_create.php deleted file mode 100644 index 11772f75..00000000 --- a/app/Templates/comment_create.php +++ /dev/null @@ -1,19 +0,0 @@ -<div class="page-header"> - <h2><?= t('Add a comment') ?></h2> -</div> - -<form method="post" action="?controller=comment&action=save&task_id=<?= $task['id'] ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('task_id', $values) ?> - <?= Helper\form_hidden('user_id', $values) ?> - <?= Helper\form_textarea('comment', $values, $errors, array(! isset($skip_cancel) ? 'autofocus' : '', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/> - <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?php if (! isset($skip_cancel)): ?> - <?= t('or') ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - <?php endif ?> - </div> -</form> diff --git a/app/Templates/comment_edit.php b/app/Templates/comment_edit.php deleted file mode 100644 index 4ce48964..00000000 --- a/app/Templates/comment_edit.php +++ /dev/null @@ -1,17 +0,0 @@ -<div class="page-header"> - <h2><?= t('Edit a comment') ?></h2> -</div> - -<form method="post" action="?controller=comment&action=update&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_hidden('task_id', $values) ?> - <?= Helper\form_textarea('comment', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a comment').'"'), 'comment-textarea') ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</form> diff --git a/app/Templates/comment_forbidden.php b/app/Templates/comment_forbidden.php deleted file mode 100644 index eeea8404..00000000 --- a/app/Templates/comment_forbidden.php +++ /dev/null @@ -1,9 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Forbidden') ?></h2> - </div> - - <p class="alert alert-error"> - <?= t('Only administrators or the creator of the comment can access to this page.') ?> - </p> -</section>
\ No newline at end of file diff --git a/app/Templates/comment_remove.php b/app/Templates/comment_remove.php deleted file mode 100644 index 7b117781..00000000 --- a/app/Templates/comment_remove.php +++ /dev/null @@ -1,16 +0,0 @@ -<div class="page-header"> - <h2><?= t('Remove a comment') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to remove this comment?') ?> - </p> - - <?= Helper\template('comment_show', array('comment' => $comment, 'task' => $task, 'preview' => true)) ?> - - <div class="form-actions"> - <a href="?controller=comment&action=remove&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>#comment-<?= $comment['id'] ?>"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/comment_show.php b/app/Templates/comment_show.php deleted file mode 100644 index b2ccc25a..00000000 --- a/app/Templates/comment_show.php +++ /dev/null @@ -1,41 +0,0 @@ -<div class="comment <?= isset($preview) ? 'comment-preview' : '' ?>" id="comment-<?= $comment['id'] ?>"> - - <p class="comment-title"> - <span class="comment-username"><?= Helper\escape($comment['name'] ?: $comment['username']) ?></span> @ <span class="comment-date"><?= dt('%B %e, %Y at %k:%M %p', $comment['date']) ?></span> - </p> - - <div class="comment-inner"> - - <?php if (! isset($preview)): ?> - <ul class="comment-actions"> - <li><a href="#comment-<?= $comment['id'] ?>"><?= t('link') ?></a></li> - <?php if ((! isset($not_editable) || ! $not_editable) && (Helper\is_admin() || Helper\is_current_user($comment['user_id']))): ?> - <li> - <a href="?controller=comment&action=confirm&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'] ?>"><?= t('remove') ?></a> - </li> - <li> - <a href="?controller=comment&action=edit&task_id=<?= $task['id'] ?>&comment_id=<?= $comment['id'] ?>"><?= t('edit') ?></a> - </li> - <?php endif ?> - </ul> - <?php endif ?> - - <div class="markdown"> - <?php if (isset($is_public) && $is_public): ?> - <?= Helper\markdown( - $comment['comment'], - array( - 'controller' => 'task', - 'action' => 'readonly', - 'params' => array( - 'token' => $project['token'] - ) - ) - ) ?> - <?php else: ?> - <?= Helper\markdown($comment['comment']) ?> - <?php endif ?> - </div> - - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/config_about.php b/app/Templates/config_about.php deleted file mode 100644 index 3f34f802..00000000 --- a/app/Templates/config_about.php +++ /dev/null @@ -1,41 +0,0 @@ -<div class="page-header"> - <h2><?= t('About') ?></h2> -</div> -<section class="listing"> - <ul> - <li> - <?= t('Official website:') ?> - <a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a> - </li> - <li> - <?= t('Application version:') ?> - <strong><?= APP_VERSION ?></strong> - </li> - </ul> -</section> - -<div class="page-header"> - <h2><?= t('Database') ?></h2> -</div> -<section class="listing"> - <ul> - <li> - <?= t('Database driver:') ?> - <strong><?= Helper\escape(DB_DRIVER) ?></strong> - </li> - <?php if (DB_DRIVER === 'sqlite'): ?> - <li> - <?= t('Database size:') ?> - <strong><?= Helper\format_bytes($db_size) ?></strong> - </li> - <li> - <?= Helper\a(t('Download the database'), 'config', 'downloadDb', array(), true) ?> - <?= t('(Gzip compressed Sqlite file)') ?> - </li> - <li> - <?= Helper\a(t('Optimize the database'), 'config', 'optimizeDb', array(), true) ?> - <?= t('(VACUUM command)') ?> - </li> - <?php endif ?> - </ul> -</section>
\ No newline at end of file diff --git a/app/Templates/config_api.php b/app/Templates/config_api.php deleted file mode 100644 index 037ea08d..00000000 --- a/app/Templates/config_api.php +++ /dev/null @@ -1,18 +0,0 @@ -<div class="page-header"> - <h2><?= t('API') ?></h2> -</div> -<section class="listing"> - <ul> - <li> - <?= t('API token:') ?> - <strong><?= Helper\escape($values['api_token']) ?></strong> - </li> - <li> - <?= t('API endpoint:') ?> - <input type="text" readonly="readonly" value="<?= Helper\get_current_base_url().'jsonrpc.php' ?>"> - </li> - <li> - <?= Helper\a(t('Reset token'), 'config', 'token', array('type' => 'api'), true) ?> - </li> - </ul> -</section>
\ No newline at end of file diff --git a/app/Templates/config_application.php b/app/Templates/config_application.php deleted file mode 100644 index 97071bd0..00000000 --- a/app/Templates/config_application.php +++ /dev/null @@ -1,27 +0,0 @@ -<div class="page-header"> - <h2><?= t('Application settings') ?></h2> -</div> -<section> -<form method="post" action="<?= Helper\u('config', 'application') ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_label(t('Application URL'), 'application_url') ?> - <?= Helper\form_text('application_url', $values, $errors, array('placeholder="http://example.kanboar.net/"')) ?><br/> - <p class="form-help"><?= t('Example: http://example.kanboard.net/ (used by email notifications)') ?></p> - - <?= Helper\form_label(t('Language'), 'application_language') ?> - <?= Helper\form_select('application_language', $languages, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Timezone'), 'application_timezone') ?> - <?= Helper\form_select('application_timezone', $timezones, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Date format'), 'application_date_format') ?> - <?= Helper\form_select('application_date_format', $date_formats, $values, $errors) ?><br/> - <p class="form-help"><?= t('ISO format is always accepted, example: "%s" and "%s"', date('Y-m-d'), date('Y_m_d')) ?></p> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form> -</section>
\ No newline at end of file diff --git a/app/Templates/config_board.php b/app/Templates/config_board.php deleted file mode 100644 index f260d084..00000000 --- a/app/Templates/config_board.php +++ /dev/null @@ -1,29 +0,0 @@ -<div class="page-header"> - <h2><?= t('Board settings') ?></h2> -</div> -<section> -<form method="post" action="<?= Helper\u('config', 'board') ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_label(t('Task highlight period'), 'board_highlight_period') ?> - <?= Helper\form_number('board_highlight_period', $values, $errors) ?><br/> - <p class="form-help"><?= t('Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)') ?></p> - - <?= Helper\form_label(t('Refresh interval for public board'), 'board_public_refresh_interval') ?> - <?= Helper\form_number('board_public_refresh_interval', $values, $errors) ?><br/> - <p class="form-help"><?= t('Frequency in second (60 seconds by default)') ?></p> - - <?= Helper\form_label(t('Refresh interval for private board'), 'board_private_refresh_interval') ?> - <?= Helper\form_number('board_private_refresh_interval', $values, $errors) ?><br/> - <p class="form-help"><?= t('Frequency in second (0 to disable this feature, 10 seconds by default)') ?></p> - - <?= Helper\form_label(t('Default columns for new projects (Comma-separated)'), 'board_columns') ?> - <?= Helper\form_text('board_columns', $values, $errors) ?><br/> - <p class="form-help"><?= t('Default values are "%s"', $default_columns) ?></p> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form> -</section>
\ No newline at end of file diff --git a/app/Templates/config_layout.php b/app/Templates/config_layout.php deleted file mode 100644 index 3aacb9b7..00000000 --- a/app/Templates/config_layout.php +++ /dev/null @@ -1,13 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Settings') ?></h2> - </div> - <section class="config-show" id="config-section"> - - <?= Helper\template('config_sidebar') ?> - - <div class="config-show-main"> - <?= $config_content_for_layout ?> - </div> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/config_sidebar.php b/app/Templates/config_sidebar.php deleted file mode 100644 index d96159b8..00000000 --- a/app/Templates/config_sidebar.php +++ /dev/null @@ -1,22 +0,0 @@ -<div class="config-show-sidebar"> - <h2><?= t('Actions') ?></h2> - <div class="config-show-actions"> - <ul> - <li> - <?= Helper\a(t('About'), 'config', 'index') ?> - </li> - <li> - <?= Helper\a(t('Application settings'), 'config', 'application') ?> - </li> - <li> - <?= Helper\a(t('Board settings'), 'config', 'board') ?> - </li> - <li> - <?= Helper\a(t('Webhooks'), 'config', 'webhook') ?> - </li> - <li> - <?= Helper\a(t('API'), 'config', 'api') ?> - </li> - </ul> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/config_webhook.php b/app/Templates/config_webhook.php deleted file mode 100644 index 052a2a99..00000000 --- a/app/Templates/config_webhook.php +++ /dev/null @@ -1,38 +0,0 @@ -<div class="page-header"> - <h2><?= t('Webhook settings') ?></h2> -</div> -<section> -<form method="post" action="<?= Helper\u('config', 'webhook') ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_label(t('Webhook URL for task creation'), 'webhook_url_task_creation') ?> - <?= Helper\form_text('webhook_url_task_creation', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Webhook URL for task modification'), 'webhook_url_task_modification') ?> - <?= Helper\form_text('webhook_url_task_modification', $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form> -</section> - -<div class="page-header"> - <h2><?= t('URL and token') ?></h2> -</div> -<section class="listing"> - <ul> - <li> - <?= t('Webhook token:') ?> - <strong><?= Helper\escape($values['webhook_token']) ?></strong> - </li> - <li> - <?= t('URL for task creation:') ?> - <input type="text" readonly="readonly" value="<?= Helper\get_current_base_url().Helper\u('webhook', 'task', array('token' => $values['webhook_token'])) ?>"> - </li> - <li> - <?= Helper\a(t('Reset token'), 'config', 'token', array('type' => 'webhook'), true) ?> - </li> - </ul> -</section>
\ No newline at end of file diff --git a/app/Templates/event_comment_create.php b/app/Templates/event_comment_create.php deleted file mode 100644 index d2f6f97b..00000000 --- a/app/Templates/event_comment_create.php +++ /dev/null @@ -1,7 +0,0 @@ -<p class="activity-title"> - <?= e('%s commented the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> -</p> -<div class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em><br/> - <div class="markdown"><?= Helper\markdown($comment['comment']) ?></div> -</div>
\ No newline at end of file diff --git a/app/Templates/event_comment_update.php b/app/Templates/event_comment_update.php deleted file mode 100644 index 27cc0be6..00000000 --- a/app/Templates/event_comment_update.php +++ /dev/null @@ -1,7 +0,0 @@ -<p class="activity-title"> - <?= e('%s updated a comment on the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> -</p> -<div class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em><br/> - <div class="markdown"><?= Helper\markdown($comment['comment']) ?></div> -</div>
\ No newline at end of file diff --git a/app/Templates/event_task_assignee_change.php b/app/Templates/event_task_assignee_change.php deleted file mode 100644 index b346325e..00000000 --- a/app/Templates/event_task_assignee_change.php +++ /dev/null @@ -1,12 +0,0 @@ -<p class="activity-title"> - <?= e( - '%s change the assignee of the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to %s', - Helper\escape($author), - $task_id, - $task_id, - Helper\escape($task['assignee_name'] ?: $task['assignee_username']) - ) ?> -</p> -<p class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em> -</p>
\ No newline at end of file diff --git a/app/Templates/event_task_close.php b/app/Templates/event_task_close.php deleted file mode 100644 index 48d25678..00000000 --- a/app/Templates/event_task_close.php +++ /dev/null @@ -1,6 +0,0 @@ -<p class="activity-title"> - <?= e('%s closed the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> -</p> -<p class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em> -</p>
\ No newline at end of file diff --git a/app/Templates/event_task_create.php b/app/Templates/event_task_create.php deleted file mode 100644 index 2515af05..00000000 --- a/app/Templates/event_task_create.php +++ /dev/null @@ -1,6 +0,0 @@ -<p class="activity-title"> - <?= e('%s created the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> -</p> -<p class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em> -</p>
\ No newline at end of file diff --git a/app/Templates/event_task_move_column.php b/app/Templates/event_task_move_column.php deleted file mode 100644 index f2aac8f7..00000000 --- a/app/Templates/event_task_move_column.php +++ /dev/null @@ -1,6 +0,0 @@ -<p class="activity-title"> - <?= e('%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the column "%s"', Helper\escape($author), $task_id, $task_id, Helper\escape($task['column_title'])) ?> -</p> -<p class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em> -</p>
\ No newline at end of file diff --git a/app/Templates/event_task_move_position.php b/app/Templates/event_task_move_position.php deleted file mode 100644 index 26cdeb13..00000000 --- a/app/Templates/event_task_move_position.php +++ /dev/null @@ -1,6 +0,0 @@ -<p class="activity-title"> - <?= e('%s moved the task <a href="?controller=task&action=show&task_id=%d">#%d</a> to the position #%d in the column "%s"', Helper\escape($author), $task_id, $task_id, $task['position'], Helper\escape($task['column_title'])) ?> -</p> -<p class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em> -</p>
\ No newline at end of file diff --git a/app/Templates/event_task_open.php b/app/Templates/event_task_open.php deleted file mode 100644 index 9623be74..00000000 --- a/app/Templates/event_task_open.php +++ /dev/null @@ -1,6 +0,0 @@ -<p class="activity-title"> - <?= e('%s open the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> -</p> -<p class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em> -</p>
\ No newline at end of file diff --git a/app/Templates/event_task_update.php b/app/Templates/event_task_update.php deleted file mode 100644 index a270b936..00000000 --- a/app/Templates/event_task_update.php +++ /dev/null @@ -1,6 +0,0 @@ -<p class="activity-title"> - <?= e('%s updated the task <a href="?controller=task&action=show&task_id=%d">#%d</a>', Helper\escape($author), $task_id, $task_id) ?> -</p> -<p class="activity-description"> - <em><?= Helper\escape($task['title']) ?></em> -</p>
\ No newline at end of file diff --git a/app/Templates/file_new.php b/app/Templates/file_new.php deleted file mode 100644 index 7f7f1d1c..00000000 --- a/app/Templates/file_new.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Attach a document') ?></h2> -</div> - -<form action="?controller=file&action=save&task_id=<?= $task['id'] ?>" method="post" enctype="multipart/form-data"> - <?= Helper\form_csrf() ?> - <input type="file" name="files[]" multiple /> - <div class="form-help"><?= t('Maximum size: ') ?><?= is_integer($max_size) ? Helper\format_bytes($max_size) : $max_size ?></div> - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/file_open.php b/app/Templates/file_open.php deleted file mode 100644 index aa181d64..00000000 --- a/app/Templates/file_open.php +++ /dev/null @@ -1,6 +0,0 @@ -<div class="page-header"> - <h2><?= Helper\escape($file['name']) ?></h2> - <div class="task-file-viewer"> - <img src="?controller=file&action=image&file_id=<?= $file['id'] ?>&task_id=<?= $file['task_id'] ?>" alt="<?= Helper\escape($file['name']) ?>"/> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/file_remove.php b/app/Templates/file_remove.php deleted file mode 100644 index af77591c..00000000 --- a/app/Templates/file_remove.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Remove a file') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to remove this file: "%s"?', Helper\escape($file['name'])) ?> - </p> - - <div class="form-actions"> - <a href="?controller=file&action=remove&task_id=<?= $task['id'] ?>&file_id=<?= $file['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/file_show.php b/app/Templates/file_show.php deleted file mode 100644 index 3832a0f5..00000000 --- a/app/Templates/file_show.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php if (! empty($files)): ?> -<div id="attachments" class="task-show-section"> - - <div class="page-header"> - <h2><?= t('Attachments') ?></h2> - </div> - - <ul class="task-show-files"> - <?php foreach ($files as $file): ?> - <li> - <a href="?controller=file&action=download&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a> - <span class="task-show-file-actions"> - <?php if ($file['is_image']): ?> - <a href="?controller=file&action=open&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>" class="file-popover"><?= t('open') ?></a>, - <?php endif ?> - <a href="?controller=file&action=confirm&file_id=<?= $file['id'] ?>&task_id=<?= $task['id'] ?>"><?= t('remove') ?></a> - </span> - </li> - <?php endforeach ?> - </ul> - -</div> -<?php endif ?>
\ No newline at end of file diff --git a/app/Templates/layout.php b/app/Templates/layout.php deleted file mode 100644 index a86d613b..00000000 --- a/app/Templates/layout.php +++ /dev/null @@ -1,84 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width"> - <meta name="mobile-web-app-capable" content="yes"> - <meta name="robots" content="noindex,nofollow"> - - <?php if (isset($board_public_refresh_interval)): ?> - <meta http-equiv="refresh" content="<?= $board_public_refresh_interval ?>" > - <?php endif ?> - - <?php if (! isset($not_editable)): ?> - <?= Helper\js('assets/js/jquery-1.11.1.min.js') ?> - <?= Helper\js('assets/js/jquery-ui-1.10.4.custom.min.js') ?> - <?= Helper\js('assets/js/jquery.ui.touch-punch.min.js') ?> - <?= Helper\js('assets/js/chosen.jquery.min.js') ?> - <?= Helper\js('assets/js/app.js') ?> - <?php endif ?> - - <?= Helper\css('assets/css/app.css') ?> - <?= Helper\css('assets/css/font-awesome.min.css') ?> - <?= Helper\css('assets/css/jquery-ui-1.10.4.custom.css'); ?> - <?= Helper\css('assets/css/chosen.min.css'); ?> - - <link rel="icon" type="image/png" href="assets/img/favicon.png"> - <link rel="apple-touch-icon" href="assets/img/touch-icon-iphone.png"> - <link rel="apple-touch-icon" sizes="72x72" href="assets/img/touch-icon-ipad.png"> - <link rel="apple-touch-icon" sizes="114x114" href="assets/img/touch-icon-iphone-retina.png"> - <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png"> - - <title><?= isset($title) ? Helper\escape($title).' - Kanboard' : 'Kanboard' ?></title> - </head> - <body> - <?php if (isset($no_layout) && $no_layout): ?> - <?= $content_for_layout ?> - <?php else: ?> - <header> - <nav> - <a class="logo" href="?">kanboard</a> - - <ul> - <?php if (isset($board_selector) && ! empty($board_selector)): ?> - <li> - <select id="board-selector" data-placeholder="<?= t('Display another project') ?>"> - <option value=""></option> - <?php foreach($board_selector as $board_id => $board_name): ?> - <option value="<?= $board_id ?>"><?= Helper\escape($board_name) ?></option> - <?php endforeach ?> - </select> - </li> - <?php endif ?> - <li <?= isset($menu) && $menu === 'dashboard' ? 'class="active"' : '' ?>> - <a href="?controller=app"><?= t('Dashboard') ?></a> - </li> - <li <?= isset($menu) && $menu === 'boards' ? 'class="active"' : '' ?>> - <a href="?controller=board"><?= t('Boards') ?></a> - </li> - <li <?= isset($menu) && $menu === 'projects' ? 'class="active"' : '' ?>> - <a href="?controller=project"><?= t('Projects') ?></a> - </li> - <?php if (Helper\is_admin()): ?> - <li <?= isset($menu) && $menu === 'users' ? 'class="active"' : '' ?>> - <a href="?controller=user"><?= t('Users') ?></a> - </li> - <li class="hide-tablet <?= isset($menu) && $menu === 'config' ? 'active' : '' ?>"> - <a href="?controller=config"><?= t('Settings') ?></a> - </li> - <?php endif ?> - <li> - <a href="?controller=user&action=logout<?= Helper\param_csrf() ?>"><?= t('Logout') ?></a> - <span class="username">(<a href="?controller=user&action=show&user_id=<?= Helper\get_user_id() ?>"><?= Helper\escape(Helper\get_username()) ?></a>)</span> - </li> - </ul> - </nav> - </header> - <section class="page"> - <?= Helper\flash('<div class="alert alert-success alert-fade-out">%s</div>') ?> - <?= Helper\flash_error('<div class="alert alert-error">%s</div>') ?> - <?= $content_for_layout ?> - </section> - <?php endif ?> - </body> -</html> diff --git a/app/Templates/notification_comment_creation.php b/app/Templates/notification_comment_creation.php deleted file mode 100644 index 5b334d76..00000000 --- a/app/Templates/notification_comment_creation.php +++ /dev/null @@ -1,7 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<h3><?= t('New comment posted by %s', $comment['name'] ?: $comment['username']) ?></h3> - -<?= Helper\markdown($comment['comment']) ?> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_comment_update.php b/app/Templates/notification_comment_update.php deleted file mode 100644 index 04aafb85..00000000 --- a/app/Templates/notification_comment_update.php +++ /dev/null @@ -1,7 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<h3><?= t('Comment updated') ?></h3> - -<?= Helper\markdown($comment['comment']) ?> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_file_creation.php b/app/Templates/notification_file_creation.php deleted file mode 100644 index d8636820..00000000 --- a/app/Templates/notification_file_creation.php +++ /dev/null @@ -1,5 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<h3><?= t('New attachment added "%s"', $file['name']) ?></h3> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_footer.php b/app/Templates/notification_footer.php deleted file mode 100644 index 533621f4..00000000 --- a/app/Templates/notification_footer.php +++ /dev/null @@ -1,6 +0,0 @@ -<hr/> -Kanboard - -<?php if ($application_url): ?> - - <a href="<?= $application_url.'?controller=task&action=show&task_id='.$task['id'] ?>"><?= t('view the task on Kanboard') ?></a>. -<?php endif ?> diff --git a/app/Templates/notification_subtask_creation.php b/app/Templates/notification_subtask_creation.php deleted file mode 100644 index 2ddfc649..00000000 --- a/app/Templates/notification_subtask_creation.php +++ /dev/null @@ -1,17 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<h3><?= t('New sub-task') ?></h3> - -<ul> - <li><?= t('Title:') ?> <?= Helper\escape($subtask['title']) ?></li> - <li><?= t('Status:') ?> <?= Helper\escape($subtask['status_name']) ?></li> - <li><?= t('Assignee:') ?> <?= Helper\escape($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> - <li> - <?= t('Time tracking:') ?> - <?php if (! empty($subtask['time_estimated'])): ?> - <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> - <?php endif ?> - </li> -</ul> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_subtask_update.php b/app/Templates/notification_subtask_update.php deleted file mode 100644 index 999edbf9..00000000 --- a/app/Templates/notification_subtask_update.php +++ /dev/null @@ -1,21 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<h3><?= t('Sub-task updated') ?></h3> - -<ul> - <li><?= t('Title:') ?> <?= Helper\escape($subtask['title']) ?></li> - <li><?= t('Status:') ?> <?= Helper\escape($subtask['status_name']) ?></li> - <li><?= t('Assignee:') ?> <?= Helper\escape($subtask['name'] ?: $subtask['username'] ?: '?') ?></li> - <li> - <?= t('Time tracking:') ?> - <?php if (! empty($subtask['time_spent'])): ?> - <strong><?= Helper\escape($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> - <?php endif ?> - - <?php if (! empty($subtask['time_estimated'])): ?> - <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> - <?php endif ?> - </li> -</ul> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_task_close.php b/app/Templates/notification_task_close.php deleted file mode 100644 index d56e71bb..00000000 --- a/app/Templates/notification_task_close.php +++ /dev/null @@ -1,5 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<p><?= t('The task #%d have been closed.', $task['id']) ?></p> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_task_due.php b/app/Templates/notification_task_due.php deleted file mode 100644 index ae02f64e..00000000 --- a/app/Templates/notification_task_due.php +++ /dev/null @@ -1,15 +0,0 @@ -<h2><?= t('List of due tasks for the project "%s"', $project) ?></h2> - -<ul> - <?php foreach ($tasks as $task): ?> - <li> - (<strong>#<?= $task['id'] ?></strong>) - <?= Helper\escape($task['title']) ?> - <?php if ($task['assignee_username']): ?> - (<strong><?= t('Assigned to %s', $task['assignee_name'] ?: $task['assignee_username']) ?></strong>) - <?php endif ?> - </li> - <?php endforeach ?> -</ul> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_task_move_column.php b/app/Templates/notification_task_move_column.php deleted file mode 100644 index c3f94df7..00000000 --- a/app/Templates/notification_task_move_column.php +++ /dev/null @@ -1,11 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<ul> - <li> - <?= t('Column on the board:') ?> - <strong><?= Helper\escape($task['column_title']) ?></strong> - </li> - <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li> -</ul> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_task_move_position.php b/app/Templates/notification_task_move_position.php deleted file mode 100644 index c3f94df7..00000000 --- a/app/Templates/notification_task_move_position.php +++ /dev/null @@ -1,11 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<ul> - <li> - <?= t('Column on the board:') ?> - <strong><?= Helper\escape($task['column_title']) ?></strong> - </li> - <li><?= t('Task position:').' '.Helper\escape($task['position']) ?></li> -</ul> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/notification_task_open.php b/app/Templates/notification_task_open.php deleted file mode 100644 index 5d9f7d5b..00000000 --- a/app/Templates/notification_task_open.php +++ /dev/null @@ -1,5 +0,0 @@ -<h2><?= Helper\escape($task['title']) ?> (#<?= $task['id'] ?>)</h2> - -<p><?= t('The task #%d have been opened.', $task['id']) ?></p> - -<?= Helper\template('notification_footer', array('task' => $task, 'application_url' => $application_url)) ?>
\ No newline at end of file diff --git a/app/Templates/project_activity.php b/app/Templates/project_activity.php deleted file mode 100644 index d07ba86a..00000000 --- a/app/Templates/project_activity.php +++ /dev/null @@ -1,18 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('%s\'s activity', $project['name']) ?></h2> - <ul> - <li><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li> - <li><?= Helper\a(t('Search'), 'project', 'search', array('project_id' => $project['id'])) ?></li> - <li><?= Helper\a(t('Completed tasks'), 'project', 'tasks', array('project_id' => $project['id'])) ?></li> - <li><?= Helper\a(t('List of projects'), 'project', 'index') ?></li> - </ul> - </div> - <section> - <?php if ($project['is_public']): ?> - <p class="pull-right"><i class="fa fa-rss-square"></i> <?= Helper\a(t('RSS feed'), 'project', 'feed', array('token' => $project['token'])) ?></p> - <?php endif ?> - - <?= Helper\template('project_events', array('events' => $events)) ?> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/project_duplicate.php b/app/Templates/project_duplicate.php deleted file mode 100644 index a926dcd1..00000000 --- a/app/Templates/project_duplicate.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Clone this project') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to duplicate this project: "%s"?', $project['name']) ?> - </p> - - <div class="form-actions"> - <?= Helper\a(t('Yes'), 'project', 'duplicate', array('project_id' => $project['id'], 'duplicate' => 'yes'), true, 'btn btn-red') ?> - <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'show', array('project_id' => $project['id'])) ?> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/project_edit.php b/app/Templates/project_edit.php deleted file mode 100644 index 8eb2110d..00000000 --- a/app/Templates/project_edit.php +++ /dev/null @@ -1,15 +0,0 @@ -<div class="page-header"> - <h2><?= t('Edit project') ?></h2> -</div> -<form method="post" action="<?= Helper\u('project', 'update', array('project_id' => $values['id'])) ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('id', $values) ?> - - <?= Helper\form_label(t('Name'), 'name') ?> - <?= Helper\form_text('name', $values, $errors, array('required')) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/project_export.php b/app/Templates/project_export.php deleted file mode 100644 index 02eb389f..00000000 --- a/app/Templates/project_export.php +++ /dev/null @@ -1,24 +0,0 @@ -<div class="page-header"> - <h2> - <?= t('Tasks exportation for "%s"', $project['name']) ?> - </h2> -</div> - -<form method="get" action="?" autocomplete="off"> - - <?= Helper\form_hidden('controller', $values) ?> - <?= Helper\form_hidden('action', $values) ?> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Start Date'), 'from') ?> - <?= Helper\form_text('from', $values, $errors, array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?><br/> - - <?= Helper\form_label(t('End Date'), 'to') ?> - <?= Helper\form_text('to', $values, $errors, array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?> - - <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/project_index.php b/app/Templates/project_index.php deleted file mode 100644 index b575e958..00000000 --- a/app/Templates/project_index.php +++ /dev/null @@ -1,49 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Projects') ?><span id="page-counter"> (<?= $nb_projects ?>)</span></h2> - <ul> - <?php if (Helper\is_admin()): ?> - <li><?= Helper\a(t('New project'), 'project', 'create') ?></li> - <?php endif ?> - <li><?= Helper\a(t('New private project'), 'project', 'create', array('private' => 1)) ?></li> - </ul> - </div> - <section> - <?php if (empty($active_projects) && empty($inactive_projects)): ?> - <p class="alert"><?= t('No project') ?></p> - <?php else: ?> - - <?php if (! empty($active_projects)): ?> - <h3><?= t('Active projects') ?></h3> - <ul class="project-listing"> - <?php foreach ($active_projects as $project): ?> - <li> - <?php if ($project['is_public']): ?> - <i class="fa fa-share-alt fa-fw"></i> - <?php endif ?> - <?php if ($project['is_private']): ?> - <i class="fa fa-lock fa-fw"></i> - <?php endif ?> - <?= Helper\a(Helper\escape($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?> - </li> - <?php endforeach ?> - </ul> - <?php endif ?> - - <?php if (! empty($inactive_projects)): ?> - <h3><?= t('Inactive projects') ?></h3> - <ul class="project-listing"> - <?php foreach ($inactive_projects as $project): ?> - <li> - <?php if ($project['is_private']): ?> - <i class="fa fa-lock"></i> - <?php endif ?> - <?= Helper\a(Helper\escape($project['name']), 'project', 'show', array('project_id' => $project['id'])) ?> - </li> - <?php endforeach ?> - </ul> - <?php endif ?> - - <?php endif ?> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/project_layout.php b/app/Templates/project_layout.php deleted file mode 100644 index d69bbd53..00000000 --- a/app/Templates/project_layout.php +++ /dev/null @@ -1,17 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Project "%s"', $project['name']) ?> (#<?= $project['id'] ?>)</h2> - <ul> - <li><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li> - <li><?= Helper\a(t('All projects'), 'project', 'index') ?></li> - </ul> - </div> - <section class="project-show" id="project-section"> - - <?= Helper\template('project_sidebar', array('project' => $project)) ?> - - <div class="project-show-main"> - <?= $project_content_for_layout ?> - </div> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/project_new.php b/app/Templates/project_new.php deleted file mode 100644 index e1ea5af7..00000000 --- a/app/Templates/project_new.php +++ /dev/null @@ -1,22 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= empty($values['is_private']) ? t('New project') : t('New private project') ?></h2> - <ul> - <li><?= Helper\a(t('All projects'), 'project', 'index') ?></li> - </ul> - </div> - <section> - <form method="post" action="<?= Helper\u('project', 'save') ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('is_private', $values) ?> - <?= Helper\form_label(t('Name'), 'name') ?> - <?= Helper\form_text('name', $values, $errors, array('autofocus', 'required')) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> <?= Helper\a(t('cancel'), 'project', 'index') ?> - </div> - </form> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/project_search.php b/app/Templates/project_search.php deleted file mode 100644 index 7d5d8795..00000000 --- a/app/Templates/project_search.php +++ /dev/null @@ -1,37 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2> - <?= t('Search in the project "%s"', $project['name']) ?> - <?php if (! empty($nb_tasks)): ?> - <span id="page-counter"> (<?= $nb_tasks ?>)</span> - <?php endif ?> - </h2> - <ul> - <li><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li> - <li><?= Helper\a(t('Completed tasks'), 'project', 'tasks', array('project_id' => $project['id'])) ?></li> - <li><?= Helper\a(t('Activity'), 'project', 'activity', array('project_id' => $project['id'])) ?></li> - <li><?= Helper\a(t('List of projects'), 'project', 'index') ?></li> - </ul> - </div> - <section> - <form method="get" action="?" autocomplete="off"> - <?= Helper\form_hidden('controller', $values) ?> - <?= Helper\form_hidden('action', $values) ?> - <?= Helper\form_hidden('project_id', $values) ?> - <?= Helper\form_text('search', $values, array(), array('autofocus', 'required', 'placeholder="'.t('Search').'"')) ?> - <input type="submit" value="<?= t('Search') ?>" class="btn btn-blue"/> - </form> - - <?php if (empty($tasks) && ! empty($values['search'])): ?> - <p class="alert"><?= t('Nothing found.') ?></p> - <?php elseif (! empty($tasks)): ?> - <?= Helper\template('task_table', array( - 'tasks' => $tasks, - 'categories' => $categories, - 'columns' => $columns, - 'pagination' => $pagination, - )) ?> - <?php endif ?> - - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/project_share.php b/app/Templates/project_share.php deleted file mode 100644 index 8edcbbc0..00000000 --- a/app/Templates/project_share.php +++ /dev/null @@ -1,19 +0,0 @@ -<div class="page-header"> - <h2><?= t('Public access') ?></h2> -</div> - -<?php if ($project['is_public']): ?> - - <div class="listing"> - <ul class="no-bullet"> - <li><strong><i class="fa fa-share-alt"></i> <?= Helper\a(t('Public link'), 'board', 'readonly', array('token' => $project['token'])) ?></strong></li> - <li><strong><i class="fa fa-rss-square"></i> <?= Helper\a(t('RSS feed'), 'project', 'feed', array('token' => $project['token'])) ?></strong></li> - </ul> - <input type="text" readonly="readonly" value="<?= Helper\get_current_base_url().Helper\u('board', 'readonly', array('token' => $project['token'])) ?>"/> - </div> - - <?= Helper\a(t('Disable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> - -<?php else: ?> - <?= Helper\a(t('Enable public access'), 'project', 'share', array('project_id' => $project['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> -<?php endif ?> diff --git a/app/Templates/project_show.php b/app/Templates/project_show.php deleted file mode 100644 index facdc60a..00000000 --- a/app/Templates/project_show.php +++ /dev/null @@ -1,55 +0,0 @@ -<div class="page-header"> - <h2><?= t('Summary') ?></h2> -</div> -<ul class="listing"> - <li><strong><?= $project['is_active'] ? t('Active') : t('Inactive') ?></strong></li> - - <?php if ($project['is_private']): ?> - <li><i class="fa fa-lock"></i> <?= t('This project is private') ?></li> - <?php endif ?> - - <?php if ($project['is_public']): ?> - <li><i class="fa fa-share-alt"></i> <?= Helper\a(t('Public link'), 'board', 'readonly', array('token' => $project['token'])) ?></li> - <li><i class="fa fa-rss-square"></i> <?= Helper\a(t('RSS feed'), 'project', 'feed', array('token' => $project['token'])) ?></li> - <?php else: ?> - <li><?= t('Public access disabled') ?></li> - <?php endif ?> - - <?php if ($project['last_modified']): ?> - <li><?= dt('Last modified on %B %e, %Y at %k:%M %p', $project['last_modified']) ?></li> - <?php endif ?> - - <?php if ($stats['nb_tasks'] > 0): ?> - - <?php if ($stats['nb_active_tasks'] > 0): ?> - <li><?= Helper\a(t('%d tasks on the board', $stats['nb_active_tasks']), 'board', 'show', array('project_id' => $project['id'])) ?></li> - <?php endif ?> - - <?php if ($stats['nb_inactive_tasks'] > 0): ?> - <li><?= Helper\a(t('%d closed tasks', $stats['nb_inactive_tasks']), 'project', 'tasks', array('project_id' => $project['id'])) ?></li> - <?php endif ?> - - <li><?= t('%d tasks in total', $stats['nb_tasks']) ?></li> - - <?php else: ?> - <li><?= t('No task for this project') ?></li> - <?php endif ?> -</ul> - -<div class="page-header"> - <h2><?= t('Board') ?></h2> -</div> -<table class="table-stripped"> - <tr> - <th width="50%"><?= t('Column') ?></th> - <th><?= t('Task limit') ?></th> - <th><?= t('Active tasks') ?></th> - </tr> - <?php foreach ($stats['columns'] as $column): ?> - <tr> - <td><?= Helper\escape($column['title']) ?></td> - <td><?= $column['task_limit'] ?: '∞' ?></td> - <td><?= $column['nb_active_tasks'] ?></td> - </tr> - <?php endforeach ?> -</table> diff --git a/app/Templates/project_sidebar.php b/app/Templates/project_sidebar.php deleted file mode 100644 index 7bad1f0e..00000000 --- a/app/Templates/project_sidebar.php +++ /dev/null @@ -1,49 +0,0 @@ -<div class="project-show-sidebar"> - <h2><?= t('Actions') ?></h2> - <div class="project-show-actions"> - <ul> - <li> - <a href="?controller=project&action=show&project_id=<?= $project['id'] ?>"><?= t('Summary') ?></a> - </li> - - <?php if (Helper\is_admin() || $project['is_private']): ?> - <li> - <a href="?controller=project&action=export&project_id=<?= $project['id'] ?>"><?= t('Tasks Export') ?></a> - </li> - <li> - <a href="?controller=project&action=share&project_id=<?= $project['id'] ?>"><?= t('Public access') ?></a> - </li> - <li> - <a href="?controller=project&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit project') ?></a> - </li> - <li> - <a href="?controller=board&action=edit&project_id=<?= $project['id'] ?>"><?= t('Edit board') ?></a> - </li> - <li> - <a href="?controller=category&action=index&project_id=<?= $project['id'] ?>"><?= t('Category management') ?></a> - </li> - <?php if (Helper\is_admin()): ?> - <li> - <a href="?controller=project&action=users&project_id=<?= $project['id'] ?>"><?= t('User management') ?></a> - </li> - <?php endif ?> - <li> - <a href="?controller=action&action=index&project_id=<?= $project['id'] ?>"><?= t('Automatic actions') ?></a> - </li> - <li> - <a href="?controller=project&action=duplicate&project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Duplicate') ?></a> - </li> - <li> - <?php if ($project['is_active']): ?> - <a href="?controller=project&action=disable&project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Disable') ?></a> - <?php else: ?> - <a href="?controller=project&action=enable&project_id=<?= $project['id'].Helper\param_csrf() ?>"><?= t('Enable') ?></a> - <?php endif ?> - </li> - <li> - <a href="?controller=project&action=remove&project_id=<?= $project['id'] ?>"><?= t('Remove') ?></a> - </li> - <?php endif ?> - </ul> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/project_tasks.php b/app/Templates/project_tasks.php deleted file mode 100644 index 7b6f2d9c..00000000 --- a/app/Templates/project_tasks.php +++ /dev/null @@ -1,23 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Completed tasks for "%s"', $project['name']) ?><span id="page-counter"> (<?= $nb_tasks ?>)</span></h2> - <ul> - <li><a href="?controller=board&action=show&project_id=<?= $project['id'] ?>"><?= t('Back to the board') ?></a></li> - <li><a href="?controller=project&action=search&project_id=<?= $project['id'] ?>"><?= t('Search') ?></a></li> - <li><a href="?controller=project&action=activity&project_id=<?= $project['id'] ?>"><?= t('Activity') ?></a></li> - <li><a href="?controller=project&action=index"><?= t('List of projects') ?></a></li> - </ul> - </div> - <section> - <?php if (empty($tasks)): ?> - <p class="alert"><?= t('No task') ?></p> - <?php else: ?> - <?= Helper\template('task_table', array( - 'tasks' => $tasks, - 'categories' => $categories, - 'columns' => $columns, - 'pagination' => $pagination, - )) ?> - <?php endif ?> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/project_users.php b/app/Templates/project_users.php deleted file mode 100644 index 35079df6..00000000 --- a/app/Templates/project_users.php +++ /dev/null @@ -1,57 +0,0 @@ -<div class="page-header"> - <h2><?= t('List of authorized users') ?></h2> -</div> - -<?php if ($project['is_everybody_allowed']): ?> - <div class="alert alert-info"><?= t('Everybody have access to this project.') ?></div> -<?php else: ?> - - <?php if (empty($users['allowed'])): ?> - <div class="alert alert-error"><?= t('Nobody have access to this project.') ?></div> - <?php else: ?> - <div class="listing"> - <p><?= t('Only those users have access to this project:') ?></p> - <ul> - <?php foreach ($users['allowed'] as $user_id => $username): ?> - <li> - <strong><?= Helper\escape($username) ?></strong> - <?php if ($project['is_private'] == 0): ?> - (<?= Helper\a(t('revoke'), 'project', 'revoke', array('project_id' => $project['id'], 'user_id' => $user_id), true) ?>) - <?php endif ?> - </li> - <?php endforeach ?> - </ul> - <p><?= t('Don\'t forget that administrators have access to everything.') ?></p> - </div> - <?php endif ?> - - <?php if ($project['is_private'] == 0 && ! empty($users['not_allowed'])): ?> - <form method="post" action="<?= Helper\u('project', 'allow', array('project_id' => $project['id'])) ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('project_id', array('project_id' => $project['id'])) ?> - - <?= Helper\form_label(t('User'), 'user_id') ?> - <?= Helper\form_select('user_id', $users['not_allowed']) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Allow this user') ?>" class="btn btn-blue"/> - </div> - </form> - <?php endif ?> - -<?php endif ?> - -<?php if ($project['is_private'] == 0): ?> -<form method="post" action="<?= Helper\u('project', 'allowEverybody', array('project_id' => $project['id'])) ?>"> - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('id', array('id' => $project['id'])) ?> - <?= Helper\form_checkbox('is_everybody_allowed', t('Allow everybody to access to this project'), 1, $project['is_everybody_allowed']) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - </div> -</form> -<?php endif ?> diff --git a/app/Templates/subtask_create.php b/app/Templates/subtask_create.php deleted file mode 100644 index c8ee556b..00000000 --- a/app/Templates/subtask_create.php +++ /dev/null @@ -1,27 +0,0 @@ -<div class="page-header"> - <h2><?= t('Add a sub-task') ?></h2> -</div> - -<form method="post" action="?controller=subtask&action=save&task_id=<?= $task['id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('task_id', $values) ?> - - <?= Helper\form_label(t('Title'), 'title') ?> - <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/> - - <?= Helper\form_label(t('Assignee'), 'user_id') ?> - <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?> - <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> - - <?= Helper\form_checkbox('another_subtask', t('Create another sub-task'), 1, isset($values['another_subtask']) && $values['another_subtask'] == 1) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</form> diff --git a/app/Templates/subtask_edit.php b/app/Templates/subtask_edit.php deleted file mode 100644 index 91690d0a..00000000 --- a/app/Templates/subtask_edit.php +++ /dev/null @@ -1,32 +0,0 @@ -<div class="page-header"> - <h2><?= t('Edit a sub-task') ?></h2> -</div> - -<form method="post" action="?controller=subtask&action=update&task_id=<?= $task['id'] ?>&subtask_id=<?= $subtask['id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_hidden('task_id', $values) ?> - - <?= Helper\form_label(t('Title'), 'title') ?> - <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/> - - <?= Helper\form_label(t('Status'), 'status') ?> - <?= Helper\form_select('status', $status_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Assignee'), 'user_id') ?> - <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?> - <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> - - <?= Helper\form_label(t('Time spent'), 'time_spent') ?> - <?= Helper\form_numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</form> diff --git a/app/Templates/subtask_remove.php b/app/Templates/subtask_remove.php deleted file mode 100644 index 12c99cf1..00000000 --- a/app/Templates/subtask_remove.php +++ /dev/null @@ -1,16 +0,0 @@ -<div class="page-header"> - <h2><?= t('Remove a sub-task') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to remove this sub-task?') ?> - </p> - - <p><strong><?= Helper\escape($subtask['title']) ?></strong></p> - - <div class="form-actions"> - <a href="?controller=subtask&action=remove&task_id=<?= $task['id'] ?>&subtask_id=<?= $subtask['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>#subtasks"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/subtask_show.php b/app/Templates/subtask_show.php deleted file mode 100644 index f1b0466f..00000000 --- a/app/Templates/subtask_show.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php if (! empty($subtasks)): ?> -<div id="subtasks" class="task-show-section"> - - <div class="page-header"> - <h2><?= t('Sub-Tasks') ?></h2> - </div> - - <table class="subtasks-table"> - <tr> - <th width="40%"><?= t('Title') ?></th> - <th><?= t('Status') ?></th> - <th><?= t('Assignee') ?></th> - <th><?= t('Time tracking') ?></th> - <?php if (! isset($not_editable)): ?> - <th><?= t('Actions') ?></th> - <?php endif ?> - </tr> - <?php foreach ($subtasks as $subtask): ?> - <tr> - <td><?= Helper\escape($subtask['title']) ?></td> - <td> - <?php if (!isset($not_editable)): ?> - <a href="<?= Helper\u('subtask', 'toggleStatus', array('task_id' => $task['id'], 'subtask_id' => $subtask['id'])) ?>"> - <?php endif ?> - <?php if ($subtask['status'] == 0): ?> - <i class="fa fa-square-o fa-fw"></i><i class="fa"> <?= Helper\escape($subtask['status_name']) ?></i> - <?php elseif ($subtask['status'] == 1): ?> - <i class="fa fa-gears fa-fw"></i><i class="fa"> <?= Helper\escape($subtask['status_name']) ?></i> - <?php else: ?> - <i class="fa fa-check-square-o fa-fw"></i><i class="fa"> <?= Helper\escape($subtask['status_name']) ?></i> - <?php endif ?> - <?php if (! isset($not_editable)): ?> - </a> - <?php endif ?> - </td> - - <td> - <?php if (! empty($subtask['username'])): ?> - <?= Helper\escape($subtask['name'] ?: $subtask['username']) ?> - <?php endif ?> - </td> - <td> - <?php if (! empty($subtask['time_spent'])): ?> - <strong><?= Helper\escape($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?> - <?php endif ?> - - <?php if (! empty($subtask['time_estimated'])): ?> - <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?> - <?php endif ?> - </td> - <?php if (! isset($not_editable)): ?> - <td> - <?= Helper\a(t('Edit'), 'subtask', 'edit', array('task_id' => $task['id'], 'subtask_id' => $subtask['id'])) ?> - <?= t('or') ?> - <?= Helper\a(t('Remove'), 'subtask', 'confirm', array('task_id' => $task['id'], 'subtask_id' => $subtask['id'])) ?> - </td> - <?php endif ?> - </tr> - <?php endforeach ?> - </table> - - <?php if (! isset($not_editable)): ?> - <form method="post" action="<?= Helper\u('subtask', 'save', array('task_id' => $task['id'])) ?>" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('task_id', array('task_id' => $task['id'])) ?> - <?= Helper\form_text('title', array(), array(), array('required', 'placeholder="'.t('Type here to create a new sub-task').'"')) ?> - <input type="submit" value="<?= t('Add') ?>" class="btn btn-blue"/> - </form> - <?php endif ?> - -</div> -<?php endif ?> diff --git a/app/Templates/task_close.php b/app/Templates/task_close.php deleted file mode 100644 index 2abfd032..00000000 --- a/app/Templates/task_close.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Close a task') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to close this task: "%s"?', Helper\escape($task['title'])) ?> - </p> - - <div class="form-actions"> - <a href="?controller=task&action=close&confirmation=yes&task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/task_duplicate.php b/app/Templates/task_duplicate.php deleted file mode 100644 index ef903f1d..00000000 --- a/app/Templates/task_duplicate.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Duplicate a task') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to duplicate this task?') ?> - </p> - - <div class="form-actions"> - <a href="?controller=task&action=duplicate&confirmation=yes&task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/task_duplicate_project.php b/app/Templates/task_duplicate_project.php deleted file mode 100644 index 86d2114a..00000000 --- a/app/Templates/task_duplicate_project.php +++ /dev/null @@ -1,24 +0,0 @@ -<div class="page-header"> - <h2><?= t('Duplicate the task to another project') ?></h2> -</div> - -<?php if (empty($projects_list)): ?> - <p class="alert"><?= t('No project') ?></p> -<?php else: ?> - - <form method="post" action="?controller=task&action=copy&task_id=<?= $task['id'] ?>&project_id=<?= $task['project_id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_label(t('Project'), 'project_id') ?> - <?= Helper\form_select('project_id', $projects_list, $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> - </form> - -<?php endif ?>
\ No newline at end of file diff --git a/app/Templates/task_edit.php b/app/Templates/task_edit.php deleted file mode 100644 index 73e00a31..00000000 --- a/app/Templates/task_edit.php +++ /dev/null @@ -1,51 +0,0 @@ -<div class="page-header"> - <h2><?= t('Edit a task') ?></h2> -</div> -<section id="task-section"> -<form method="post" action="?controller=task&action=update&task_id=<?= $task['id'] ?>&ajax=<?= $ajax ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <div class="form-column"> - - <?= Helper\form_label(t('Title'), 'title') ?> - <?= Helper\form_text('title', $values, $errors, array('required')) ?><br/> - - <?= Helper\form_label(t('Description'), 'description') ?> - <?= Helper\form_textarea('description', $values, $errors) ?><br/> - <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> - - </div> - - <div class="form-column"> - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Assignee'), 'owner_id') ?> - <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Category'), 'category_id') ?> - <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Color'), 'color_id') ?> - <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Complexity'), 'score') ?> - <?= Helper\form_number('score', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Due Date'), 'date_due') ?> - <?= Helper\form_text('date_due', $values, $errors, array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?><br/> - <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> - </div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <?php if ($ajax): ?> - <a href="?controller=board&action=show&project_id=<?= $task['project_id'] ?>"><?= t('cancel') ?></a> - <?php else: ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - <?php endif ?> - </div> -</form> -</section> diff --git a/app/Templates/task_edit_description.php b/app/Templates/task_edit_description.php deleted file mode 100644 index 2d2a4d0b..00000000 --- a/app/Templates/task_edit_description.php +++ /dev/null @@ -1,22 +0,0 @@ -<div class="page-header"> - <h2><?= t('Edit the description') ?></h2> -</div> - -<form method="post" action="?controller=task&action=description&task_id=<?= $task['id'] ?>&ajax=<?= $ajax ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_textarea('description', $values, $errors, array('autofocus', 'required', 'placeholder="'.t('Leave a description').'"'), 'description-textarea') ?><br/> - <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <?php if ($ajax): ?> - <a href="?controller=board&action=show&project_id=<?= $task['project_id'] ?>"><?= t('cancel') ?></a> - <?php else: ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - <?php endif ?> - </div> -</form> diff --git a/app/Templates/task_layout.php b/app/Templates/task_layout.php deleted file mode 100644 index ca0a413f..00000000 --- a/app/Templates/task_layout.php +++ /dev/null @@ -1,16 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= Helper\escape($task['project_name']) ?> > <?= t('Task #%d', $task['id']) ?></h2> - <ul> - <li><a href="?controller=board&action=show&project_id=<?= $task['project_id'] ?>"><?= t('Back to the board') ?></a></li> - </ul> - </div> - <section class="task-show" id="task-section"> - - <?= Helper\template('task_sidebar', array('task' => $task, 'hide_remove_menu' => isset($hide_remove_menu))) ?> - - <div class="task-show-main"> - <?= $task_content_for_layout ?> - </div> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/task_move_project.php b/app/Templates/task_move_project.php deleted file mode 100644 index 3bc3bcb8..00000000 --- a/app/Templates/task_move_project.php +++ /dev/null @@ -1,24 +0,0 @@ -<div class="page-header"> - <h2><?= t('Move the task to another project') ?></h2> -</div> - -<?php if (empty($projects_list)): ?> - <p class="alert"><?= t('No project') ?></p> -<?php else: ?> - - <form method="post" action="?controller=task&action=move&task_id=<?= $task['id'] ?>&project_id=<?= $task['project_id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_label(t('Project'), 'project_id') ?> - <?= Helper\form_select('project_id', $projects_list, $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> - </form> - -<?php endif ?>
\ No newline at end of file diff --git a/app/Templates/task_new.php b/app/Templates/task_new.php deleted file mode 100644 index 51142165..00000000 --- a/app/Templates/task_new.php +++ /dev/null @@ -1,55 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('New task') ?></h2> - </div> - <section id="task-section"> - <form method="post" action="?controller=task&action=save" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <div class="form-column"> - <?= Helper\form_label(t('Title'), 'title') ?> - <?= Helper\form_text('title', $values, $errors, array('autofocus', 'required')) ?><br/> - - <?= Helper\form_label(t('Description'), 'description') ?> - <?= Helper\form_textarea('description', $values, $errors) ?><br/> - <div class="form-help"><a href="http://kanboard.net/documentation/syntax-guide" target="_blank" rel="noreferrer"><?= t('Write your text in Markdown') ?></a></div> - - <?php if (! isset($duplicate)): ?> - <?= Helper\form_checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> - <?php endif ?> - </div> - - <div class="form-column"> - <?= Helper\form_hidden('project_id', $values) ?> - - <?= Helper\form_label(t('Assignee'), 'owner_id') ?> - <?= Helper\form_select('owner_id', $users_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Category'), 'category_id') ?> - <?= Helper\form_select('category_id', $categories_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Column'), 'column_id') ?> - <?= Helper\form_select('column_id', $columns_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Color'), 'color_id') ?> - <?= Helper\form_select('color_id', $colors_list, $values, $errors) ?><br/> - - <?= Helper\form_label(t('Complexity'), 'score') ?> - <?= Helper\form_number('score', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Original estimate'), 'time_estimated') ?> - <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/> - - <?= Helper\form_label(t('Due Date'), 'date_due') ?> - <?= Helper\form_text('date_due', $values, $errors, array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?><br/> - <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> - </div> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> <a href="?controller=board&action=show&project_id=<?= $values['project_id'] ?>"><?= t('cancel') ?></a> - </div> - </form> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/task_open.php b/app/Templates/task_open.php deleted file mode 100644 index d28970e3..00000000 --- a/app/Templates/task_open.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Open a task') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to open this task: "%s"?', Helper\escape($task['title'])) ?> - </p> - - <div class="form-actions"> - <a href="?controller=task&action=open&confirmation=yes&task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/task_public.php b/app/Templates/task_public.php deleted file mode 100644 index 13fef1ed..00000000 --- a/app/Templates/task_public.php +++ /dev/null @@ -1,27 +0,0 @@ -<section id="main" class="public-task"> - - <?= Helper\template('task_details', array('task' => $task, 'project' => $project)) ?> - - <p class="pull-right"><?= Helper\a(t('Back to the board'), 'board', 'readonly', array('token' => $project['token'])) ?></p> - - <?= Helper\template('task_show_description', array( - 'task' => $task, - 'project' => $project, - 'is_public' => true - )) ?> - - <?= Helper\template('subtask_show', array( - 'task' => $task, - 'subtasks' => $subtasks, - 'not_editable' => true - )) ?> - - <?= Helper\template('task_comments', array( - 'task' => $task, - 'comments' => $comments, - 'project' => $project, - 'not_editable' => true, - 'is_public' => true, - )) ?> - -</section>
\ No newline at end of file diff --git a/app/Templates/task_remove.php b/app/Templates/task_remove.php deleted file mode 100644 index 496ac2d8..00000000 --- a/app/Templates/task_remove.php +++ /dev/null @@ -1,14 +0,0 @@ -<div class="page-header"> - <h2><?= t('Remove a task') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"> - <?= t('Do you really want to remove this task: "%s"?', Helper\escape($task['title'])) ?> - </p> - - <div class="form-actions"> - <a href="?controller=task&action=remove&confirmation=yes&task_id=<?= $task['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/task_show.php b/app/Templates/task_show.php deleted file mode 100644 index ec5d5da4..00000000 --- a/app/Templates/task_show.php +++ /dev/null @@ -1,7 +0,0 @@ -<?= Helper\template('task_details', array('task' => $task, 'project' => $project)) ?> -<?= Helper\template('task_time', array('values' => $values, 'date_format' => $date_format, 'date_formats' => $date_formats)) ?> -<?= Helper\template('task_show_description', array('task' => $task)) ?> -<?= Helper\template('subtask_show', array('task' => $task, 'subtasks' => $subtasks)) ?> -<?= Helper\template('task_timesheet', array('timesheet' => $timesheet)) ?> -<?= Helper\template('file_show', array('task' => $task, 'files' => $files)) ?> -<?= Helper\template('task_comments', array('task' => $task, 'comments' => $comments, 'project' => $project)) ?>
\ No newline at end of file diff --git a/app/Templates/task_sidebar.php b/app/Templates/task_sidebar.php deleted file mode 100644 index 4cffd5fa..00000000 --- a/app/Templates/task_sidebar.php +++ /dev/null @@ -1,26 +0,0 @@ -<div class="task-show-sidebar"> - <h2><?= t('Actions') ?></h2> - <div class="task-show-actions"> - <ul> - <li><a href="?controller=task&action=show&task_id=<?= $task['id'] ?>"><?= t('Summary') ?></a></li> - <li><a href="?controller=task&action=edit&task_id=<?= $task['id'] ?>"><?= t('Edit the task') ?></a></li> - <li><a href="?controller=task&action=description&task_id=<?= $task['id'] ?>"><?= t('Edit the description') ?></a></li> - <li><a href="?controller=subtask&action=create&task_id=<?= $task['id'] ?>"><?= t('Add a sub-task') ?></a></li> - <li><a href="?controller=comment&action=create&task_id=<?= $task['id'] ?>"><?= t('Add a comment') ?></a></li> - <li><a href="?controller=file&action=create&task_id=<?= $task['id'] ?>"><?= t('Attach a document') ?></a></li> - <li><a href="?controller=task&action=duplicate&project_id=<?= $task['project_id'] ?>&task_id=<?= $task['id'] ?>"><?= t('Duplicate') ?></a></li> - <li><a href="?controller=task&action=copy&project_id=<?= $task['project_id'] ?>&task_id=<?= $task['id'] ?>"><?= t('Duplicate to another project') ?></a></li> - <li><a href="?controller=task&action=move&project_id=<?= $task['project_id'] ?>&task_id=<?= $task['id'] ?>"><?= t('Move to another project') ?></a></li> - <li> - <?php if ($task['is_active'] == 1): ?> - <a href="?controller=task&action=close&task_id=<?= $task['id'] ?>"><?= t('Close this task') ?></a> - <?php else: ?> - <a href="?controller=task&action=open&task_id=<?= $task['id'] ?>"><?= t('Open this task') ?></a> - <?php endif ?> - </li> - <?php if (! $hide_remove_menu): ?> - <li><a href="?controller=task&action=remove&task_id=<?= $task['id'] ?>"><?= t('Remove') ?></a></li> - <?php endif ?> - </ul> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/task_table.php b/app/Templates/task_table.php deleted file mode 100644 index fa04fa55..00000000 --- a/app/Templates/task_table.php +++ /dev/null @@ -1,56 +0,0 @@ -<table> - <tr> - <th><?= Helper\order(t('Id'), 'tasks.id', $pagination) ?></th> - <th><?= Helper\order(t('Column'), 'tasks.column_id', $pagination) ?></th> - <th><?= Helper\order(t('Category'), 'tasks.category_id', $pagination) ?></th> - <th><?= Helper\order(t('Title'), 'tasks.title', $pagination) ?></th> - <th><?= Helper\order(t('Assignee'), 'users.username', $pagination) ?></th> - <th><?= Helper\order(t('Due date'), 'tasks.date_due', $pagination) ?></th> - <th><?= Helper\order(t('Date created'), 'tasks.date_creation', $pagination) ?></th> - <th><?= Helper\order(t('Date completed'), 'tasks.date_completed', $pagination) ?></th> - <th><?= Helper\order(t('Status'), 'tasks.is_active', $pagination) ?></th> - </tr> - <?php foreach ($tasks as $task): ?> - <tr> - <td class="task-table task-<?= $task['color_id'] ?>"> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>">#<?= Helper\escape($task['id']) ?></a> - </td> - <td> - <?= Helper\in_list($task['column_id'], $columns) ?> - </td> - <td> - <?= Helper\in_list($task['category_id'], $categories, '') ?> - </td> - <td> - <a href="?controller=task&action=show&task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a> - </td> - <td> - <?php if ($task['assignee_username']): ?> - <?= Helper\escape($task['assignee_name'] ?: $task['assignee_username']) ?> - <?php else: ?> - <?= t('Unassigned') ?> - <?php endif ?> - </td> - <td> - <?= dt('%B %e, %Y', $task['date_due']) ?> - </td> - <td> - <?= dt('%B %e, %Y at %k:%M %p', $task['date_creation']) ?> - </td> - <td> - <?php if ($task['date_completed']): ?> - <?= dt('%B %e, %Y at %k:%M %p', $task['date_completed']) ?> - <?php endif ?> - </td> - <td> - <?php if ($task['is_active'] == \Model\Task::STATUS_OPEN): ?> - <?= t('Open') ?> - <?php else: ?> - <?= t('Closed') ?> - <?php endif ?> - </td> - </tr> - <?php endforeach ?> -</table> - -<?= Helper\paginate($pagination) ?> diff --git a/app/Templates/task_time.php b/app/Templates/task_time.php deleted file mode 100644 index 11a76303..00000000 --- a/app/Templates/task_time.php +++ /dev/null @@ -1,15 +0,0 @@ -<form method="post" action="<?= Helper\u('task', 'time', array('task_id' => $values['id'])) ?>" class="form-inline task-time-form" autocomplete="off"> - <?= Helper\form_csrf() ?> - <?= Helper\form_hidden('id', $values) ?> - - <?= Helper\form_label(t('Start date'), 'date_started') ?> - <?= Helper\form_text('date_started', $values, array(), array('placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?> - - <?= Helper\form_label(t('Time estimated'), 'time_estimated') ?> - <?= Helper\form_numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?> - - <?= Helper\form_label(t('Time spent'), 'time_spent') ?> - <?= Helper\form_numeric('time_spent', $values, array(), array('placeholder="'.t('hours').'"')) ?> - - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> -</form>
\ No newline at end of file diff --git a/app/Templates/task_timesheet.php b/app/Templates/task_timesheet.php deleted file mode 100644 index cd093657..00000000 --- a/app/Templates/task_timesheet.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php if ($timesheet['time_estimated'] > 0 || $timesheet['time_spent'] > 0): ?> - -<div class="page-header"> - <h2><?= t('Time tracking') ?></h2> -</div> - -<ul class="listing"> - <li><?= t('Estimate:') ?> <strong><?= Helper\escape($timesheet['time_estimated']) ?></strong> <?= t('hours') ?></li> - <li><?= t('Spent:') ?> <strong><?= Helper\escape($timesheet['time_spent']) ?></strong> <?= t('hours') ?></li> - <li><?= t('Remaining:') ?> <strong><?= Helper\escape($timesheet['time_remaining']) ?></strong> <?= t('hours') ?></li> -</ul> - -<?php endif ?>
\ No newline at end of file diff --git a/app/Templates/user_edit.php b/app/Templates/user_edit.php deleted file mode 100644 index 14063d49..00000000 --- a/app/Templates/user_edit.php +++ /dev/null @@ -1,30 +0,0 @@ -<div class="page-header"> - <h2><?= t('Edit user') ?></h2> -</div> -<form method="post" action="?controller=user&action=edit&user_id=<?= $user['id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_hidden('is_ldap_user', $values) ?> - - <?= Helper\form_label(t('Username'), 'username') ?> - <?= Helper\form_text('username', $values, $errors, array('required', $values['is_ldap_user'] == 1 ? 'readonly' : '')) ?><br/> - - <?= Helper\form_label(t('Name'), 'name') ?> - <?= Helper\form_text('name', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Email'), 'email') ?> - <?= Helper\form_email('email', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Default project'), 'default_project_id') ?> - <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/> - - <?php if (Helper\is_admin()): ?> - <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?><br/> - <?php endif ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <?= t('or') ?> <a href="?controller=user&action=show&user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/user_index.php b/app/Templates/user_index.php deleted file mode 100644 index d4e1bbf9..00000000 --- a/app/Templates/user_index.php +++ /dev/null @@ -1,71 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('Users') ?><span id="page-counter"> (<?= $nb_users ?>)</span></h2> - <?php if (Helper\is_admin()): ?> - <ul> - <li><a href="?controller=user&action=create"><?= t('New user') ?></a></li> - </ul> - <?php endif ?> - </div> - <section> - <?php if (empty($users)): ?> - <p class="alert"><?= t('No user') ?></p> - <?php else: ?> - <table> - <tr> - <th><?= t('Id') ?></th> - <th><?= t('Username') ?></th> - <th><?= t('Name') ?></th> - <th><?= t('Email') ?></th> - <th><?= t('Administrator') ?></th> - <th><?= t('Default project') ?></th> - <th><?= t('Notifications') ?></th> - <th><?= t('External accounts') ?></th> - <th><?= t('Account type') ?></th> - </tr> - <?php foreach ($users as $user): ?> - <tr> - <td> - <a href="?controller=user&action=show&user_id=<?= $user['id'] ?>">#<?= $user['id'] ?></a> - </td> - <td> - <a href="?controller=user&action=show&user_id=<?= $user['id'] ?>"><?= Helper\escape($user['username']) ?></a> - </td> - <td> - <?= Helper\escape($user['name']) ?> - </td> - <td> - <?= Helper\escape($user['email']) ?> - </td> - <td> - <?= $user['is_admin'] ? t('Yes') : t('No') ?> - </td> - <td> - <?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None'); ?> - </td> - <td> - <?php if ($user['notifications_enabled'] == 1): ?> - <?= t('Enabled') ?> - <?php else: ?> - <?= t('Disabled') ?> - <?php endif ?> - </td> - <td> - <ul class="no-bullet"> - <?php if ($user['google_id']): ?> - <li><i class="fa fa-google"></i> <?= t('Google account linked') ?></li> - <?php endif ?> - <?php if ($user['github_id']): ?> - <li><i class="fa fa-github"></i> <?= t('Github account linked') ?></li> - <?php endif ?> - </ul> - </td> - <td> - <?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?> - </td> - </tr> - <?php endforeach ?> - </table> - <?php endif ?> - </section> -</section> diff --git a/app/Templates/user_layout.php b/app/Templates/user_layout.php deleted file mode 100644 index 7462b3f0..00000000 --- a/app/Templates/user_layout.php +++ /dev/null @@ -1,19 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= Helper\escape($user['name'] ?: $user['username']).' (#'.$user['id'].')' ?></h2> - <?php if (Helper\is_admin()): ?> - <ul> - <li><a href="?controller=user&action=index"><?= t('All users') ?></a></li> - <li><a href="?controller=user&action=create"><?= t('New user') ?></a></li> - </ul> - <?php endif ?> - </div> - <section class="user-show" id="user-section"> - - <?= Helper\template('user_sidebar', array('user' => $user)) ?> - - <div class="user-show-main"> - <?= $user_content_for_layout ?> - </div> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/user_login.php b/app/Templates/user_login.php deleted file mode 100644 index cf92cd4d..00000000 --- a/app/Templates/user_login.php +++ /dev/null @@ -1,42 +0,0 @@ -<div class="form-login"> - - <div class="page-header"> - <h1><?= t('Sign in') ?></h1> - </div> - - <?php if (isset($errors['login'])): ?> - <p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p> - <?php endif ?> - - <form method="post" action="?controller=user&action=check&redirect_query=<?= urlencode($redirect_query) ?>"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_label(t('Username'), 'username') ?> - <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/> - - <?= Helper\form_label(t('Password'), 'password') ?> - <?= Helper\form_password('password', $values, $errors, array('required')) ?> - - <?= Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?><br/> - - <ul> - <?php if (GOOGLE_AUTH): ?> - <li> - <a href="?controller=user&action=google"><?= t('Login with my Google Account') ?></a> - </li> - <?php endif ?> - - <?php if (GITHUB_AUTH): ?> - <li> - <a href="?controller=user&action=gitHub"><?= t('Login with my GitHub Account') ?></a> - </li> - <?php endif ?> - </ul> - - <div class="form-actions"> - <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/> - </div> - </form> - -</div>
\ No newline at end of file diff --git a/app/Templates/user_new.php b/app/Templates/user_new.php deleted file mode 100644 index 158813cb..00000000 --- a/app/Templates/user_new.php +++ /dev/null @@ -1,39 +0,0 @@ -<section id="main"> - <div class="page-header"> - <h2><?= t('New user') ?></h2> - <ul> - <li><a href="?controller=user"><?= t('All users') ?></a></li> - </ul> - </div> - <section> - <form method="post" action="?controller=user&action=save" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_label(t('Username'), 'username') ?> - <?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/> - - <?= Helper\form_label(t('Name'), 'name') ?> - <?= Helper\form_text('name', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Email'), 'email') ?> - <?= Helper\form_email('email', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Password'), 'password') ?> - <?= Helper\form_password('password', $values, $errors, array('required')) ?><br/> - - <?= Helper\form_label(t('Confirmation'), 'confirmation') ?> - <?= Helper\form_password('confirmation', $values, $errors, array('required')) ?><br/> - - <?= Helper\form_label(t('Default project'), 'default_project_id') ?> - <?= Helper\form_select('default_project_id', $projects, $values, $errors) ?><br/> - - <?= Helper\form_checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> <a href="?controller=user"><?= t('cancel') ?></a> - </div> - </form> - </section> -</section>
\ No newline at end of file diff --git a/app/Templates/user_notifications.php b/app/Templates/user_notifications.php deleted file mode 100644 index 13dd9809..00000000 --- a/app/Templates/user_notifications.php +++ /dev/null @@ -1,22 +0,0 @@ -<div class="page-header"> - <h2><?= t('Email notifications') ?></h2> -</div> - -<form method="post" action="?controller=user&action=notifications&user_id=<?= $user['id'] ?>" autocomplete="off"> - - <?= Helper\form_csrf() ?> - - <?= Helper\form_checkbox('notifications_enabled', t('Enable email notifications'), '1', $notifications['notifications_enabled'] == 1) ?><br/> - - <p><?= t('I want to receive notifications only for those projects:') ?><br/><br/></p> - - <div class="form-checkbox-group"> - <?php foreach ($projects as $project_id => $project_name): ?> - <?= Helper\form_checkbox('projects['.$project_id.']', $project_name, '1', isset($notifications['project_'.$project_id])) ?> - <?php endforeach ?> - </div> - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> - <?= t('or') ?> <a href="?controller=user&action=show&user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a> - </div> -</form>
\ No newline at end of file diff --git a/app/Templates/user_password.php b/app/Templates/user_password.php deleted file mode 100644 index 5da38595..00000000 --- a/app/Templates/user_password.php +++ /dev/null @@ -1,23 +0,0 @@ -<div class="page-header"> - <h2><?= t('Password modification') ?></h2> -</div> - -<form method="post" action="?controller=user&action=password&user_id=<?= $user['id'] ?>" autocomplete="off"> - - <?= Helper\form_hidden('id', $values) ?> - <?= Helper\form_csrf() ?> - - <?= Helper\form_label(t('Current password for the user "%s"', Helper\get_username()), 'current_password') ?> - <?= Helper\form_password('current_password', $values, $errors) ?><br/> - - <?= Helper\form_label(t('New password for the user "%s"', Helper\get_username($user)), 'password') ?> - <?= Helper\form_password('password', $values, $errors) ?><br/> - - <?= Helper\form_label(t('Confirmation'), 'confirmation') ?> - <?= Helper\form_password('confirmation', $values, $errors) ?><br/> - - <div class="form-actions"> - <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <?= t('or') ?> <a href="?controller=user&action=show&user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a> - </div> - -</form>
\ No newline at end of file diff --git a/app/Templates/user_remove.php b/app/Templates/user_remove.php deleted file mode 100644 index c20ccbba..00000000 --- a/app/Templates/user_remove.php +++ /dev/null @@ -1,12 +0,0 @@ -<div class="page-header"> - <h2><?= t('Remove user') ?></h2> -</div> - -<div class="confirm"> - <p class="alert alert-info"><?= t('Do you really want to remove this user: "%s"?', $user['name'] ?: $user['username']) ?></p> - - <div class="form-actions"> - <a href="?controller=user&action=remove&confirmation=yes&user_id=<?= $user['id'].Helper\param_csrf() ?>" class="btn btn-red"><?= t('Yes') ?></a> - <?= t('or') ?> <a href="?controller=user&action=show&user_id=<?= $user['id'] ?>"><?= t('cancel') ?></a> - </div> -</div>
\ No newline at end of file diff --git a/app/Templates/user_show.php b/app/Templates/user_show.php deleted file mode 100644 index 1c843751..00000000 --- a/app/Templates/user_show.php +++ /dev/null @@ -1,12 +0,0 @@ -<div class="page-header"> - <h2><?= t('Summary') ?></h2> -</div> -<ul class="listing"> - <li><?= t('Username:') ?> <strong><?= Helper\escape($user['username']) ?></strong></li> - <li><?= t('Name:') ?> <strong><?= Helper\escape($user['name']) ?></strong></li> - <li><?= t('Email:') ?> <strong><?= Helper\escape($user['email']) ?></strong></li> - <li><?= t('Default project:') ?> <strong><?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? Helper\escape($projects[$user['default_project_id']]) : t('None'); ?></strong></li> - <li><?= t('Notifications:') ?> <strong><?= $user['notifications_enabled'] == 1 ? t('Enabled') : t('Disabled') ?></strong></li> - <li><?= t('Group:') ?> <strong><?= $user['is_admin'] ? t('Administrator') : t('Regular user') ?></strong></li> - <li><?= t('Account type:') ?> <strong><?= $user['is_ldap_user'] ? t('Remote') : t('Local') ?></strong></li> -</ul> diff --git a/app/Templates/user_sidebar.php b/app/Templates/user_sidebar.php deleted file mode 100644 index 9d8f8b46..00000000 --- a/app/Templates/user_sidebar.php +++ /dev/null @@ -1,42 +0,0 @@ -<div class="project-show-sidebar"> - <h2><?= t('Actions') ?></h2> - <div class="user-show-actions"> - <ul> - <li> - <a href="?controller=user&action=show&user_id=<?= $user['id'] ?>"><?= t('Summary') ?></a> - </li> - - <?php if (Helper\is_admin() || Helper\is_current_user($user['id'])): ?> - <li> - <a href="?controller=user&action=edit&user_id=<?= $user['id'] ?>"><?= t('Edit profile') ?></a> - </li> - - <?php if ($user['is_ldap_user'] == 0): ?> - <li> - <a href="?controller=user&action=password&user_id=<?= $user['id'] ?>"><?= t('Change password') ?></a> - </li> - <?php endif ?> - - <li> - <a href="?controller=user&action=notifications&user_id=<?= $user['id'] ?>"><?= t('Email notifications') ?></a> - </li> - <li> - <a href="?controller=user&action=external&user_id=<?= $user['id'] ?>"><?= t('External accounts') ?></a> - </li> - <li> - <a href="?controller=user&action=last&user_id=<?= $user['id'] ?>"><?= t('Last logins') ?></a> - </li> - <li> - <a href="?controller=user&action=sessions&user_id=<?= $user['id'] ?>"><?= t('Persistent connections') ?></a> - </li> - <?php endif ?> - - <?php if (Helper\is_admin()): ?> - <li> - <a href="?controller=user&action=remove&user_id=<?= $user['id'] ?>"><?= t('Remove') ?></a> - </li> - <?php endif ?> - - </ul> - </div> -</div>
\ No newline at end of file diff --git a/app/check_setup.php b/app/check_setup.php index afb08f6a..065b8e10 100644 --- a/app/check_setup.php +++ b/app/check_setup.php @@ -33,3 +33,8 @@ if (! extension_loaded('mbstring')) { if (! is_writable('data')) { die('The directory "data" must be writeable by your web server user'); } + +// Fix wrong value for arg_separator.output, used by the function http_build_query() +if (ini_get('arg_separator.output') === '&') { + ini_set('arg_separator.output', '&'); +} diff --git a/app/common.php b/app/common.php index 1ace3d81..f4485267 100644 --- a/app/common.php +++ b/app/common.php @@ -1,17 +1,18 @@ <?php -// Common file between cli and web interface +require 'vendor/autoload.php'; -require __DIR__.'/Core/Loader.php'; -require __DIR__.'/helpers.php'; -require __DIR__.'/functions.php'; +// Automatically parse environment configuration (Heroku) +if (getenv('DATABASE_URL')) { -use Core\Loader; -use Core\Registry; + $dbopts = parse_url(getenv('DATABASE_URL')); -// Include password_compat for PHP < 5.5 -if (version_compare(PHP_VERSION, '5.5.0', '<')) { - require __DIR__.'/../vendor/password.php'; + define('DB_DRIVER', $dbopts['scheme']); + define('DB_USERNAME', $dbopts["user"]); + define('DB_PASSWORD', $dbopts["pass"]); + define('DB_HOSTNAME', $dbopts["host"]); + define('DB_PORT', isset($dbopts["port"]) ? $dbopts["port"] : null); + define('DB_NAME', ltrim($dbopts["path"],'/')); } // Include custom config file @@ -21,12 +22,8 @@ if (file_exists('config.php')) { require __DIR__.'/constants.php'; -$loader = new Loader; -$loader->setPath('app'); -$loader->setPath('vendor'); -$loader->execute(); - -$registry = new Registry; -$registry->db = setup_db(); -$registry->event = setup_events(); -$registry->mailer = function() { return setup_mailer(); }; +$container = new Pimple\Container; +$container->register(new ServiceProvider\LoggingProvider); +$container->register(new ServiceProvider\DatabaseProvider); +$container->register(new ServiceProvider\ClassProvider); +$container->register(new ServiceProvider\EventDispatcherProvider); diff --git a/app/constants.php b/app/constants.php index 93075892..0b934569 100644 --- a/app/constants.php +++ b/app/constants.php @@ -1,7 +1,8 @@ <?php -// Custom session save path -defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', ''); +// Enable/disable debug +defined('DEBUG') or define('DEBUG', false); +defined('DEBUG_FILE') or define('DEBUG_FILE', __DIR__.'/../data/debug.log'); // Application version defined('APP_VERSION') or define('APP_VERSION', 'master'); @@ -20,11 +21,13 @@ defined('DB_USERNAME') or define('DB_USERNAME', 'root'); defined('DB_PASSWORD') or define('DB_PASSWORD', ''); defined('DB_HOSTNAME') or define('DB_HOSTNAME', 'localhost'); defined('DB_NAME') or define('DB_NAME', 'kanboard'); +defined('DB_PORT') or define('DB_PORT', null); // LDAP configuration defined('LDAP_AUTH') or define('LDAP_AUTH', false); defined('LDAP_SERVER') or define('LDAP_SERVER', ''); defined('LDAP_PORT') or define('LDAP_PORT', 389); +defined('LDAP_START_TLS') or define('LDAP_START_TLS', false); defined('LDAP_SSL_VERIFY') or define('LDAP_SSL_VERIFY', true); defined('LDAP_BIND_TYPE') or define('LDAP_BIND_TYPE', 'anonymous'); defined('LDAP_USERNAME') or define('LDAP_USERNAME', null); @@ -33,6 +36,8 @@ defined('LDAP_ACCOUNT_BASE') or define('LDAP_ACCOUNT_BASE', ''); defined('LDAP_USER_PATTERN') or define('LDAP_USER_PATTERN', ''); defined('LDAP_ACCOUNT_FULLNAME') or define('LDAP_ACCOUNT_FULLNAME', 'displayname'); defined('LDAP_ACCOUNT_EMAIL') or define('LDAP_ACCOUNT_EMAIL', 'mail'); +defined('LDAP_ACCOUNT_ID') or define('LDAP_ACCOUNT_ID', ''); +defined('LDAP_USERNAME_CASE_SENSITIVE') or define('LDAP_USERNAME_CASE_SENSITIVE', false); // Google authentication defined('GOOGLE_AUTH') or define('GOOGLE_AUTH', false); @@ -51,7 +56,7 @@ defined('REVERSE_PROXY_DEFAULT_ADMIN') or define('REVERSE_PROXY_DEFAULT_ADMIN', defined('REVERSE_PROXY_DEFAULT_DOMAIN') or define('REVERSE_PROXY_DEFAULT_DOMAIN', ''); // Mail configuration -defined('MAIL_FROM') or define('MAIL_FROM', 'notifications@kanboard.net'); +defined('MAIL_FROM') or define('MAIL_FROM', 'notifications@kanboard.local'); defined('MAIL_TRANSPORT') or define('MAIL_TRANSPORT', 'mail'); defined('MAIL_SMTP_HOSTNAME') or define('MAIL_SMTP_HOSTNAME', ''); defined('MAIL_SMTP_PORT') or define('MAIL_SMTP_PORT', 25); @@ -59,6 +64,21 @@ defined('MAIL_SMTP_USERNAME') or define('MAIL_SMTP_USERNAME', ''); defined('MAIL_SMTP_PASSWORD') or define('MAIL_SMTP_PASSWORD', ''); defined('MAIL_SMTP_ENCRYPTION') or define('MAIL_SMTP_ENCRYPTION', null); defined('MAIL_SENDMAIL_COMMAND') or define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'); +defined('POSTMARK_API_TOKEN') or define('POSTMARK_API_TOKEN', ''); +defined('MAILGUN_API_TOKEN') or define('MAILGUN_API_TOKEN', ''); +defined('MAILGUN_DOMAIN') or define('MAILGUN_DOMAIN', ''); // Enable or disable "Strict-Transport-Security" HTTP header defined('ENABLE_HSTS') or define('ENABLE_HSTS', true); + +// Enable or disable "X-Frame-Options: DENY" HTTP header +defined('ENABLE_XFRAME') or define('ENABLE_XFRAME', true); + +// Default files directory +defined('FILES_DIR') or define('FILES_DIR', 'data/files/'); + +// Escape html inside markdown text +defined('MARKDOWN_ESCAPE_HTML') or define('MARKDOWN_ESCAPE_HTML', true); + +// API alternative authentication header, the default is HTTP Basic Authentication defined in RFC2617 +defined('API_AUTHENTICATION_HEADER') or define('API_AUTHENTICATION_HEADER', ''); diff --git a/app/functions.php b/app/functions.php index c39eaf98..e4b38cdd 100644 --- a/app/functions.php +++ b/app/functions.php @@ -1,143 +1,6 @@ <?php -use Core\Event; use Core\Translator; -use PicoDb\Database; - -/** - * Send a debug message to the log files - * - * @param mixed $message Variable or string - */ -function debug($message) -{ - if (! is_string($message)) { - $message = var_export($message, true); - } - - error_log($message.PHP_EOL, 3, 'data/debug.log'); -} - -/** - * Setup events - * - * @return Core\Event - */ -function setup_events() -{ - return new Event; -} - -/** - * Setup the mailer according to the configuration - * - * @return Swift_SmtpTransport - */ -function setup_mailer() -{ - require_once __DIR__.'/../vendor/swiftmailer/swift_required.php'; - - switch (MAIL_TRANSPORT) { - case 'smtp': - $transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT); - $transport->setUsername(MAIL_SMTP_USERNAME); - $transport->setPassword(MAIL_SMTP_PASSWORD); - $transport->setEncryption(MAIL_SMTP_ENCRYPTION); - break; - case 'sendmail': - $transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND); - break; - default: - $transport = Swift_MailTransport::newInstance(); - } - - return $transport; -} - -/** - * Setup the database driver and execute schema migration - * - * @return PicoDb\Database - */ -function setup_db() -{ - switch (DB_DRIVER) { - case 'sqlite': - $db = setup_sqlite(); - break; - - case 'mysql': - $db = setup_mysql(); - break; - - case 'postgres': - $db = setup_postgres(); - break; - - default: - die('Database driver not supported'); - } - - if ($db->schema()->check(Schema\VERSION)) { - return $db; - } - else { - $errors = $db->getLogMessages(); - die('Unable to migrate database schema: <br/><br/><strong>'.(isset($errors[0]) ? $errors[0] : 'Unknown error').'</strong>'); - } -} - -/** - * Setup the Sqlite database driver - * - * @return PicoDb\Database - */ -function setup_sqlite() -{ - require_once __DIR__.'/Schema/Sqlite.php'; - - return new Database(array( - 'driver' => 'sqlite', - 'filename' => DB_FILENAME - )); -} - -/** - * Setup the Mysql database driver - * - * @return PicoDb\Database - */ -function setup_mysql() -{ - require_once __DIR__.'/Schema/Mysql.php'; - - return new Database(array( - 'driver' => 'mysql', - 'hostname' => DB_HOSTNAME, - 'username' => DB_USERNAME, - 'password' => DB_PASSWORD, - 'database' => DB_NAME, - 'charset' => 'utf8', - )); -} - -/** - * Setup the Postgres database driver - * - * @return PicoDb\Database - */ -function setup_postgres() -{ - require_once __DIR__.'/Schema/Postgres.php'; - - return new Database(array( - 'driver' => 'postgres', - 'hostname' => DB_HOSTNAME, - 'username' => DB_USERNAME, - 'password' => DB_PASSWORD, - 'database' => DB_NAME, - )); -} /** * Translate a string @@ -146,8 +9,7 @@ function setup_postgres() */ function t() { - $t = new Translator; - return call_user_func_array(array($t, 'translate'), func_get_args()); + return call_user_func_array(array(Translator::getInstance(), 'translate'), func_get_args()); } /** @@ -157,19 +19,7 @@ function t() */ function e() { - $t = new Translator; - return call_user_func_array(array($t, 'translateNoEscaping'), func_get_args()); -} - -/** - * Translate a currency - * - * @return string - */ -function c($value) -{ - $t = new Translator; - return $t->currency($value); + return call_user_func_array(array(Translator::getInstance(), 'translateNoEscaping'), func_get_args()); } /** @@ -179,8 +29,7 @@ function c($value) */ function n($value) { - $t = new Translator; - return $t->number($value); + return Translator::getInstance()->number($value); } /** @@ -190,8 +39,7 @@ function n($value) */ function dt($format, $timestamp) { - $t = new Translator; - return $t->datetime($format, $timestamp); + return Translator::getInstance()->datetime($format, $timestamp); } /** @@ -200,6 +48,7 @@ function dt($format, $timestamp) * @todo Improve this function * @return mixed */ -function p($value, $t1, $t2) { +function p($value, $t1, $t2) +{ return $value > 1 ? $t2 : $t1; } diff --git a/app/helpers.php b/app/helpers.php deleted file mode 100644 index cd6d630e..00000000 --- a/app/helpers.php +++ /dev/null @@ -1,662 +0,0 @@ -<?php - -namespace Helper; - -/** - * Template helpers - * - */ - -use Core\Security; -use Core\Template; -use Core\Tool; -use Parsedown\Parsedown; - -/** - * Append a CSRF token to a query string - * - * @return string - */ -function param_csrf() -{ - return '&csrf_token='.Security::getCSRFToken(); -} - -/** - * Add a Javascript asset - * - * @param string $filename Filename - * @return string - */ -function js($filename) -{ - return '<script type="text/javascript" src="'.$filename.'?'.filemtime($filename).'"></script>'; -} - -/** - * Add a stylesheet asset - * - * @param string $filename Filename - * @return string - */ -function css($filename) -{ - return '<link rel="stylesheet" href="'.$filename.'?'.filemtime($filename).'" media="screen">'; -} - -/** - * Load a template - * - * @param string $name Template name - * @param array $args Template parameters - * @return string - */ -function template($name, array $args = array()) -{ - $tpl = new Template; - return $tpl->load($name, $args); -} - -/** - * Check if the given user_id is the connected user - * - * @param integer $user_id User id - * @return boolean - */ -function is_current_user($user_id) -{ - return $_SESSION['user']['id'] == $user_id; -} - -/** - * Check if the current user is administrator - * - * @return boolean - */ -function is_admin() -{ - return $_SESSION['user']['is_admin'] == 1; -} - -/** - * Return the username - * - * @param array $user User properties (optional) - * @return string - */ -function get_username(array $user = array()) -{ - return ! empty($user) ? ($user['name'] ?: $user['username']) - : ($_SESSION['user']['name'] ?: $_SESSION['user']['username']); -} - -/** - * Get the current user id - * - * @return integer - */ -function get_user_id() -{ - return $_SESSION['user']['id']; -} - -/** - * Markdown transformation - * - * @param string $text Markdown content - * @param array $link Link parameters for replacement - * @return string - */ -function markdown($text, array $link = array('controller' => 'task', 'action' => 'show', 'params' => array())) -{ - $html = Parsedown::instance() - ->setMarkupEscaped(true) # escapes markup (HTML) - ->text($text); - - // Replace task #123 by a link to the task - $html = preg_replace_callback('!#(\d+)!i', function($matches) use ($link) { - return a( - $matches[0], - $link['controller'], - $link['action'], - $link['params'] + array('task_id' => $matches[1]) - ); - }, $html); - - return $html; -} - -/** - * Get the current URL without the querystring - * - * @return string - */ -function get_current_base_url() -{ - $url = Tool::isHTTPS() ? 'https://' : 'http://'; - $url .= $_SERVER['SERVER_NAME']; - $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; - $url .= dirname($_SERVER['PHP_SELF']) !== '/' ? dirname($_SERVER['PHP_SELF']).'/' : '/'; - - return $url; -} - -/** - * HTML escaping - * - * @param string $value Value to escape - * @return string - */ -function escape($value) -{ - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); -} - -/** - * Dispplay the flash session message - * - * @param string $html HTML wrapper - * @return string - */ -function flash($html) -{ - $data = ''; - - if (isset($_SESSION['flash_message'])) { - $data = sprintf($html, escape($_SESSION['flash_message'])); - unset($_SESSION['flash_message']); - } - - return $data; -} - -/** - * Display the flash session error message - * - * @param string $html HTML wrapper - * @return string - */ -function flash_error($html) -{ - $data = ''; - - if (isset($_SESSION['flash_error_message'])) { - $data = sprintf($html, escape($_SESSION['flash_error_message'])); - unset($_SESSION['flash_error_message']); - } - - return $data; -} - -/** - * Format a file size - * - * @param integer $size Size in bytes - * @param integer $precision Precision - * @return string - */ -function format_bytes($size, $precision = 2) -{ - $base = log($size) / log(1024); - $suffixes = array('', 'k', 'M', 'G', 'T'); - - return round(pow(1024, $base - floor($base)), $precision).$suffixes[(int)floor($base)]; -} - -/** - * Truncate a long text - * - * @param string $value Text - * @param integer $max_length Max Length - * @param string $end Text end - * @return string - */ -function summary($value, $max_length = 85, $end = '[...]') -{ - $length = strlen($value); - - if ($length > $max_length) { - return substr($value, 0, $max_length).' '.$end; - } - - return $value; -} - -/** - * Return true if needle is contained in the haystack - * - * @param string $haystack Haystack - * @param string $needle Needle - * @return boolean - */ -function contains($haystack, $needle) -{ - return strpos($haystack, $needle) !== false; -} - -/** - * Return a value from a dictionary - * - * @param mixed $id Key - * @param array $listing Dictionary - * @param string $default_value Value displayed when the key doesn't exists - * @return string - */ -function in_list($id, array $listing, $default_value = '?') -{ - if (isset($listing[$id])) { - return escape($listing[$id]); - } - - return $default_value; -} - -/** - * Display the form error class - * - * @param array $errors Error list - * @param string $name Field name - * @return string - */ -function error_class(array $errors, $name) -{ - return ! isset($errors[$name]) ? '' : ' form-error'; -} - -/** - * Display a list of form errors - * - * @param array $errors List of errors - * @param string $name Field name - * @return string - */ -function error_list(array $errors, $name) -{ - $html = ''; - - if (isset($errors[$name])) { - - $html .= '<ul class="form-errors">'; - - foreach ($errors[$name] as $error) { - $html .= '<li>'.escape($error).'</li>'; - } - - $html .= '</ul>'; - } - - return $html; -} - -/** - * Get an escaped form value - * - * @param mixed $values Values - * @param string $name Field name - * @return string - */ -function form_value($values, $name) -{ - if (isset($values->$name)) { - return 'value="'.escape($values->$name).'"'; - } - - return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : ''; -} - -/** - * Hidden CSRF token field - * - * @return string - */ -function form_csrf() -{ - return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>'; -} - -/** - * Display a hidden form field - * - * @param string $name Field name - * @param array $values Form values - * @return string - */ -function form_hidden($name, array $values = array()) -{ - return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).'/>'; -} - -/** - * Display a select field - * - * @param string $name Field name - * @param array $options Options - * @param array $values Form values - * @param array $errors Form errors - * @param string $class CSS class - * @return string - */ -function form_select($name, array $options, array $values = array(), array $errors = array(), $class = '') -{ - $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'">'; - - foreach ($options as $id => $value) { - - $html .= '<option value="'.escape($id).'"'; - - if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"'; - if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"'; - - $html .= '>'.escape($value).'</option>'; - } - - $html .= '</select>'; - $html .= error_list($errors, $name); - - return $html; -} - -/** - * Display a radio field group - * - * @param string $name Field name - * @param array $options Options - * @param array $values Form values - * @return string - */ -function form_radios($name, array $options, array $values = array()) -{ - $html = ''; - - foreach ($options as $value => $label) { - $html .= form_radio($name, $label, $value, isset($values[$name]) && $values[$name] == $value); - } - - return $html; -} - -/** - * Display a radio field - * - * @param string $name Field name - * @param string $label Form label - * @param string $value Form value - * @param boolean $selected Field selected or not - * @param string $class CSS class - * @return string - */ -function form_radio($name, $label, $value, $selected = false, $class = '') -{ - return '<label><input type="radio" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($selected ? 'selected="selected"' : '').'>'.escape($label).'</label>'; -} - -/** - * Display a checkbox field - * - * @param string $name Field name - * @param string $label Form label - * @param string $value Form value - * @param boolean $checked Field selected or not - * @param string $class CSS class - * @return string - */ -function form_checkbox($name, $label, $value, $checked = false, $class = '') -{ - return '<label><input type="checkbox" name="'.$name.'" class="'.$class.'" value="'.escape($value).'" '.($checked ? 'checked="checked"' : '').'> '.escape($label).'</label>'; -} - -/** - * Display a form label - * - * @param string $name Field name - * @param string $label Form label - * @param array $attributes HTML attributes - * @return string - */ -function form_label($label, $name, array $attributes = array()) -{ - return '<label for="form-'.$name.'" '.implode(' ', $attributes).'>'.escape($label).'</label>'; -} - -/** - * Display a textarea - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - $class .= error_class($errors, $name); - - $html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '; - $html .= implode(' ', $attributes).'>'; - $html .= isset($values->$name) ? escape($values->$name) : isset($values[$name]) ? $values[$name] : ''; - $html .= '</textarea>'; - if (in_array('required', $attributes)) $html .= '<span class="form-required">*</span>'; - $html .= error_list($errors, $name); - - return $html; -} - -/** - * Display a input field - * - * @param string $type HMTL input tag type - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_input($type, $name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - $class .= error_class($errors, $name); - - $html = '<input type="'.$type.'" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" '; - $html .= implode(' ', $attributes).'/>'; - if (in_array('required', $attributes)) $html .= '<span class="form-required">*</span>'; - $html .= error_list($errors, $name); - - return $html; -} - -/** - * Display a text field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('text', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a password field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('password', $name, $values, $errors, $attributes, $class); -} - -/** - * Display an email field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('email', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a date field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_date($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('date', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a number field - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_number($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('number', $name, $values, $errors, $attributes, $class); -} - -/** - * Display a numeric field (allow decimal number) - * - * @param string $name Field name - * @param array $values Form values - * @param array $errors Form errors - * @param array $attributes HTML attributes - * @param string $class CSS class - * @return string - */ -function form_numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') -{ - return form_input('text', $name, $values, $errors, $attributes, $class.' form-numeric'); -} - -/** - * Link - * - * a('link', 'task', 'show', array('task_id' => $task_id)) - * - * @param string $label Link label - * @param string $controller Controller name - * @param string $action Action name - * @param array $params Url parameters - * @param boolean $csrf Add a CSRF token - * @param string $class CSS class attribute - * @return string - */ -function a($label, $controller, $action, array $params = array(), $csrf = false, $class = '') -{ - return '<a href="'.u($controller, $action, $params, $csrf).'" class="'.$class.'"/>'.$label.'</a>'; -} - -/** - * URL query string - * - * u('task', 'show', array('task_id' => $task_id)) - * - * @param string $controller Controller name - * @param string $action Action name - * @param array $params Url parameters - * @param boolean $csrf Add a CSRF token - * @return string - */ -function u($controller, $action, array $params = array(), $csrf = false) -{ - $html = '?controller='.$controller.'&action='.$action; - - if ($csrf) { - $params['csrf_token'] = Security::getCSRFToken(); - } - - foreach ($params as $key => $value) { - $html .= '&'.$key.'='.$value; - } - - return $html; -} - -/** - * Pagination links - * - * @param array $pagination Pagination information - * @return string - */ -function paginate(array $pagination) -{ - extract($pagination); - - $html = '<div id="pagination">'; - $html .= '<span id="pagination-previous">'; - - if ($pagination['offset'] > 0) { - $offset = $pagination['offset'] - $limit; - $html .= a('← '.t('Previous'), $controller, $action, $params + compact('offset', 'order', 'direction')); - } - else { - $html .= '← '.t('Previous'); - } - - $html .= '</span>'; - $html .= '<span id="pagination-next">'; - - if (($total - $pagination['offset']) > $limit) { - $offset = $pagination['offset'] + $limit; - $html .= a(t('Next').' →', $controller, $action, $params + compact('offset', 'order', 'direction')); - } - else { - $html .= t('Next').' →'; - } - - $html .= '</span>'; - $html .= '</div>'; - - return $html; -} - -/** - * Column sorting (work with pagination) - * - * @param string $label Column title - * @param string $column SQL column name - * @param array $pagination Pagination information - * @return string - */ -function order($label, $column, array $pagination) -{ - extract($pagination); - - $prefix = ''; - - if ($order === $column) { - $prefix = $direction === 'DESC' ? '▼ ' : '▲ '; - $direction = $direction === 'DESC' ? 'ASC' : 'DESC'; - } - - $order = $column; - - return $prefix.a($label, $controller, $action, $params + compact('offset', 'order', 'direction')); -} |