diff --git a/sources/.htaccess b/sources/.htaccess new file mode 100644 index 0000000..0d873f5 --- /dev/null +++ b/sources/.htaccess @@ -0,0 +1,9 @@ + + Options -MultiViews + + SetEnv HTTP_MOD_REWRITE On + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + diff --git a/sources/ChangeLog b/sources/ChangeLog new file mode 100644 index 0000000..ddfb2f7 --- /dev/null +++ b/sources/ChangeLog @@ -0,0 +1,143 @@ +Version 1.0.18 +-------------- + +New features: + +* Include documentation in the application +* Add Gitlab authentication +* Add users and categories filters on the board +* Add hide/show columns +* Add Gantt chart for projects and tasks +* Add new role "Project Administrator" +* Add login bruteforce protection with captcha and account lockdown +* Add new api procedures: getDefaultTaskColor(), getDefaultTaskColors() and getColorList() +* Add user api access +* Add config parameter to define session duration +* Add config parameter to disable/enable RememberMe authentication +* Add start/end date for projects +* Add new automated action to change task color based on the task link +* Add milestone marker in board task +* Add search in task title when using an integer only input +* Add Portuguese (European) translation +* Add Norwegian translation + +Improvements: + +* Add handle to move tasks on touch devices +* Improve file attachments tooltip on the board +* Adjust automatically the height of the placeholder during drag and drop +* Show all tasks when using no search criteria +* Add column vertical scrolling +* Set dynamically column height based on viewport size +* Enable support for Github Enterprise when using Github Authentication +* Update iCalendar library to display organizer name +* Improve sidebar menus +* Add no referrer policy in meta tags +* Run automated unit tests with Sqlite/Mysql/Postgres on Travis-ci +* Add Makefile and remove the scripts directory + +Bug fixes: + +* Wrong template name for subtasks tooltip due to previous refactoring +* Fix broken url for closed tasks in project view +* Fix permission issue when changing the url manually +* Fix bug task estimate is reseted when using subtask timer +* Fix screenshot feature with Firefox 40 +* Fix bug when uploading files with cyrilic characters + +Version 1.0.17 +-------------- + +New features: + +* Added url rewrite and new routes +* Added new search engine with advanced syntax +* Added global search section +* Added search form on the dashboard +* Added new dashboard layout +* Added new layout for board/calendar/list views +* Added filters helper for search forms +* Added settings option to disable subtask timer +* Added settings option to include or exclude closed tasks into CFD +* Added settings option to define the default task color +* Added new config option to disable automatic creation of LDAP accounts +* Added loading icon on board view +* Prompt user when moving or duplicate a task to another project +* Added current values when moving/duplicate a task to another project and add a loading icon +* Added memory consumption in debug log +* Added form to create remote user +* Added edit form for user authentication +* Added config option to hide login form +* Display OAuth2 urls on integration page +* Added keyboard shortcuts to switch between board/calendar/list view +* Added keyboard shortcut to focus on the search box +* Added Slack channel override +* Added new report: Lead and cycle time for projects +* Added new report: Average time spent into each column +* Added task analytics +* Added icon to set automatically the start date +* Added datetime picker for start date + +Improvements: + +* Updated documentation +* Display user initials when tasks are in collapsed mode +* Show title in tooltip for collapsed tasks +* Improve alert box fadeout to avoid an empty space +* Set focus on the dropdown for category popover +* Make escape keyboard shorcut global +* Check the box remember me by default +* Store redirect login url in session instead of using url parameter +* Update Gitlab webhook +* Do not rewrite remember me cookie for each request +* Set the assignee as organizer for ical events +* Increase date range for ics export +* Reduce spacing on cards +* Move board collapse/expand mode to server side to avoid board flickering +* Use ajax requests for board collapse/expand +* Do not set anchor for the default swimlane on the link back to board +* Replace timeserie axis to category axis for charts +* Hide task age in compact mode +* Improve quick-add subtasks form +* Reduce the size of the filter box for smaller screen +* Added icon to hide/show sidebar +* Update GitLab logo +* Improve Dockerfile + +Translations: + +* Added Czech translation +* Updated Spanish translation +* Updated German Translation + +Bug fixes: + +* Screenshot dropdown: unexpected scroll down on the board view and focus lost when clicking on the drop zone +* No creator when duplicating a task +* Avoid the creation of multiple subtask timer for the same task and user + +Code refactoring: + +* Split task controller into smaller classes +* Remove method Category::getBoardCategories() +* Rewrite movePosition() to improve performances +* Refactoring of Github and Google authentication + +Breaking changes: + +* New OAuth url for Google and Github authentication + +API: + +* Add urls in api response for tasks and projects + +Other: + +* Added automated Docker build +* Remove edit recurrence from the task menu on the board +* Switch to MIT License instead of AGPLv3 + +Version 1.0.0 to 1.0.16 +----------------------- + +* See commit history and website news diff --git a/sources/app/Action/TaskAssignColorLink.php b/sources/app/Action/TaskAssignColorLink.php new file mode 100644 index 0000000..d055f22 --- /dev/null +++ b/sources/app/Action/TaskAssignColorLink.php @@ -0,0 +1,84 @@ + t('Color'), + 'link_id' => t('Link type'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'link_id', + ); + } + + /** + * Execute the action (change 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['link_id'] == $this->getParam('link_id'); + } +} diff --git a/sources/app/Action/TaskEmail.php b/sources/app/Action/TaskEmail.php new file mode 100644 index 0000000..363a01c --- /dev/null +++ b/sources/app/Action/TaskEmail.php @@ -0,0 +1,97 @@ + t('Column'), + 'user_id' => t('User that will receive the email'), + 'subject' => t('Email subject'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_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) + { + $user = $this->user->getById($this->getParam('user_id')); + + if (! empty($user['email'])) { + + $task = $this->taskFinder->getDetails($data['task_id']); + + $this->emailClient->send( + $user['email'], + $user['name'] ?: $user['username'], + $this->getParam('subject'), + $this->template->render('notification/task_create', array('task' => $task, 'application_url' => $this->config->get('application_url'))) + ); + + return true; + } + + return 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('column_id'); + } +} diff --git a/sources/app/Action/TaskMoveColumnCategoryChange.php b/sources/app/Action/TaskMoveColumnCategoryChange.php new file mode 100644 index 0000000..a0f41ba --- /dev/null +++ b/sources/app/Action/TaskMoveColumnCategoryChange.php @@ -0,0 +1,89 @@ + t('Destination column'), + 'category_id' => t('Category'), + ); + } + + /** + * Get the required parameter for the event + * + * @access public + * @return string[] + */ + public function getEventRequiredParameters() + { + return array( + 'task_id', + 'column_id', + 'project_id', + 'category_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'] + ); + } + + /** + * 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('dest_column_id') && $data['category_id'] == $this->getParam('category_id'); + } +} diff --git a/sources/app/Action/TaskUpdateStartDate.php b/sources/app/Action/TaskUpdateStartDate.php new file mode 100644 index 0000000..4cd50c9 --- /dev/null +++ b/sources/app/Action/TaskUpdateStartDate.php @@ -0,0 +1,83 @@ + 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/sources/app/Api/Action.php b/sources/app/Api/Action.php new file mode 100644 index 0000000..a67e915 --- /dev/null +++ b/sources/app/Api/Action.php @@ -0,0 +1,98 @@ +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/sources/app/Api/App.php b/sources/app/Api/App.php new file mode 100644 index 0000000..9b3ceb9 --- /dev/null +++ b/sources/app/Api/App.php @@ -0,0 +1,37 @@ +config->get('application_timezone'); + } + + public function getVersion() + { + return APP_VERSION; + } + + public function getDefaultTaskColor() + { + return $this->color->getDefaultColor(); + } + + public function getDefaultTaskColors() + { + return $this->color->getDefaultColors(); + } + + public function getColorList() + { + return $this->color->getList(); + } +} diff --git a/sources/app/Api/Auth.php b/sources/app/Api/Auth.php new file mode 100644 index 0000000..18fe9ff --- /dev/null +++ b/sources/app/Api/Auth.php @@ -0,0 +1,40 @@ +container['dispatcher']->dispatch('api.bootstrap', new Event); + + if ($username !== 'jsonrpc' && ! $this->authentication->hasCaptcha($username) && $this->authentication->authenticate($username, $password)) { + $this->checkProcedurePermission(true, $method); + $this->userSession->refresh($this->user->getByUsername($username)); + } + else if ($username === 'jsonrpc' && $password === $this->config->get('api_token')) { + $this->checkProcedurePermission(false, $method); + } + else { + throw new AuthenticationFailure('Wrong credentials'); + } + } +} diff --git a/sources/app/Api/Base.php b/sources/app/Api/Base.php new file mode 100644 index 0000000..17c7f79 --- /dev/null +++ b/sources/app/Api/Base.php @@ -0,0 +1,113 @@ +both_allowed_procedures); + $is_user_procedure = in_array($procedure, $this->user_allowed_procedures); + + if ($is_user && ! $is_both_procedure && ! $is_user_procedure) { + throw new AccessDeniedException('Permission denied'); + } + else if (! $is_user && ! $is_both_procedure && $is_user_procedure) { + throw new AccessDeniedException('Permission denied'); + } + } + + public function checkProjectPermission($project_id) + { + if ($this->userSession->isLogged() && ! $this->projectPermission->isUserAllowed($project_id, $this->userSession->getId())) { + throw new AccessDeniedException('Permission denied'); + } + } + + public function checkTaskPermission($task_id) + { + if ($this->userSession->isLogged()) { + $this->checkProjectPermission($this->taskFinder->getProjectId($task_id)); + } + } + + protected function formatTask($task) + { + if (! empty($task)) { + $task['url'] = $this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), '', true); + $task['color'] = $this->color->getColorProperties($task['color_id']); + } + + return $task; + } + + protected function formatTasks($tasks) + { + if (! empty($tasks)) { + foreach ($tasks as &$task) { + $task = $this->formatTask($task); + } + } + + return $tasks; + } + + protected function formatProject($project) + { + if (! empty($project)) { + $project['url'] = array( + 'board' => $this->helper->url->to('board', 'show', array('project_id' => $project['id']), '', true), + 'calendar' => $this->helper->url->to('calendar', 'show', array('project_id' => $project['id']), '', true), + 'list' => $this->helper->url->to('listing', 'show', array('project_id' => $project['id']), '', true), + ); + } + + return $project; + } + + protected function formatProjects($projects) + { + if (! empty($projects)) { + foreach ($projects as &$project) { + $project = $this->formatProject($project); + } + } + + return $projects; + } +} diff --git a/sources/app/Api/Board.php b/sources/app/Api/Board.php new file mode 100644 index 0000000..93b99cc --- /dev/null +++ b/sources/app/Api/Board.php @@ -0,0 +1,53 @@ +checkProjectPermission($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/sources/app/Api/Category.php b/sources/app/Api/Category.php new file mode 100644 index 0000000..ad3c5ef --- /dev/null +++ b/sources/app/Api/Category.php @@ -0,0 +1,49 @@ +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/sources/app/Api/Comment.php b/sources/app/Api/Comment.php new file mode 100644 index 0000000..e40968b --- /dev/null +++ b/sources/app/Api/Comment.php @@ -0,0 +1,51 @@ +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/sources/app/Api/File.php b/sources/app/Api/File.php new file mode 100644 index 0000000..97aa9d8 --- /dev/null +++ b/sources/app/Api/File.php @@ -0,0 +1,53 @@ +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, $blob) + { + return $this->file->uploadContent($project_id, $task_id, $filename, $blob); + } + + public function removeFile($file_id) + { + return $this->file->remove($file_id); + } + + public function removeAllFiles($task_id) + { + return $this->file->removeAll($task_id); + } +} diff --git a/sources/app/Api/Link.php b/sources/app/Api/Link.php new file mode 100644 index 0000000..d883013 --- /dev/null +++ b/sources/app/Api/Link.php @@ -0,0 +1,111 @@ +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/sources/app/Api/Me.php b/sources/app/Api/Me.php new file mode 100644 index 0000000..29a8052 --- /dev/null +++ b/sources/app/Api/Me.php @@ -0,0 +1,55 @@ +session['user']; + } + + public function getMyDashboard() + { + $user_id = $this->userSession->getId(); + $projects = $this->project->getQueryColumnStats($this->projectPermission->getActiveMemberProjectIds($user_id))->findAll(); + $tasks = $this->taskFinder->getUserQuery($user_id)->findAll(); + + return array( + 'projects' => $this->formatProjects($projects), + 'tasks' => $this->formatTasks($tasks), + 'subtasks' => $this->subtask->getUserQuery($user_id, array(SubTaskModel::STATUS_TODO, SubtaskModel::STATUS_INPROGRESS))->findAll(), + ); + } + + public function getMyActivityStream() + { + return $this->projectActivity->getProjects($this->projectPermission->getActiveMemberProjectIds($this->userSession->getId()), 100); + } + + public function createMyPrivateProject($name, $description = null) + { + $values = array( + 'name' => $name, + 'description' => $description, + 'is_private' => 1, + ); + + list($valid,) = $this->project->validateCreation($values); + return $valid ? $this->project->create($values, $this->userSession->getId(), true) : false; + } + + public function getMyProjectsList() + { + return $this->projectPermission->getMemberProjects($this->userSession->getId()); + } +} diff --git a/sources/app/Api/Project.php b/sources/app/Api/Project.php new file mode 100644 index 0000000..c3ae503 --- /dev/null +++ b/sources/app/Api/Project.php @@ -0,0 +1,86 @@ +checkProjectPermission($project_id); + return $this->formatProject($this->project->getById($project_id)); + } + + public function getProjectByName($name) + { + return $this->formatProject($this->project->getByName($name)); + } + + public function getAllProjects() + { + return $this->formatProjects($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/sources/app/Api/ProjectPermission.php b/sources/app/Api/ProjectPermission.php new file mode 100644 index 0000000..7dd2dec --- /dev/null +++ b/sources/app/Api/ProjectPermission.php @@ -0,0 +1,27 @@ +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/sources/app/Api/Subtask.php b/sources/app/Api/Subtask.php new file mode 100644 index 0000000..2b8e7a1 --- /dev/null +++ b/sources/app/Api/Subtask.php @@ -0,0 +1,64 @@ +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/sources/app/Api/Swimlane.php b/sources/app/Api/Swimlane.php new file mode 100644 index 0000000..fb40841 --- /dev/null +++ b/sources/app/Api/Swimlane.php @@ -0,0 +1,77 @@ +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/sources/app/Api/Task.php b/sources/app/Api/Task.php new file mode 100644 index 0000000..946a9e8 --- /dev/null +++ b/sources/app/Api/Task.php @@ -0,0 +1,128 @@ +checkTaskPermission($task_id); + return $this->formatTask($this->taskFinder->getById($task_id)); + } + + public function getTaskByReference($project_id, $reference) + { + $this->checkProjectPermission($project_id); + return $this->formatTask($this->taskFinder->getByReference($project_id, $reference)); + } + + public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN) + { + $this->checkProjectPermission($project_id); + return $this->formatTasks($this->taskFinder->getAll($project_id, $status_id)); + } + + public function getOverdueTasks() + { + return $this->taskFinder->getOverdueTasks(); + } + + public function openTask($task_id) + { + $this->checkTaskPermission($task_id); + return $this->taskStatus->open($task_id); + } + + public function closeTask($task_id) + { + $this->checkTaskPermission($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) + { + $this->checkProjectPermission($project_id); + 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, $reference = '') + { + $this->checkProjectPermission($project_id); + + $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, + 'reference' => $reference, + ); + + 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, $owner_id = null, + $creator_id = null, $date_due = null, $description = null, $category_id = null, $score = null, + $recurrence_status = null, $recurrence_trigger = null, $recurrence_factor = null, + $recurrence_timeframe = null, $recurrence_basedate = null, $reference = null) + { + $this->checkTaskPermission($id); + + $values = array( + 'id' => $id, + 'title' => $title, + 'project_id' => $project_id, + 'color_id' => $color_id, + 'owner_id' => $owner_id, + 'creator_id' => $creator_id, + 'date_due' => $date_due, + 'description' => $description, + 'category_id' => $category_id, + 'score' => $score, + 'recurrence_status' => $recurrence_status, + 'recurrence_trigger' => $recurrence_trigger, + 'recurrence_factor' => $recurrence_factor, + 'recurrence_timeframe' => $recurrence_timeframe, + 'recurrence_basedate' => $recurrence_basedate, + 'reference' => $reference, + ); + + 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/sources/app/Api/TaskLink.php b/sources/app/Api/TaskLink.php new file mode 100644 index 0000000..6b23d05 --- /dev/null +++ b/sources/app/Api/TaskLink.php @@ -0,0 +1,77 @@ +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/sources/app/Api/User.php b/sources/app/Api/User.php new file mode 100644 index 0000000..4884c45 --- /dev/null +++ b/sources/app/Api/User.php @@ -0,0 +1,87 @@ +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, $is_project_admin = 0) + { + $values = array( + 'username' => $username, + 'password' => $password, + 'confirmation' => $password, + 'name' => $name, + 'email' => $email, + 'is_admin' => $is_admin, + 'is_project_admin' => $is_project_admin, + ); + + list($valid,) = $this->user->validateCreation($values); + return $valid ? $this->user->create($values) : false; + } + + public function createLdapUser($username = '', $email = '', $is_admin = 0, $is_project_admin = 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, + 'is_project_admin' => $is_project_admin, + ); + + return $this->user->create($values); + } + + public function updateUser($id, $username = null, $name = null, $email = null, $is_admin = null, $is_project_admin = null) + { + $values = array( + 'id' => $id, + 'username' => $username, + 'name' => $name, + 'email' => $email, + 'is_admin' => $is_admin, + 'is_project_admin' => $is_project_admin, + ); + + 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/sources/app/Auth/Github.php b/sources/app/Auth/Github.php new file mode 100644 index 0000000..2d1b7b2 --- /dev/null +++ b/sources/app/Auth/Github.php @@ -0,0 +1,122 @@ +user->getByGithubId($github_id); + + if (! empty($user)) { + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); + return true; + } + + return false; + } + + /** + * Unlink a Github account for a given user + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function unlink($user_id) + { + return $this->user->update(array( + 'id' => $user_id, + 'github_id' => '', + )); + } + + /** + * Update the user table based on the Github profile information + * + * @access public + * @param integer $user_id User id + * @param array $profile Github profile + * @return boolean + */ + public function updateUser($user_id, array $profile) + { + $user = $this->user->getById($user_id); + + return $this->user->update(array( + 'id' => $user_id, + 'github_id' => $profile['id'], + 'email' => empty($user['email']) ? $profile['email'] : $user['email'], + 'name' => empty($user['name']) ? $profile['name'] : $user['name'], + )); + } + + /** + * Get OAuth2 configured service + * + * @access public + * @return \Core\OAuth2 + */ + public function getService() + { + if (empty($this->service)) { + $this->service = $this->oauth->createService( + GITHUB_CLIENT_ID, + GITHUB_CLIENT_SECRET, + $this->helper->url->to('oauth', 'github', array(), '', true), + GITHUB_OAUTH_AUTHORIZE_URL, + GITHUB_OAUTH_TOKEN_URL, + array() + ); + } + + return $this->service; + } + + /** + * Get Github profile + * + * @access public + * @param string $code + * @return array + */ + public function getProfile($code) + { + $this->getService()->getAccessToken($code); + + return $this->httpClient->getJson( + GITHUB_API_URL.'user', + array($this->getService()->getAuthorizationHeader()) + ); + } +} diff --git a/sources/app/Auth/Gitlab.php b/sources/app/Auth/Gitlab.php new file mode 100644 index 0000000..d9a985b --- /dev/null +++ b/sources/app/Auth/Gitlab.php @@ -0,0 +1,122 @@ +user->getByGitlabId($gitlab_id); + + if (! empty($user)) { + $this->userSession->refresh($user); + $this->container['dispatcher']->dispatch('auth.success', new AuthEvent(self::AUTH_NAME, $user['id'])); + return true; + } + + return false; + } + + /** + * Unlink a Gitlab account for a given user + * + * @access public + * @param integer $user_id User id + * @return boolean + */ + public function unlink($user_id) + { + return $this->user->update(array( + 'id' => $user_id, + 'gitlab_id' => '', + )); + } + + /** + * Update the user table based on the Gitlab profile information + * + * @access public + * @param integer $user_id User id + * @param array $profile Gitlab profile + * @return boolean + */ + public function updateUser($user_id, array $profile) + { + $user = $this->user->getById($user_id); + + return $this->user->update(array( + 'id' => $user_id, + 'gitlab_id' => $profile['id'], + 'email' => empty($user['email']) ? $profile['email'] : $user['email'], + 'name' => empty($user['name']) ? $profile['name'] : $user['name'], + )); + } + + /** + * Get OAuth2 configured service + * + * @access public + * @return \Core\OAuth2 + */ + public function getService() + { + if (empty($this->service)) { + $this->service = $this->oauth->createService( + GITLAB_CLIENT_ID, + GITLAB_CLIENT_SECRET, + $this->helper->url->to('oauth', 'gitlab', array(), '', true), + GITLAB_OAUTH_AUTHORIZE_URL, + GITLAB_OAUTH_TOKEN_URL, + array() + ); + } + + return $this->service; + } + + /** + * Get Gitlab profile + * + * @access public + * @param string $code + * @return array + */ + public function getProfile($code) + { + $this->getService()->getAccessToken($code); + + return $this->httpClient->getJson( + GITLAB_API_URL.'user', + array($this->getService()->getAuthorizationHeader()) + ); + } +} diff --git a/sources/app/Console/LocaleComparator.php b/sources/app/Console/LocaleComparator.php new file mode 100644 index 0000000..6eec42d --- /dev/null +++ b/sources/app/Console/LocaleComparator.php @@ -0,0 +1,82 @@ +setName('locale:compare') + ->setDescription('Compare application translations with the '.self::REF_LOCALE.' locale'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $strings = array(); + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('app')); + $it->rewind(); + + while ($it->valid()) { + + if (! $it->isDot() && substr($it->key(), -4) === '.php') { + $strings = array_merge($strings, $this->search($it->key())); + } + + $it->next(); + } + + $this->compare(array_unique($strings)); + } + + public function show(array $strings) + { + foreach ($strings as $string) { + echo " '".str_replace("'", "\'", $string)."' => '',".PHP_EOL; + } + } + + public function compare(array $strings) + { + $reference_file = 'app/Locale/'.self::REF_LOCALE.'/translations.php'; + $reference = include $reference_file; + + echo str_repeat('#', 70).PHP_EOL; + echo 'MISSING STRINGS'.PHP_EOL; + echo str_repeat('#', 70).PHP_EOL; + $this->show(array_diff($strings, array_keys($reference))); + + echo str_repeat('#', 70).PHP_EOL; + echo 'USELESS STRINGS'.PHP_EOL; + echo str_repeat('#', 70).PHP_EOL; + $this->show(array_diff(array_keys($reference), $strings)); + } + + public function search($filename) + { + $content = file_get_contents($filename); + $strings = array(); + + if (preg_match_all('/\b[et]\((\'\K.*?\') *[\)\,]/', $content, $matches) && isset($matches[1])) { + $strings = $matches[1]; + } + + if (preg_match_all('/\bdt\((\'\K.*?\') *[\)\,]/', $content, $matches) && isset($matches[1])) { + $strings = array_merge($strings, $matches[1]); + } + + array_walk($strings, function(&$value) { + $value = trim($value, "'"); + $value = str_replace("\'", "'", $value); + }); + + return $strings; + } +} diff --git a/sources/app/Console/LocaleSync.php b/sources/app/Console/LocaleSync.php new file mode 100644 index 0000000..ab95651 --- /dev/null +++ b/sources/app/Console/LocaleSync.php @@ -0,0 +1,57 @@ +setName('locale:sync') + ->setDescription('Synchronize all translations based on the '.self::REF_LOCALE.' locale'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $reference_file = 'app/Locale/'.self::REF_LOCALE.'/translations.php'; + $reference = include $reference_file; + + foreach (new DirectoryIterator('app/Locale') as $fileInfo) { + + if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->getFilename() !== self::REF_LOCALE) { + + $filename = 'app/Locale/'.$fileInfo->getFilename().'/translations.php'; + echo $fileInfo->getFilename().' ('.$filename.')'.PHP_EOL; + + file_put_contents($filename, $this->updateFile($reference, $filename)); + } + } + } + + public function updateFile(array $reference, $outdated_file) + { + $outdated = include $outdated_file; + + $output = ' $value) { + + if (! empty($outdated[$key])) { + $output .= " '".str_replace("'", "\'", $key)."' => '".str_replace("'", "\'", $outdated[$key])."',\n"; + } + else { + $output .= " // '".str_replace("'", "\'", $key)."' => '',\n"; + } + } + + $output .= ");\n"; + return $output; + } +} diff --git a/sources/app/Console/ProjectDailyColumnStatsExport.php b/sources/app/Console/ProjectDailyColumnStatsExport.php new file mode 100644 index 0000000..b983066 --- /dev/null +++ b/sources/app/Console/ProjectDailyColumnStatsExport.php @@ -0,0 +1,34 @@ +setName('export:daily-project-column-stats') + ->setDescription('Daily project column stats 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->projectDailyColumnStats->getAggregatedMetrics( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Tool::csv($data); + } + } +} diff --git a/sources/app/Console/ProjectDailyStatsCalculation.php b/sources/app/Console/ProjectDailyStatsCalculation.php new file mode 100644 index 0000000..4b77c55 --- /dev/null +++ b/sources/app/Console/ProjectDailyStatsCalculation.php @@ -0,0 +1,28 @@ +setName('projects:daily-stats') + ->setDescription('Calculate daily statistics 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->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d')); + $this->projectDailyStats->updateTotals($project['id'], date('Y-m-d')); + } + } +} diff --git a/sources/app/Controller/Activity.php b/sources/app/Controller/Activity.php new file mode 100644 index 0000000..234e4be --- /dev/null +++ b/sources/app/Controller/Activity.php @@ -0,0 +1,45 @@ +getProject(); + + $this->response->html($this->template->layout('activity/project', array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + 'events' => $this->projectActivity->getProject($project['id']), + 'project' => $project, + 'title' => t('%s\'s activity', $project['name']) + ))); + } + + /** + * Display task activities + * + * @access public + */ + public function task() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('activity/task', array( + 'title' => $task['title'], + 'task' => $task, + 'events' => $this->projectActivity->getTask($task['id']), + ))); + } +} diff --git a/sources/app/Controller/Auth.php b/sources/app/Controller/Auth.php new file mode 100644 index 0000000..bb1154e --- /dev/null +++ b/sources/app/Controller/Auth.php @@ -0,0 +1,85 @@ +userSession->isLogged()) { + $this->response->redirect($this->helper->url->to('app', 'index')); + } + + $this->response->html($this->template->layout('auth/index', array( + 'captcha' => isset($values['username']) && $this->authentication->hasCaptcha($values['username']), + 'errors' => $errors, + 'values' => $values, + 'no_layout' => true, + 'title' => t('Login') + ))); + } + + /** + * Check credentials + * + * @access public + */ + public function check() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->authentication->validateForm($values); + + if ($valid) { + + if (! empty($this->session['login_redirect']) && ! filter_var($this->session['login_redirect'], FILTER_VALIDATE_URL)) { + $redirect = $this->session['login_redirect']; + unset($this->session['login_redirect']); + $this->response->redirect($redirect); + } + + $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')); + } + + /** + * Display captcha image + * + * @access public + */ + public function captcha() + { + $this->response->contentType('image/jpeg'); + + $builder = new CaptchaBuilder; + $builder->build(); + $this->session['captcha'] = $builder->getPhrase(); + $builder->output(); + } +} diff --git a/sources/app/Controller/Column.php b/sources/app/Controller/Column.php new file mode 100644 index 0000000..89c495a --- /dev/null +++ b/sources/app/Controller/Column.php @@ -0,0 +1,170 @@ +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/sources/app/Controller/Doc.php b/sources/app/Controller/Doc.php new file mode 100644 index 0000000..19644b8 --- /dev/null +++ b/sources/app/Controller/Doc.php @@ -0,0 +1,51 @@ +helper->url; + $data = file_get_contents($filename); + list($title,, $content) = explode("\n", $data, 3); + + $replaceUrl = function (array $matches) use ($url) { + return '('.$url->to('doc', 'show', array('file' => str_replace('.markdown', '', $matches[1]))).')'; + }; + + $content = preg_replace_callback('/\((.*.markdown)\)/', $replaceUrl, $data); + + return array( + 'content' => Parsedown::instance()->text($content), + 'title' => $title !== 'Documentation' ? t('Documentation: %s', $title) : $title, + ); + } + + public function show() + { + $filename = $this->request->getStringParam('file', 'index'); + + if (! preg_match('/^[a-z0-9\-]+/', $filename)) { + $filename = 'index'; + } + + $filename = __DIR__.'/../../doc/'.$filename.'.markdown'; + + if (! file_exists($filename)) { + $filename = __DIR__.'/../../doc/index.markdown'; + } + + $this->response->html($this->template->layout('doc/show', $this->readFile($filename) + array( + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + ))); + } +} diff --git a/sources/app/Controller/Feed.php b/sources/app/Controller/Feed.php new file mode 100644 index 0000000..c08919c --- /dev/null +++ b/sources/app/Controller/Feed.php @@ -0,0 +1,56 @@ +request->getStringParam('token'); + $user = $this->user->getByToken($token); + + // Token verification + if (empty($user)) { + $this->forbidden(true); + } + + $projects = $this->projectPermission->getActiveMemberProjects($user['id']); + + $this->response->xml($this->template->render('feed/user', array( + 'events' => $this->projectActivity->getProjects(array_keys($projects)), + 'user' => $user, + ))); + } + + /** + * RSS feed for a project + * + * @access public + */ + public function project() + { + $token = $this->request->getStringParam('token'); + $project = $this->project->getByToken($token); + + // Token verification + if (empty($project)) { + $this->forbidden(true); + } + + $this->response->xml($this->template->render('feed/project', array( + 'events' => $this->projectActivity->getProject($project['id']), + 'project' => $project, + ))); + } +} diff --git a/sources/app/Controller/Gantt.php b/sources/app/Controller/Gantt.php new file mode 100644 index 0000000..a2d3f36 --- /dev/null +++ b/sources/app/Controller/Gantt.php @@ -0,0 +1,151 @@ +userSession->isAdmin()) { + $project_ids = $this->project->getAllIds(); + } + else { + $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId()); + } + + $this->response->html($this->template->layout('gantt/projects', array( + 'projects' => $this->project->getGanttBars($project_ids), + 'title' => t('Gantt chart for all projects'), + 'board_selector' => $this->projectPermission->getAllowedProjects($this->userSession->getId()), + ))); + } + + /** + * Save new project start date and end date + */ + public function saveProjectDate() + { + $values = $this->request->getJson(); + + $result = $this->project->update(array( + 'id' => $values['id'], + 'start_date' => $this->dateParser->getIsoDate(strtotime($values['start'])), + 'end_date' => $this->dateParser->getIsoDate(strtotime($values['end'])), + )); + + if (! $result) { + $this->response->json(array('message' => 'Unable to save project'), 400); + } + + $this->response->json(array('message' => 'OK'), 201); + } + + /** + * Show Gantt chart for one project + */ + public function project() + { + $params = $this->getProjectFilters('gantt', 'project'); + $filter = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id']); + $sorting = $this->request->getStringParam('sorting', 'board'); + + if ($sorting === 'date') { + $filter->getQuery()->asc(TaskModel::TABLE.'.date_started')->asc(TaskModel::TABLE.'.date_creation'); + } + else { + $filter->getQuery()->asc('column_position')->asc(TaskModel::TABLE.'.position'); + } + + $this->response->html($this->template->layout('gantt/project', $params + array( + 'users_list' => $this->projectPermission->getMemberList($params['project']['id'], false), + 'sorting' => $sorting, + 'tasks' => $filter->toGanttBars(), + ))); + } + + /** + * Save new task start date and due date + */ + public function saveTaskDate() + { + $this->getProject(); + $values = $this->request->getJson(); + + $result = $this->taskModification->update(array( + 'id' => $values['id'], + 'date_started' => strtotime($values['start']), + 'date_due' => strtotime($values['end']), + )); + + if (! $result) { + $this->response->json(array('message' => 'Unable to save task'), 400); + } + + $this->response->json(array('message' => 'OK'), 201); + } + + /** + * Simplified form to create a new task + * + * @access public + */ + public function task(array $values = array(), array $errors = array()) + { + $project = $this->getProject(); + + $this->response->html($this->template->render('gantt/task_creation', array( + 'errors' => $errors, + 'values' => $values + array( + 'project_id' => $project['id'], + 'column_id' => $this->board->getFirstColumn($project['id']), + 'position' => 1 + ), + 'users_list' => $this->projectPermission->getMemberList($project['id'], true, false, true), + 'colors_list' => $this->color->getList(), + 'categories_list' => $this->category->getList($project['id']), + 'swimlanes_list' => $this->swimlane->getList($project['id'], false, true), + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => $project['name'].' > '.t('New task') + ))); + } + + /** + * Validate and save a new task + * + * @access public + */ + public function saveTask() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateCreation($values); + + if ($valid) { + + $task_id = $this->taskCreation->create($values); + + if ($task_id !== false) { + $this->session->flash(t('Task created successfully.')); + $this->response->redirect($this->helper->url->to('gantt', 'project', array('project_id' => $project['id']))); + } + else { + $this->session->flashError(t('Unable to create your task.')); + } + } + + $this->task($values, $errors); + } +} diff --git a/sources/app/Controller/Ical.php b/sources/app/Controller/Ical.php new file mode 100644 index 0000000..0129915 --- /dev/null +++ b/sources/app/Controller/Ical.php @@ -0,0 +1,98 @@ +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('-2 month')); + $end = $this->request->getStringParam('end', strtotime('+6 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/sources/app/Controller/Listing.php b/sources/app/Controller/Listing.php new file mode 100644 index 0000000..2c197e3 --- /dev/null +++ b/sources/app/Controller/Listing.php @@ -0,0 +1,37 @@ +getProjectFilters('listing', 'show'); + $query = $this->taskFilter->search($params['filters']['search'])->filterByProject($params['project']['id'])->getQuery(); + + $paginator = $this->paginator + ->setUrl('listing', 'show', array('project_id' => $params['project']['id'])) + ->setMax(30) + ->setOrder(TaskModel::TABLE.'.id') + ->setDirection('DESC') + ->setQuery($query) + ->calculate(); + + $this->response->html($this->template->layout('listing/show', $params + array( + 'paginator' => $paginator, + ))); + } +} diff --git a/sources/app/Controller/Oauth.php b/sources/app/Controller/Oauth.php new file mode 100644 index 0000000..b0f13dc --- /dev/null +++ b/sources/app/Controller/Oauth.php @@ -0,0 +1,133 @@ +step1('google'); + } + + /** + * Link or authenticate a Github account + * + * @access public + */ + public function github() + { + $this->step1('github'); + } + + /** + * Link or authenticate a Gitlab account + * + * @access public + */ + public function gitlab() + { + $this->step1('gitlab'); + } + + /** + * Unlink external account + * + * @access public + */ + public function unlink($backend = '') + { + $backend = $this->request->getStringParam('backend', $backend); + $this->checkCSRFParam(); + + if ($this->authentication->backend($backend)->unlink($this->userSession->getId())) { + $this->session->flash(t('Your external account is not linked anymore to your profile.')); + } + else { + $this->session->flashError(t('Unable to unlink your external account.')); + } + + $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + } + + /** + * Redirect to the provider if no code received + * + * @access private + */ + private function step1($backend) + { + $code = $this->request->getStringParam('code'); + + if (! empty($code)) { + $this->step2($backend, $code); + } + else { + $this->response->redirect($this->authentication->backend($backend)->getService()->getAuthorizationUrl()); + } + } + + /** + * Link or authenticate the user + * + * @access private + */ + private function step2($backend, $code) + { + $profile = $this->authentication->backend($backend)->getProfile($code); + + if ($this->userSession->isLogged()) { + $this->link($backend, $profile); + } + + $this->authenticate($backend, $profile); + } + + /** + * Link the account + * + * @access private + */ + private function link($backend, $profile) + { + if (empty($profile)) { + $this->session->flashError(t('External authentication failed')); + } + else { + $this->session->flash(t('Your external account is linked to your profile successfully.')); + $this->authentication->backend($backend)->updateUser($this->userSession->getId(), $profile); + } + + $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + } + + /** + * Authenticate the account + * + * @access private + */ + private function authenticate($backend, $profile) + { + if (! empty($profile) && $this->authentication->backend($backend)->authenticate($profile['id'])) { + $this->response->redirect($this->helper->url->to('app', 'index')); + } + else { + $this->response->html($this->template->layout('auth/index', array( + 'errors' => array('login' => t('External authentication failed')), + 'values' => array(), + 'no_layout' => true, + 'title' => t('Login') + ))); + } + } +} diff --git a/sources/app/Controller/Projectuser.php b/sources/app/Controller/Projectuser.php new file mode 100644 index 0000000..4456ce3 --- /dev/null +++ b/sources/app/Controller/Projectuser.php @@ -0,0 +1,134 @@ +projectPermission->getAllowedProjects($this->userSession->getId()); + $params['content_for_sublayout'] = $this->template->render($template, $params); + $params['filter'] = array('user_id' => $params['user_id']); + + return $this->template->layout('project_user/layout', $params); + } + + private function common() + { + $user_id = $this->request->getIntegerParam('user_id', UserModel::EVERYBODY_ID); + + if ($this->userSession->isAdmin()) { + $project_ids = $this->project->getAllIds(); + } + else { + $project_ids = $this->projectPermission->getMemberProjectIds($this->userSession->getId()); + } + + return array($user_id, $project_ids, $this->user->getList(true)); + } + + private function role($is_owner, $action, $title, $title_user) + { + list($user_id, $project_ids, $users) = $this->common(); + + $query = $this->projectPermission->getQueryByRole($project_ids, $is_owner)->callback(array($this->project, 'applyColumnStats')); + + if ($user_id !== UserModel::EVERYBODY_ID) { + $query->eq(UserModel::TABLE.'.id', $user_id); + $title = t($title_user, $users[$user_id]); + } + + $paginator = $this->paginator + ->setUrl('projectuser', $action, array('user_id' => $user_id)) + ->setMax(30) + ->setOrder('projects.name') + ->setQuery($query) + ->calculate(); + + $this->response->html($this->layout('project_user/roles', array( + 'paginator' => $paginator, + 'title' => $title, + 'user_id' => $user_id, + 'users' => $users, + ))); + } + + private function tasks($is_active, $action, $title, $title_user) + { + list($user_id, $project_ids, $users) = $this->common(); + + $query = $this->taskFinder->getProjectUserOverviewQuery($project_ids, $is_active); + + if ($user_id !== UserModel::EVERYBODY_ID) { + $query->eq(TaskModel::TABLE.'.owner_id', $user_id); + $title = t($title_user, $users[$user_id]); + } + + $paginator = $this->paginator + ->setUrl('projectuser', $action, array('user_id' => $user_id)) + ->setMax(50) + ->setOrder(TaskModel::TABLE.'.id') + ->setQuery($query) + ->calculate(); + + $this->response->html($this->layout('project_user/tasks', array( + 'paginator' => $paginator, + 'title' => $title, + 'user_id' => $user_id, + 'users' => $users, + ))); + } + + /** + * Display the list of project managers + * + */ + public function managers() + { + $this->role(1, 'managers', t('People who are project managers'), 'Projects where "%s" is manager'); + } + + /** + * Display the list of project members + * + */ + public function members() + { + $this->role(0, 'members', t('People who are project members'), 'Projects where "%s" is member'); + } + + /** + * Display the list of open taks + * + */ + public function opens() + { + $this->tasks(TaskModel::STATUS_OPEN, 'opens', t('Open tasks'), 'Open tasks assigned to "%s"'); + } + + /** + * Display the list of closed tasks + * + */ + public function closed() + { + $this->tasks(TaskModel::STATUS_CLOSED, 'closed', t('Closed tasks'), 'Closed tasks assigned to "%s"'); + } +} diff --git a/sources/app/Controller/Search.php b/sources/app/Controller/Search.php new file mode 100644 index 0000000..f6dc7a3 --- /dev/null +++ b/sources/app/Controller/Search.php @@ -0,0 +1,51 @@ +projectPermission->getAllowedProjects($this->userSession->getId()); + $search = urldecode($this->request->getStringParam('search')); + $nb_tasks = 0; + + $paginator = $this->paginator + ->setUrl('search', 'index', array('search' => $search)) + ->setMax(30) + ->setOrder('tasks.id') + ->setDirection('DESC'); + + if ($search !== '') { + + $query = $this + ->taskFilter + ->search($search) + ->filterByProjects(array_keys($projects)) + ->getQuery(); + + $paginator + ->setQuery($query) + ->calculate(); + + $nb_tasks = $paginator->getTotal(); + } + + $this->response->html($this->template->layout('search/index', array( + 'board_selector' => $projects, + 'values' => array( + 'search' => $search, + 'controller' => 'search', + 'action' => 'index', + ), + 'paginator' => $paginator, + 'title' => t('Search tasks').($nb_tasks > 0 ? ' ('.$nb_tasks.')' : '') + ))); + } +} diff --git a/sources/app/Controller/Taskcreation.php b/sources/app/Controller/Taskcreation.php new file mode 100644 index 0000000..ff25c5d --- /dev/null +++ b/sources/app/Controller/Taskcreation.php @@ -0,0 +1,83 @@ +getProject(); + $method = $this->request->isAjax() ? 'render' : 'layout'; + $swimlanes_list = $this->swimlane->getList($project['id'], false, true); + + 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', $this->color->getDefaultColor()), + 'owner_id' => $this->request->getIntegerParam('owner_id'), + 'another_task' => $this->request->getIntegerParam('another_task'), + ); + } + + $this->response->html($this->template->$method('task_creation/form', array( + 'ajax' => $this->request->isAjax(), + 'errors' => $errors, + 'values' => $values + array('project_id' => $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']), + 'swimlanes_list' => $swimlanes_list, + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => $project['name'].' > '.t('New task') + ))); + } + + /** + * Validate and save a new task + * + * @access public + */ + public function save() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateCreation($values); + + if ($valid) { + + if ($this->taskCreation->create($values)) { + $this->session->flash(t('Task created successfully.')); + + if (isset($values['another_task']) && $values['another_task'] == 1) { + unset($values['title']); + unset($values['description']); + $this->response->redirect($this->helper->url->to('taskcreation', 'create', $values)); + } + else { + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project['id']))); + } + } + else { + $this->session->flashError(t('Unable to create your task.')); + } + } + + $this->create($values, $errors); + } +} diff --git a/sources/app/Controller/Taskduplication.php b/sources/app/Controller/Taskduplication.php new file mode 100644 index 0000000..aebbcfc --- /dev/null +++ b/sources/app/Controller/Taskduplication.php @@ -0,0 +1,145 @@ +getTask(); + + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + $task_id = $this->taskDuplication->duplicate($task['id']); + + if ($task_id > 0) { + $this->session->flash(t('Task created successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } else { + $this->session->flashError(t('Unable to create this task.')); + $this->response->redirect($this->helper->url->to('taskduplication', 'duplicate', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + } + + $this->response->html($this->taskLayout('task_duplication/duplicate', array( + 'task' => $task, + ))); + } + + /** + * Move a task to another project + * + * @access public + */ + public function move() + { + $task = $this->getTask(); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + list($valid,) = $this->taskValidator->validateProjectModification($values); + + if ($valid && $this->taskDuplication->moveToProject($task['id'], + $values['project_id'], + $values['swimlane_id'], + $values['column_id'], + $values['category_id'], + $values['owner_id'])) { + + $this->session->flash(t('Task updated successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $values['project_id'], 'task_id' => $task['id']))); + } + + $this->session->flashError(t('Unable to update your task.')); + } + + $this->chooseDestination($task, 'task_duplication/move'); + } + + /** + * Duplicate a task to another project + * + * @access public + */ + public function copy() + { + $task = $this->getTask(); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + list($valid,) = $this->taskValidator->validateProjectModification($values); + + if ($valid && $this->taskDuplication->duplicateToProject($task['id'], + $values['project_id'], + $values['swimlane_id'], + $values['column_id'], + $values['category_id'], + $values['owner_id'])) { + + $this->session->flash(t('Task created successfully.')); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + + $this->session->flashError(t('Unable to create your task.')); + } + + $this->chooseDestination($task, 'task_duplication/copy'); + } + + /** + * Choose destination when move/copy task to another project + * + * @access private + * @param array $task + * @param string $template + */ + private function chooseDestination(array $task, $template) + { + $values = array(); + $projects_list = $this->projectPermission->getActiveMemberProjects($this->userSession->getId()); + + unset($projects_list[$task['project_id']]); + + if (! empty($projects_list)) { + $dst_project_id = $this->request->getIntegerParam('dst_project_id', key($projects_list)); + + $swimlanes_list = $this->swimlane->getList($dst_project_id, false, true); + $columns_list = $this->board->getColumnsList($dst_project_id); + $categories_list = $this->category->getList($dst_project_id); + $users_list = $this->projectPermission->getMemberList($dst_project_id); + + $values = $this->taskDuplication->checkDestinationProjectValues($task); + $values['project_id'] = $dst_project_id; + } + else { + $swimlanes_list = array(); + $columns_list = array(); + $categories_list = array(); + $users_list = array(); + } + + $this->response->html($this->taskLayout($template, array( + 'values' => $values, + 'task' => $task, + 'projects_list' => $projects_list, + 'swimlanes_list' => $swimlanes_list, + 'columns_list' => $columns_list, + 'categories_list' => $categories_list, + 'users_list' => $users_list, + ))); + } +} diff --git a/sources/app/Controller/Taskmodification.php b/sources/app/Controller/Taskmodification.php new file mode 100644 index 0000000..56d2b9f --- /dev/null +++ b/sources/app/Controller/Taskmodification.php @@ -0,0 +1,212 @@ +getTask(); + $this->taskModification->update(array('id' => $task['id'], 'date_started' => time())); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + + /** + * Update time tracking information + * + * @access public + */ + public function time() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid,) = $this->taskValidator->validateTimeModification($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($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + + /** + * Edit description form + * + * @access public + */ + public function description() + { + $task = $this->getTask(); + $ajax = $this->request->isAjax() || $this->request->getIntegerParam('ajax'); + + if ($this->request->isPost()) { + + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateDescriptionCreation($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($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); + } + else { + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + } + } + else { + $values = $task; + $errors = array(); + } + + $params = array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'ajax' => $ajax, + ); + + if ($ajax) { + $this->response->html($this->template->render('task_modification/edit_description', $params)); + } + else { + $this->response->html($this->taskLayout('task_modification/edit_description', $params)); + } + } + + /** + * Display a form to edit a task + * + * @access public + */ + public function edit(array $values = array(), array $errors = array()) + { + $task = $this->getTask(); + $ajax = $this->request->isAjax(); + + if (empty($values)) { + $values = $task; + } + + $this->dateParser->format($values, array('date_due')); + + $params = array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + '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, + ); + + if ($ajax) { + $this->response->html($this->template->render('task_modification/edit_task', $params)); + } + else { + $this->response->html($this->taskLayout('task_modification/edit_task', $params)); + } + } + + /** + * Validate and update a task + * + * @access public + */ + public function update() + { + $task = $this->getTask(); + $values = $this->request->getValues(); + + list($valid, $errors) = $this->taskValidator->validateModification($values); + + if ($valid) { + + if ($this->taskModification->update($values)) { + $this->session->flash(t('Task updated successfully.')); + + if ($this->request->getIntegerParam('ajax')) { + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $task['project_id']))); + } + else { + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + } + else { + $this->session->flashError(t('Unable to update your task.')); + } + } + + $this->edit($values, $errors); + } + + /** + * Edit recurrence form + * + * @access public + */ + public function recurrence() + { + $task = $this->getTask(); + + 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.')); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + } + else { + $values = $task; + $errors = array(); + } + + $params = array( + 'values' => $values, + 'errors' => $errors, + 'task' => $task, + 'recurrence_status_list' => $this->task->getRecurrenceStatusList(), + 'recurrence_trigger_list' => $this->task->getRecurrenceTriggerList(), + 'recurrence_timeframe_list' => $this->task->getRecurrenceTimeframeList(), + 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), + ); + + $this->response->html($this->taskLayout('task_modification/edit_recurrence', $params)); + } +} diff --git a/sources/app/Controller/Taskstatus.php b/sources/app/Controller/Taskstatus.php new file mode 100644 index 0000000..a47d9da --- /dev/null +++ b/sources/app/Controller/Taskstatus.php @@ -0,0 +1,79 @@ +getTask(); + $redirect = $this->request->getStringParam('redirect'); + + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + + if ($this->taskStatus->close($task['id'])) { + $this->session->flash(t('Task closed successfully.')); + } else { + $this->session->flashError(t('Unable to close this task.')); + } + + 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']))); + } + + if ($this->request->isAjax()) { + $this->response->html($this->template->render('task_status/close', array( + 'task' => $task, + 'redirect' => $redirect, + ))); + } + + $this->response->html($this->taskLayout('task_status/close', array( + 'task' => $task, + 'redirect' => $redirect, + ))); + } + + /** + * Open a task + * + * @access public + */ + public function open() + { + $task = $this->getTask(); + + if ($this->request->getStringParam('confirmation') === 'yes') { + + $this->checkCSRFParam(); + + 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($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } + + $this->response->html($this->taskLayout('task_status/open', array( + 'task' => $task, + ))); + } +} diff --git a/sources/app/Controller/Timer.php b/sources/app/Controller/Timer.php new file mode 100644 index 0000000..2a4531b --- /dev/null +++ b/sources/app/Controller/Timer.php @@ -0,0 +1,35 @@ +request->getIntegerParam('project_id'); + $task_id = $this->request->getIntegerParam('task_id'); + $subtask_id = $this->request->getIntegerParam('subtask_id'); + $timer = $this->request->getStringParam('timer'); + + if ($timer === 'start') { + $this->subtaskTimeTracking->logStartTime($subtask_id, $this->userSession->getId()); + } + else if ($timer === 'stop') { + $this->subtaskTimeTracking->logEndTime($subtask_id, $this->userSession->getId()); + $this->subtaskTimeTracking->updateTaskTimeTracking($task_id); + } + + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $project_id, 'task_id' => $task_id)).'#subtasks'); + } +} diff --git a/sources/app/Core/Base.php b/sources/app/Core/Base.php new file mode 100644 index 0000000..3db0cf7 --- /dev/null +++ b/sources/app/Core/Base.php @@ -0,0 +1,118 @@ +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/sources/app/Core/EmailClient.php b/sources/app/Core/EmailClient.php new file mode 100644 index 0000000..b198650 --- /dev/null +++ b/sources/app/Core/EmailClient.php @@ -0,0 +1,49 @@ +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 'sendgrid': + $this->sendgrid->sendEmail($email, $name, $subject, $html, $author); + break; + 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/sources/app/Core/Lexer.php b/sources/app/Core/Lexer.php new file mode 100644 index 0000000..d7e6fde --- /dev/null +++ b/sources/app/Core/Lexer.php @@ -0,0 +1,162 @@ + 'T_ASSIGNEE', + "/^(color:)/" => 'T_COLOR', + "/^(due:)/" => 'T_DUE', + "/^(updated:)/" => 'T_UPDATED', + "/^(modified:)/" => 'T_UPDATED', + "/^(created:)/" => 'T_CREATED', + "/^(status:)/" => 'T_STATUS', + "/^(description:)/" => 'T_DESCRIPTION', + "/^(category:)/" => 'T_CATEGORY', + "/^(column:)/" => 'T_COLUMN', + "/^(project:)/" => 'T_PROJECT', + "/^(swimlane:)/" => 'T_SWIMLANE', + "/^(ref:)/" => 'T_REFERENCE', + "/^(reference:)/" => 'T_REFERENCE', + "/^(\s+)/" => 'T_WHITESPACE', + '/^([<=>]{0,2}[0-9]{4}-[0-9]{2}-[0-9]{2})/' => 'T_DATE', + '/^(yesterday|tomorrow|today)/' => 'T_DATE', + '/^("(.*?)")/' => 'T_STRING', + "/^(\w+)/" => 'T_STRING', + "/^(#\d+)/" => 'T_STRING', + ); + + /** + * Tokenize input string + * + * @access public + * @param string $input + * @return array + */ + public function tokenize($input) + { + $tokens = array(); + $this->offset = 0; + + while (isset($input[$this->offset])) { + + $result = $this->match(substr($input, $this->offset)); + + if ($result === false) { + return array(); + } + + $tokens[] = $result; + } + + return $tokens; + } + + /** + * Find a token that match and move the offset + * + * @access public + * @param string $string + * @return array|boolean + */ + public function match($string) + { + foreach ($this->tokenMap as $pattern => $name) { + if (preg_match($pattern, $string, $matches)) { + + $this->offset += strlen($matches[1]); + + return array( + 'match' => trim($matches[1], '"'), + 'token' => $name, + ); + } + } + + return false; + } + + /** + * Change the output of tokenizer to be easily parsed by the database filter + * + * Example: ['T_ASSIGNEE' => ['user1', 'user2'], 'T_TITLE' => 'task title'] + * + * @access public + * @param array $tokens + * @return array + */ + public function map(array $tokens) + { + $map = array( + 'T_TITLE' => '', + ); + + while (false !== ($token = current($tokens))) { + + switch ($token['token']) { + case 'T_ASSIGNEE': + case 'T_COLOR': + case 'T_CATEGORY': + case 'T_COLUMN': + case 'T_PROJECT': + case 'T_SWIMLANE': + $next = next($tokens); + + if ($next !== false && $next['token'] === 'T_STRING') { + $map[$token['token']][] = $next['match']; + } + + break; + + case 'T_STATUS': + case 'T_DUE': + case 'T_UPDATED': + case 'T_CREATED': + case 'T_DESCRIPTION': + case 'T_REFERENCE': + $next = next($tokens); + + if ($next !== false && ($next['token'] === 'T_DATE' || $next['token'] === 'T_STRING')) { + $map[$token['token']] = $next['match']; + } + + break; + + default: + $map['T_TITLE'] .= $token['match']; + break; + } + + next($tokens); + } + + $map['T_TITLE'] = trim($map['T_TITLE']); + + if (empty($map['T_TITLE'])) { + unset($map['T_TITLE']); + } + + return $map; + } +} diff --git a/sources/app/Core/OAuth2.php b/sources/app/Core/OAuth2.php new file mode 100644 index 0000000..a7d04f3 --- /dev/null +++ b/sources/app/Core/OAuth2.php @@ -0,0 +1,120 @@ +clientId = $clientId; + $this->secret = $secret; + $this->callbackUrl = $callbackUrl; + $this->authUrl = $authUrl; + $this->tokenUrl = $tokenUrl; + $this->scopes = $scopes; + + return $this; + } + + /** + * Get authorization url + * + * @access public + * @return string + */ + public function getAuthorizationUrl() + { + $params = array( + 'response_type' => 'code', + 'client_id' => $this->clientId, + 'redirect_uri' => $this->callbackUrl, + 'scope' => implode(' ', $this->scopes), + ); + + return $this->authUrl.'?'.http_build_query($params); + } + + /** + * Get authorization header + * + * @access public + * @return string + */ + public function getAuthorizationHeader() + { + if (strtolower($this->tokenType) === 'bearer') { + return 'Authorization: Bearer '.$this->accessToken; + } + + return ''; + } + + /** + * Get access token + * + * @access public + * @param string $code + * @return string + */ + public function getAccessToken($code) + { + if (empty($this->accessToken) && ! empty($code)) { + + $params = array( + 'code' => $code, + 'client_id' => $this->clientId, + 'client_secret' => $this->secret, + 'redirect_uri' => $this->callbackUrl, + 'grant_type' => 'authorization_code', + ); + + $response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true); + + $this->tokenType = isset($response['token_type']) ? $response['token_type'] : ''; + $this->accessToken = isset($response['access_token']) ? $response['access_token'] : ''; + } + + return $this->accessToken; + } + + /** + * Set access token + * + * @access public + * @param string $token + * @param string $type + * @return string + */ + public function setAccessToken($token, $type = 'bearer') + { + $this->accessToken = $token; + $this->tokenType = $type; + } +} diff --git a/sources/app/Event/TaskLinkEvent.php b/sources/app/Event/TaskLinkEvent.php new file mode 100644 index 0000000..9499eef --- /dev/null +++ b/sources/app/Event/TaskLinkEvent.php @@ -0,0 +1,7 @@ +router->getController(); + } + + /** + * Get router action + * + * @access public + * @return string + */ + public function getRouterAction() + { + return $this->router->getAction(); + } + + /** + * 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 = '
'.$this->helper->e($this->session['flash_message']).'
'; + unset($this->session['flash_message']); + } + else if (isset($this->session['flash_error_message'])) { + $html = '
'.$this->helper->e($this->session['flash_error_message']).'
'; + unset($this->session['flash_error_message']); + } + + return $html; + } +} diff --git a/sources/app/Helper/Asset.php b/sources/app/Helper/Asset.php new file mode 100644 index 0000000..fd555e0 --- /dev/null +++ b/sources/app/Helper/Asset.php @@ -0,0 +1,62 @@ +helper->url->dir().$filename.'?'.filemtime($filename).'">'; + } + + /** + * 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 ''; + } + + /** + * Get custom css + * + * @access public + * @return string + */ + public function customCss() + { + if ($this->config->get('application_stylesheet')) { + return ''; + } + + return ''; + } + + /** + * Get CSS for task colors + * + * @access public + * @return string + */ + public function colorCss() + { + return ''; + } +} diff --git a/sources/app/Helper/Board.php b/sources/app/Helper/Board.php new file mode 100644 index 0000000..452a3b7 --- /dev/null +++ b/sources/app/Helper/Board.php @@ -0,0 +1,24 @@ +userSession->isBoardCollapsed($project_id); + } +} diff --git a/sources/app/Helper/Dt.php b/sources/app/Helper/Dt.php new file mode 100644 index 0000000..b338fdc --- /dev/null +++ b/sources/app/Helper/Dt.php @@ -0,0 +1,114 @@ +diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds'); + } + + /** + * 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 < 900) { + return t('<15m'); + } + if ($diff < 1200) { + return t('<30m'); + } + else if ($diff < 3600) { + return t('<1h'); + } + else if ($diff < 86400) { + return '~'.t('%dh', $diff / 3600); + } + + return t('%dd', ($now - $timestamp) / 86400); + } + + /** + * 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/sources/app/Helper/File.php b/sources/app/Helper/File.php new file mode 100644 index 0000000..a35e428 --- /dev/null +++ b/sources/app/Helper/File.php @@ -0,0 +1,56 @@ +'; + } + + /** + * 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 '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 = ''; + $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 ''; + } + + /** + * 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 ''; + } + + /** + * 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 ''; + } + + /** + * 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 = ''; + $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 = 'formValue($values, $name).' class="'.$class.'" '; + $html .= implode(' ', $attributes).'>'; + + if (in_array('required', $attributes)) { + $html .= '*'; + } + + $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 .= ''; + } + + 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/sources/app/Helper/Subtask.php b/sources/app/Helper/Subtask.php new file mode 100644 index 0000000..6348ebd --- /dev/null +++ b/sources/app/Helper/Subtask.php @@ -0,0 +1,42 @@ +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/sources/app/Helper/Task.php b/sources/app/Helper/Task.php new file mode 100644 index 0000000..79c412e --- /dev/null +++ b/sources/app/Helper/Task.php @@ -0,0 +1,37 @@ +color->getList(); + } + + 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/sources/app/Helper/Text.php b/sources/app/Helper/Text.php new file mode 100644 index 0000000..4b8e99b --- /dev/null +++ b/sources/app/Helper/Text.php @@ -0,0 +1,72 @@ +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)]; + } + + /** + * 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/sources/app/Helper/Url.php b/sources/app/Helper/Url.php new file mode 100644 index 0000000..22e9035 --- /dev/null +++ b/sources/app/Helper/Url.php @@ -0,0 +1,170 @@ +link($label, 'doc', 'show', array('file' => $file), false, '', '', true); + } + + /** + * 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 ''.$label.''; + } + + /** + * HTML 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 + * @param boolean $absolute Absolute or relative link + * @return string + */ + public function href($controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false) + { + return $this->build('&', $controller, $action, $params, $csrf, $anchor, $absolute); + } + + /** + * Generate controller/action url + * + * @access public + * @param string $controller Controller name + * @param string $action Action name + * @param array $params Url parameters + * @param string $anchor Link Anchor + * @param boolean $absolute Absolute or relative link + * @return string + */ + public function to($controller, $action, array $params = array(), $anchor = '', $absolute = false) + { + return $this->build('&', $controller, $action, $params, false, $anchor, $absolute); + } + + /** + * Get application base url + * + * @access public + * @return string + */ + public function base() + { + if (empty($this->base)) { + $this->base = $this->config->get('application_url') ?: $this->server(); + } + + return $this->base; + } + + /** + * Get application base directory + * + * @access public + * @return string + */ + public function dir() + { + if (empty($this->directory) && isset($_SERVER['REQUEST_METHOD'])) { + $this->directory = str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])); + $this->directory = $this->directory !== '/' ? $this->directory.'/' : '/'; + $this->directory = str_replace('//', '/', $this->directory); + } + + return $this->directory; + } + + /** + * Get current server base url + * + * @access public + * @return string + */ + public function server() + { + if (empty($_SERVER['SERVER_NAME'])) { + return 'http://localhost/'; + } + + $url = Request::isHTTPS() ? 'https://' : 'http://'; + $url .= $_SERVER['SERVER_NAME']; + $url .= $_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443 ? '' : ':'.$_SERVER['SERVER_PORT']; + $url .= $this->dir() ?: '/'; + + return $url; + } + + /** + * Build relative url + * + * @access private + * @param string $separator Querystring argument separator + * @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 + * @param boolean $absolute Absolute or relative link + * @return string + */ + private function build($separator, $controller, $action, array $params = array(), $csrf = false, $anchor = '', $absolute = false) + { + $path = $this->router->findUrl($controller, $action, $params); + $qs = array(); + + if (empty($path)) { + $qs['controller'] = $controller; + $qs['action'] = $action; + $qs += $params; + } + + if ($csrf) { + $qs['csrf_token'] = Security::getCSRFToken(); + } + + if (! empty($qs)) { + $path .= '?'.http_build_query($qs, '', $separator); + } + + return ($absolute ? $this->base() : $this->dir()).$path.(empty($anchor) ? '' : '#'.$anchor); + } +} diff --git a/sources/app/Helper/User.php b/sources/app/Helper/User.php new file mode 100644 index 0000000..cb596fb --- /dev/null +++ b/sources/app/Helper/User.php @@ -0,0 +1,147 @@ +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(); + } + + /** + * Return if the logged user is project admin + * + * @access public + * @return boolean + */ + public function isProjectAdmin() + { + return $this->userSession->isProjectAdmin(); + } + + /** + * Check for project administration actions access (Project Admin group) + * + * @access public + * @return boolean + */ + public function isProjectAdministrationAllowed($project_id) + { + if ($this->userSession->isAdmin()) { + return true; + } + + return $this->memoryCache->proxy('acl', 'handleProjectAdminPermissions', $project_id); + } + + /** + * Check for project management actions access (Regular users who are Project Managers) + * + * @access public + * @return boolean + */ + public function isProjectManagementAllowed($project_id) + { + if ($this->userSession->isAdmin()) { + return true; + } + + return $this->memoryCache->proxy('acl', 'handleProjectManagerPermissions', $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 ''.$this->helper->e($alt).''; + } + + return ''; + } +} diff --git a/sources/app/Integration/HipchatWebhook.php b/sources/app/Integration/HipchatWebhook.php new file mode 100644 index 0000000..1d08e51 --- /dev/null +++ b/sources/app/Integration/HipchatWebhook.php @@ -0,0 +1,94 @@ +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 = ''; + $html .= ''.$project['name'].''.(isset($event['task']['title']) ? '
'.$event['task']['title'] : '').'
'; + $html .= $this->projectActivity->getTitle($event); + + if ($this->config->get('application_url')) { + $html .= '
'; + $html .= t('view the task on Kanboard').''; + } + + $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/sources/app/Integration/Jabber.php b/sources/app/Integration/Jabber.php new file mode 100644 index 0000000..eaf1c5a --- /dev/null +++ b/sources/app/Integration/Jabber.php @@ -0,0 +1,129 @@ +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->helper->url->to('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id), '', true); + } + + $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/sources/app/Integration/Mailgun.php b/sources/app/Integration/Mailgun.php new file mode 100644 index 0000000..1451b21 --- /dev/null +++ b/sources/app/Integration/Mailgun.php @@ -0,0 +1,97 @@ + 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/sources/app/Integration/Postmark.php b/sources/app/Integration/Postmark.php new file mode 100644 index 0000000..dbb70ae --- /dev/null +++ b/sources/app/Integration/Postmark.php @@ -0,0 +1,97 @@ + 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/sources/app/Integration/Sendgrid.php b/sources/app/Integration/Sendgrid.php new file mode 100644 index 0000000..902749f --- /dev/null +++ b/sources/app/Integration/Sendgrid.php @@ -0,0 +1,100 @@ + SENDGRID_API_USER, + 'api_key' => SENDGRID_API_KEY, + 'to' => $email, + 'toname' => $name, + 'from' => MAIL_FROM, + 'fromname' => $author, + 'html' => $html, + 'subject' => $subject, + ); + + $this->httpClient->postForm('https://api.sendgrid.com/api/mail.send.json', $payload); + } + + /** + * Parse incoming email + * + * @access public + * @param array $payload Incoming email + * @return boolean + */ + public function receiveEmail(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/sources/app/Integration/Smtp.php b/sources/app/Integration/Smtp.php new file mode 100644 index 0000000..ad2f30f --- /dev/null +++ b/sources/app/Integration/Smtp.php @@ -0,0 +1,71 @@ +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/sources/app/Library/password.php b/sources/app/Library/password.php new file mode 100644 index 0000000..c6e84cb --- /dev/null +++ b/sources/app/Library/password.php @@ -0,0 +1,227 @@ + + * @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/sources/app/Locale/cs_CZ/translations.php b/sources/app/Locale/cs_CZ/translations.php new file mode 100644 index 0000000..557a62c --- /dev/null +++ b/sources/app/Locale/cs_CZ/translations.php @@ -0,0 +1,1070 @@ + ',', + 'number.thousands_separator' => '.', + 'None' => 'žádné', + 'edit' => 'editovat', + 'Edit' => 'Editovat', + 'remove' => 'odstranit', + 'Remove' => 'Odstranit', + 'Update' => 'Akualizovat', + 'Yes' => 'Ano', + 'No' => 'Ne', + 'cancel' => 'Zrušit', + 'or' => 'nebo', + 'Yellow' => 'Žlutá', + 'Blue' => 'Modrá', + 'Green' => 'Zelená', + 'Purple' => 'Fialová', + 'Red' => 'Červená', + 'Orange' => 'Oranžová', + 'Grey' => 'Šedá', + // 'Brown' => '', + // 'Deep Orange' => '', + // 'Dark Grey' => '', + // 'Pink' => '', + // 'Teal' => '', + // 'Cyan' => '', + // 'Lime' => '', + // 'Light Green' => '', + // 'Amber' => '', + 'Save' => 'Uložit', + 'Login' => 'Přihlásit se', + 'Official website:' => 'Oficiální stránky:', + 'Unassigned' => 'Nepřiřazeno', + 'View this task' => 'Zobrazit úkol', + 'Remove user' => 'Odebrat uživatele', + 'Do you really want to remove this user: "%s"?' => 'Opravdu chcete odebrat uživatele: "%s"?', + 'New user' => 'Nový uživatel', + 'All users' => 'Všichni uživatelé', + 'Username' => 'Uživatelské jméno', + 'Password' => 'Heslo', + 'Administrator' => 'Administrátor', + 'Sign in' => 'Registrace', + 'Users' => 'Uživatelé', + 'No user' => 'Žádný uživatel', + 'Forbidden' => 'Zakázat projekt', + 'Access Forbidden' => 'Přístup zakázán', + 'Edit user' => 'Upravit uživatele', + 'Logout' => 'Odhlásit', + 'Bad username or password' => 'Chybné uživatelské jméno nebo heslo', + 'Edit project' => 'Editovat projekt', + 'Name' => 'Jméno', + 'Projects' => 'Projekty', + 'No project' => 'Žádný projekt', + 'Project' => 'Projekt', + 'Status' => 'Status', + 'Tasks' => 'Úkoly', + 'Board' => 'Nástěnka', + 'Actions' => 'Akce', + 'Inactive' => 'Neaktivní', + 'Active' => 'Aktivní', + 'Add this column' => 'Přidat sloupec', + '%d tasks on the board' => '%d úkolů na nástěnce', + '%d tasks in total' => '%d úkolů celkem', + 'Unable to update this board.' => 'Nástěnku není možné aktualizovat', + 'Edit board' => 'Editace nástěnky', + 'Disable' => 'Zakázat projekt', + 'Enable' => 'Povolit projekt', + 'New project' => 'Nový projekt', + 'Do you really want to remove this project: "%s"?' => 'Opravdu chcete vyjmout projekt: "%s"?', + 'Remove project' => 'Vyjmout projekt', + 'Edit the board for "%s"' => 'Editace nástěnky pro "%s" ', + 'All projects' => 'Všechny projekty', + 'Change columns' => 'Změna sloupců', + 'Add a new column' => 'Přidat nový sloupec', + 'Title' => 'Název', + 'Nobody assigned' => 'Nepřiřazena žádná osoba', + 'Assigned to %s' => 'Přiřazeno uživateli: %s', + 'Remove a column' => 'Vyjmout sloupec', + 'Remove a column from a board' => 'Vyjmout sloupec z nástěnky', + 'Unable to remove this column.' => 'Tento sloupec nelze odstranit', + 'Do you really want to remove this column: "%s"?' => 'Opravdu chcete vyjmout tento sloupec: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Tato akce vyjme všechny úkoly přiřazený k tomuto sloupci!', + 'Settings' => 'Nastavení', + 'Application settings' => 'Nastavení aplikace', + 'Language' => 'Čeština', + 'Webhook token:' => 'Webhook Token:', + 'API token:' => 'API Token:', + 'Database size:' => 'Velikost databáze:', + 'Download the database' => 'Stáhnout databázi', + 'Optimize the database' => 'Optimalizovat databázi', + '(VACUUM command)' => '(Vyčištění)', + '(Gzip compressed Sqlite file)' => '(Gzip )', + 'Close a task' => 'Uzavřít úkol', + 'Edit a task' => 'Editovat úkol', + 'Column' => 'Sloupec', + 'Color' => 'Barva', + 'Assignee' => 'Přiřazeno uživateli', + 'Create another task' => 'Vytvořit další úkol', + 'New task' => 'Nový úkol', + 'Open a task' => 'Otevřít úkol', + 'Do you really want to open this task: "%s"?' => 'Opravdu chcete znovuotevřít tento úkol: "%s"?', + 'Back to the board' => 'Zpět na nástěnku', + 'Created on %B %e, %Y at %k:%M %p' => 'Vytvořeno dne %d.%m.%Y v čase %H:%M', + 'There is nobody assigned' => 'Není přiřazeno žádnému uživateli', + 'Column on the board:' => 'Sloupec:', + 'Status is open' => 'Status je otevřený', + 'Status is closed' => 'Status je uzavřený', + 'Close this task' => 'Uzavřít úkol', + 'Open this task' => 'Aufgabe wieder öffnen', + 'There is no description.' => 'Bez popisu', + 'Add a new task' => 'Přidat nový úkol', + 'The username is required' => 'Uživatelské jméno je vyžadováno', + 'The maximum length is %d characters' => 'Maximální délka je %d znaků', + 'The minimum length is %d characters' => 'Minimální délka je %d znaků', + 'The password is required' => 'Heslo je vyžadováno', + 'This value must be an integer' => 'Je vyžadována číselná hodnota', + 'The username must be unique' => 'Uživatelské jméno musí být jedinečné', + 'The user id is required' => 'Uživatelské ID je vyžadováno', + 'Passwords don\'t match' => 'Heslo se neshoduje', + 'The confirmation is required' => 'Je vyžadováno potvrzení', + 'The project is required' => 'Projekt je vyžadován', + 'The id is required' => 'ID je vyžadováno', + 'The project id is required' => 'ID projektu je vyžadováno', + 'The project name is required' => 'Jméno projektu je vyžadováno', + 'This project must be unique' => 'Jméno projektu musí být jedinečné', + 'The title is required' => 'Nadpis je vyžadován', + 'Settings saved successfully.' => 'Nastavení bylo úspěšně uloženo', + 'Unable to save your settings.' => 'Vaše nastavení nelze uložit.', + 'Database optimization done.' => 'Optimalizace databáze byla provedena.', + 'Your project have been created successfully.' => 'Projekt byl úspěšně vytvořen.', + 'Unable to create your project.' => 'Projekt nelze vytvořit.', + 'Project updated successfully.' => 'Projekt byl úspěšně aktualizován', + 'Unable to update this project.' => 'Projekt nebylo možné aktualizovat.', + 'Unable to remove this project.' => 'Projekt nebylo možné odstranit.', + 'Project removed successfully.' => 'Projekt byl odstraněn.', + 'Project activated successfully.' => 'Projekt byl povolen.', + 'Unable to activate this project.' => 'Aktivace projektu selhala.', + 'Project disabled successfully.' => 'Projekt byl zakázán.', + 'Unable to disable this project.' => 'Zakázání projektu selhalo.', + 'Unable to open this task.' => 'Nelze otevření tento úkol.', + 'Task opened successfully.' => 'Úkol byl úspěšně otevřen.', + 'Unable to close this task.' => 'Nelze uzavřít tento úkol.', + 'Task closed successfully.' => 'Úkol byl úspěšně uzavřen.', + 'Unable to update your task.' => 'Aktualizace úkolu se nezdařila.', + 'Task updated successfully.' => 'Úkol byl úspěšně aktualizován.', + 'Unable to create your task.' => 'Úkol nelze vytvořit.', + 'Task created successfully.' => 'Úkol byl úspěšně vytvořen.', + 'User created successfully.' => 'Uživatel byl úspěšně vytvořen.', + 'Unable to create your user.' => 'Uživatele nebylo možné vytvořit.', + 'User updated successfully.' => 'Uživatel byl úspěšně aktualizován.', + 'Unable to update your user.' => 'Uživatele nebylo možné aktualizovat.', + 'User removed successfully.' => 'Uživatel byl vymazán.', + 'Unable to remove this user.' => 'Uživatele nebylo možné odebrat.', + 'Board updated successfully.' => 'Nástěnka byla úspěšně aktualizována.', + 'Ready' => 'Připraveno', + 'Backlog' => 'Nevyřízené', + 'Work in progress' => 'V řešení', + 'Done' => 'Dokončeno', + 'Application version:' => 'Verze:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Dokončeno %d.%m.%Y v %H:%M', + '%B %e, %Y at %k:%M %p' => '%d.%m.%Y v %H:%M', + 'Date created' => 'Datum vytvoření', + 'Date completed' => 'Datum dokončení', + 'Id' => 'ID', + '%d closed tasks' => '%d dokončených úkolů', + 'No task for this project' => 'Tento projekt nemá žádné úkoly', + 'Public link' => 'Veřejný odkaz', + 'There is no column in your project!' => 'V projektu není žádný sloupec!', + 'Change assignee' => 'Změna přiřazení k uživatelům', + 'Change assignee for the task "%s"' => 'Změna přiřazení uživatele pro úkol "%s"', + 'Timezone' => 'Časová zóna', + 'Sorry, I didn\'t find this information in my database!' => 'Omlouváme se, tuto informaci nelze nalézt!', + 'Page not found' => 'Stránka nenalezena', + 'Complexity' => 'Složitost', + 'Task limit' => 'Maximální počet úkolů', + 'Task count' => 'Počet úkolů', + 'Edit project access list' => 'Upravit přístupový seznam projektu', + 'Allow this user' => 'Povolit tomuto uživateli', + 'Don\'t forget that administrators have access to everything.' => 'Nezapomeňte, že administrátoři mají přistup ke všem údajům.', + 'Revoke' => 'Odebrat', + 'List of authorized users' => 'Seznam autorizovaných uživatelů', + 'User' => 'Uživatel', + 'Nobody have access to this project.' => 'Nikdo nemá přístup k tomuto projektu.', + 'Comments' => 'Komentáře', + 'Write your text in Markdown' => 'Můžete použít i Markdown-syntaxi', + 'Leave a comment' => 'Zanechte komentář', + 'Comment is required' => 'Komentář je vyžadován', + 'Leave a description' => 'Vložte popis', + 'Comment added successfully.' => 'Komentář byl úspěšně přidán.', + 'Unable to create your comment.' => 'Komentář nelze vytvořit.', + 'Edit this task' => 'Editace úkolu', + 'Due Date' => 'Datum splnění', + 'Invalid date' => 'Neplatné datum', + 'Must be done before %B %e, %Y' => 'Musí být dokončeno do %d.%m.%Y ', + '%B %e, %Y' => '%d.%m.%Y', + // '%b %e, %Y' => '', + 'Automatic actions' => 'Automaticky vykonávané akce', + 'Your automatic action have been created successfully.' => 'Vaše akce byla úspěšně vytvořena.', + 'Unable to create your automatic action.' => 'Vaší akci nebylo možné vytvořit.', + 'Remove an action' => 'Odstranit akci', + 'Unable to remove this action.' => 'Tuto akci nelze odstranit.', + 'Action removed successfully.' => 'Akce byla úspěšně odstraněna.', + 'Automatic actions for the project "%s"' => 'Automaticky vykonávané akce pro projekt "%s"', + 'Defined actions' => 'Definované akce', + 'Add an action' => 'Přidat akci', + 'Event name' => 'Název události', + 'Action name' => 'Název akce', + 'Action parameters' => 'Parametry akce', + 'Action' => 'Akce', + 'Event' => 'Událost', + 'When the selected event occurs execute the corresponding action.' => 'Kdykoliv se vybraná událost objeví, vykonat odpovídající akci.', + 'Next step' => 'Další krok', + 'Define action parameters' => 'Definovat parametry akce', + 'Save this action' => 'Uložit akci', + 'Do you really want to remove this action: "%s"?' => 'Skutečně chcete odebrat tuto akci: "%s"?', + 'Remove an automatic action' => 'Odebrat automaticky prováděnou akci', + 'Assign the task to a specific user' => 'Přiřadit tento úkol konkrétnímu uživateli', + 'Assign the task to the person who does the action' => 'Přiřadit úkol osobě, která akci provádí', + 'Duplicate the task to another project' => 'Duplikovat úkol do jiného projektu', + 'Move a task to another column' => 'Přesun úkolu do jiného sloupce', + 'Task modification' => 'Modifikace úkolu', + 'Task creation' => 'Vytváření úkolu', + 'Closing a task' => 'Uzavření úkolu', + 'Assign a color to a specific user' => 'Přiřadit barvu konkrétnímu uživateli', + 'Column title' => 'Název sloupce', + 'Position' => 'Pozice', + 'Move Up' => 'Posunout nahoru', + 'Move Down' => 'Posunout dolu', + 'Duplicate to another project' => 'Vytvořit kopii v jiném projektu', + 'Duplicate' => 'Vytvořit kopii', + 'link' => 'Link', + 'Comment updated successfully.' => 'Komentář byl úspěšně aktualizován.', + 'Unable to update your comment.' => 'Nelze upravit Váš komentář.', + 'Remove a comment' => 'Odebrat komentář', + 'Comment removed successfully.' => 'Komentář byl smazán.', + 'Unable to remove this comment.' => 'Komentář nelze odebrat.', + 'Do you really want to remove this comment?' => 'Skutečně chcete odebrat tento komentář?', + 'Only administrators or the creator of the comment can access to this page.' => 'Přístup k této stránce mají pouze administrátoři nebo vlastníci komentáře.', + 'Current password for the user "%s"' => 'Aktuální heslo pro uživatele "%s"', + 'The current password is required' => 'Heslo je vyžadováno', + 'Wrong password' => 'Neplatné heslo', + 'Unknown' => 'Neznámý', + 'Last logins' => 'Poslední přihlášení', + 'Login date' => 'Datum přihlášení', + 'Authentication method' => 'Autentifikační metoda', + 'IP address' => 'IP adresa', + 'User agent' => 'User Agent', + 'Persistent connections' => 'Trvalé připojení', + 'No session.' => 'doposud žádná relace.', + 'Expiration date' => 'Datum expirace', + 'Remember Me' => 'Zapamatovat si', + 'Creation date' => 'Datum vytvoření', + 'Everybody' => 'Kdokoliv', + 'Open' => 'Otevřené', + 'Closed' => 'Uzavřené', + 'Search' => 'Vyhledat', + 'Nothing found.' => 'Nenalezena žádná položka.', + 'Due date' => 'Plánovaný termín', + 'Others formats accepted: %s and %s' => 'Akceptovány jiné formáty: %s und %s', + 'Description' => 'Podrobný popis', + '%d comments' => '%d komentářů', + '%d comment' => '%d komentář', + 'Email address invalid' => 'Neplatná e-mailová adresa', + // 'Your external account is not linked anymore to your profile.' => '', + // 'Unable to unlink your external account.' => '', + // 'External authentication failed' => '', + // 'Your external account is linked to your profile successfully.' => '', + 'Email' => 'E-Mail', + 'Link my Google Account' => 'Propojit s Google účtem', + 'Unlink my Google Account' => 'Odpojit Google účet', + 'Login with my Google Account' => 'Přihlášení pomocí Google účtu', + 'Project not found.' => 'Projekt nebyl nalezen.', + 'Task removed successfully.' => 'Úkol byl úspěšně odebrán.', + 'Unable to remove this task.' => 'Tento úkol nelze odebrat.', + 'Remove a task' => 'Odebrat úkol', + 'Do you really want to remove this task: "%s"?' => 'Opravdu chcete odebrat úkol: "%s"?', + 'Assign automatically a color based on a category' => 'Automaticky přiřadit barvu v závislosti na kategorii', + 'Assign automatically a category based on a color' => 'Automaticky přiřadit kategorii v závislosti na barvě', + 'Task creation or modification' => 'Vytváření nebo úprava úkolu', + 'Category' => 'Kategorie', + 'Category:' => 'Kategorie:', + 'Categories' => 'Kategorie', + 'Category not found.' => 'Kategorie není nalezena.', + 'Your category have been created successfully.' => 'Kategorie byla úspěšně vytvořena.', + 'Unable to create your category.' => 'Kategorii nelze vytvořit.', + 'Your category have been updated successfully.' => 'Kategorie byla úspěšně aktualizována.', + 'Unable to update your category.' => 'Kategorii nelze aktualizovat.', + 'Remove a category' => 'Odstranit kategorii', + 'Category removed successfully.' => 'Kategorie byla odstraněna.', + 'Unable to remove this category.' => 'Kategorie nemhla být odstraněna.', + 'Category modification for the project "%s"' => 'Aktualizace kategoire pro projekt "%s" ', + 'Category Name' => 'Název kategorie', + 'Add a new category' => 'Přidat kategorii', + 'Do you really want to remove this category: "%s"?' => 'Skutečně chcete odebrat kategorii: "%s"?', + 'All categories' => 'Všechny kategorie', + 'No category' => 'Žádná kategorie', + 'The name is required' => 'Název je vyžadován', + 'Remove a file' => 'Odstranit sougor', + 'Unable to remove this file.' => 'Soubor nelze odebrat.', + 'File removed successfully.' => 'Soubor byl úspěšně odebrán.', + 'Attach a document' => 'Vložit dokument', + 'Do you really want to remove this file: "%s"?' => 'Skutečně chcete odebrat soubor: "%s"?', + 'open' => 'Otevřít', + 'Attachments' => 'Přílohy', + 'Edit the task' => 'Upravit úkol', + 'Edit the description' => 'Upravit popis', + 'Add a comment' => 'Přidat komentář', + 'Edit a comment' => 'Upravit komentář', + 'Summary' => 'Souhrn', + 'Time tracking' => 'Sledování času', + 'Estimate:' => 'Odhad:', + 'Spent:' => 'Stráveno:', + 'Do you really want to remove this sub-task?' => 'Skutečně chcete odebrat dílčí úkol?', + 'Remaining:' => 'Zbývá:', + 'hours' => 'hodin', + 'spent' => 'Stráveno', + 'estimated' => 'odhadnuto', + 'Sub-Tasks' => 'Dílčí úkoly', + 'Add a sub-task' => 'Přidat dílčí úkol', + 'Original estimate' => 'Časový odhad', + 'Create another sub-task' => 'Vytvořit další dílčí úkol', + 'Time spent' => 'Strávený čas', + 'Edit a sub-task' => 'Upravid dílčí úkol', + 'Remove a sub-task' => 'Odstranit dílčí úkol', + 'The time must be a numeric value' => 'Zadejte numerickou hodnotu času', + 'Todo' => 'Seznam úkolů', + 'In progress' => 'Zpracováváme', + 'Sub-task removed successfully.' => 'Dílčí úkol byl smazán.', + 'Unable to remove this sub-task.' => 'Tento dílčí úkol nelze odebrat.', + 'Sub-task updated successfully.' => 'Dílčí úkol byl aktualizován.', + 'Unable to update your sub-task.' => 'Nelze aktualizovat dílčí úkol.', + 'Unable to create your sub-task.' => 'Nelze vytvořit dílčí úkol.', + 'Sub-task added successfully.' => 'Dílčí úkol byl úspěšně přidán.', + 'Maximum size: ' => 'Maximální velikost: ', + 'Unable to upload the file.' => 'Soubor nelze nahrát.', + 'Display another project' => 'Zobrazit jiný projekt', + // 'Login with my Github Account' => '', + // 'Link my Github Account' => '', + // 'Unlink my Github Account' => '', + 'Created by %s' => 'Vytvořeno uživatelem %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Poslední úprava dne %d.%m.%Y v čase %H:%M', + 'Tasks Export' => 'Export úkolů', + 'Tasks exportation for "%s"' => 'Export úkolů pro "%s"', + 'Start Date' => 'Počáteční datum', + 'End Date' => 'Konečné datum', + 'Execute' => 'Spustit', + 'Task Id' => 'Úkol ID', + 'Creator' => 'Vlastník', + 'Modification date' => 'Datum úpravy', + 'Completion date' => 'Datum dokončení', + 'Clone' => 'Kopie', + 'Project cloned successfully.' => 'Kopie projektu byla úspěšně vytvořena.', + 'Unable to clone this project.' => 'Kopii projektu nelze vytvořit.', + 'Email notifications' => 'Upozornění E-Mailem ', + 'Enable email notifications' => 'Povolit upozornění pomocí e-mailů', + 'Task position:' => 'Pořadí úkolu:', + 'The task #%d have been opened.' => 'Úkol #%d byl znovu otevřen.', + 'The task #%d have been closed.' => 'Úkol #%d byl uzavřen.', + 'Sub-task updated' => 'Dílčí úkol byl aktualizován', + 'Title:' => 'Nadpis', + 'Status:' => 'Stav', + 'Assignee:' => 'Přiřazeno:', + 'Time tracking:' => 'Sledování času:', + 'New sub-task' => 'Nový dílčí úkol', + 'New attachment added "%s"' => 'Byla přidána nová příloha "%s".', + 'Comment updated' => 'Komentář byl aktualizován.', + 'New comment posted by %s' => 'Nový komentář publikovaný uživatelem %s', + 'New attachment' => 'Nová příloha', + 'New comment' => 'Nový komentář', + 'New subtask' => 'Nový dílčí úkol', + 'Subtask updated' => 'Dílčí úkol byl aktualizován', + 'Task updated' => 'Úkol byl aktualizován', + 'Task closed' => 'Úkol byl uzavřen', + 'Task opened' => 'Úkol byl otevřen', + 'I want to receive notifications only for those projects:' => 'Přeji si dostávat upozornění pouze pro následující projekty:', + 'view the task on Kanboard' => 'Zobrazit úkol na Kanboard', + 'Public access' => 'Veřejný přístup', + 'User management' => 'Správa uživatelů', + 'Active tasks' => 'Aktivní úkoly', + 'Disable public access' => 'Zakázat veřejný přístup', + 'Enable public access' => 'Povolit veřejný přístup', + 'Public access disabled' => 'Veřejný přístup zakázán', + 'Do you really want to disable this project: "%s"?' => 'Opravdu chcete zakázat projekt: "%s"', + 'Do you really want to enable this project: "%s"?' => 'Opravdu chcete znovu povolit projekt: "%s"', + 'Project activation' => 'Aktivace projektu', + 'Move the task to another project' => 'Přesunutí úkolu do jiného projektu', + 'Move to another project' => 'Přesunout do jiného projektu', + 'Do you really want to duplicate this task?' => 'Opravdu chcete vytořit kopii tohoto úkolu?', + 'Duplicate a task' => 'Vytvořit kopii úkolu', + 'External accounts' => 'Externí účty', + 'Account type' => 'typ účtu', + 'Local' => 'Lokální', + 'Remote' => 'Vzdálený', + 'Enabled' => 'Povoleno', + 'Disabled' => 'Zakázáno', + 'Google account linked' => 'Google účet byl propojen', + 'Github account linked' => 'Mit Githubaccount verbunden', + 'Username:' => 'Uživatelské jméno:', + 'Name:' => 'Jméno:', + 'Email:' => 'e-mail', + 'Notifications:' => 'Upozornění:', + 'Notifications' => 'Upozornění', + 'Group:' => 'Skupina', + 'Regular user' => 'Pravidelný uživatel', + 'Account type:' => 'Typ účtu:', + 'Edit profile' => 'Upravit profil', + 'Change password' => 'Změnit heslo', + 'Password modification' => 'Změna hesla', + 'External authentications' => 'Vzdálená autorizace', + 'Google Account' => 'Google účet', + 'Github Account' => 'github účet', + 'Never connected.' => 'Zatím nikdy nespojen.', + 'No account linked.' => 'Žádné propojení účtu.', + 'Account linked.' => 'Propojení účtu', + 'No external authentication enabled.' => 'Není povolena žádná vzdálená autorizace.', + 'Password modified successfully.' => 'Heslo bylo úspěšně změněno.', + 'Unable to change the password.' => 'Nelze změnit heslo.', + 'Change category for the task "%s"' => 'Změna kategorie pro úkol "%s" ', + 'Change category' => 'Změna kategorie', + '%s updated the task %s' => '%s aktualizoval úkol %s ', + '%s opened the task %s' => '%s znovu otevřel úkol %s ', + '%s moved the task %s to the position #%d in the column "%s"' => '%s přesunul úkol %s na pozici #%d ve sloupci "%s" ', + '%s moved the task %s to the column "%s"' => '%s přesunul úkol %s do sloupce "%s" ', + '%s created the task %s' => '%s vytvořil úkol %s ', + '%s closed the task %s' => '%s uzavřel %s ', + '%s created a subtask for the task %s' => '%s vytvořil dílčí úkol pro úkol %s ', + '%s updated a subtask for the task %s' => '%s aktualizoval dílčí úkol pro úkol %s ', + 'Assigned to %s with an estimate of %s/%sh' => 'Přiřazeno uživateli %s s časovým odhadem práce %s/%s dní', + 'Not assigned, estimate of %sh' => 'Nepřiřazeno, časový odhad práce je %s dní', + '%s updated a comment on the task %s' => '%s aktualizoval komentář k úkolu %s ', + '%s commented the task %s' => '%s přidal komentář k úkolu %s ', + '%s\'s activity' => 'Aktivity projektu %s', + 'RSS feed' => 'RSS kanál', + '%s updated a comment on the task #%d' => '%s aktualizoval komnetář k úkolu #%d ', + '%s commented on the task #%d' => '%s přidal komentář k úkolu #%d ', + '%s updated a subtask for the task #%d' => '%s aktualizoval dílčí úkol úkolu #%d ', + '%s created a subtask for the task #%d' => '%s vytvořil dílčí úkol úkolu #%d ', + '%s updated the task #%d' => '%s aktualizoval úkol #%d ', + '%s created the task #%d' => '%s vytvořil úkol #%d ', + '%s closed the task #%d' => '%s uzavřel úkol #%d ', + '%s open the task #%d' => '%s znovu otevřel úkol #%d ', + '%s moved the task #%d to the column "%s"' => '%s přesunul úkol #%d do sloupce "%s" ', + '%s moved the task #%d to the position %d in the column "%s"' => '%s přesunul úkol #%d na pozici %d ve sloupci "%s" ', + 'Activity' => 'Aktivity', + 'Default values are "%s"' => 'Standardní hodnoty jsou: "%s"', + 'Default columns for new projects (Comma-separated)' => 'Výchozí sloupce pro nové projekty (odděleny čárkou)', + 'Task assignee change' => 'Změna přiřazení uživatelů', + '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', + '%s changed the assignee of the task %s to %s' => '%s změnil řešitele úkolu %s na uživatele %s', + 'New password for the user "%s"' => 'Nové heslo pro uživatele "%s"', + 'Choose an event' => 'Vybrat událost', + 'Github commit received' => 'Github commit empfangen', + 'Github issue opened' => 'Github Fehler geöffnet', + 'Github issue closed' => 'Github Fehler geschlossen', + 'Github issue reopened' => 'Github Fehler erneut geöffnet', + 'Github issue assignee change' => 'Github Fehlerzuständigkeit geändert', + 'Github issue label change' => 'Github Fehlerkennzeichnung verändert', + 'Create a task from an external provider' => 'Vytvořit úkol externím poskytovatelem', + 'Change the assignee based on an external username' => 'Změna přiřazení uživatele závislá na externím uživateli', + 'Change the category based on an external label' => 'Změna kategorie závislá na externím popisku', + 'Reference' => 'Reference', + 'Reference: %s' => 'Reference: %s', + // 'Label' => '', + 'Database' => 'Datenbank', + 'About' => 'O projektu', + 'Database driver:' => 'Databáze', + 'Board settings' => 'Nastavení nástěnky', + 'URL and token' => 'URL a Token', + 'Webhook settings' => 'Webhook nastavení', + 'URL for task creation:' => 'URL pro vytvoření úkolu', + 'Reset token' => 'Token reset', + 'API endpoint:' => 'API endpoint', + 'Refresh interval for private board' => 'Interval automatického obnovování pro soukromé nástěnky', + 'Refresh interval for public board' => 'Interval automatického obnovování pro veřejné nástěnky', + 'Task highlight period' => 'Task highlight period', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Interval (v sekundách), ve kterém je považovány úpravy úkolů za aktuální (0 pro zakázání, 2 dny ve výchozím nastavení)', + 'Frequency in second (60 seconds by default)' => 'Frekvence v sekundách (60 sekund ve výchozím nastavení)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvence v sekundách (0 pro zákaz této vlastnosti, 10 sekund ve výchozím nastavení)', + 'Application URL' => 'URL aplikace', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Ukázka: http://example.kanboard.net/ (použit v emailovém upozornění)', + 'Token regenerated.' => 'Token byl opětovně generován.', + 'Date format' => 'Formát datumu', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO formát je vždy akceptován, například: "%s" a "%s"', + 'New private project' => 'Nový soukromý projekt', + 'This project is private' => 'Tento projekt je soukromuý', + 'Type here to create a new sub-task' => 'Uveďte zde pro vytvoření nového dílčího úkolu', + 'Add' => 'Přidat', + 'Estimated time: %s hours' => 'Předpokládaný čas: %s hodin', + 'Time spent: %s hours' => 'Doba trvání: %s hodin', + 'Started on %B %e, %Y' => 'Zahájeno %B %e %Y', + 'Start date' => 'Počáteční datum', + 'Time estimated' => 'Odhadovaný čas', + 'There is nothing assigned to you.' => 'Nemáte přiřazenou žádnou položku.', + 'My tasks' => 'Moje úkoly', + 'Activity stream' => 'Přehled aktivit', + 'Dashboard' => 'Nástěnka', + 'Confirmation' => 'Potvrzení', + 'Allow everybody to access to this project' => 'Umožní přístup komukoliv k tomuto projektu', + 'Everybody have access to this project.' => 'Přístup k tomuto projektu má kdokoliv.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + 'Github webhooks' => 'Github Webhook', + 'Help on Github webhooks' => 'Hilfe für Github Webhooks', + 'Create a comment from an external provider' => 'Vytvořit komentář pomocí externího poskytovatele', + 'Github issue comment created' => 'Github Fehler Kommentar hinzugefügt', + 'Project management' => 'Správa projektů', + 'My projects' => 'Moje projekty', + 'Columns' => 'Sloupce', + 'Task' => 'Úkol', + 'Your are not member of any project.' => 'V žádném projektu nejste členem.', + 'Percentage' => 'Procenta', + 'Number of tasks' => 'Počet úkolů', + 'Task distribution' => 'Rozdělení úkolů', + 'Reportings' => 'Reporty', + 'Task repartition for "%s"' => 'Rozdělení úkolů pro "%s"', + 'Analytics' => 'Analýza', + 'Subtask' => 'Dílčí úkoly', + 'My subtasks' => 'Moje dílčí úkoly', + 'User repartition' => 'Rozdělení podle uživatelů', + 'User repartition for "%s"' => 'Rozdělení podle uživatelů pro "%s"', + 'Clone this project' => 'Duplokovat projekt', + 'Column removed successfully.' => 'Sloupec byl odstraněn.', + 'Github Issue' => 'Github Issue', + 'Not enough data to show the graph.' => 'Pro zobrazení grafu není dostatek dat.', + 'Previous' => 'Předchozí', + 'The id must be an integer' => 'ID musí být celé číslo', + 'The project id must be an integer' => 'ID projektu musí být celé číslo', + 'The status must be an integer' => 'Status musí být celé číslo', + 'The subtask id is required' => 'Je požadováno id dílčího úkolu', + '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' => 'Kumulativní diagram', + 'Cumulative flow diagram for "%s"' => 'Kumulativní diagram pro "%s"', + 'Daily project summary' => 'Denní přehledy', + 'Daily project summary export' => 'Export denních přehledů', + 'Daily project summary export for "%s"' => 'Export denních přehledů pro "%s"', + 'Exports' => 'Exporty', + 'This export contains the number of tasks per column grouped per day.' => 'Tento export obsahuje počet úkolů pro jednotlivé sloupce seskupených podle dní.', + 'Nothing to preview...' => 'Žádná položka k zobrazení ...', + 'Preview' => 'Náhled', + 'Write' => 'Režim psaní', + 'Active swimlanes' => 'Aktive Swimlane', + 'Add a new swimlane' => 'Přidat nový řádek', + 'Change default swimlane' => 'Standard Swimlane ändern', + 'Default swimlane' => 'Výchozí Swimlane', + 'Do you really want to remove this swimlane: "%s"?' => 'Diese Swimlane wirklich ändern: "%s"?', + 'Inactive swimlanes' => 'Inaktive Swimlane', + 'Set project manager' => 'Nastavit práva vedoucího projektu', + 'Set project member' => 'Nastavit práva člena projektu', + 'Remove a swimlane' => 'Odstranit swimlane', + 'Rename' => 'Přejmenovat', + '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)' => 'Výchozí kategorie pro nové projekty (oddělené čárkou)', + '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' => 'Integrace', + 'Integration with third-party services' => 'Integration von Fremdleistungen', + 'Role for this project' => 'Role pro tento projekt', + 'Project manager' => 'Projektový manažer', + 'Project member' => 'Člen projektu', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Manažer projektu může změnit nastavení projektu a zároveň má více práv než standardní uživatel.', + 'Gitlab Issue' => 'Gitlab Fehler', + 'Subtask Id' => 'Dílčí úkol Id', + 'Subtasks' => 'Dílčí úkoly', + 'Subtasks Export' => 'Export dílčích úkolů', + 'Subtasks exportation for "%s"' => 'Export dílčích úkolů pro "%s"', + 'Task Title' => 'Název úkolu', + 'Untitled' => 'bez názvu', + 'Application default' => 'Standardní hodnoty', + 'Language:' => 'Jazyk:', + 'Timezone:' => 'Časová zóna:', + 'All columns' => 'Všechny sloupce', + 'Calendar' => 'Kalendář', + 'Next' => 'Další', + // '#%d' => '', + 'All swimlanes' => 'Alle Swimlanes', + 'All colors' => 'Všechny barvy', + 'All status' => 'Všechny stavy', + 'Moved to column %s' => 'Přesunuto do sloupce %s ', + 'Change description' => 'Změna podrobného popisu', + 'User dashboard' => 'Nástěnka uživatele', + 'Allow only one subtask in progress at the same time for a user' => 'Umožnit uživateli práci pouze na jednom dílčím úkolu ve stejném čase', + 'Edit column "%s"' => 'Upravit sloupec "%s" ', + 'Select the new status of the subtask: "%s"' => 'Wähle einen neuen Status für Teilaufgabe: "%s"', + 'Subtask timesheet' => 'Časový rozvrh dílčích úkolů', + 'There is nothing to show.' => 'Žádná položka k zobrazení', + 'Time Tracking' => 'Sledování času', + 'You already have one subtask in progress' => 'Jeden dílčí úkol již aktuálně řešíte', + 'Which parts of the project do you want to duplicate?' => 'Které části projektu chcete duplikovat?', + // 'Disallow login form' => '', + 'Bitbucket commit received' => 'Bitbucket commit erhalten', + 'Bitbucket webhooks' => 'Bitbucket webhooks', + 'Help on Bitbucket webhooks' => 'Hilfe für Bitbucket webhooks', + 'Start' => 'Začátek', + 'End' => 'Konec', + 'Task age in days' => 'Doba trvání úkolu ve dnech', + 'Days in this column' => 'Dní v tomto sloupci', + '%dd' => '%d d', + 'Add a link' => 'Přidat odkaz', + 'Add a new link' => 'Přidat nový odkaz', + '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.' => 'Propojení bylo úspěšně přidáno.', + 'Link updated successfully.' => 'Propojení bylo úspěšně aktualizováno.', + 'Link removed successfully.' => 'Propojení bylo úspěšně odebráno.', + 'Link labels' => 'Seznam odkazů', + 'Link modification' => 'Úpravy odkazů', + 'Links' => 'Odkazy', + 'Link settings' => 'Nastavení odkazů', + 'Opposite label' => 'Opačný text', + 'Remove a link' => 'Odstranit odkaz', + 'Task\'s links' => 'Související odkazy', + 'The labels must be different' => 'názvy musí být odlišné', + 'There is no link.' => 'Nejsou zde žádné odkazy', + 'This label must be unique' => 'Tento název musí být jedinečný', + 'Unable to create your link.' => 'Nelze vytvořit toto propojení.', + 'Unable to update your link.' => 'Nelze aktualizovat toto propojení.', + 'Unable to remove this link.' => 'Nelze odstranit toto propojení', + 'relates to' => 'souvisí s', + 'blocks' => 'blokuje', + 'is blocked by' => 'je blokován', + 'duplicates' => 'duplikuje', + 'is duplicated by' => 'je duplikován', + 'is a child of' => 'je podřízený', + 'is a parent of' => 'je nadřízený', + 'targets milestone' => 'targets milestone', + 'is a milestone of' => 'is a milestone of', + 'fixes' => 'nahrazuje', + 'is fixed by' => 'je nahrazen', + 'This task' => 'Tento úkol', + '<1h' => '<1h', + '%dh' => '%dh', + // '%b %e' => '', + 'Expand tasks' => 'Rozpbalit úkoly', + 'Collapse tasks' => 'Sbalit úkoly', + 'Expand/collapse tasks' => 'Rozbalit / sbalit úkoly', + 'Close dialog box' => 'Zavřít dialogové okno', + 'Submit a form' => 'Odeslat formulář', + 'Board view' => 'Zobrazení nástěnky', + 'Keyboard shortcuts' => 'Klávesnicové zkratky', + 'Open board switcher' => 'Otevřít přepínač nástěnek', + 'Application' => 'Aplikace', + 'since %B %e, %Y at %k:%M %p' => 'dne %d.%m.%Y v čase %H:%M', + 'Compact view' => 'Kompaktní zobrazení', + 'Horizontal scrolling' => 'Horizontální rolování', + 'Compact/wide view' => 'Kompaktní/plné zobrazení', + 'No results match:' => 'Žádná shoda:', + 'Remove hourly rate' => 'Stundensatz entfernen', + 'Do you really want to remove this hourly rate?' => 'Opravdu chcete odstranit tuto hodinovou sazbu?', + 'Hourly rates' => 'Hodinové sazby', + 'Hourly rate' => 'Hodinová sazba', + 'Currency' => 'Měna', + 'Effective date' => 'Datum účinnosti', + 'Add new rate' => 'Přidat novou hodinovou sazbu', + 'Rate removed successfully.' => 'Sazba byla úspěšně odstraněna', + 'Unable to remove this rate.' => 'Sazbu nelze odstranit.', + 'Unable to save the hourly rate.' => 'Hodinovou sazbu nelze uložit', + 'Hourly rate created successfully.' => 'Hodinová sazba byla úspěšně vytvořena.', + 'Start time' => 'Počáteční datum', + 'End time' => 'Konečné datum', + 'Comment' => 'Komentář', + 'All day' => 'Všechny dny', + 'Day' => 'Den', + 'Manage timetable' => 'Spravovat pracovní dobu', + 'Overtime timetable' => 'Přesčasy', + 'Time off timetable' => 'Pracovní volno', + 'Timetable' => 'Pracovní doba', + 'Work timetable' => 'Pracovní doba', + 'Week timetable' => 'Týdenní pracovní doba', + 'Day timetable' => 'Denní pracovní doba', + 'From' => 'Od', + 'To' => 'Do', + 'Time slot created successfully.' => 'Časový úsek byl úspěšně vytvořen.', + 'Unable to save this time slot.' => 'Nelze uložit tento časový úsek.', + 'Time slot removed successfully.' => 'Časový úsek byl odstraněn.', + 'Unable to remove this time slot.' => 'Nelze odstranit tento časový úsek', + 'Do you really want to remove this time slot?' => 'Opravdu chcete odstranit tento časový úsek?', + 'Remove time slot' => 'Odstranit časový úsek', + 'Add new time slot' => 'Přidat nový časový úsek', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Tato pracovní doba se použije když je zaškrtnuto políčko "Celý den" pro plánovanou pracovní dobu i přesčas .', + 'Files' => 'Soubory', + 'Images' => 'Obrázky', + 'Private project' => 'Soukromý projekt', + 'Amount' => 'Částka', + // 'AUD - Australian Dollar' => '', + 'Budget' => 'Rozpočet', + 'Budget line' => 'Položka rozpočtu', + 'Budget line removed successfully.' => 'Položka rozpočtu byla odstraněna', + 'Budget lines' => 'Položky rozpočtu', + // 'CAD - Canadian Dollar' => '', + // 'CHF - Swiss Francs' => '', + 'Cost' => 'Cena', + 'Cost breakdown' => 'Rozpis nákladů', + 'Custom Stylesheet' => 'Vlastní šablony stylů', + 'download' => 'Stáhnout', + 'Do you really want to remove this budget line?' => 'Opravdu chcete odstranit tuto rozpočtovou řádku?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Náklady', + 'GBP - British Pound' => 'GBP - Britská Libra', + 'INR - Indian Rupee' => 'INR - Indische Rupien', + 'JPY - Japanese Yen' => 'JPY - Japanischer Yen', + 'New budget line' => 'Nová položka rozpočtu', + '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.' => 'Položka rozpočtu byla úspěšně vytvořena.', + 'Unable to create the budget line.' => 'Nelze vytvořit rozpočtovou řádku.', + 'Unable to remove this budget line.' => 'Nelze vyjmout rozpočtovou řádku.', + 'USD - US Dollar' => 'USD - US Dollar', + 'Remaining' => 'Zbývající', + 'Destination column' => 'Cílový sloupec', + 'Move the task to another column when assigned to a user' => 'Přesunout úkol do jiného sloupce, když je úkol přiřazen uživateli.', + 'Move the task to another column when assignee is cleared' => 'Přesunout úkol do jiného sloupce, když je pověření uživatele vymazáno.', + 'Source column' => 'Zdrojový sloupec', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => 'Změny etap', + 'Executer' => 'Vykonavatel', + 'Time spent in the column' => 'Trvání jednotlivých etap', + 'Task transitions' => 'Přesuny úkolů', + 'Task transitions export' => 'Export přesunů mezi sloupci', + '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' => 'Aktuální kurzy', + 'Rate' => 'Kurz', + 'Change reference currency' => 'Změnit referenční měnu', + 'Add a new currency rate' => 'Přidat nový směnný kurz', + 'Currency rates are used to calculate project budget.' => 'Měnové sazby se používají k výpočtu rozpočtu projektu.', + 'Reference currency' => 'Referenční měna', + 'The currency rate have been added successfully.' => 'Směnný kurz byl úspěšně přidán.', + 'Unable to add this currency rate.' => 'Nelze přidat tento směnný kurz', + 'Send notifications to a Slack channel' => 'Zaslání upozornění do Slack kanálu', + 'Webhook URL' => 'Webhook URL', + 'Help on Slack integration' => 'Nápověda pro Slack integraci.', + '%s remove the assignee of the task %s' => '%s odstranil přiřazení úkolu %s ', + 'Send notifications to Hipchat' => 'Odeslat upozornění přes 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' => 'Informace', + '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' => 'Dvouúrovňová autorizace', + 'Enable/disable two factor authentication' => 'Povolit / zakázat dvou úrovňovou autorizaci', + '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' => 'Kontrola mého kódu', + 'Secret key: ' => 'Tajný klíč', + 'Test your device' => 'Test Vašeho zařízení', + 'Assign a color when the task is moved to a specific column' => 'Přiřadit barvu, když je úkol přesunut do konkrétního sloupce', + '%s via Kanboard' => '%s via Kanboard', + 'uploaded by: %s' => 'Nahráno uživatelem: %s', + 'uploaded on: %s' => 'Nahráno dne: %s', + 'size: %s' => 'Velikost: %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).' => 'Graf zobrazuje složitost úkolů v čase (Zbývající práce).', + 'Screenshot taken %s' => 'Screenshot aufgenommen %s ', + 'Add a screenshot' => 'Přidat snímek obrazovky', + '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.' => 'Identifikátor projektu je volitelný alfanumerický kód používaný k identifikaci vašeho projektu.', + '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' => 'Zrušit dvou stupňovou autorizaci', + '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' => 'Upravit opakování', + // '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' => 'Nastavení kalendáře', + // 'Project calendar view' => '', + 'Project settings' => 'Nastavení projektu', + 'Show subtasks based on the time tracking' => 'Zobrazit dílčí úkoly závislé na sledování času', + 'Show tasks based on the creation date' => 'Zobrazit úkoly podle datumu vytvoření', + 'Show tasks based on the start date' => 'Zobrazit úkoly podle datumu zahájení', + 'Subtasks time tracking' => 'Dílčí úkoly s časovačem', + 'User calendar view' => 'Zobrazení kalendáře uživatele', + 'Automatically update the start date' => 'Automaticky aktualizovat počáteční datum', + // 'iCal feed' => '', + 'Preferences' => 'Předvolby', + 'Security' => 'Zabezpečení ', + 'Two factor authentication disabled' => 'Dvouúrovňová autorizace zakázána.', + 'Two factor authentication enabled' => 'Dvouúrovňová autorizace povolena.', + 'Unable to update this user.' => 'Uživatele nelze aktualizovat.', + 'There is no user management for private projects.' => 'Pro soukromé projekty není aplikována správa uživatelů.', + 'User that will receive the email' => 'Uživatel, který dostane E-mail', + 'Email subject' => 'E-mail Předmět', + 'Date' => 'Datum', + // 'By @%s on Bitbucket' => '', + // 'Bitbucket Issue' => '', + // 'Commit made by @%s on Bitbucket' => '', + // 'Commit made by @%s on Github' => '', + // 'By @%s on Github' => '', + // 'Commit made by @%s on Gitlab' => '', + 'Add a comment log when moving the task between columns' => 'Přidat komentář když je úkol přesouván mezi sloupci', + 'Move the task to another column when the category is changed' => 'Přesun úkolu do jiného sloupce když je změněna kategorie', + 'Send a task by email to someone' => 'Poslat někomu úkol poštou', + 'Reopen a task' => 'Znovu otevřít úkol', + // 'Bitbucket issue opened' => '', + // 'Bitbucket issue closed' => '', + // 'Bitbucket issue reopened' => '', + // 'Bitbucket issue assignee change' => '', + // 'Bitbucket issue comment created' => '', + 'Column change' => 'Spalte geändert', + 'Position change' => 'Position geändert', + 'Swimlane change' => 'Swimlane geändert', + 'Assignee change' => 'Zuordnung geändert', + '[%s] Overdue tasks' => '[%s] überfallige Aufgaben', + 'Notification' => 'Benachrichtigungen', + '%s moved the task #%d to the first swimlane' => '%s hat die Aufgabe #%d in die erste Swimlane verschoben', + '%s moved the task #%d to the swimlane "%s"' => '%s hat die Aufgabe #%d in die Swimlane "%s" verschoben', + // 'Swimlane' => '', + 'Budget overview' => 'Budget Übersicht', + 'Type' => 'Typ', + 'There is not enough data to show something.' => 'Es gibt nicht genug Daten für die Anzeige', + // 'Gravatar' => '', + // 'Hipchat' => '', + // 'Slack' => '', + '%s moved the task %s to the first swimlane' => '%s hat die Aufgabe %s in die erste Swimlane verschoben', + '%s moved the task %s to the swimlane "%s"' => '%s hat die Aufgaben %s in die Swimlane "%s" verschoben', + 'This report contains all subtasks information for the given date range.' => 'Report obsahuje všechny informace o dílčích úkolech pro daný časový úsek', + 'This report contains all tasks information for the given date range.' => 'Report obsahuje informace o všech úkolech pro daný časový úsek.', + 'Project activities for %s' => 'Aktivity projektu %s', + 'view the board on Kanboard' => 'Pinnwand in Kanboard anzeigen', + 'The task have been moved to the first swimlane' => 'Die Aufgabe wurde in die erste Swimlane verschoben', + 'The task have been moved to another swimlane:' => 'Die Aufgaben wurde in ene andere Swimlane verschoben', + 'Overdue tasks for the project "%s"' => 'Überfällige Aufgaben für das Projekt "%s"', + 'New title: %s' => 'Neuer Titel: %s', + 'The task is not assigned anymore' => 'Die Aufgabe ist nicht mehr zugewiesen', + 'New assignee: %s' => 'Neue Zuordnung: %s', + 'There is no category now' => 'Nyní neexistuje žádná kategorie', + 'New category: %s' => 'Nová kategorie: %s', + 'New color: %s' => 'Nová barva: %s', + 'New complexity: %d' => 'Nová složitost: %d', + 'The due date have been removed' => 'Datum dokončení byl odstraněn', + 'There is no description anymore' => 'Ještě neexistuje žádný popis', + 'Recurrence settings have been modified' => 'Nastavení opakování bylo změněno', + 'Time spent changed: %sh' => 'Verbrauchte Zeit geändert: %sh', + 'Time estimated changed: %sh' => 'Geschätzte Zeit geändert: %sh', + 'The field "%s" have been updated' => 'Das Feld "%s" wurde verändert', + 'The description have been modified' => 'Die Beschreibung wurde geändert', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Soll die Aufgabe "%s" wirklich geschlossen werden? (einschließlich Teilaufgaben)', + // 'Swimlane: %s' => '', + 'I want to receive notifications for:' => 'Chci dostávat upozornění na:', + 'All tasks' => 'Všechny úkoly', + 'Only for tasks assigned to me' => 'pouze pro moje úkoly', + 'Only for tasks created by me' => 'pouze pro mnou vytvořené úkoly', + 'Only for tasks created by me and assigned to me' => 'pouze pro mnou vytvořené a mě přiřazené úkoly', + // '%A' => '', + // '%b %e, %Y, %k:%M %p' => '', + 'New due date: %B %e, %Y' => 'Neues Ablaufdatum: %B %e, %Y', + 'Start date changed: %B %e, %Y' => 'Neues Beginndatum: %B %e, %Y', + // '%k:%M %p' => '', + // '%%Y-%%m-%%d' => '', + 'Total for all columns' => 'S', + 'You need at least 2 days of data to show the chart.' => 'Potřebujete nejméně data ze dvou dnů pro zobrazení grafu', + '<15m' => '<15min.', + '<30m' => '<30min.', + 'Stop timer' => 'Zastavit časovač', + 'Start timer' => 'Spustit časovač', + 'Add project member' => 'Přidat člena projektu', + 'Enable notifications' => 'Povolit notifikace', + 'My activity stream' => 'Přehled mých aktivit', + 'My calendar' => 'Můj kalendář', + 'Search tasks' => 'Hledání úkolů', + 'Back to the calendar' => 'Zpět do kalendáře', + 'Filters' => 'Filtry', + 'Reset filters' => 'Resetovat filtry', + 'My tasks due tomorrow' => 'Moje zítřejší úkoly', + 'Tasks due today' => 'Dnešní úkoly', + 'Tasks due tomorrow' => 'Zítřejší úkoly', + 'Tasks due yesterday' => 'Včerejší úkoly', + 'Closed tasks' => 'Uzavřené úkoly', + 'Open tasks' => 'Otevřené úkoly', + 'Not assigned' => 'Nepřiřazené', + 'View advanced search syntax' => 'Zobrazit syntaxi rozšířeného vyhledávání', + 'Overview' => 'Přehled', + '%b %e %Y' => '%b %e %Y', + 'Board/Calendar/List view' => 'Nástěnka/Kalendář/Zobrazení seznamu', + 'Switch to the board view' => 'Přepnout na nástěnku', + 'Switch to the calendar view' => 'Přepnout na kalendář', + 'Switch to the list view' => 'Přepnout na seznam zobrazení', + 'Go to the search/filter box' => 'Zobrazit vyhledávání/filtrování', + 'There is no activity yet.' => 'Doposud nejsou žádné aktivity.', + 'No tasks found.' => 'Nenalezen žádný úkol.', + 'Keyboard shortcut: "%s"' => 'Klávesová zkratka: "%s"', + 'List' => 'Seznam', + 'Filter' => 'Filtr', + 'Advanced search' => 'Rozšířené hledání', + 'Example of query: ' => 'Příklad dotazu: ', + 'Search by project: ' => 'Hledat podle projektu: ', + 'Search by column: ' => 'Hledat podle sloupce: ', + 'Search by assignee: ' => 'Hledat podle přiřazené osoby: ', + 'Search by color: ' => 'Hledat podle barvy: ', + 'Search by category: ' => 'Hledat podle kategorie: ', + 'Search by description: ' => 'Hledat podle popisu: ', + 'Search by due date: ' => 'Hledat podle termínu: ', + 'Lead and Cycle time for "%s"' => 'Dodací lhůta a doba cyklu pro "%s"', + 'Average time spent into each column for "%s"' => 'Průměrná doba strávená v každé fázi pro "%s"', + 'Average time spent into each column' => 'Průměrná doba strávená v každé fázi', + 'Average time spent' => 'Průměrná strávená doba', + // 'This chart show the average time spent into each column for the last %d tasks.' => '', + 'Average Lead and Cycle time' => 'Průměrná dodací lhůta a doba cyklu', + 'Average lead time: ' => 'Průměrná dodací lhůta: ', + 'Average cycle time: ' => 'Průměrná doba cyklu: ', + 'Cycle Time' => 'Doba cyklu', + 'Lead Time' => 'Dodací lhůta', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Graf ukazuje průměrnou dodací lhůtu a dobu cyklu pro posledních %d úkolů v průběhu času', + 'Average time into each column' => 'Průměrná doba v každé fázi', + 'Lead and cycle time' => 'Dodací lhůta a doba cyklu', + 'Google Authentication' => 'Ověřování pomocí služby Google', + 'Help on Google authentication' => 'Nápověda k ověřování pomocí služby Google', + 'Github Authentication' => 'Ověřování pomocí služby Github', + 'Help on Github authentication' => 'Nápověda k ověřování pomocí služby Github', + 'Channel/Group/User (Optional)' => 'Kanál/Skupina/Uživatel (volitelně)', + 'Lead time: ' => 'Dodací lhůta: ', + 'Cycle time: ' => 'Doba cyklu: ', + 'Time spent into each column' => 'Čas strávený v každé fázi', + 'The lead time is the duration between the task creation and the completion.' => 'Lead time (dodací lhůta) je čas od založení úkolu do jeho dokončení.', + 'The cycle time is the duration between the start date and the completion.' => 'Doba cyklu je doba trvání mezi zahájením a dokončením úkolu.', + 'If the task is not closed the current time is used instead of the completion date.' => 'Jestliže není úkol uzavřen, místo termínu dokončení je použit aktuální čas.', + 'Set automatically the start date' => 'Nastavit automaticky počáteční datum', + 'Edit Authentication' => 'Upravit ověřování', + 'Google Id' => 'Google ID', + 'Github Id' => 'Github ID', + 'Remote user' => 'Vzdálený uživatel', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Hesla vzdáleným uživatelům se neukládají do databáze Kanboard. Naříklad: LDAP, Google a Github účty.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Pokud zaškrtnete políčko "Zakázat přihlašovací formulář", budou pověření zadané do přihlašovacího formuláře ignorovány.', + 'By @%s on Gitlab' => 'uživatelem @%s na Gitlab', + 'Gitlab issue comment created' => 'Vytvořen komentář problému na Gitlab', + 'New remote user' => 'Nový vzdálený uživatel', + 'New local user' => 'Nový lokální uživatel', + 'Default task color' => 'Výchozí barva úkolu', + 'Hide sidebar' => 'Schovat postranní panel', + 'Expand sidebar' => 'Rozbalit postranní panel', + 'This feature does not work with all browsers.' => 'Tato funkcionalita nefunguje ve všech prohlížečích.', + 'There is no destination project available.' => 'Není dostupný žádný cílový projekt.', + // 'Trigger automatically subtask time tracking' => '', + 'Include closed tasks in the cumulative flow diagram' => 'začlenit dokončené úkoly do kumulativního flow diagramu', + 'Current swimlane: %s' => 'Aktuální swimlane: %s', + 'Current column: %s' => 'Aktuální fáze: %s', + 'Current category: %s' => 'Aktuální kategorie: %s', + 'no category' => 'kategorie nenastavena', + 'Current assignee: %s' => 'Aktuálně přiřazený uživatel: %s', + 'not assigned' => 'nepřiřazeno', + 'Author:' => 'Autor:', + 'contributors' => 'přispěvatelé', + 'License:' => 'Licence:', + 'License' => 'Licence', + 'Project Administrator' => 'Administrátor projektu', + 'Enter the text below' => 'Zadejte text níže', + 'Gantt chart for %s' => 'Gantt graf pro %s', + 'Sort by position' => 'Třídit podle pozice', + 'Sort by date' => 'Třídit podle datumu', + 'Add task' => 'Přidat úkol', + 'Start date:' => 'Termín zahájení:', + 'Due date:' => 'Termín dokončení:', + 'There is no start date or due date for this task.' => 'Úkol nemá nastaven termín zahájení a dokončení.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Posunutím nebo prodloužením úkolu se změní počáteční a konečné datum úkolu. ', + 'There is no task in your project.' => 'Projekt neobsahuje žádné úkoly.', + 'Gantt chart' => 'Gantt graf', + // 'People who are project managers' => '', + // 'People who are project members' => '', + // 'NOK - Norwegian Krone' => '', + // 'Show this column' => '', + // 'Hide this column' => '', + // 'open file' => '', + // 'End date' => '', + // 'Users overview' => '', + // 'Managers' => '', + // 'Members' => '', + // 'Shared project' => '', + // 'Project managers' => '', + // 'Project members' => '', + // 'Gantt chart for all projects' => '', + // 'Projects list' => '', + // 'Gantt chart for this project' => '', + // 'Project board' => '', + // 'End date:' => '', + // 'There is no start date or end date for this project.' => '', + // 'Projects Gantt chart' => '', + // 'Start date: %s' => '', + // 'End date: %s' => '', + // 'Link type' => '', + // 'Change task color when using a specific task link' => '', + // 'Task link creation or modification' => '', + // 'Login with my Gitlab Account' => '', + // 'Milestone' => '', + // 'Gitlab Authentication' => '', + // 'Help on Gitlab authentication' => '', + // 'Gitlab Id' => '', + // 'Gitlab Account' => '', + // 'Link my Gitlab Account' => '', + // 'Unlink my Gitlab Account' => '', + // 'Documentation: %s' => '', + // 'Switch to the Gantt chart view' => '', + // 'Reset the search/filter box' => '', + // 'Documentation' => '', + // 'Table of contents' => '', + // 'Gantt' => '', + // 'Help with project permissions' => '', +); diff --git a/sources/app/Locale/nb_NO/translations.php b/sources/app/Locale/nb_NO/translations.php new file mode 100644 index 0000000..155d49b --- /dev/null +++ b/sources/app/Locale/nb_NO/translations.php @@ -0,0 +1,1070 @@ + '', + // 'number.thousands_separator' => '', + 'None' => 'Ingen', + 'edit' => 'rediger', + 'Edit' => 'Rediger', + 'remove' => 'fjern', + 'Remove' => 'Fjern', + 'Update' => 'Oppdater', + 'Yes' => 'Ja', + 'No' => 'Nei', + 'cancel' => 'avbryt', + 'or' => 'eller', + 'Yellow' => 'Gul', + 'Blue' => 'Blå', + 'Green' => 'Grønn', + 'Purple' => 'Lilla', + 'Red' => 'Rød', + 'Orange' => 'Orange', + 'Grey' => 'Grå', + // 'Brown' => '', + // 'Deep Orange' => '', + // 'Dark Grey' => '', + // 'Pink' => '', + // 'Teal' => '', + // 'Cyan' => '', + // 'Lime' => '', + // 'Light Green' => '', + // 'Amber' => '', + 'Save' => 'Lagre', + 'Login' => 'Logg inn', + 'Official website:' => 'Offisiell webside:', + 'Unassigned' => 'Ikke tildelt', + 'View this task' => 'Se denne oppgaven', + 'Remove user' => 'Fjern bruker', + 'Do you really want to remove this user: "%s"?' => 'Vil du fjerne denne brukeren: "%s"?', + 'New user' => 'Ny bruker', + 'All users' => 'Alle brukere', + 'Username' => 'Brukernavn', + 'Password' => 'Passord', + 'Administrator' => 'Administrator', + 'Sign in' => 'Logg inn', + 'Users' => 'Brukere', + 'No user' => 'Ingen bruker', + 'Forbidden' => 'Ikke tillatt', + 'Access Forbidden' => 'Adgang ikke tillatt', + 'Edit user' => 'Rediger bruker', + 'Logout' => 'Logg ut', + 'Bad username or password' => 'Feil brukernavn eller passord', + 'Edit project' => 'Endre prosjekt', + 'Name' => 'Navn', + 'Projects' => 'Prosjekter', + 'No project' => 'Ingen prosjekter', + 'Project' => 'Prosjekt', + 'Status' => 'Status', + 'Tasks' => 'Oppgaver', + 'Board' => 'Hovedside', + 'Actions' => 'Handlinger', + 'Inactive' => 'Inaktiv', + 'Active' => 'Aktiv', + 'Add this column' => 'Legg til denne kolonnen', + '%d tasks on the board' => '%d Oppgaver på hovedsiden', + '%d tasks in total' => '%d Oppgaver i alt', + 'Unable to update this board.' => 'Ikke mulig at oppdatere hovedsiden', + 'Edit board' => 'Endre prosjektsiden', + 'Disable' => 'Deaktiver', + 'Enable' => 'Aktiver', + 'New project' => 'Nytt prosjekt', + 'Do you really want to remove this project: "%s"?' => 'Vil du fjerne dette prosjektet: "%s"?', + 'Remove project' => 'Fjern prosjekt', + 'Edit the board for "%s"' => 'Endre prosjektsiden for "%s"', + 'All projects' => 'Alle prosjekter', + 'Change columns' => 'Endre kolonner', + 'Add a new column' => 'Legg til en ny kolonne', + 'Title' => 'Tittel', + 'Nobody assigned' => 'Ikke tildelt', + 'Assigned to %s' => 'Tildelt: %s', + 'Remove a column' => 'Fjern en kolonne', + 'Remove a column from a board' => 'Fjern en kolonne fra et board', + 'Unable to remove this column.' => 'Ikke mulig fjerne denne kolonnen', + 'Do you really want to remove this column: "%s"?' => 'Vil du fjerne denne kolonnen: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Denne handlingen vil SLETTE ALLE OPPGAVER tilknyttet denne kolonnen', + 'Settings' => 'Innstillinger', + 'Application settings' => 'Applikasjonsinnstillinger', + 'Language' => 'Språk', + 'Webhook token:' => 'Webhook token:', + 'API token:' => 'API Token:', + 'Database size:' => 'Databasestørrelse:', + 'Download the database' => 'Last ned databasen', + 'Optimize the database' => 'Optimaliser databasen', + '(VACUUM command)' => '(VACUUM kommando)', + '(Gzip compressed Sqlite file)' => '(Gzip-komprimert Sqlite fil)', + 'Close a task' => 'Lukk en oppgave', + 'Edit a task' => 'Endre en oppgave', + 'Column' => 'Kolonne', + 'Color' => 'Farge', + 'Assignee' => 'Tildelt', + 'Create another task' => 'Opprett en annen oppgave', + 'New task' => 'Ny oppgave', + 'Open a task' => 'Åpne en oppgave', + 'Do you really want to open this task: "%s"?' => 'Vil du åpe denne oppgaven: "%s"?', + 'Back to the board' => 'Tilbake til prosjektsiden', + 'Created on %B %e, %Y at %k:%M %p' => 'Opprettet %d.%m.%Y - %H:%M', + 'There is nobody assigned' => 'Mangler tildeling', + 'Column on the board:' => 'Kolonne:', + 'Status is open' => 'Status: åpen', + 'Status is closed' => 'Status: lukket', + 'Close this task' => 'Lukk oppgaven', + 'Open this task' => 'Åpne denne oppgaven', + 'There is no description.' => 'Det er ingen beskrivelse.', + 'Add a new task' => 'Opprett ny oppgave', + 'The username is required' => 'Brukernavn er påkrevd', + 'The maximum length is %d characters' => 'Den maksimale lengden er %d tegn', + 'The minimum length is %d characters' => 'Den minimale lengden er %d tegn', + 'The password is required' => 'Passord er påkrevet', + 'This value must be an integer' => 'Denne verdien skal være et tall', + 'The username must be unique' => 'Brukernavnet skal være unikt', + 'The user id is required' => 'Bruker-id er påkrevet', + 'Passwords don\'t match' => 'Passordene stemmer ikke overens', + 'The confirmation is required' => 'Bekreftelse er nødvendig', + 'The project is required' => 'Prosjektet er påkrevet', + 'The id is required' => 'Id\'en er påkrevd', + 'The project id is required' => 'Prosjektet-id er påkrevet', + 'The project name is required' => 'Prosjektnavn er påkrevet', + 'This project must be unique' => 'Prosjektnavnet skal være unikt', + 'The title is required' => 'Tittel er pårevet', + 'Settings saved successfully.' => 'Innstillinger lagret.', + 'Unable to save your settings.' => 'Innstillinger kunne ikke lagres.', + 'Database optimization done.' => 'Databaseoptimering er fullført.', + 'Your project have been created successfully.' => 'Ditt prosjekt er opprettet.', + 'Unable to create your project.' => 'Prosjektet kunne ikke opprettes', + 'Project updated successfully.' => 'Prosjektet er oppdatert.', + 'Unable to update this project.' => 'Prosjektet kunne ikke oppdateres.', + 'Unable to remove this project.' => 'Prosjektet kunne ikke slettes.', + 'Project removed successfully.' => 'Prosjektet er slettet.', + 'Project activated successfully.' => 'Prosjektet er aktivert.', + 'Unable to activate this project.' => 'Prosjektet kunne ikke aktiveres.', + 'Project disabled successfully.' => 'Prosjektet er deaktiveret.', + 'Unable to disable this project.' => 'Prosjektet kunne ikke deaktiveres.', + 'Unable to open this task.' => 'Oppgaven kunne ikke åpnes.', + 'Task opened successfully.' => 'Oppgaven er åpnet.', + 'Unable to close this task.' => 'Oppgaven kunne ikke åpnes.', + 'Task closed successfully.' => 'Oppgaven er lukket.', + 'Unable to update your task.' => 'Oppgaven kunne ikke oppdateres.', + 'Task updated successfully.' => 'Oppgaven er oppdatert.', + 'Unable to create your task.' => 'Oppgave kunne ikke opprettes.', + 'Task created successfully.' => 'Oppgaven er opprettet.', + 'User created successfully.' => 'Brukeren er opprettet.', + 'Unable to create your user.' => 'Brukeren kunne ikke opprettes.', + 'User updated successfully.' => 'Brukeren er opdateret', + 'Unable to update your user.' => 'Din bruker kunne ikke oppdateres.', + 'User removed successfully.' => 'Brukeren er fjernet.', + 'Unable to remove this user.' => 'Brukeren kunne ikke slettes.', + 'Board updated successfully.' => 'Hovedsiden er oppdatert.', + 'Ready' => 'Klar', + 'Backlog' => 'Backlog', + 'Work in progress' => 'Under arbeid', + 'Done' => 'Utført', + 'Application version:' => 'Versjon:', + 'Completed on %B %e, %Y at %k:%M %p' => 'Fullført %d.%m.%Y - %H:%M', + '%B %e, %Y at %k:%M %p' => '%d.%m.%Y - %H:%M', + 'Date created' => 'Dato for opprettelse', + 'Date completed' => 'Dato for fullført', + 'Id' => 'ID', + '%d closed tasks' => '%d lukkede oppgaver', + 'No task for this project' => 'Ingen oppgaver i dette prosjektet', + 'Public link' => 'Offentligt lenke', + 'There is no column in your project!' => 'Det er ingen kolonner i dette prosjektet!', + 'Change assignee' => 'Tildel oppgaven til andre', + 'Change assignee for the task "%s"' => 'Endre tildeling av oppgaven: "%s"', + 'Timezone' => 'Tidssone', + 'Sorry, I didn\'t find this information in my database!' => 'Denne informasjonen kunne ikke finnes i databasen!', + 'Page not found' => 'Siden er ikke funnet', + 'Complexity' => 'Kompleksitet', + 'Task limit' => 'Oppgave begrensning', + // 'Task count' => '', + 'Edit project access list' => 'Endre tillatelser for prosjektet', + 'Allow this user' => 'Tillat denne brukeren', + 'Don\'t forget that administrators have access to everything.' => 'Hust at administratorer har tilgang til alt.', + 'Revoke' => 'Fjern', + 'List of authorized users' => 'Liste over autoriserte brukere', + 'User' => 'Bruker', + 'Nobody have access to this project.' => 'Ingen har tilgang til dette prosjektet.', + 'Comments' => 'Kommentarer', + 'Write your text in Markdown' => 'Skriv din tekst i markdown', + 'Leave a comment' => 'Legg inn en kommentar', + 'Comment is required' => 'Kommentar må legges inn', + 'Leave a description' => 'Legg inn en beskrivelse...', + 'Comment added successfully.' => 'Kommentaren er lagt til.', + 'Unable to create your comment.' => 'Din kommentar kunne ikke opprettes.', + 'Edit this task' => 'Rediger oppgaven', + 'Due Date' => 'Forfallsdato', + 'Invalid date' => 'Ugyldig dato', + 'Must be done before %B %e, %Y' => 'Skal være utført innen %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 opprettet.', + 'Unable to create your automatic action.' => 'Din automatiske handling kunne ikke opprettes.', + 'Remove an action' => 'Fjern en handling', + 'Unable to remove this action.' => 'Handlingen kunne ikke fjernes.', + 'Action removed successfully.' => 'Handlingen er fjernet.', + 'Automatic actions for the project "%s"' => 'Automatiske handlinger for prosjektet "%s"', + 'Defined actions' => 'Definerte handlinger', + 'Add an action' => 'Legg til en handling', + 'Event name' => 'Begivenhet', + 'Action name' => 'Handling', + 'Action parameters' => 'Handlingsparametre', + 'Action' => 'Handling', + 'Event' => 'Begivenhet', + 'When the selected event occurs execute the corresponding action.' => 'Når den valgtebegivenheten oppstår, utføre tilsvarende handlin.', + 'Next step' => 'Neste', + 'Define action parameters' => 'Definer handlingsparametre', + 'Save this action' => 'Lagre handlingen', + 'Do you really want to remove this action: "%s"?' => 'Vil du virkelig slette denne handlingen: "%s"?', + 'Remove an automatic action' => 'Fjern en automatisk handling', + 'Assign the task to a specific user' => 'Tildel oppgaven til en bestemt bruker', + 'Assign the task to the person who does the action' => 'Tildel oppgaven til den person, som utfører handlingen', + 'Duplicate the task to another project' => 'Kopier oppgaven til et annet prosjekt', + 'Move a task to another column' => 'Flytt oppgaven til en annen kolonne', + 'Task modification' => 'Oppgaveendring', + 'Task creation' => 'Oppgaveoprettelse', + 'Closing a task' => 'Lukke en oppgave', + 'Assign a color to a specific user' => 'Tildel en farge til en bestemt bruker', + 'Column title' => 'Kolonne tittel', + 'Position' => 'Posisjon', + 'Move Up' => 'Flytt opp', + 'Move Down' => 'Flytt ned', + 'Duplicate to another project' => 'Kopier til et annet prosjekt', + 'Duplicate' => 'Kopier', + 'link' => 'link', + 'Comment updated successfully.' => 'Kommentar oppdatert.', + 'Unable to update your comment.' => 'Din kommentar kunne ikke oppdateres.', + 'Remove a comment' => 'Fjern en kommentar', + 'Comment removed successfully.' => 'Kommentaren ble fjernet.', + 'Unable to remove this comment.' => 'Kommentaren kunne ikke fjernes.', + 'Do you really want to remove this comment?' => 'Vil du virkelig fjerne denne kommentaren?', + 'Only administrators or the creator of the comment can access to this page.' => 'Kun administrator eller brukeren, som har oprettet kommentaren har adgang til denne siden.', + 'Current password for the user "%s"' => 'Aktivt passord for brukeren "%s"', + 'The current password is required' => 'Passord er påkrevet', + 'Wrong password' => 'Feil passord', + 'Unknown' => 'Ukjent', + 'Last logins' => 'Siste login', + 'Login date' => 'Login dato', + 'Authentication method' => 'Godkjenningsmetode', + 'IP address' => 'IP Adresse', + 'User agent' => 'User Agent', + 'Persistent connections' => 'Varige forbindelser', + 'No session.' => 'Ingen session.', + 'Expiration date' => 'Utløpsdato', + 'Remember Me' => 'Husk meg', + 'Creation date' => 'Opprettelsesdato', + 'Everybody' => 'Alle', + 'Open' => 'Åpen', + 'Closed' => 'Lukket', + 'Search' => 'Søk', + 'Nothing found.' => 'Intet funnet.', + 'Due date' => 'Forfallsdato', + 'Others formats accepted: %s and %s' => 'Andre formater: %s og %s', + 'Description' => 'Beskrivelse', + '%d comments' => '%d kommentarer', + '%d comment' => '%d kommentar', + 'Email address invalid' => 'Ugyldig epost', + // 'Your external account is not linked anymore to your profile.' => '', + // 'Unable to unlink your external account.' => '', + // 'External authentication failed' => '', + // 'Your external account is linked to your profile successfully.' => '', + 'Email' => 'Epost', + 'Link my Google Account' => 'Knytt til min Google-konto', + 'Unlink my Google Account' => 'Fjern knytningen til min Google-konto', + 'Login with my Google Account' => 'Login med min Google-konto', + 'Project not found.' => 'Prosjekt ikke funnet.', + 'Task removed successfully.' => 'Oppgaven er fjernet.', + 'Unable to remove this task.' => 'Oppgaven kunne ikke fjernes.', + 'Remove a task' => 'Fjern en oppgave', + 'Do you really want to remove this task: "%s"?' => 'Vil du virkelig fjerne denne opgave: "%s"?', + 'Assign automatically a color based on a category' => 'Tildel automatisk en farge baseret for en kategori', + 'Assign automatically a category based on a color' => 'Tildel automatisk en kategori basert på en farve', + 'Task creation or modification' => 'Oppgaveopprettelse eller endring', + 'Category' => 'Kategori', + 'Category:' => 'Kategori:', + 'Categories' => 'Kategorier', + 'Category not found.' => 'Kategori ikke funnet.', + 'Your category have been created successfully.' => 'Kategorien er opprettet.', + 'Unable to create your category.' => 'Kategorien kunne ikke opprettes.', + 'Your category have been updated successfully.' => 'Kategorien er oppdatert.', + 'Unable to update your category.' => 'Kategorien kunne ikke oppdateres.', + 'Remove a category' => 'Fjern en kategori', + 'Category removed successfully.' => 'Kategorien er fjernet.', + 'Unable to remove this category.' => 'Kategorien kunne ikke fjernes.', + 'Category modification for the project "%s"' => 'Endring av kategori for prosjektet "%s"', + 'Category Name' => 'Kategorinavn', + 'Add a new category' => 'Legg til ny kategori', + 'Do you really want to remove this category: "%s"?' => 'Vil du virkelig fjerne kategorien: "%s"?', + 'All categories' => 'Alle kategorier', + 'No category' => 'Ingen kategori', + 'The name is required' => 'Navnet er påkrevet', + 'Remove a file' => 'Fjern en fil', + 'Unable to remove this file.' => 'Filen kunne ikke fjernes.', + 'File removed successfully.' => 'Filen er fjernet.', + 'Attach a document' => 'Legg til et dokument', + 'Do you really want to remove this file: "%s"?' => 'Vil du virkelig fjerne filen: "%s"?', + 'open' => 'åpen', + 'Attachments' => 'Vedleggr', + 'Edit the task' => 'Rediger oppgaven', + 'Edit the description' => 'Rediger beskrivelsen', + 'Add a comment' => 'Legg til en kommentar', + 'Edit a comment' => 'Rediger en kommentar', + 'Summary' => 'Sammendrag', + 'Time tracking' => 'Tidsregistrering', + 'Estimate:' => 'Estimat:', + 'Spent:' => 'Brukt:', + 'Do you really want to remove this sub-task?' => 'Vil du virkelig fjerne denne deloppgaven?', + 'Remaining:' => 'Gjenværende:', + 'hours' => 'timer', + 'spent' => 'brukt', + 'estimated' => 'estimat', + 'Sub-Tasks' => 'Deloppgave', + 'Add a sub-task' => 'Legg til en deloppgave', + 'Original estimate' => 'Original estimering', + 'Create another sub-task' => 'Legg til en ny deloppgave', + 'Time spent' => 'Tidsforbruk', + 'Edit a sub-task' => 'Rediger en deloppgave', + 'Remove a sub-task' => 'Fjern en deloppgave', + 'The time must be a numeric value' => 'Tiden skal være en nummerisk erdi', + 'Todo' => 'Gjøremål', + 'In progress' => 'Under arbeid', + 'Sub-task removed successfully.' => 'Deloppgaven er fjernet.', + 'Unable to remove this sub-task.' => 'Deloppgaven kunne ikke fjernes.', + 'Sub-task updated successfully.' => 'Deloppgaven er opdateret.', + 'Unable to update your sub-task.' => 'Deloppgaven kunne ikke opdateres.', + 'Unable to create your sub-task.' => 'Deloppgaven kunne ikke oprettes.', + 'Sub-task added successfully.' => 'Deloppgaven er lagt til.', + 'Maximum size: ' => 'Maksimum størrelse: ', + 'Unable to upload the file.' => 'Filen kunne ikke lastes opp.', + 'Display another project' => 'Vis annet prosjekt...', + // 'Login with my Github Account' => '', + // 'Link my Github Account' => '', + // 'Unlink my Github Account' => '', + 'Created by %s' => 'Opprettet av %s', + 'Last modified on %B %e, %Y at %k:%M %p' => 'Sist endret %d.%m.%Y - %H:%M', + 'Tasks Export' => 'Oppgave eksport', + 'Tasks exportation for "%s"' => 'Oppgaveeksportering for "%s"', + 'Start Date' => 'Start-dato', + 'End Date' => 'Slutt-dato', + 'Execute' => 'KKjør', + 'Task Id' => 'Oppgave ID', + 'Creator' => 'Laget av', + 'Modification date' => 'Endringsdato', + 'Completion date' => 'Ferdigstillingsdato', + 'Clone' => 'Kopier', + 'Project cloned successfully.' => 'Prosjektet er kopiert.', + 'Unable to clone this project.' => 'Prosjektet kunne ikke kopieres', + 'Email notifications' => 'Epostvarslinger', + 'Enable email notifications' => 'Aktiver eposvarslinger', + 'Task position:' => 'Oppgaveposisjon:', + 'The task #%d have been opened.' => 'Oppgaven #%d er åpnet.', + 'The task #%d have been closed.' => 'Oppgaven #%d er lukket.', + 'Sub-task updated' => 'Deloppgaven er oppdatert', + 'Title:' => 'Tittel:', + 'Status:' => 'Status:', + 'Assignee:' => 'Ansvarlig:', + 'Time tracking:' => 'Tidsmåling:', + 'New sub-task' => 'Ny deloppgave', + 'New attachment added "%s"' => 'Nytt vedlegg er lagt tilet "%s"', + 'Comment updated' => 'Kommentar oppdatert', + 'New comment posted by %s' => 'Ny kommentar fra %s', + 'New attachment' => 'Nytt vedlegg', + 'New comment' => 'Ny kommentar', + 'New subtask' => 'Ny deloppgave', + 'Subtask updated' => 'Deloppgave oppdatert', + 'Task updated' => 'Oppgave oppdatert', + 'Task closed' => 'Oppgave lukket', + 'Task opened' => 'Oppgave åpnet', + 'I want to receive notifications only for those projects:' => 'Jeg vil kun ha varslinger for disse prosjekter:', + 'view the task on Kanboard' => 'se oppgaven påhovedsiden', + 'Public access' => 'Offentlig tilgang', + 'User management' => 'Brukere', + 'Active tasks' => 'Aktive oppgaver', + 'Disable public access' => 'Deaktiver offentlig tilgang', + 'Enable public access' => 'Aktiver offentlig tilgang', + 'Public access disabled' => 'Offentlig tilgang er deaktivert', + 'Do you really want to disable this project: "%s"?' => 'Vil du virkelig deaktivere prosjektet: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Vil du virkelig aktivere prosjektet: "%s"?', + 'Project activation' => 'Prosjekt aktivering', + 'Move the task to another project' => 'Flytt oppgaven til et annet prosjekt', + 'Move to another project' => 'Flytt til et annet prosjekt', + 'Do you really want to duplicate this task?' => 'Vil du virkelig kopiere denne oppgaven?', + 'Duplicate a task' => 'Kopier en oppgave', + 'External accounts' => 'Eksterne kontoer', + 'Account type' => 'Kontotype', + 'Local' => 'Lokal', + 'Remote' => 'Fjernstyrt', + 'Enabled' => 'Aktiv', + 'Disabled' => 'Deaktivert', + 'Google account linked' => 'Google-konto knyttet', + 'Github account linked' => 'GitHub-konto knyttet', + 'Username:' => 'Brukernavn', + 'Name:' => 'Navn:', + 'Email:' => 'Epost:', + 'Notifications:' => 'Varslinger:', + 'Notifications' => 'Varslinger', + 'Group:' => 'Gruppe:', + 'Regular user' => 'Normal bruker', + 'Account type:' => 'Konto type:', + 'Edit profile' => 'Rediger profil', + 'Change password' => 'Endre passord', + 'Password modification' => 'Passordendring', + 'External authentications' => 'Ekstern godkjenning', + 'Google Account' => 'Google-konto', + 'Github Account' => 'GitHub-konto', + 'Never connected.' => 'Aldri knyttet.', + 'No account linked.' => 'Ingen kontoer knyttet.', + 'Account linked.' => 'Konto knyttet.', + 'No external authentication enabled.' => 'Ingen eksterne godkjenninger aktiveret.', + 'Password modified successfully.' => 'Passord er endret.', + 'Unable to change the password.' => 'Passordet kuenne ikke endres.', + 'Change category for the task "%s"' => 'Endre kategori for oppgaven "%s"', + 'Change category' => 'Endre kategori', + '%s updated the task %s' => '%s oppdaterte oppgaven %s', + '%s opened the task %s' => '%s åpnet oppgaven %s', + '%s moved the task %s to the position #%d in the column "%s"' => '%s flyttet oppgaven %s til posisjonen #%d i kolonnen "%s"', + '%s moved the task %s to the column "%s"' => '%s flyttet oppgaven %s til kolonnen "%s"', + '%s created the task %s' => '%s opprettet oppgaven %s', + '%s closed the task %s' => '%s lukket oppgaven %s', + '%s created a subtask for the task %s' => '%s opprettet en deloppgave for oppgaven %s', + '%s updated a subtask for the task %s' => '%s oppdaterte en deloppgave for oppgaven %s', + 'Assigned to %s with an estimate of %s/%sh' => 'Tildelt til %s med et estimat på %s/%sh', + 'Not assigned, estimate of %sh' => 'Ikke tildelt, estimert til %sh', + '%s updated a comment on the task %s' => '%s oppdaterte en kommentar til oppgaven %s', + '%s commented the task %s' => '%s har kommentert oppgaven %s', + '%s\'s activity' => '%s\'s aktvitet', + 'RSS feed' => 'RSS feed', + '%s updated a comment on the task #%d' => '%s oppdaterte en kommentar til oppgaven #%d', + '%s commented on the task #%d' => '%s kommenterte oppgaven #%d', + '%s updated a subtask for the task #%d' => '%s oppdaterte en deloppgave til oppgaven #%d', + '%s created a subtask for the task #%d' => '%s opprettet en deloppgave til oppgaven #%d', + '%s updated the task #%d' => '%s oppdaterte oppgaven #%d', + '%s created the task #%d' => '%s opprettet oppgaven #%d', + '%s closed the task #%d' => '%s lukket oppgaven #%d', + '%s open the task #%d' => '%s åpnet oppgaven #%d', + '%s moved the task #%d to the column "%s"' => '%s flyttet oppgaven #%d til kolonnen "%s"', + '%s moved the task #%d to the position %d in the column "%s"' => '%s flyttet oppgaven #%d til posisjonen %d i kolonnen "%s"', + 'Activity' => 'Aktivitetslogg', + 'Default values are "%s"' => 'Standardverdier er "%s"', + 'Default columns for new projects (Comma-separated)' => 'Standard kolonne for nye prosjekter (komma-separert)', + 'Task assignee change' => 'Endring av oppgaveansvarlig', + '%s change the assignee of the task #%d to %s' => '%s endre ansvarlig for oppgaven #%d til %s', + '%s changed the assignee of the task %s to %s' => '%s endret ansvarlig for oppgaven %s til %s', + 'New password for the user "%s"' => 'Nytt passord for brukeren "%s"', + 'Choose an event' => 'Velg en hendelse', + 'Github commit received' => 'Github forpliktelse mottatt', + 'Github issue opened' => 'Github problem åpnet', + 'Github issue closed' => 'Github problem lukket', + 'Github issue reopened' => 'Github problem gjenåpnet', + 'Github issue assignee change' => 'Endre ansvarlig for Github problem', + 'Github issue label change' => 'Endre etikett for Github problem', + 'Create a task from an external provider' => 'Oppret en oppgave fra en ekstern tilbyder', + 'Change the assignee based on an external username' => 'Endre ansvarlige baseret på et eksternt brukernavn', + 'Change the category based on an external label' => 'Endre kategorien basert på en ekstern etikett', + 'Reference' => 'Referanse', + 'Reference: %s' => 'Referanse: %s', + 'Label' => 'Etikett', + 'Database' => 'Database', + 'About' => 'Om', + 'Database driver:' => 'Database driver:', + 'Board settings' => 'Innstillinger for ptosjektside', + 'URL and token' => 'URL og token', + 'Webhook settings' => 'Webhook innstillinger', + 'URL for task creation:' => 'URL for oppgaveopprettelse:', + 'Reset token' => 'Resette token', + 'API endpoint:' => 'API endpoint:', + 'Refresh interval for private board' => 'Oppdateringsintervall for privat hovedside', + 'Refresh interval for public board' => 'Oppdateringsintervall for offentlig hovedside', + 'Task highlight period' => 'Fremhevingsperiode for oppgave', + 'Period (in second) to consider a task was modified recently (0 to disable, 2 days by default)' => 'Periode for å anta at en oppgave nylig ble endretg (0 for å deaktivere, 2 dager som standard)', + 'Frequency in second (60 seconds by default)' => 'Frekevens i sekunder (60 sekunder som standard)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frekvens i sekunder (0 for å deaktivere denne funksjonen, 10 sekunder som standard)', + 'Application URL' => 'Applikasjons URL', + 'Example: http://example.kanboard.net/ (used by email notifications)' => 'Eksempel: http://example.kanboard.net/ (bruges til email notifikationer)', + 'Token regenerated.' => 'Token regenerert.', + 'Date format' => 'Datoformat', + 'ISO format is always accepted, example: "%s" and "%s"' => 'ISO format er alltid akseptert, eksempelvis: "%s" og "%s"', + 'New private project' => 'Nytt privat prosjekt', + 'This project is private' => 'Dette projektet er privat', + 'Type here to create a new sub-task' => 'Skriv her for å opprette en ny deloppgave', + 'Add' => 'Legg til', + 'Estimated time: %s hours' => 'Estimert tid: %s timer', + 'Time spent: %s hours' => 'Tid brukt: %s timer', + 'Started on %B %e, %Y' => 'Startet %d.%m.%Y ', + 'Start date' => 'Start dato', + 'Time estimated' => 'Tid estimert', + 'There is nothing assigned to you.' => 'Ingen er tildelt deg.', + 'My tasks' => 'Mine oppgaver', + 'Activity stream' => 'Aktivitetslogg', + 'Dashboard' => 'Hovedsiden', + 'Confirmation' => 'Bekreftelse', + 'Allow everybody to access to this project' => 'Gi alle tilgang til dette prosjektet', + 'Everybody have access to this project.' => 'Alle har tilgang til dette prosjektet', + // 'Webhooks' => '', + // 'API' => '', + // 'Github webhooks' => '', + // 'Help on Github webhooks' => '', + 'Create a comment from an external provider' => 'Opprett en kommentar fra en ekstern tilbyder', + // 'Github issue comment created' => '', + 'Project management' => 'Prosjektinnstillinger', + 'My projects' => 'Mine prosjekter', + 'Columns' => 'Kolonner', + 'Task' => 'Oppgave', + // 'Your are not member of any project.' => '', + // 'Percentage' => '', + // 'Number of tasks' => '', + // 'Task distribution' => '', + // 'Reportings' => '', + // 'Task repartition for "%s"' => '', + 'Analytics' => 'Analyser', + // 'Subtask' => '', + 'My subtasks' => 'Mine deloppgaver', + // 'User repartition' => '', + // 'User repartition for "%s"' => '', + 'Clone this project' => 'Kopier dette prosjektet', + // 'Column removed successfully.' => '', + // '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' => 'Eksporter', + // 'This export contains the number of tasks per column grouped per day.' => '', + 'Nothing to preview...' => 'Ingenting å forhåndsvise', + 'Preview' => 'Forhåndsvisning', + 'Write' => 'Skriv', + 'Active swimlanes' => 'Aktive svæmmebaner', + 'Add a new swimlane' => 'Legg til en ny svømmebane', + 'Change default swimlane' => 'Endre standard svømmebane', + 'Default swimlane' => 'Standard svømmebane', + // 'Do you really want to remove this swimlane: "%s"?' => '', + // 'Inactive swimlanes' => '', + 'Set project manager' => 'Velg prosjektleder', + 'Set project member' => 'Velg prosjektmedlem', + 'Remove a swimlane' => 'Fjern en svømmebane', + 'Rename' => 'Endre navn', + 'Show default swimlane' => 'Vis standard svømmebane', + // 'Swimlane modification for the project "%s"' => '', + // 'Swimlane not found.' => '', + // 'Swimlane removed successfully.' => '', + 'Swimlanes' => 'Svømmebaner', + 'Swimlane updated successfully.' => 'Svæmmebane oppdatert', + // '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' => 'Integrasjoner', + 'Integration with third-party services' => 'Integrasjoner med tredje-parts tjenester', + // 'Role for this project' => '', + 'Project manager' => 'Prosjektleder', + 'Project member' => 'Prosjektmedlem', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Prosjektlederen kan endre flere innstillinger for prosjektet enn den en vanlig bruker kan.', + // 'Gitlab Issue' => '', + // 'Subtask Id' => '', + // 'Subtasks' => '', + // 'Subtasks Export' => '', + // 'Subtasks exportation for "%s"' => '', + // 'Task Title' => '', + // 'Untitled' => '', + 'Application default' => 'Standardinstilling', + 'Language:' => 'Språk', + 'Timezone:' => 'Tidssone', + // 'All columns' => '', + 'Calendar' => 'Kalender', + // 'Next' => '', + // '#%d' => '', + // 'All swimlanes' => '', + // 'All colors' => '', + // 'All status' => '', + // 'Moved to column %s' => '', + 'Change description' => 'Endre beskrivelse', + 'User dashboard' => 'Brukerens hovedside', + // 'Allow only one subtask in progress at the same time for a user' => '', + // 'Edit column "%s"' => '', + // 'Select the new status of the subtask: "%s"' => '', + 'Subtask timesheet' => 'Tidsskjema for deloppgaver', + 'There is nothing to show.' => 'Ingen data å vise', + // 'Time Tracking' => '', + // 'You already have one subtask in progress' => '', + 'Which parts of the project do you want to duplicate?' => 'Hvilke deler av dette prosjektet ønsker du å kopiere?', + // 'Disallow login form' => '', + // 'Bitbucket commit received' => '', + // 'Bitbucket webhooks' => '', + // 'Help on Bitbucket webhooks' => '', + // 'Start' => '', + // 'End' => '', + 'Task age in days' => 'Dager siden oppgaven ble opprettet', + 'Days in this column' => 'Dager siden oppgaven ble lagt i denne kolonnen', + // '%dd' => '', + 'Add a link' => 'Legg til en relasjon', + 'Add a new link' => 'Legg til en ny relasjon', + // '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.' => 'Ny relasjon er lagt til', + 'Link updated successfully.' => 'Relasjon er oppdatert', + 'Link removed successfully.' => 'Relasjon er fjernet', + 'Link labels' => 'Relasjonsetiketter', + 'Link modification' => 'Relasjonsmodifisering', + 'Links' => 'Relasjoner', + 'Link settings' => 'Relasjonsinnstillinger', + 'Opposite label' => 'Etikett for relatert motsatt oppgave', + 'Remove a link' => 'Fjern relasjon', + // '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' => 'relatert til', + 'blocks' => 'blokkerer', + 'is blocked by' => 'blokkeres av', + 'duplicates' => 'kopierer', + 'is duplicated by' => 'er en kopi av', + 'is a child of' => 'er en underordnet oppgave av', + 'is a parent of' => 'er en overordnet oppgave av', + 'targets milestone' => 'milepel', + 'is a milestone of' => 'er en milepel av', + 'fixes' => 'løser', + 'is fixed by' => 'løses av', + 'This task' => 'Denne oppgaven', + // '<1h' => '', + // '%dh' => '', + // '%b %e' => '', + 'Expand tasks' => 'Utvid oppgavevisning', + 'Collapse tasks' => 'Komprimer oppgavevisning', + 'Expand/collapse tasks' => 'Utvide/komprimere oppgavevisning', + // 'Close dialog box' => '', + // 'Submit a form' => '', + // 'Board view' => '', + 'Keyboard shortcuts' => 'Hurtigtaster', + // 'Open board switcher' => '', + // 'Application' => '', + 'since %B %e, %Y at %k:%M %p' => 'siden %B %e, %Y at %k:%M %p', + 'Compact view' => 'Kompakt visning', + 'Horizontal scrolling' => 'Bla horisontalt', + 'Compact/wide view' => 'Kompakt/bred visning', + 'No results match:' => 'Ingen resultater', + // 'Remove hourly rate' => '', + // 'Do you really want to remove this hourly rate?' => '', + 'Hourly rates' => 'Timepriser', + 'Hourly rate' => 'Timepris', + 'Currency' => 'Valuta', + // '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' => 'Tidstabell', + 'Overtime timetable' => 'Overtidstabell', + 'Time off timetable' => 'Fritidstabell', + 'Timetable' => 'Tidstabell', + 'Work timetable' => 'Arbeidstidstabell', + 'Week timetable' => 'Uketidstabell', + 'Day timetable' => 'Dagtidstabell', + 'From' => 'Fra', + 'To' => 'Til', + // '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' => 'Filer', + 'Images' => 'Bilder', + 'Private project' => 'Privat prosjekt', + // 'Amount' => '', + // 'AUD - Australian Dollar' => '', + 'Budget' => 'Budsjett', + // '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' => 'Flytt oppgaven til en annen kolonne når den er tildelt en bruker', + 'Move the task to another column when assignee is cleared' => 'Flytt oppgaven til en annen kolonne når ppgavetildeling fjernes ', + // 'Source column' => '', + // 'Show subtask estimates (forecast of future work)' => '', + 'Transitions' => 'Statusendringer', + // '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' => 'Valutakurser', + // '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' => 'Dobbel godkjenning', + // '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' => 'Endre til en valgt farge hvis en oppgave flyttes til en spesifikk kolonne', + // '%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' => 'Legg til et skjermbilde', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Ta et skjermbilde og trykk CTRL+V for å lime det inn her.', + 'Screenshot uploaded successfully.' => 'Skjermbilde opplastet', + // 'SEK - Swedish Krona' => '', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Prosjektkoden er en alfanumerisk kode som kan brukes for å identifisere prosjektet', + 'Identifier' => 'Prosjektkode', + // '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' => 'Endre gjentakelser', + 'Generate recurrent task' => 'Opprett gjentagende oppgave', + 'Trigger to generate recurrent task' => 'Betingelse for å generere gjentakende oppgave', + 'Factor to calculate new due date' => 'Faktor for å beregne ny tidsfrist', + 'Timeframe to calculate new due date' => 'Tidsramme for å beregne ny tidsfrist', + 'Base date to calculate new due date' => 'Grunnlagsdato for å beregne ny tidsfrist', + 'Action date' => 'Hendelsesdato', + 'Base date to calculate new due date: ' => 'Grunnlagsdato for å beregne ny tidsfrist', + 'This task has created this child task: ' => 'Denne oppgaven har opprettet denne relaterte oppgaven', + 'Day(s)' => 'Dager', + 'Existing due date' => 'Eksisterende forfallsdato', + 'Factor to calculate new due date: ' => 'Faktor for å beregne ny tidsfrist', + 'Month(s)' => 'Måneder', + 'Recurrence' => 'Gjentakelse', + 'This task has been created by: ' => 'Denne oppgaven er opprettet av:', + // 'Recurrent task has been generated:' => '', + // 'Timeframe to calculate new due date: ' => '', + // 'Trigger to generate recurrent task: ' => '', + 'When task is closed' => 'Når oppgaven er lukket', + 'When task is moved from first column' => 'Når oppgaven er flyttet fra første kolon', + 'When task is moved to last column' => 'Når oppgaven er flyttet til siste kolonne', + 'Year(s)' => 'år', + // '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' => 'Kalenderinstillinger', + 'Project calendar view' => 'Visning prosjektkalender', + 'Project settings' => 'Prosjektinnstillinger', + // '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' => 'Oppdater automatisk start-datoen', + // 'iCal feed' => '', + 'Preferences' => 'Preferanser', + 'Security' => 'Sikkerhet', + 'Two factor authentication disabled' => 'Dobbelgodkjenning deaktivert', + 'Two factor authentication enabled' => 'Dobbelgodkjenning aktivert', + // 'Unable to update this user.' => '', + // 'There is no user management for private projects.' => '', + // 'User that will receive the email' => '', + // 'Email subject' => '', + // 'Date' => '', + // 'By @%s on Bitbucket' => '', + // 'Bitbucket Issue' => '', + // 'Commit made by @%s on Bitbucket' => '', + // 'Commit made by @%s on Github' => '', + // 'By @%s on Github' => '', + // 'Commit made by @%s on Gitlab' => '', + 'Add a comment log when moving the task between columns' => 'Legg til en kommentar i loggen når en oppgave flyttes mellom kolonnene', + 'Move the task to another column when the category is changed' => 'Flytt oppgaven til en annen kolonne når kategorien endres', + 'Send a task by email to someone' => 'Send en oppgave på epost til noen', + // 'Reopen a task' => '', + // 'Bitbucket issue opened' => '', + // 'Bitbucket issue closed' => '', + // 'Bitbucket issue reopened' => '', + // 'Bitbucket issue assignee change' => '', + // 'Bitbucket issue comment created' => '', + 'Column change' => 'Endret kolonne', + 'Position change' => 'Posisjonsendring', + 'Swimlane change' => 'Endret svømmebane', + 'Assignee change' => 'Endret eier', + // '[%s] Overdue tasks' => '', + 'Notification' => 'Varsel', + // '%s moved the task #%d to the first swimlane' => '', + // '%s moved the task #%d to the swimlane "%s"' => '', + // 'Swimlane' => '', + // 'Budget overview' => '', + // 'Type' => '', + // 'There is not enough data to show something.' => '', + // 'Gravatar' => '', + // 'Hipchat' => '', + // 'Slack' => '', + // '%s moved the task %s to the first swimlane' => '', + // '%s moved the task %s to the swimlane "%s"' => '', + // 'This report contains all subtasks information for the given date range.' => '', + // 'This report contains all tasks information for the given date range.' => '', + // 'Project activities for %s' => '', + // 'view the board on Kanboard' => '', + // 'The task have been moved to the first swimlane' => '', + // 'The task have been moved to another swimlane:' => '', + // 'Overdue tasks for the project "%s"' => '', + // 'New title: %s' => '', + // 'The task is not assigned anymore' => '', + // 'New assignee: %s' => '', + // 'There is no category now' => '', + // 'New category: %s' => '', + // 'New color: %s' => '', + // 'New complexity: %d' => '', + // 'The due date have been removed' => '', + // 'There is no description anymore' => '', + // 'Recurrence settings have been modified' => '', + // 'Time spent changed: %sh' => '', + // 'Time estimated changed: %sh' => '', + // 'The field "%s" have been updated' => '', + // 'The description have been modified' => '', + // 'Do you really want to close the task "%s" as well as all subtasks?' => '', + // 'Swimlane: %s' => '', + // 'I want to receive notifications for:' => '', + 'All tasks' => 'Alle oppgaver', + // 'Only for tasks assigned to me' => '', + // 'Only for tasks created by me' => '', + // 'Only for tasks created by me and assigned to me' => '', + // '%A' => '', + // '%b %e, %Y, %k:%M %p' => '', + // 'New due date: %B %e, %Y' => '', + // 'Start date changed: %B %e, %Y' => '', + // '%k:%M %p' => '', + // '%%Y-%%m-%%d' => '', + // 'Total for all columns' => '', + // 'You need at least 2 days of data to show the chart.' => '', + // '<15m' => '', + // '<30m' => '', + 'Stop timer' => 'Stopp timer', + 'Start timer' => 'Start timer', + 'Add project member' => 'Legg til prosjektmedlem', + 'Enable notifications' => 'Aktiver varslinger', + // 'My activity stream' => '', + // 'My calendar' => '', + // 'Search tasks' => '', + // 'Back to the calendar' => '', + // 'Filters' => '', + // 'Reset filters' => '', + // 'My tasks due tomorrow' => '', + // 'Tasks due today' => '', + // 'Tasks due tomorrow' => '', + // 'Tasks due yesterday' => '', + // 'Closed tasks' => '', + // 'Open tasks' => '', + // 'Not assigned' => '', + // 'View advanced search syntax' => '', + // 'Overview' => '', + // '%b %e %Y' => '', + // 'Board/Calendar/List view' => '', + // 'Switch to the board view' => '', + // 'Switch to the calendar view' => '', + // 'Switch to the list view' => '', + // 'Go to the search/filter box' => '', + // 'There is no activity yet.' => '', + // 'No tasks found.' => '', + // 'Keyboard shortcut: "%s"' => '', + // 'List' => '', + // 'Filter' => '', + // 'Advanced search' => '', + // 'Example of query: ' => '', + // 'Search by project: ' => '', + // 'Search by column: ' => '', + // 'Search by assignee: ' => '', + // 'Search by color: ' => '', + // 'Search by category: ' => '', + // 'Search by description: ' => '', + // 'Search by due date: ' => '', + // 'Lead and Cycle time for "%s"' => '', + // 'Average time spent into each column for "%s"' => '', + // 'Average time spent into each column' => '', + // 'Average time spent' => '', + // 'This chart show the average time spent into each column for the last %d tasks.' => '', + // 'Average Lead and Cycle time' => '', + // 'Average lead time: ' => '', + // 'Average cycle time: ' => '', + // 'Cycle Time' => '', + // 'Lead Time' => '', + // 'This chart show the average lead and cycle time for the last %d tasks over the time.' => '', + // 'Average time into each column' => '', + // 'Lead and cycle time' => '', + // 'Google Authentication' => '', + // 'Help on Google authentication' => '', + // 'Github Authentication' => '', + // 'Help on Github authentication' => '', + // 'Channel/Group/User (Optional)' => '', + // 'Lead time: ' => '', + // 'Cycle time: ' => '', + // 'Time spent into each column' => '', + // 'The lead time is the duration between the task creation and the completion.' => '', + // 'The cycle time is the duration between the start date and the completion.' => '', + // 'If the task is not closed the current time is used instead of the completion date.' => '', + // 'Set automatically the start date' => '', + // 'Edit Authentication' => '', + // 'Google Id' => '', + // 'Github Id' => '', + // 'Remote user' => '', + // 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => '', + // 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => '', + // 'By @%s on Gitlab' => '', + // 'Gitlab issue comment created' => '', + // 'New remote user' => '', + // 'New local user' => '', + // 'Default task color' => '', + // 'Hide sidebar' => '', + // 'Expand sidebar' => '', + // 'This feature does not work with all browsers.' => '', + // 'There is no destination project available.' => '', + // 'Trigger automatically subtask time tracking' => '', + // 'Include closed tasks in the cumulative flow diagram' => '', + // 'Current swimlane: %s' => '', + // 'Current column: %s' => '', + // 'Current category: %s' => '', + // 'no category' => '', + // 'Current assignee: %s' => '', + // 'not assigned' => '', + // 'Author:' => '', + // 'contributors' => '', + // 'License:' => '', + // 'License' => '', + // 'Project Administrator' => '', + // 'Enter the text below' => '', + // 'Gantt chart for %s' => '', + // 'Sort by position' => '', + // 'Sort by date' => '', + // 'Add task' => '', + // 'Start date:' => '', + // 'Due date:' => '', + // 'There is no start date or due date for this task.' => '', + // 'Moving or resizing a task will change the start and due date of the task.' => '', + // 'There is no task in your project.' => '', + // 'Gantt chart' => '', + // 'People who are project managers' => '', + // 'People who are project members' => '', + // 'NOK - Norwegian Krone' => '', + // 'Show this column' => '', + // 'Hide this column' => '', + // 'open file' => '', + // 'End date' => '', + // 'Users overview' => '', + // 'Managers' => '', + // 'Members' => '', + // 'Shared project' => '', + // 'Project managers' => '', + // 'Project members' => '', + // 'Gantt chart for all projects' => '', + // 'Projects list' => '', + // 'Gantt chart for this project' => '', + // 'Project board' => '', + // 'End date:' => '', + // 'There is no start date or end date for this project.' => '', + // 'Projects Gantt chart' => '', + // 'Start date: %s' => '', + // 'End date: %s' => '', + // 'Link type' => '', + // 'Change task color when using a specific task link' => '', + // 'Task link creation or modification' => '', + // 'Login with my Gitlab Account' => '', + // 'Milestone' => '', + // 'Gitlab Authentication' => '', + // 'Help on Gitlab authentication' => '', + // 'Gitlab Id' => '', + // 'Gitlab Account' => '', + // 'Link my Gitlab Account' => '', + // 'Unlink my Gitlab Account' => '', + // 'Documentation: %s' => '', + // 'Switch to the Gantt chart view' => '', + // 'Reset the search/filter box' => '', + // 'Documentation' => '', + // 'Table of contents' => '', + // 'Gantt' => '', + // 'Help with project permissions' => '', +); diff --git a/sources/app/Locale/pt_PT/translations.php b/sources/app/Locale/pt_PT/translations.php new file mode 100644 index 0000000..d19a31e --- /dev/null +++ b/sources/app/Locale/pt_PT/translations.php @@ -0,0 +1,1070 @@ + ',', + 'number.thousands_separator' => ' ', + 'None' => 'Nenhum', + 'edit' => 'editar', + 'Edit' => 'Editar', + 'remove' => 'remover', + 'Remove' => 'Remover', + 'Update' => 'Actualizar', + 'Yes' => 'Sim', + 'No' => 'Não', + 'cancel' => 'cancelar', + 'or' => 'ou', + 'Yellow' => 'Amarelo', + 'Blue' => 'Azul', + 'Green' => 'Verde', + 'Purple' => 'Roxo', + 'Red' => 'Vermelho', + 'Orange' => 'Laranja', + 'Grey' => 'Cinza', + 'Brown' => 'Castanho', + 'Deep Orange' => 'Laranja escuro', + 'Dark Grey' => 'Cinza escuro', + 'Pink' => 'Rosa', + 'Teal' => 'Turquesa', + 'Cyan' => 'Azul intenso', + 'Lime' => 'Verde limão', + 'Light Green' => 'Verde claro', + 'Amber' => 'Âmbar', + 'Save' => 'Guardar', + 'Login' => 'Login', + 'Official website:' => 'Site oficial:', + 'Unassigned' => 'Não Atribuída', + 'View this task' => 'Ver esta tarefa', + 'Remove user' => 'Remover utilizador', + 'Do you really want to remove this user: "%s"?' => 'Pretende mesmo remover este utilizador: "%s"?', + 'New user' => 'Novo utilizador', + 'All users' => 'Todos os utilizadores', + 'Username' => 'Nome de utilizador', + 'Password' => 'Senha', + 'Administrator' => 'Administrador', + 'Sign in' => 'Entrar', + 'Users' => 'Utilizadores', + 'No user' => 'Sem utilizador', + 'Forbidden' => 'Proibido', + 'Access Forbidden' => 'Acesso negado', + 'Edit user' => 'Editar utilizador', + 'Logout' => 'Sair', + 'Bad username or password' => 'Utilizador ou senha inválidos', + 'Edit project' => 'Editar projecto', + 'Name' => 'Nome', + 'Projects' => 'Projectos', + 'No project' => 'Nenhum projecto', + 'Project' => 'Projecto', + 'Status' => 'Estado', + 'Tasks' => 'Tarefas', + 'Board' => 'Quadro', + 'Actions' => 'Acções', + 'Inactive' => 'Inactivo', + 'Active' => 'Activo', + '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.' => 'Não foi possível actualizar este quadro.', + 'Edit board' => 'Editar quadro', + 'Disable' => 'Desactivar', + 'Enable' => 'Activar', + 'New project' => 'Novo projecto', + 'Do you really want to remove this project: "%s"?' => 'Tem a certeza que quer remover este projecto: "%s" ?', + 'Remove project' => 'Remover projecto', + 'Edit the board for "%s"' => 'Editar o quadro para "%s"', + 'All projects' => 'Todos os projectos', + 'Change columns' => 'Modificar colunas', + 'Add a new column' => 'Adicionar uma nova coluna', + 'Title' => 'Título', + 'Nobody assigned' => 'Ninguém assignado', + '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.' => 'Não foi possível remover esta coluna.', + 'Do you really want to remove this column: "%s"?' => 'Tem a certeza que quer remover esta coluna: "%s"?', + 'This action will REMOVE ALL TASKS associated to this column!' => 'Esta acçã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:', + 'Database size:' => 'Tamanho da base de dados:', + 'Download the database' => 'Download da base de dados', + 'Optimize the database' => 'Otimizar a base de dados', + '(VACUUM command)' => '(Comando VACUUM)', + '(Gzip compressed Sqlite file)' => '(Arquivo Sqlite comprimido com Gzip)', + 'Close a task' => 'Finalizar uma tarefa', + 'Edit a task' => 'Editar uma tarefa', + 'Column' => 'Coluna', + 'Color' => 'Cor', + 'Assignee' => 'Assignado', + '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"?' => 'Tem a certeza que quer 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 assignado', + 'Column on the board:' => 'Coluna no quadro:', + 'Status is open' => 'Estado é aberto', + 'Status is closed' => 'Estado é 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 utilizador é 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 utilizador deve ser único', + 'The user id is required' => 'O ID de utilizador é obrigatório', + 'Passwords don\'t match' => 'As senhas não coincidem', + 'The confirmation is required' => 'A confirmação é obrigatória', + 'The project is required' => 'O projecto é obrigatório', + 'The id is required' => 'O ID é obrigatório', + 'The project id is required' => 'O ID do projecto é obrigatório', + 'The project name is required' => 'O nome do projecto é obrigatório', + 'This project must be unique' => 'Este projecto deve ser único', + 'The title is required' => 'O título é obrigatório', + 'Settings saved successfully.' => 'Configurações guardadas com sucesso.', + 'Unable to save your settings.' => 'Não é possível guardar as suas configurações.', + 'Database optimization done.' => 'Otimização da base de dados finalizada.', + 'Your project have been created successfully.' => 'Projecto foi criado com sucesso.', + 'Unable to create your project.' => 'Não é possível criar o projecto.', + 'Project updated successfully.' => 'Projecto actualizado com sucesso.', + 'Unable to update this project.' => 'Não é possível actualizar este projecto.', + 'Unable to remove this project.' => 'Não é possível remover este projecto.', + 'Project removed successfully.' => 'Projecto removido com sucesso.', + 'Project activated successfully.' => 'Projecto activado com sucesso.', + 'Unable to activate this project.' => 'Não é possível activar este projecto.', + 'Project disabled successfully.' => 'Projecto desactivado com sucesso.', + 'Unable to disable this project.' => 'Não é possível desactivar este projecto.', + '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 actualizar a sua tarefa.', + 'Task updated successfully.' => 'Tarefa actualizada 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.' => 'Utilizador criado com sucesso.', + 'Unable to create your user.' => 'Não é possível criar o seu Utilizador.', + 'User updated successfully.' => 'Utilizador actualizado com sucesso.', + 'Unable to update your user.' => 'Não é possível actualizar o seu Utilizador.', + 'User removed successfully.' => 'Utilizador removido com sucesso.', + 'Unable to remove this user.' => 'Não é possível remover este Utilizador.', + 'Board updated successfully.' => 'Quadro actualizado 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', + '%d closed tasks' => '%d tarefas finalizadas', + 'No task for this project' => 'Não há tarefa para este projecto', + 'Public link' => 'Link público', + 'There is no column in your project!' => 'Não há colunas no seu projecto!', + 'Change assignee' => 'Mudar a assignação', + 'Change assignee for the task "%s"' => 'Modificar assignaçã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 na minha base de dados!', + 'Page not found' => 'Página não encontrada', + 'Complexity' => 'Complexidade', + 'Task limit' => 'Limite da tarefa', + 'Task count' => 'Número de tarefas', + 'Edit project access list' => 'Editar lista de acesso ao projecto', + 'Allow this user' => 'Permitir este utilizador', + 'Don\'t forget that administrators have access to everything.' => 'Não se esqueça que administradores têm acesso a tudo.', + 'Revoke' => 'Revogar', + 'List of authorized users' => 'Lista de utilizadores autorizados', + 'User' => 'Utilizador', + 'Nobody have access to this project.' => 'Ninguém tem acesso a este projecto.', + 'Comments' => 'Comentários', + 'Write your text in Markdown' => 'Escreva o 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.', + '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' => 'Acções automáticas', + 'Your automatic action have been created successfully.' => 'A sua acção automática foi criada com sucesso.', + 'Unable to create your automatic action.' => 'Não é possível criar a sua acção automática.', + 'Remove an action' => 'Remover uma acção', + 'Unable to remove this action.' => 'Não é possível remover esta acção.', + 'Action removed successfully.' => 'Acção removida com sucesso.', + 'Automatic actions for the project "%s"' => 'Acções automáticas para o projecto "%s"', + 'Defined actions' => 'Acções definidas', + 'Add an action' => 'Adicionar Acção', + 'Event name' => 'Nome do evento', + 'Action name' => 'Nome da acção', + 'Action parameters' => 'Parâmetros da acção', + 'Action' => 'Acção', + 'Event' => 'Evento', + 'When the selected event occurs execute the corresponding action.' => 'Quando o evento selecionado ocorrer execute a acção correspondente.', + 'Next step' => 'Próximo passo', + 'Define action parameters' => 'Definir parêmetros da acção', + 'Save this action' => 'Guardar esta acção', + 'Do you really want to remove this action: "%s"?' => 'Tem a certeza que quer remover esta acção: "%s"?', + 'Remove an automatic action' => 'Remover uma acção automática', + 'Assign the task to a specific user' => 'Designar a tarefa para um utilizador específico', + 'Assign the task to the person who does the action' => 'Designar a tarefa para a pessoa que executa a acção', + 'Duplicate the task to another project' => 'Duplicar a tarefa para um outro projecto', + 'Move a task to another column' => 'Mover a tarefa para outra coluna', + 'Task modification' => 'Modificação de tarefa', + 'Task creation' => 'Criação de tarefa', + 'Closing a task' => 'A finalizar uma tarefa', + 'Assign a color to a specific user' => 'Designar uma cor para um utilizador 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 projecto', + 'Duplicate' => 'Duplicar', + 'link' => 'link', + 'Comment updated successfully.' => 'Comentário actualizado com sucesso.', + 'Unable to update your comment.' => 'Não é possível actualizar 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?' => 'Tem a certeza que quer 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.', + 'Current password for the user "%s"' => 'Senha atual para o utilizador "%s"', + 'The current password is required' => 'A senha atual é obrigatória', + 'Wrong password' => 'Senha incorreta', + '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', + 'Everybody' => 'Todos', + 'Open' => 'Aberto', + 'Closed' => 'Finalizado', + 'Search' => 'Pesquisar', + 'Nothing found.' => 'Nada encontrado.', + '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 external account is not linked anymore to your profile.' => 'A sua conta externa já não se encontra vinculada a este perfil', + 'Unable to unlink your external account.' => 'Não foi possivel desvincular a sua conta externa', + 'External authentication failed' => 'Autenticação externa falhou', + 'Your external account is linked to your profile successfully.' => 'A sua conta externa foi vinculada com sucesso ao seu perfil', + 'Email' => 'E-mail', + 'Link my Google Account' => 'Vincular a minha Conta do Google', + 'Unlink my Google Account' => 'Desvincular a minha Conta do Google', + 'Login with my Google Account' => 'Entrar com a minha Conta do Google', + 'Project not found.' => 'Projecto não encontrado.', + '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"?' => 'Tem a certeza que quer remover esta tarefa: "%s"', + 'Assign automatically a color based on a category' => 'Atribuir automaticamente uma cor com base numa categoria', + 'Assign automatically a category based on a color' => 'Atribuir automaticamente uma categoria com base numa 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.' => 'A 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 actualizada com sucesso.', + 'Unable to update your category.' => 'Não foi possível actualizar a sua categoria.', + 'Remove a category' => 'Remover uma categoria', + 'Category removed successfully.' => 'Categoria removida 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 projecto "%s"', + 'Category Name' => 'Nome da Categoria', + 'Add a new category' => 'Adicionar uma nova categoria', + 'Do you really want to remove this category: "%s"?' => 'Tem a certeza que quer remover esta categoria: "%s"', + 'All categories' => 'Todas as categorias', + 'No category' => 'Nenhuma 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"?' => 'Tem a certeza que quer 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?' => 'Tem a certeza que quer 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' => 'A 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' => 'Mostrar outro projecto', + 'Login with my Github Account' => 'Entrar com a 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', + 'Project cloned successfully.' => 'Projecto clonado com sucesso.', + 'Unable to clone this project.' => 'Não foi possível clonar este projecto.', + 'Email notifications' => 'Notificações por email', + 'Enable email notifications' => 'Activar 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:' => 'Estado:', + 'Assignee:' => 'Assignado:', + 'Time tracking:' => 'Controle de tempo:', + 'New sub-task' => 'Nova subtarefa', + 'New attachment added "%s"' => 'Novo anexo adicionado "%s"', + 'Comment updated' => 'Comentário actualizado', + 'New comment posted by %s' => 'Novo comentário por %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', + 'I want to receive notifications only for those projects:' => 'Quero receber notificações apenas destes projectos:', + 'view the task on Kanboard' => 'ver a tarefa no Kanboard', + 'Public access' => 'Acesso público', + 'User management' => 'Gestão de utilizadores', + 'Active tasks' => 'Tarefas activas', + 'Disable public access' => 'Desactivar o acesso público', + 'Enable public access' => 'Activar o acesso público', + 'Public access disabled' => 'Acesso público desactivado', + 'Do you really want to disable this project: "%s"?' => 'Tem a certeza que quer desactivar este projecto: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Tem a certeza que quer activar este projecto: "%s"?', + 'Project activation' => 'Activação do projecto', + 'Move the task to another project' => 'Mover a tarefa para outro projecto', + 'Move to another project' => 'Mover para outro projecto', + 'Do you really want to duplicate this task?' => 'Tem a certeza que quer duplicar esta tarefa?', + 'Duplicate a task' => 'Duplicar uma tarefa', + 'External accounts' => 'Contas externas', + 'Account type' => 'Tipo de conta', + 'Local' => 'Local', + 'Remote' => 'Remoto', + 'Enabled' => 'Activado', + 'Disabled' => 'Desactivado', + 'Google account linked' => 'Conta do Google associada', + 'Github account linked' => 'Conta do Github associada', + 'Username:' => 'Utilizador:', + 'Name:' => 'Nome:', + 'Email:' => 'E-mail:', + 'Notifications:' => 'Notificações:', + 'Notifications' => 'Notificações', + 'Group:' => 'Grupo:', + 'Regular user' => 'Utilizador 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 activa.', + '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 actualizou 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 actualizou 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 assignado, estimado em %sh', + '%s updated a comment on the task %s' => '%s actualizou o comentário na tarefa %s', + '%s commented the task %s' => '%s comentou a tarefa %s', + '%s\'s activity' => 'Atividades de%s', + 'RSS feed' => 'Feed RSS', + '%s updated a comment on the task #%d' => '%s actualizou 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 actualizou 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 actualizou 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' => 'Actividade', + 'Default values are "%s"' => 'Os valores padrão são "%s"', + 'Default columns for new projects (Comma-separated)' => 'Colunas padrão para novos projectos (Separado por vírgula)', + 'Task assignee change' => 'Mudar assignação da tarefa', + '%s change the assignee of the task #%d to %s' => '%s mudou a assignação da tarefa #%d para %s', + '%s changed the assignee of the task %s to %s' => '%s mudou a assignação da tarefa %s para %s', + 'New password for the user "%s"' => 'Nova senha para o utilizador "%s"', + 'Choose an event' => 'Escolher um evento', + 'Github commit received' => 'Recebido commit do Github', + 'Github issue opened' => 'Problema aberto no Github', + 'Github issue closed' => 'Problema fechado no Github', + 'Github issue reopened' => 'Problema reaberto no Github', + 'Github issue assignee change' => 'Alterar assignação ao problema no Githubnge', + 'Github issue label change' => 'Alterar etiqueta do problema no Github', + '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 assignação com base num utilizador externo', + 'Change the category based on an external label' => 'Alterar categoria com base num rótulo externo', + 'Reference' => 'Referência', + 'Reference: %s' => 'Referência: %s', + 'Label' => 'Rótulo', + 'Database' => 'Base de dados', + 'About' => 'Sobre', + 'Database driver:' => 'Driver da base de dados:', + 'Board settings' => 'Configurações do Quadro', + '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' => 'Redefinir token', + 'API endpoint:' => 'API endpoint:', + 'Refresh interval for private board' => 'Intervalo de actualização para um quadro privado', + 'Refresh interval for public board' => 'Intervalo de actualização para um quadro 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 desactivar, 2 dias por defeito)', + 'Frequency in second (60 seconds by default)' => 'Frequência em segundos (60 segundos por defeito)', + 'Frequency in second (0 to disable this feature, 10 seconds by default)' => 'Frequência em segundos (0 para desactivar este recurso, 10 segundos por defeito)', + '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 da data', + 'ISO format is always accepted, example: "%s" and "%s"' => 'O formato ISO é sempre aceite, exemplo: "%s" e "%s"', + 'New private project' => 'Novo projecto privado', + 'This project is private' => 'Este projecto é privado', + 'Type here to create a new sub-task' => 'Escreva 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 assignado a si.', + 'My tasks' => 'As minhas tarefas', + 'Activity stream' => 'Atividades Recentes', + 'Dashboard' => 'Painel de Controlo', + 'Confirmation' => 'Confirmação', + 'Allow everybody to access to this project' => 'Permitir a todos os acesso a este projecto', + 'Everybody have access to this project.' => 'Todos possuem acesso a este projecto.', + 'Webhooks' => 'Webhooks', + 'API' => 'API', + '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' => 'Criado comentário ao problema no Github', + 'Project management' => 'Gestão de projectos', + 'My projects' => 'Os meus projectos', + 'Columns' => 'Colunas', + 'Task' => 'Tarefas', + 'Your are not member of any project.' => 'Você não é membro de nenhum projecto.', + 'Percentage' => 'Percentagem', + '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' => 'As minhas subtarefas', + 'User repartition' => 'Redistribuição de utilizador', + 'User repartition for "%s"' => 'Redistribuição de utilizador para "%s"', + 'Clone this project' => 'Clonar este projecto', + 'Column removed successfully.' => 'Coluna removida com sucesso.', + 'Github Issue' => 'Problema no Github', + '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 projecto deve ser um inteiro', + 'The status must be an integer' => 'O estado 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 utilizador 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 projecto', + 'Daily project summary export' => 'Exportação diária do resumo do projecto', + 'Daily project summary export for "%s"' => 'Exportação diária do resumo do projecto 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' => 'Activar 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"?' => 'Tem a certeza que quer remover este swimlane: "%s"?', + 'Inactive swimlanes' => 'Desactivar swimlanes', + 'Set project manager' => 'Definir gerente do projecto', + 'Set project member' => 'Definir membro do projecto', + 'Remove a swimlane' => 'Remover um swimlane', + 'Rename' => 'Renomear', + 'Show default swimlane' => 'Mostrar swimlane padrão', + 'Swimlane modification for the project "%s"' => 'Modificação de swimlane para o projecto "%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 projectos (Separadas por vírgula)', + 'Gitlab commit received' => 'Commit recebido do Gitlab', + 'Gitlab issue opened' => 'Problema aberto no Gitlab', + 'Gitlab issue closed' => 'Problema fechado no Gitlab', + '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 projecto', + 'Project manager' => 'Gerente do projecto', + 'Project member' => 'Membro do projecto', + 'A project manager can change the settings of the project and have more privileges than a standard user.' => 'Um gerente do projecto pode alterar as configurações do projecto e ter mais privilégios que um utilizador padrão.', + 'Gitlab Issue' => 'Problema Gitlab', + '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' => 'Calendário', + 'Next' => 'Próximo', + // '#%d' => '', + 'All swimlanes' => 'Todos os swimlane', + 'All colors' => 'Todas as cores', + 'All status' => 'Todos os estados', + 'Moved to column %s' => 'Mover para a coluna %s', + 'Change description' => 'Modificar a descrição', + 'User dashboard' => 'Painel de Controlo do utilizador', + 'Allow only one subtask in progress at the same time for a user' => 'Permitir apenas uma subtarefa em andamento ao mesmo tempo para um utilizador', + 'Edit column "%s"' => 'Editar a coluna "%s"', + 'Select the new status of the subtask: "%s"' => 'Selecionar um novo estado para a subtarefa: "%s"', + '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' => 'Já tem uma subtarefa em andamento', + 'Which parts of the project do you want to duplicate?' => 'Quais as partes do projecto que deseja duplicar?', + 'Disallow login form' => 'Desactivar login', + '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"?' => 'Tem a certeza que quer remover esta associação: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Tem a certeza que quer 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' => 'está 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 objectivo', + 'is a milestone of' => 'é um objectivo 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' => 'Vista do Quadro', + 'Keyboard shortcuts' => 'Atalhos de teclado', + 'Open board switcher' => 'Abrir o comutador de painel', + 'Application' => 'Aplicação', + 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M', + 'Compact view' => 'Vista reduzida', + 'Horizontal scrolling' => 'Deslocamento 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?' => 'Tem a certeza que quer remover esta taxa horária?', + 'Hourly rates' => 'Taxas horárias', + 'Hourly rate' => 'Taxa horária', + 'Currency' => 'Moeda', + 'Effective date' => 'Data efectiva', + '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 um dia', + 'From' => 'Desde', + 'To' => 'A', + 'Time slot created successfully.' => 'Intervalo de tempo criado com sucesso.', + 'Unable to save this time slot.' => 'Impossível guardar este intervalo de tempo.', + 'Time slot removed successfully.' => 'Intervalo de tempo removido com sucesso.', + 'Unable to remove this time slot.' => 'Impossível remover esse intervalo de tempo.', + 'Do you really want to remove this time slot?' => 'Tem a certeza que quer 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' => 'Projecto 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 estilos personalizada', + 'download' => 'transferir', + 'Do you really want to remove this budget line?' => 'Tem a certeza que quer 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 adicionar esta rubrica orçamental.', + 'Unable to remove this budget line.' => 'Impossível 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 utilizador', + '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 utilizador 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 projecto.', + '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 adicionar essa taxa de câmbio.', + 'Send notifications to a Slack channel' => 'Enviar as notificações por 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 assignada à 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' => 'Activar imagem Gravatar', + 'Information' => 'Informações', + 'Check two factor authentication code' => 'Verificação do código de autenticação com factor duplo', + 'The two factor authentication code is not valid.' => 'O código de autenticação com factor duplo não é válido', + 'The two factor authentication code is valid.' => 'O código de autenticação com factor duplo é válido', + 'Code' => 'Código', + 'Two factor authentication' => 'Autenticação com factor duplo', + 'Enable/disable two factor authentication' => 'Activar/Desactivar autenticação com factor 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).' => 'Guarde esta chave secreta no seu software TOTP (por exemplo Google Authenticator ou FreeOTP).', + 'Check my code' => 'Verificar 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 "%s"', + '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 tirada a %s', + 'Add a screenshot' => 'Adicionar uma Screenshot', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Tire 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 projecto é um código alfanumérico opcional utilizado para identificar o seu projecto.', + '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' => 'Desactivar autenticação com dois factores', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Tem a certeza que quer desactivar a autenticação com dois factores para esse utilizador: "%s"?', + 'Edit link' => 'Editar um link', + 'Start to type task title...' => 'Escreva 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' => 'Activador para gerar tarefa recorrente', + 'Factor to calculate new due date' => 'Factor 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 acçã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: ' => 'Factor 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: ' => 'Activador 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-utilizador', + '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 projecto do calendário', + 'Project settings' => 'Configurações dos projectos', + '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' => 'Actualizar automaticamente a data de início', + 'iCal feed' => 'Subscrição iCal', + 'Preferences' => 'Preferências', + 'Security' => 'Segurança', + 'Two factor authentication disabled' => 'Autenticação com factor duplo desactivado', + 'Two factor authentication enabled' => 'Autenticação com factor duplo activado', + 'Unable to update this user.' => 'Impossível de actualizar este utilizador.', + 'There is no user management for private projects.' => 'Não há gestão de utilizadores para projectos privados.', + 'User that will receive the email' => 'O utilizador que vai receber o e-mail', + 'Email subject' => 'Assunto do e-mail', + 'Date' => 'Data', + 'By @%s on Bitbucket' => 'Por @%s no Bitbucket', + 'Bitbucket Issue' => 'Problema Bitbucket', + 'Commit made by @%s on Bitbucket' => 'Commit feito por @%s no Bitbucket', + 'Commit made by @%s on Github' => 'Commit feito por @%s no Github', + 'By @%s on Github' => 'Por @%s no Github', + 'Commit made by @%s on Gitlab' => 'Commit feito por @%s no Gitlab', + 'Add a comment log when moving the task between columns' => 'Adicionar um comentário de log quando uma tarefa é movida para uma outra coluna', + 'Move the task to another column when the category is changed' => 'Mover uma tarefa para outra coluna quando a categoria mudar', + 'Send a task by email to someone' => 'Enviar uma tarefa por e-mail a alguém', + 'Reopen a task' => 'Reabrir uma tarefa', + 'Bitbucket issue opened' => 'Problema aberto no Bitbucket', + 'Bitbucket issue closed' => 'Problema fechado no Bitbucket', + 'Bitbucket issue reopened' => 'Problema reaberto no Bitbucket', + 'Bitbucket issue assignee change' => 'Alterar assignação do problema no Bitbucket', + 'Bitbucket issue comment created' => 'Comentário ao problema adicionado ao Bitbucket', + 'Column change' => 'Mudança de coluna', + 'Position change' => 'Mudança de posição', + 'Swimlane change' => 'Mudança de swimlane', + 'Assignee change' => 'Mudança de assignação', + '[%s] Overdue tasks' => '[%s] Tarefas atrasadas', + 'Notification' => 'Notificação', + '%s moved the task #%d to the first swimlane' => '%s moveu a tarefa n° %d no primeiro swimlane', + '%s moved the task #%d to the swimlane "%s"' => '%s moveu a tarefa n° %d no swimlane "%s"', + 'Swimlane' => 'Swimlane', + 'Budget overview' => 'Visão geral do orçamento', + 'Type' => 'Tipo', + 'There is not enough data to show something.' => 'Não há dados suficientes para mostrar alguma coisa.', + 'Gravatar' => 'Gravatar', + 'Hipchat' => 'Hipchat', + 'Slack' => 'Slack', + '%s moved the task %s to the first swimlane' => '%s moveu a tarefa %s no primeiro swimlane', + '%s moved the task %s to the swimlane "%s"' => '%s moveu a tarefa %s no swimlane "%s"', + 'This report contains all subtasks information for the given date range.' => 'Este relatório contém informações de todas as sub-tarefas para o período selecionado.', + 'This report contains all tasks information for the given date range.' => 'Este relatório contém informações de todas as tarefas para o período selecionado.', + 'Project activities for %s' => 'Actividade do projecto "%s"', + 'view the board on Kanboard' => 'ver o painel no Kanboard', + 'The task have been moved to the first swimlane' => 'A tarefa foi movida para o primeiro Swimlane', + 'The task have been moved to another swimlane:' => 'A tarefa foi movida para outro Swimlane:', + 'Overdue tasks for the project "%s"' => 'Tarefas atrasadas para o projecto "%s"', + 'New title: %s' => 'Novo título: %s', + 'The task is not assigned anymore' => 'Tarefa já não está atribuída', + 'New assignee: %s' => 'Novo assignado: %s', + 'There is no category now' => 'Já não existe categoria', + 'New category: %s' => 'Nova categoria: %s', + 'New color: %s' => 'Nova cor: %s', + 'New complexity: %d' => 'Nova complexidade: %d', + 'The due date have been removed' => 'A data limite foi retirada', + 'There is no description anymore' => 'Já não há descrição', + 'Recurrence settings have been modified' => 'As configurações da recorrência foram modificadas', + 'Time spent changed: %sh' => 'O tempo despendido foi mudado: %sh', + 'Time estimated changed: %sh' => 'O tempo estimado foi mudado/ %sh', + 'The field "%s" have been updated' => 'O campo "%s" foi actualizada', + 'The description have been modified' => 'A descrição foi modificada', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Tem a certeza que quer fechar a tarefa "%s" e todas as suas sub-tarefas?', + 'Swimlane: %s' => 'Swimlane: %s', + 'I want to receive notifications for:' => 'Eu quero receber as notificações para:', + 'All tasks' => 'Todas as tarefas', + 'Only for tasks assigned to me' => 'Somente as tarefas atribuídas a mim', + 'Only for tasks created by me' => 'Apenas as tarefas que eu criei', + 'Only for tasks created by me and assigned to me' => 'Apenas as tarefas que eu criei e aquelas atribuídas a mim', + '%A' => '%A', + '%b %e, %Y, %k:%M %p' => '%d/%m/%Y %H:%M', + 'New due date: %B %e, %Y' => 'Nova data limite: %d/%m/%Y', + 'Start date changed: %B %e, %Y' => 'Data de início alterada: %d/%m/%Y', + '%k:%M %p' => '%H:%M', + '%%Y-%%m-%%d' => '%%d/%%m/%%Y', + 'Total for all columns' => 'Total para todas as colunas', + 'You need at least 2 days of data to show the chart.' => 'Precisa de pelo menos 2 dias de dados para visualizar o gráfico.', + '<15m' => '<15m', + '<30m' => '<30m', + 'Stop timer' => 'Parar temporizador', + 'Start timer' => 'Iniciar temporizador', + 'Add project member' => 'Adicionar um membro ao projecto', + 'Enable notifications' => 'Activar as notificações', + 'My activity stream' => 'O meu feed de actividade', + 'My calendar' => 'A minha agenda', + 'Search tasks' => 'Pesquisar tarefas', + 'Back to the calendar' => 'Voltar ao calendário', + 'Filters' => 'Filtros', + 'Reset filters' => 'Redefinir os filtros', + 'My tasks due tomorrow' => 'A minhas tarefas que expiram amanhã', + 'Tasks due today' => 'Tarefas que expiram hoje', + 'Tasks due tomorrow' => 'Tarefas que expiram amanhã', + 'Tasks due yesterday' => 'Tarefas que expiraram ontem', + 'Closed tasks' => 'Tarefas fechadas', + 'Open tasks' => 'Tarefas abertas', + 'Not assigned' => 'Não assignada', + 'View advanced search syntax' => 'Ver sintaxe avançada de pesquisa', + 'Overview' => 'Visão global', + '%b %e %Y' => '%b %e %Y', + 'Board/Calendar/List view' => 'Vista Painel/Calendário/Lista', + 'Switch to the board view' => 'Mudar para o modo Painel', + 'Switch to the calendar view' => 'Mudar para o modo Calendário', + 'Switch to the list view' => 'Mudar para o modo Lista', + 'Go to the search/filter box' => 'Ir para o campo de pesquisa', + 'There is no activity yet.' => 'Ainda não há nenhuma actividade.', + 'No tasks found.' => 'Nenhuma tarefa encontrada', + 'Keyboard shortcut: "%s"' => 'Tecla de atalho: "%s"', + 'List' => 'Lista', + 'Filter' => 'Filtro', + 'Advanced search' => 'Pesquisa avançada', + 'Example of query: ' => 'Exemplo de consulta: ', + 'Search by project: ' => 'Pesquisar por projecto: ', + 'Search by column: ' => 'Pesquisar por coluna: ', + 'Search by assignee: ' => 'Pesquisar por assignado: ', + 'Search by color: ' => 'Pesquisar por cor: ', + 'Search by category: ' => 'Pesquisar por categoria: ', + 'Search by description: ' => 'Pesquisar por descrição: ', + 'Search by due date: ' => 'Pesquisar por data de expiração: ', + 'Lead and Cycle time for "%s"' => 'Tempo de Espera e Ciclo para "%s"', + 'Average time spent into each column for "%s"' => 'Tempo médio gasto em cada coluna para "%s"', + 'Average time spent into each column' => 'Tempo médio gasto em cada coluna', + 'Average time spent' => 'Tempo médio gasto', + 'This chart show the average time spent into each column for the last %d tasks.' => 'Este gráfico mostra o tempo médio gasto em cada coluna nas últimas %d tarefas.', + 'Average Lead and Cycle time' => 'Tempo de Espera e Ciclo médio', + 'Average lead time: ' => 'Tempo médio de Espera: ', + 'Average cycle time: ' => 'Tempo médio de Ciclo: ', + 'Cycle Time' => 'Tempo de Ciclo', + 'Lead Time' => 'Tempo de Espera', + 'This chart show the average lead and cycle time for the last %d tasks over the time.' => 'Este gráfico mostra o tempo médio de espera e ciclo para as últimas %d tarefas realizadas.', + 'Average time into each column' => 'Tempo médio em cada coluna', + 'Lead and cycle time' => 'Tempo de Espera e Ciclo', + 'Google Authentication' => 'Autenticação Google', + 'Help on Google authentication' => 'Ajuda com autenticação Google', + 'Github Authentication' => 'Autenticação Github', + 'Help on Github authentication' => 'Ajuda com autenticação Github', + 'Channel/Group/User (Optional)' => 'Canal/Grupo/Utilizador (Opcional)', + 'Lead time: ' => 'Tempo de Espera: ', + 'Cycle time: ' => 'Tempo de Ciclo: ', + 'Time spent into each column' => 'Tempo gasto em cada coluna', + 'The lead time is the duration between the task creation and the completion.' => 'O tempo de espera é a duração entre a criação e o fim da tarefa', + 'The cycle time is the duration between the start date and the completion.' => 'O tempo de ciclo é a duração entre a data de inicio e o fim da tarefa', + 'If the task is not closed the current time is used instead of the completion date.' => 'Se a tarefa não estiver fechada o hora actual será usada em vez da hora de conclusão', + 'Set automatically the start date' => 'Definir data de inicio automáticamente', + 'Edit Authentication' => 'Editar Autenticação', + 'Google Id' => 'Id Google', + 'Github Id' => 'Id Github', + 'Remote user' => 'Utilizador remoto', + 'Remote users do not store their password in Kanboard database, examples: LDAP, Google and Github accounts.' => 'Utilizadores remotos não guardam a password na base de dados do Kanboard, por exemplo: LDAP, contas do Google e Github.', + 'If you check the box "Disallow login form", credentials entered in the login form will be ignored.' => 'Se activar a opção "Desactivar login", as credenciais digitadas no login serão ignoradas.', + 'By @%s on Gitlab' => 'Por @%s no Gitlab', + 'Gitlab issue comment created' => 'Comentário a problema no Gitlab adicionado', + 'New remote user' => 'Novo utilizador remoto', + 'New local user' => 'Novo utilizador local', + 'Default task color' => 'Cor de tarefa por defeito', + 'Hide sidebar' => 'Esconder barra lateral', + 'Expand sidebar' => 'Expandir barra lateral', + 'This feature does not work with all browsers.' => 'Esta funcionalidade não funciona em todos os browsers', + 'There is no destination project available.' => 'Não há projecto de destino disponivel', + 'Trigger automatically subtask time tracking' => 'Activar automáticamente subtarefa de controlo de tempo', + 'Include closed tasks in the cumulative flow diagram' => 'Incluir tarefas fechadas no diagrama de fluxo acumulado', + 'Current swimlane: %s' => 'Swimlane actual: %s', + 'Current column: %s' => 'Coluna actual: %s', + 'Current category: %s' => 'Categoria actual: %s', + 'no category' => 'sem categoria', + 'Current assignee: %s' => 'Assignado a: %s', + 'not assigned' => 'não assignado', + 'Author:' => 'Autor:', + 'contributors' => 'contribuidores', + 'License:' => 'Licença:', + 'License' => 'Licença', + 'Project Administrator' => 'Administrador do Projecto', + 'Enter the text below' => 'Escreva o texto em baixo', + 'Gantt chart for %s' => 'Gráfico de Gantt para %s', + 'Sort by position' => 'Ordenar por posição', + 'Sort by date' => 'Ordenar por data', + 'Add task' => 'Adicionar tarefa', + 'Start date:' => 'Data de inicio:', + 'Due date:' => 'Data de vencimento:', + 'There is no start date or due date for this task.' => 'Não existe data de inicio ou data de vencimento para esta tarefa.', + 'Moving or resizing a task will change the start and due date of the task.' => 'Mover ou redimensionar a tarefa irá alterar a data de inicio e vencimento da tarefa.', + 'There is no task in your project.' => 'Não existe tarefa no seu projecto.', + 'Gantt chart' => 'Gráfico de Gantt', + 'People who are project managers' => 'Pessoas que são gestores do projecto', + 'People who are project members' => 'Pessoas que são membors do projecto', + 'NOK - Norwegian Krone' => 'NOK - Coroa Norueguesa', + 'Show this column' => 'Mostrar esta coluna', + 'Hide this column' => 'Esconder esta coluna', + 'open file' => 'abrir ficheiro', + 'End date' => 'Data de fim', + 'Users overview' => 'Visão geral de Utilizadores', + 'Managers' => 'Gestores', + 'Members' => 'Membros', + 'Shared project' => 'Projecto partilhado', + 'Project managers' => 'Gestores do projecto', + 'Project members' => 'Membros do projecto', + 'Gantt chart for all projects' => 'Gráfico de Gantt para todos os projectos', + 'Projects list' => 'Lista de projectos', + 'Gantt chart for this project' => 'Gráfico de Gantt para este projecto', + 'Project board' => 'Quadro de projecto', + 'End date:' => 'Data de fim:', + 'There is no start date or end date for this project.' => 'Não existe data de inicio ou fim para este projecto.', + 'Projects Gantt chart' => 'Gráfico de Gantt dos projectos', + 'Start date: %s' => 'Data de inicio: %s', + 'End date: %s' => 'Data de fim: %s', + 'Link type' => 'Tipo de ligação', + 'Change task color when using a specific task link' => 'Alterar cor da tarefa quando se usar um tipo especifico de ligação de tarefa', + 'Task link creation or modification' => 'Criação ou modificação de ligação de tarefa', + 'Login with my Gitlab Account' => 'Login com a minha Conta Gitlab', + 'Milestone' => 'Objectivo', + 'Gitlab Authentication' => 'Autenticação Gitlab', + 'Help on Gitlab authentication' => 'Ajuda com autenticação Gitlab', + 'Gitlab Id' => 'Id Gitlab', + 'Gitlab Account' => 'Conta Gitlab', + 'Link my Gitlab Account' => 'Connectar a minha Conta Gitlab', + 'Unlink my Gitlab Account' => 'Desconectar a minha Conta Gitlab', + // 'Documentation: %s' => '', + // 'Switch to the Gantt chart view' => '', + // 'Reset the search/filter box' => '', + // 'Documentation' => '', + // 'Table of contents' => '', + // 'Gantt' => '', + // 'Help with project permissions' => '', +); diff --git a/sources/app/Model/ProjectDailyColumnStats.php b/sources/app/Model/ProjectDailyColumnStats.php new file mode 100644 index 0000000..774ed7f --- /dev/null +++ b/sources/app/Model/ProjectDailyColumnStats.php @@ -0,0 +1,196 @@ +config->get('cfd_include_closed_tasks') == 1 ? array(Task::STATUS_OPEN, Task::STATUS_CLOSED) : array(Task::STATUS_OPEN); + + return $this->db->transaction(function($db) use ($project_id, $date, $status) { + + $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(ProjectDailyColumnStats::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'column_id' => $column_id, + 'total' => 0, + 'score' => 0, + )); + + $db->table(ProjectDailyColumnStats::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) + ->in('is_active', $status) + ->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(ProjectDailyColumnStats::TABLE) + ->columns( + ProjectDailyColumnStats::TABLE.'.column_id', + ProjectDailyColumnStats::TABLE.'.day', + ProjectDailyColumnStats::TABLE.'.total', + ProjectDailyColumnStats::TABLE.'.score', + Board::TABLE.'.title AS column_title' + ) + ->join(Board::TABLE, 'id', 'column_id') + ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(ProjectDailyColumnStats::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(ProjectDailyColumnStats::TABLE) + ->columns( + ProjectDailyColumnStats::TABLE.'.day', + 'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total', + 'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score' + ) + ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(ProjectDailyColumnStats::TABLE.'.day') + ->groupBy(ProjectDailyColumnStats::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 + * @param string $column Column to aggregate + * @return array + */ + public function getAggregatedMetrics($project_id, $from, $to, $column = 'total') + { + $columns = $this->board->getColumnsList($project_id); + $column_ids = array_keys($columns); + $metrics = array(array_merge(array(e('Date')), array_values($columns))); + $aggregates = array(); + + // Fetch metrics for the project + $records = $this->db->table(ProjectDailyColumnStats::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[$column]; + } + + // 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/sources/app/Model/ProjectDailyStats.php b/sources/app/Model/ProjectDailyStats.php new file mode 100644 index 0000000..56a5173 --- /dev/null +++ b/sources/app/Model/ProjectDailyStats.php @@ -0,0 +1,72 @@ +projectAnalytic->getAverageLeadAndCycleTime($project_id); + + return $this->db->transaction(function($db) use ($project_id, $date, $lead_cycle_time) { + + // This call will fail if the record already exists + // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE) + $db->table(ProjectDailyStats::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'avg_lead_time' => 0, + 'avg_cycle_time' => 0, + )); + + $db->table(ProjectDailyStats::TABLE) + ->eq('project_id', $project_id) + ->eq('day', $date) + ->update(array( + 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], + 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], + )); + }); + } + + /** + * 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(self::TABLE) + ->columns('day', 'avg_lead_time', 'avg_cycle_time') + ->eq(self::TABLE.'.project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(self::TABLE.'.day') + ->findAll(); + } +} diff --git a/sources/app/Model/ProjectIntegration.php b/sources/app/Model/ProjectIntegration.php new file mode 100644 index 0000000..bcbfeae --- /dev/null +++ b/sources/app/Model/ProjectIntegration.php @@ -0,0 +1,66 @@ +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)->exists()) { + 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) + ->exists(); + } +} diff --git a/sources/app/Model/TaskAnalytic.php b/sources/app/Model/TaskAnalytic.php new file mode 100644 index 0000000..33a645c --- /dev/null +++ b/sources/app/Model/TaskAnalytic.php @@ -0,0 +1,71 @@ +board->getColumnsList($task['project_id']); + $sums = $this->transition->getTimeSpentByTask($task['id']); + + foreach ($columns as $column_id => $column_title) { + + $time_spent = isset($sums[$column_id]) ? $sums[$column_id] : 0; + + if ($task['column_id'] == $column_id) { + $time_spent += ($task['date_completed'] ?: time()) - $task['date_moved']; + } + + $result[] = array( + 'id' => $column_id, + 'title' => $column_title, + 'time_spent' => $time_spent, + ); + } + + return $result; + } +} diff --git a/sources/app/Subscriber/RecurringTaskSubscriber.php b/sources/app/Subscriber/RecurringTaskSubscriber.php new file mode 100644 index 0000000..68d704f --- /dev/null +++ b/sources/app/Subscriber/RecurringTaskSubscriber.php @@ -0,0 +1,38 @@ + 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/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php new file mode 100644 index 0000000..e45b2c9 --- /dev/null +++ b/sources/app/Subscriber/SubtaskTimeTrackingSubscriber.php @@ -0,0 +1,47 @@ + 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 (isset($event['status']) && $this->config->get('subtask_time_tracking') == 1) { + + $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/sources/app/Template/activity/project.php b/sources/app/Template/activity/project.php new file mode 100644 index 0000000..bc58521 --- /dev/null +++ b/sources/app/Template/activity/project.php @@ -0,0 +1,40 @@ +
+ + + render('event/events', array('events' => $events)) ?> +
\ No newline at end of file diff --git a/sources/app/Template/activity/task.php b/sources/app/Template/activity/task.php new file mode 100644 index 0000000..cc4aad0 --- /dev/null +++ b/sources/app/Template/activity/task.php @@ -0,0 +1,5 @@ + + +render('event/events', array('events' => $events)) ?> \ No newline at end of file diff --git a/sources/app/Template/analytic/avg_time_columns.php b/sources/app/Template/analytic/avg_time_columns.php new file mode 100644 index 0000000..7b9d7cf --- /dev/null +++ b/sources/app/Template/analytic/avg_time_columns.php @@ -0,0 +1,29 @@ + + + +

+ +
+ +
+ + + + + + + + + + + + +
e($column['title']) ?>dt->duration($column['average']) ?>
+ +

+ +

+
+ diff --git a/sources/app/Template/analytic/burndown.php b/sources/app/Template/analytic/burndown.php new file mode 100644 index 0000000..3dfb6ee --- /dev/null +++ b/sources/app/Template/analytic/burndown.php @@ -0,0 +1,34 @@ + + + +

+ +
+
+
+ + +
+ +
+ + form->csrf() ?> + +
+ form->label(t('Start Date'), 'from') ?> + form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> +
+ +
+ form->label(t('End Date'), 'to') ?> + form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> +
+ +
+ +
+
+ +

diff --git a/sources/app/Template/analytic/lead_cycle_time.php b/sources/app/Template/analytic/lead_cycle_time.php new file mode 100644 index 0000000..8e04bd6 --- /dev/null +++ b/sources/app/Template/analytic/lead_cycle_time.php @@ -0,0 +1,42 @@ + + +
+ +
+ + +

+ +
+ +
+ +
+ + form->csrf() ?> + +
+ form->label(t('Start Date'), 'from') ?> + form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> +
+ +
+ form->label(t('End Date'), 'to') ?> + form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> +
+ +
+ +
+
+ +

+ +

+
+ diff --git a/sources/app/Template/app/activity.php b/sources/app/Template/app/activity.php new file mode 100644 index 0000000..71a67fb --- /dev/null +++ b/sources/app/Template/app/activity.php @@ -0,0 +1,4 @@ + +render('event/events', array('events' => $events)) ?> \ No newline at end of file diff --git a/sources/app/Template/app/calendar.php b/sources/app/Template/app/calendar.php new file mode 100644 index 0000000..a154203 --- /dev/null +++ b/sources/app/Template/app/calendar.php @@ -0,0 +1,5 @@ +
+
diff --git a/sources/app/Template/app/filters_helper.php b/sources/app/Template/app/filters_helper.php new file mode 100644 index 0000000..529aa6a --- /dev/null +++ b/sources/app/Template/app/filters_helper.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/sources/app/Template/app/layout.php b/sources/app/Template/app/layout.php new file mode 100644 index 0000000..4f82121 --- /dev/null +++ b/sources/app/Template/app/layout.php @@ -0,0 +1,40 @@ +
+ + +
\ No newline at end of file diff --git a/sources/app/Template/app/overview.php b/sources/app/Template/app/overview.php new file mode 100644 index 0000000..1b16049 --- /dev/null +++ b/sources/app/Template/app/overview.php @@ -0,0 +1,13 @@ + + +render('app/projects', array('paginator' => $project_paginator)) ?> +render('app/tasks', array('paginator' => $task_paginator)) ?> +render('app/subtasks', array('paginator' => $subtask_paginator)) ?> \ No newline at end of file diff --git a/sources/app/Template/app/sidebar.php b/sources/app/Template/app/sidebar.php new file mode 100644 index 0000000..2d96600 --- /dev/null +++ b/sources/app/Template/app/sidebar.php @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/sources/app/Template/auth/index.php b/sources/app/Template/auth/index.php new file mode 100644 index 0000000..2ffc53c --- /dev/null +++ b/sources/app/Template/auth/index.php @@ -0,0 +1,50 @@ +
+ + +

e($errors['login']) ?>

+ + + +
+ + form->csrf() ?> + + form->label(t('Username'), 'username') ?> + form->text('username', $values, $errors, array('autofocus', 'required')) ?> + + form->label(t('Password'), 'password') ?> + form->password('password', $values, $errors, array('required')) ?> + + + form->label(t('Enter the text below'), 'captcha') ?> + + form->text('captcha', $values, $errors, array('required')) ?> + + + + form->checkbox('remember_me', t('Remember Me'), 1, true) ?>
+ + +
+ +
+
+ + + + + + +
\ No newline at end of file diff --git a/sources/app/Template/board/popover_assignee.php b/sources/app/Template/board/popover_assignee.php new file mode 100644 index 0000000..4af19cf --- /dev/null +++ b/sources/app/Template/board/popover_assignee.php @@ -0,0 +1,21 @@ +
+
+

+
+ + form->csrf() ?> + + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + + form->label(t('Assignee'), 'owner_id') ?> + form->select('owner_id', $users_list, $values, array(), array('autofocus')) ?>
+ +
+ + + url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> +
+
+
+
\ No newline at end of file diff --git a/sources/app/Template/board/popover_category.php b/sources/app/Template/board/popover_category.php new file mode 100644 index 0000000..f391f49 --- /dev/null +++ b/sources/app/Template/board/popover_category.php @@ -0,0 +1,21 @@ +
+
+

+
+ + form->csrf() ?> + + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + + form->label(t('Category'), 'category_id') ?> + form->select('category_id', $categories_list, $values, array(), array('autofocus')) ?>
+ +
+ + + url->link(t('cancel'), 'board', 'show', array('project_id' => $project['id']), false, 'close-popover') ?> +
+
+
+
\ No newline at end of file diff --git a/sources/app/Template/board/private_view.php b/sources/app/Template/board/private_view.php new file mode 100644 index 0000000..d4c2c65 --- /dev/null +++ b/sources/app/Template/board/private_view.php @@ -0,0 +1,18 @@ +
+ + render('project/filters', array( + 'project' => $project, + 'filters' => $filters, + 'categories_list' => $categories_list, + 'users_list' => $users_list, + 'is_board' => true, + )) ?> + + render('board/table_container', array( + 'project' => $project, + 'swimlanes' => $swimlanes, + 'board_private_refresh_interval' => $board_private_refresh_interval, + 'board_highlight_period' => $board_highlight_period, + )) ?> + +
diff --git a/sources/app/Template/board/public_view.php b/sources/app/Template/board/public_view.php new file mode 100644 index 0000000..aea7203 --- /dev/null +++ b/sources/app/Template/board/public_view.php @@ -0,0 +1,11 @@ +
+ + render('board/table_container', array( + 'project' => $project, + 'swimlanes' => $swimlanes, + 'board_private_refresh_interval' => $board_private_refresh_interval, + 'board_highlight_period' => $board_highlight_period, + 'not_editable' => true, + )) ?> + +
\ No newline at end of file diff --git a/sources/app/Template/board/table_container.php b/sources/app/Template/board/table_container.php new file mode 100644 index 0000000..98b40eb --- /dev/null +++ b/sources/app/Template/board/table_container.php @@ -0,0 +1,31 @@ +
+ + + +
+ + + + +

+ + + render('board/table_swimlane', array( + 'project' => $project, + 'swimlane' => $swimlane, + 'board_highlight_period' => $board_highlight_period, + 'hide_swimlane' => count($swimlanes) === 1, + 'not_editable' => isset($not_editable), + )) ?> + + +
+
\ No newline at end of file diff --git a/sources/app/Template/board/table_swimlane.php b/sources/app/Template/board/table_swimlane.php new file mode 100644 index 0000000..be40163 --- /dev/null +++ b/sources/app/Template/board/table_swimlane.php @@ -0,0 +1,94 @@ + + + + + + + + + + e($swimlane['name']) ?> + + + + + + + +
+ + + +
+
+ +
+ url->link('+', 'taskcreation', 'create', array('project_id' => $column['project_id'], 'column_id' => $column['id'], 'swimlane_id' => $swimlane['id']), false, 'popover', t('Add a new task')) ?> +
+ + + + e($column['title']) ?> + + + + '> + + + + + + +   + + + + + + (/e($column['task_limit']) ?>) + + + + () + + +
+ + + + + + + + + e($swimlane['name']) ?> + +
+ () +
+ + + + + + +
+ + render($not_editable ? 'board/task_public' : 'board/task_private', array( + 'project' => $project, + 'task' => $task, + 'board_highlight_period' => $board_highlight_period, + 'not_editable' => $not_editable, + )) ?> + +
+
+
+
+ e($column['title']) ?> +
+
+
+ + + \ No newline at end of file diff --git a/sources/app/Template/board/tooltip_comments.php b/sources/app/Template/board/tooltip_comments.php new file mode 100644 index 0000000..2e2c0c1 --- /dev/null +++ b/sources/app/Template/board/tooltip_comments.php @@ -0,0 +1,16 @@ +
+ +

+ + e($comment['name'] ?: $comment['username']) ?> @ + + +

+ +
+
+ text->markdown($comment['comment']) ?> +
+
+ +
diff --git a/sources/app/Template/board/tooltip_description.php b/sources/app/Template/board/tooltip_description.php new file mode 100644 index 0000000..7e0e343 --- /dev/null +++ b/sources/app/Template/board/tooltip_description.php @@ -0,0 +1,5 @@ +
+
+ text->markdown($task['description']) ?> +
+
\ No newline at end of file diff --git a/sources/app/Template/board/tooltip_files.php b/sources/app/Template/board/tooltip_files.php new file mode 100644 index 0000000..96428d3 --- /dev/null +++ b/sources/app/Template/board/tooltip_files.php @@ -0,0 +1,18 @@ + + + + + + + + + +
+ + e($file['name']) ?> +
+ url->link(t('download'), 'file', 'download', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id'])) ?> + +   url->link(t('open file'), 'file', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'file_id' => $file['id']), false, 'popover') ?> + +
\ No newline at end of file diff --git a/sources/app/Template/board/tooltip_subtasks.php b/sources/app/Template/board/tooltip_subtasks.php new file mode 100644 index 0000000..950da92 --- /dev/null +++ b/sources/app/Template/board/tooltip_subtasks.php @@ -0,0 +1,7 @@ +
+ + subtask->toggleStatus($subtask, 'board') ?> + e(empty($subtask['username']) ? '' : ' ['.$this->user->getFullname($subtask).']') ?> +
+ +
diff --git a/sources/app/Template/board/tooltip_tasklinks.php b/sources/app/Template/board/tooltip_tasklinks.php new file mode 100644 index 0000000..25aa91a --- /dev/null +++ b/sources/app/Template/board/tooltip_tasklinks.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/sources/app/Template/budget/sidebar.php b/sources/app/Template/budget/sidebar.php new file mode 100644 index 0000000..8477c05 --- /dev/null +++ b/sources/app/Template/budget/sidebar.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/sources/app/Template/column/edit.php b/sources/app/Template/column/edit.php new file mode 100644 index 0000000..a17affd --- /dev/null +++ b/sources/app/Template/column/edit.php @@ -0,0 +1,44 @@ + + +
+ + form->csrf() ?> + + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + + form->label(t('Title'), 'title') ?> + form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?> + + form->label(t('Task limit'), 'task_limit') ?> + form->number('task_limit', $values, $errors) ?> + + form->label(t('Description'), 'description') ?> + +
+ +
+ form->textarea('description', $values, $errors) ?> +
+
+
+
+ +
+
url->doc(t('Write your text in Markdown'), 'syntax-guide') ?>
+ +
+ + + url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id'])) ?> +
+
\ No newline at end of file diff --git a/sources/app/Template/column/index.php b/sources/app/Template/column/index.php new file mode 100644 index 0000000..689cbbf --- /dev/null +++ b/sources/app/Template/column/index.php @@ -0,0 +1,89 @@ + + + + + + + +

+ + + + + + + + + + + + + +
e($column['title']) ?> + + '> + + + + e($column['task_limit']) ?> +
    +
  • + url->link(t('Edit'), 'column', 'edit', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> +
  • + +
  • + url->link(t('Move Up'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'up'), true) ?> +
  • + + +
  • + url->link(t('Move Down'), 'column', 'move', array('project_id' => $project['id'], 'column_id' => $column['id'], 'direction' => 'down'), true) ?> +
  • + +
  • + url->link(t('Remove'), 'column', 'confirm', array('project_id' => $project['id'], 'column_id' => $column['id'])) ?> +
  • +
+
+ + + +

+
+ + form->csrf() ?> + + form->hidden('project_id', $values) ?> + + form->label(t('Title'), 'title') ?> + form->text('title', $values, $errors, array('required', 'maxlength="50"')) ?> + + form->label(t('Task limit'), 'task_limit') ?> + form->number('task_limit', $values, $errors) ?> + + form->label(t('Description'), 'description') ?> + +
+
+ form->textarea('description', $values, $errors) ?> +
+
+
+
+ +
+
url->doc(t('Write your text in Markdown'), 'syntax-guide') ?>
+ +
+ +
+
\ No newline at end of file diff --git a/sources/app/Template/column/remove.php b/sources/app/Template/column/remove.php new file mode 100644 index 0000000..28d0928 --- /dev/null +++ b/sources/app/Template/column/remove.php @@ -0,0 +1,15 @@ + + +
+

+ + +

+ +
+ url->link(t('Yes'), 'column', 'remove', array('project_id' => $project['id'], 'column_id' => $column['id'], 'remove' => 'yes'), true, 'btn btn-red') ?> + url->link(t('cancel'), 'column', 'index', array('project_id' => $project['id'])) ?> +
+
\ No newline at end of file diff --git a/sources/app/Template/config/calendar.php b/sources/app/Template/config/calendar.php new file mode 100644 index 0000000..1cc985c --- /dev/null +++ b/sources/app/Template/config/calendar.php @@ -0,0 +1,33 @@ + +
+
+ + form->csrf() ?> + +

+
+ 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) ?> +
+ +

+
+ 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) ?> + +

+ form->checkbox('calendar_user_subtasks_time_tracking', t('Show subtasks based on the time tracking'), 1, $values['calendar_user_subtasks_time_tracking'] == 1) ?> + form->checkbox('calendar_user_subtasks_forecast', t('Show subtask estimates (forecast of future work)'), 1, $values['calendar_user_subtasks_forecast'] == 1) ?> +
+ +
+ +
+
+
\ No newline at end of file diff --git a/sources/app/Template/config/project.php b/sources/app/Template/config/project.php new file mode 100644 index 0000000..c58a7ba --- /dev/null +++ b/sources/app/Template/config/project.php @@ -0,0 +1,28 @@ + +
+
+ + form->csrf() ?> + + form->label(t('Default task color'), 'default_color') ?> + form->select('default_color', $colors, $values, $errors) ?> + + form->label(t('Default columns for new projects (Comma-separated)'), 'board_columns') ?> + form->text('board_columns', $values, $errors) ?>
+

+ + form->label(t('Default categories for new projects (Comma-separated)'), 'project_categories') ?> + form->text('project_categories', $values, $errors) ?>
+

+ + form->checkbox('subtask_restriction', t('Allow only one subtask in progress at the same time for a user'), 1, $values['subtask_restriction'] == 1) ?> + form->checkbox('subtask_time_tracking', t('Trigger automatically subtask time tracking'), 1, $values['subtask_time_tracking'] == 1) ?> + form->checkbox('cfd_include_closed_tasks', t('Include closed tasks in the cumulative flow diagram'), 1, $values['cfd_include_closed_tasks'] == 1) ?> + +
+ +
+
+
\ No newline at end of file diff --git a/sources/app/Template/doc/show.php b/sources/app/Template/doc/show.php new file mode 100644 index 0000000..8fbadc9 --- /dev/null +++ b/sources/app/Template/doc/show.php @@ -0,0 +1,13 @@ +
+ +
+ +
+
\ No newline at end of file diff --git a/sources/app/Template/event/task_move_swimlane.php b/sources/app/Template/event/task_move_swimlane.php new file mode 100644 index 0000000..ca440db --- /dev/null +++ b/sources/app/Template/event/task_move_swimlane.php @@ -0,0 +1,19 @@ +user->avatar($email, $author) ?> + +

+ + e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) + ) ?> + + e($author), + $this->url->link(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->e($task['swimlane_name']) + ) ?> + +

+

+ e($task['title']) ?> +

\ No newline at end of file diff --git a/sources/app/Template/export/sidebar.php b/sources/app/Template/export/sidebar.php new file mode 100644 index 0000000..f204d29 --- /dev/null +++ b/sources/app/Template/export/sidebar.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/sources/app/Template/feed/project.php b/sources/app/Template/feed/project.php new file mode 100644 index 0000000..76cf6cf --- /dev/null +++ b/sources/app/Template/feed/project.php @@ -0,0 +1,27 @@ +' ?> + + <?= t('%s\'s activity', $project['name']) ?> + + + + url->href('feed', 'project', array('token' => $project['token']), false, '', true) ?> + url->base() ?>assets/img/favicon.png + + + + <?= $e['event_title'] ?> + + + + + + e($e['author']) ?> + + + + ]]> + + + + \ No newline at end of file diff --git a/sources/app/Template/feed/user.php b/sources/app/Template/feed/user.php new file mode 100644 index 0000000..3e9606c --- /dev/null +++ b/sources/app/Template/feed/user.php @@ -0,0 +1,27 @@ +' ?> + + <?= t('Project activities for %s', $user['name'] ?: $user['username']) ?> + + + + url->href('feed', 'user', array('token' => $user['token']), false, '', true) ?> + url->base() ?>assets/img/favicon.png + + + + <?= $e['event_title'] ?> + + + + + + e($e['author']) ?> + + + + ]]> + + + + \ No newline at end of file diff --git a/sources/app/Template/file/screenshot.php b/sources/app/Template/file/screenshot.php new file mode 100644 index 0000000..73b72ea --- /dev/null +++ b/sources/app/Template/file/screenshot.php @@ -0,0 +1,19 @@ + + +
+

+
+ +
+ + form->csrf() ?> +
+ + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> +
+
+ +

\ No newline at end of file diff --git a/sources/app/Template/gantt/project.php b/sources/app/Template/gantt/project.php new file mode 100644 index 0000000..1face3b --- /dev/null +++ b/sources/app/Template/gantt/project.php @@ -0,0 +1,39 @@ +
+ render('project/filters', array( + 'project' => $project, + 'filters' => $filters, + 'users_list' => $users_list, + )) ?> + + + + +
+

+ +

+ +
\ No newline at end of file diff --git a/sources/app/Template/gantt/projects.php b/sources/app/Template/gantt/projects.php new file mode 100644 index 0000000..50e244a --- /dev/null +++ b/sources/app/Template/gantt/projects.php @@ -0,0 +1,36 @@ +
+ +
+ +

+ +
+ +
+
diff --git a/sources/app/Template/gantt/task_creation.php b/sources/app/Template/gantt/task_creation.php new file mode 100644 index 0000000..d0d14c1 --- /dev/null +++ b/sources/app/Template/gantt/task_creation.php @@ -0,0 +1,65 @@ + +
+ form->csrf() ?> + form->hidden('project_id', $values) ?> + form->hidden('column_id', $values) ?> + form->hidden('position', $values) ?> + +
+ form->label(t('Title'), 'title') ?> + form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?> + + form->label(t('Description'), 'description') ?> + +
+
+ form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"')) ?> +
+
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
+ +
+ form->label(t('Assignee'), 'owner_id') ?> + form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?>
+ + form->label(t('Category'), 'category_id') ?> + form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?>
+ + + form->label(t('Swimlane'), 'swimlane_id') ?> + form->select('swimlane_id', $swimlanes_list, $values, $errors, array('tabindex="5"')) ?>
+ + + form->label(t('Color'), 'color_id') ?> + form->select('color_id', $colors_list, $values, $errors, array('tabindex="7"')) ?>
+ + form->label(t('Complexity'), 'score') ?> + form->number('score', $values, $errors, array('tabindex="8"')) ?>
+ + form->label(t('Start Date'), 'date_started') ?> + form->text('date_started', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="9"'), 'form-date') ?> + + form->label(t('Due Date'), 'date_due') ?> + form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="10"'), 'form-date') ?>
+
+
+ +
+ + + url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> +
+
diff --git a/sources/app/Template/listing/show.php b/sources/app/Template/listing/show.php new file mode 100644 index 0000000..fc8a607 --- /dev/null +++ b/sources/app/Template/listing/show.php @@ -0,0 +1,61 @@ +
+ render('project/filters', array( + 'project' => $project, + 'filters' => $filters, + )) ?> + + isEmpty()): ?> +

+ isEmpty()): ?> + + + + + + + + + + + + getCollection() as $task): ?> + + + + + + + + + + + +
order(t('Id'), 'tasks.id') ?>order(t('Swimlane'), 'tasks.swimlane_id') ?>order(t('Column'), 'tasks.column_id') ?>order(t('Category'), 'tasks.category_id') ?>order(t('Title'), 'tasks.title') ?>order(t('Assignee'), 'users.username') ?>order(t('Due date'), 'tasks.date_due') ?>order(t('Status'), 'tasks.is_active') ?>
+ url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + + e($task['swimlane_name'] ?: $task['default_swimlane']) ?> + + e($task['column_name']) ?> + + e($task['category_name']) ?> + + url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + + + e($task['assignee_name'] ?: $task['assignee_username']) ?> + + + + + + + + + + + +
+ + + +
\ No newline at end of file diff --git a/sources/app/Template/notification/comment_create.php b/sources/app/Template/notification/comment_create.php new file mode 100644 index 0000000..2f2c0fa --- /dev/null +++ b/sources/app/Template/notification/comment_create.php @@ -0,0 +1,11 @@ +

e($task['title']) ?> (#)

+ + +

+ +

+ + +text->markdown($comment['comment']) ?> + +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file diff --git a/sources/app/Template/notification/file_create.php b/sources/app/Template/notification/file_create.php new file mode 100644 index 0000000..63f7d1b --- /dev/null +++ b/sources/app/Template/notification/file_create.php @@ -0,0 +1,5 @@ +

e($task['title']) ?> (#)

+ +

+ +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file diff --git a/sources/app/Template/notification/subtask_create.php b/sources/app/Template/notification/subtask_create.php new file mode 100644 index 0000000..e1c62b7 --- /dev/null +++ b/sources/app/Template/notification/subtask_create.php @@ -0,0 +1,17 @@ +

e($task['title']) ?> (#)

+ +

+ + + +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file diff --git a/sources/app/Template/notification/task_create.php b/sources/app/Template/notification/task_create.php new file mode 100644 index 0000000..1d834d4 --- /dev/null +++ b/sources/app/Template/notification/task_create.php @@ -0,0 +1,43 @@ +

e($task['title']) ?> (#)

+ + + + +

+ text->markdown($task['description']) ?> + + +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file diff --git a/sources/app/Template/notification/task_move_swimlane.php b/sources/app/Template/notification/task_move_swimlane.php new file mode 100644 index 0000000..04de7cc --- /dev/null +++ b/sources/app/Template/notification/task_move_swimlane.php @@ -0,0 +1,19 @@ +

e($task['title']) ?> (#)

+ + + +render('notification/footer', array('task' => $task, 'application_url' => $application_url)) ?> \ No newline at end of file diff --git a/sources/app/Template/notification/task_overdue.php b/sources/app/Template/notification/task_overdue.php new file mode 100644 index 0000000..a231937 --- /dev/null +++ b/sources/app/Template/notification/task_overdue.php @@ -0,0 +1,18 @@ +

+ + diff --git a/sources/app/Template/project/dropdown.php b/sources/app/Template/project/dropdown.php new file mode 100644 index 0000000..0a53cc0 --- /dev/null +++ b/sources/app/Template/project/dropdown.php @@ -0,0 +1,29 @@ +
  • + + url->link(t('Activity'), 'activity', 'project', array('project_id' => $project['id'])) ?> +
  • + + +
  • + url->link(t('Public link'), 'board', 'readonly', array('token' => $project['token']), false, '', '', true) ?> +
  • + + +user->isProjectManagementAllowed($project['id'])): ?> +
  • + + url->link(t('Analytics'), 'analytic', 'tasks', array('project_id' => $project['id'])) ?> +
  • +
  • + + url->link(t('Budget'), 'budget', 'index', array('project_id' => $project['id'])) ?> +
  • +
  • + + url->link(t('Exports'), 'export', 'tasks', array('project_id' => $project['id'])) ?> +
  • +
  • + + url->link(t('Settings'), 'project', 'show', array('project_id' => $project['id'])) ?> +
  • + diff --git a/sources/app/Template/project/filters.php b/sources/app/Template/project/filters.php new file mode 100644 index 0000000..fa50b36 --- /dev/null +++ b/sources/app/Template/project/filters.php @@ -0,0 +1,84 @@ + \ No newline at end of file diff --git a/sources/app/Template/project_user/layout.php b/sources/app/Template/project_user/layout.php new file mode 100644 index 0000000..4cf732d --- /dev/null +++ b/sources/app/Template/project_user/layout.php @@ -0,0 +1,34 @@ +
    + + +
    \ No newline at end of file diff --git a/sources/app/Template/project_user/roles.php b/sources/app/Template/project_user/roles.php new file mode 100644 index 0000000..35d1624 --- /dev/null +++ b/sources/app/Template/project_user/roles.php @@ -0,0 +1,33 @@ +isEmpty()): ?> +

    + + + + + + + + getCollection() as $project): ?> + + + + + + +
    order(t('User'), 'users.username') ?>order(t('Project'), 'projects.name') ?>
    + e($this->user->getFullname($project)) ?> + + url->link('', 'board', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Board')) ?> + url->link('', 'gantt', 'project', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Gantt chart')) ?> + url->link('', 'project', 'show', array('project_id' => $project['id']), false, 'dashboard-table-link', t('Project settings')) ?> + + e($project['project_name']) ?> + + + + e($column['title']) ?> + +
    + + + \ No newline at end of file diff --git a/sources/app/Template/project_user/sidebar.php b/sources/app/Template/project_user/sidebar.php new file mode 100644 index 0000000..8cc3f41 --- /dev/null +++ b/sources/app/Template/project_user/sidebar.php @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/sources/app/Template/project_user/tasks.php b/sources/app/Template/project_user/tasks.php new file mode 100644 index 0000000..f4fc272 --- /dev/null +++ b/sources/app/Template/project_user/tasks.php @@ -0,0 +1,46 @@ +isEmpty()): ?> +

    +isEmpty()): ?> + + + + + + + + + + + getCollection() as $task): ?> + + + + + + + + + + +
    order(t('Id'), 'tasks.id') ?>order(t('Project'), 'projects.name') ?>order(t('Column'), 'tasks.column_id') ?>order(t('Title'), 'tasks.title') ?>order(t('Assignee'), 'users.username') ?>order(t('Start date'), 'tasks.date_started') ?>order(t('Due date'), 'tasks.date_due') ?>
    + url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + + url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + + e($task['column_name']) ?> + + url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + + + e($task['assignee_name'] ?: $task['assignee_username']) ?> + + + + + + + +
    + + + diff --git a/sources/app/Template/search/index.php b/sources/app/Template/search/index.php new file mode 100644 index 0000000..329c072 --- /dev/null +++ b/sources/app/Template/search/index.php @@ -0,0 +1,44 @@ +
    + + + + + +
    +

    +

    project:"My project" assignee:me due:tomorrow

    + +

    url->doc(t('View advanced search syntax'), 'search') ?>

    +
    + isEmpty()): ?> +

    + isEmpty()): ?> + render('search/results', array( + 'paginator' => $paginator, + )) ?> + + +
    \ No newline at end of file diff --git a/sources/app/Template/search/results.php b/sources/app/Template/search/results.php new file mode 100644 index 0000000..04cb6a1 --- /dev/null +++ b/sources/app/Template/search/results.php @@ -0,0 +1,54 @@ + + + + + + + + + + + + + getCollection() as $task): ?> + + + + + + + + + + + + +
    order(t('Project'), 'tasks.project_id') ?>order(t('Id'), 'tasks.id') ?>order(t('Swimlane'), 'tasks.swimlane_id') ?>order(t('Column'), 'tasks.column_id') ?>order(t('Category'), 'tasks.category_id') ?>order(t('Title'), 'tasks.title') ?>order(t('Assignee'), 'users.username') ?>order(t('Due date'), 'tasks.date_due') ?>order(t('Status'), 'tasks.is_active') ?>
    + url->link($this->e($task['project_name']), 'board', 'show', array('project_id' => $task['project_id'])) ?> + + url->link('#'.$this->e($task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + + e($task['swimlane_name'] ?: $task['default_swimlane']) ?> + + e($task['column_name']) ?> + + e($task['category_name']) ?> + + url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + + + e($task['assignee_name'] ?: $task['assignee_username']) ?> + + + + + + + + + + + +
    + + diff --git a/sources/app/Template/task/analytics.php b/sources/app/Template/task/analytics.php new file mode 100644 index 0000000..306dd02 --- /dev/null +++ b/sources/app/Template/task/analytics.php @@ -0,0 +1,36 @@ + + +
    + +
    + +

    +
    + + + + + + + + + + + +
    e($column['title']) ?>dt->duration($column['time_spent']) ?>
    + +
    + +
    + +asset->js('assets/js/vendor/d3.v3.min.js') ?> +asset->js('assets/js/vendor/c3.min.js') ?> \ No newline at end of file diff --git a/sources/app/Template/task/changes.php b/sources/app/Template/task/changes.php new file mode 100644 index 0000000..c7fc0d5 --- /dev/null +++ b/sources/app/Template/task/changes.php @@ -0,0 +1,78 @@ + + + + +

    +
    text->markdown($task['description']) ?>
    + + \ No newline at end of file diff --git a/sources/app/Template/task/description.php b/sources/app/Template/task/description.php new file mode 100644 index 0000000..f823e7d --- /dev/null +++ b/sources/app/Template/task/description.php @@ -0,0 +1,33 @@ + +
    + + +
    + + text->markdown( + $task['description'], + array( + 'controller' => 'task', + 'action' => 'show', + 'params' => array( + 'project_id' => $task['project_id'] + ) + ) + ) ?> + + text->markdown( + $task['description'], + array( + 'controller' => 'task', + 'action' => 'readonly', + 'params' => array( + 'token' => $project['token'] + ) + ) + ) ?> + +
    +
    + \ No newline at end of file diff --git a/sources/app/Template/task/recurring_info.php b/sources/app/Template/task/recurring_info.php new file mode 100644 index 0000000..ad64ae1 --- /dev/null +++ b/sources/app/Template/task/recurring_info.php @@ -0,0 +1,37 @@ + \ No newline at end of file diff --git a/sources/app/Template/task/time_tracking_details.php b/sources/app/Template/task/time_tracking_details.php new file mode 100644 index 0000000..faa07cb --- /dev/null +++ b/sources/app/Template/task/time_tracking_details.php @@ -0,0 +1,27 @@ +render('task/time_tracking_summary', array('task' => $task)) ?> + +

    +isEmpty()): ?> +

    + + + + + + + + + + getCollection() as $record): ?> + + + + + + + + +
    order(t('User'), 'username') ?>order(t('Subtask'), 'subtask_title') ?>order(t('Start'), 'start') ?>order(t('End'), 'end') ?>order(t('Time spent'), 'time_spent') ?>
    url->link($this->e($record['user_fullname'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?>
    + + + \ No newline at end of file diff --git a/sources/app/Template/task/time_tracking_summary.php b/sources/app/Template/task/time_tracking_summary.php new file mode 100644 index 0000000..0210be7 --- /dev/null +++ b/sources/app/Template/task/time_tracking_summary.php @@ -0,0 +1,13 @@ + 0 || $task['time_spent'] > 0): ?> + + + + + + \ No newline at end of file diff --git a/sources/app/Template/task_creation/form.php b/sources/app/Template/task_creation/form.php new file mode 100644 index 0000000..8a29896 --- /dev/null +++ b/sources/app/Template/task_creation/form.php @@ -0,0 +1,84 @@ + + + + + + +
    +
    + + form->csrf() ?> + +
    + form->label(t('Title'), 'title') ?> + form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"'), 'form-input-large') ?>
    + + form->label(t('Description'), 'description') ?> + +
    +
    + form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"')) ?> +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    + +
    url->doc(t('Write your text in Markdown'), 'syntax-guide') ?>
    + + + form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?> + +
    + +
    + form->hidden('project_id', $values) ?> + + form->label(t('Assignee'), 'owner_id') ?> + form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?>
    + + form->label(t('Category'), 'category_id') ?> + form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?>
    + + + form->label(t('Swimlane'), 'swimlane_id') ?> + form->select('swimlane_id', $swimlanes_list, $values, $errors, array('tabindex="5"')) ?>
    + + + form->label(t('Column'), 'column_id') ?> + form->select('column_id', $columns_list, $values, $errors, array('tabindex="6"')) ?>
    + + form->label(t('Color'), 'color_id') ?> + form->select('color_id', $colors_list, $values, $errors, array('tabindex="7"')) ?>
    + + form->label(t('Complexity'), 'score') ?> + form->number('score', $values, $errors, array('tabindex="8"')) ?>
    + + form->label(t('Original estimate'), 'time_estimated') ?> + form->numeric('time_estimated', $values, $errors, array('tabindex="9"')) ?>
    + + form->label(t('Due Date'), 'date_due') ?> + form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="10"'), 'form-date') ?>
    +
    +
    + +
    + + url->link(t('cancel'), 'board', 'show', array('project_id' => $values['project_id']), false, 'close-popover') ?> +
    +
    +
    diff --git a/sources/app/Template/task_duplication/copy.php b/sources/app/Template/task_duplication/copy.php new file mode 100644 index 0000000..415b861 --- /dev/null +++ b/sources/app/Template/task_duplication/copy.php @@ -0,0 +1,48 @@ + + + +

    + + +
    + + form->csrf() ?> + form->hidden('id', $values) ?> + + form->label(t('Project'), 'project_id') ?> + form->select( + 'project_id', + $projects_list, + $values, + array(), + array('data-redirect="'.$this->url->href('taskduplication', 'copy', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), + 'task-reload-project-destination' + ) ?> + + + form->label(t('Swimlane'), 'swimlane_id') ?> + form->select('swimlane_id', $swimlanes_list, $values) ?> +

    + + form->label(t('Column'), 'column_id') ?> + form->select('column_id', $columns_list, $values) ?> +

    + + form->label(t('Category'), 'category_id') ?> + form->select('category_id', $categories_list, $values) ?> +

    + + form->label(t('Assignee'), 'owner_id') ?> + form->select('owner_id', $users_list, $values) ?> +

    + +
    + + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
    +
    + + \ No newline at end of file diff --git a/sources/app/Template/task_duplication/duplicate.php b/sources/app/Template/task_duplication/duplicate.php new file mode 100644 index 0000000..4b50d9c --- /dev/null +++ b/sources/app/Template/task_duplication/duplicate.php @@ -0,0 +1,15 @@ + + +
    +

    + +

    + +
    + url->link(t('Yes'), 'taskduplication', 'duplicate', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
    +
    \ No newline at end of file diff --git a/sources/app/Template/task_duplication/move.php b/sources/app/Template/task_duplication/move.php new file mode 100644 index 0000000..d8d1ba0 --- /dev/null +++ b/sources/app/Template/task_duplication/move.php @@ -0,0 +1,48 @@ + + + +

    + + +
    + + form->csrf() ?> + form->hidden('id', $values) ?> + + form->label(t('Project'), 'project_id') ?> + form->select( + 'project_id', + $projects_list, + $values, + array(), + array('data-redirect="'.$this->url->href('taskduplication', 'move', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'dst_project_id' => 'PROJECT_ID')).'"'), + 'task-reload-project-destination' + ) ?> + + + form->label(t('Swimlane'), 'swimlane_id') ?> + form->select('swimlane_id', $swimlanes_list, $values) ?> +

    + + form->label(t('Column'), 'column_id') ?> + form->select('column_id', $columns_list, $values) ?> +

    + + form->label(t('Category'), 'category_id') ?> + form->select('category_id', $categories_list, $values) ?> +

    + + form->label(t('Assignee'), 'owner_id') ?> + form->select('owner_id', $users_list, $values) ?> +

    + +
    + + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
    +
    + + \ No newline at end of file diff --git a/sources/app/Template/task_modification/edit_description.php b/sources/app/Template/task_modification/edit_description.php new file mode 100644 index 0000000..4cae939 --- /dev/null +++ b/sources/app/Template/task_modification/edit_description.php @@ -0,0 +1,38 @@ + + +
    + + form->csrf() ?> + form->hidden('id', $values) ?> + +
    + +
    + form->textarea('description', $values, $errors, array('autofocus', 'placeholder="'.t('Leave a description').'"'), 'task-show-description-textarea') ?> +
    +
    +
    +
    +
    + +
    url->doc(t('Write your text in Markdown'), 'syntax-guide') ?>
    + +
    + + + + url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id'])) ?> + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + +
    +
    diff --git a/sources/app/Template/task_modification/edit_recurrence.php b/sources/app/Template/task_modification/edit_recurrence.php new file mode 100644 index 0000000..f63f151 --- /dev/null +++ b/sources/app/Template/task_modification/edit_recurrence.php @@ -0,0 +1,47 @@ + + + +
    + 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, + )) ?> +
    + + + + +
    + + form->csrf() ?> + + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + + form->label(t('Generate recurrent task'), 'recurrence_status') ?> + form->select('recurrence_status', $recurrence_status_list, $values, $errors) ?> + + form->label(t('Trigger to generate recurrent task'), 'recurrence_trigger') ?> + form->select('recurrence_trigger', $recurrence_trigger_list, $values, $errors) ?> + + form->label(t('Factor to calculate new due date'), 'recurrence_factor') ?> + form->number('recurrence_factor', $values, $errors) ?> + + form->label(t('Timeframe to calculate new due date'), 'recurrence_timeframe') ?> + form->select('recurrence_timeframe', $recurrence_timeframe_list, $values, $errors) ?> + + form->label(t('Base date to calculate new due date'), 'recurrence_basedate') ?> + form->select('recurrence_basedate', $recurrence_basedate_list, $values, $errors) ?> + +
    + + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
    +
    + + \ No newline at end of file diff --git a/sources/app/Template/task_modification/edit_task.php b/sources/app/Template/task_modification/edit_task.php new file mode 100644 index 0000000..fe4696d --- /dev/null +++ b/sources/app/Template/task_modification/edit_task.php @@ -0,0 +1,66 @@ + +
    +
    + + form->csrf() ?> + +
    + + form->label(t('Title'), 'title') ?> + form->text('title', $values, $errors, array('autofocus', 'required', 'maxlength="200"', 'tabindex="1"')) ?>
    + + form->label(t('Description'), 'description') ?> + +
    +
    + form->textarea('description', $values, $errors, array('placeholder="'.t('Leave a description').'"', 'tabindex="2"')) ?> +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    + +
    + +
    + form->hidden('id', $values) ?> + form->hidden('project_id', $values) ?> + + form->label(t('Assignee'), 'owner_id') ?> + form->select('owner_id', $users_list, $values, $errors, array('tabindex="3"')) ?>
    + + form->label(t('Category'), 'category_id') ?> + form->select('category_id', $categories_list, $values, $errors, array('tabindex="4"')) ?>
    + + form->label(t('Color'), 'color_id') ?> + form->select('color_id', $colors_list, $values, $errors, array('tabindex="5"')) ?>
    + + form->label(t('Complexity'), 'score') ?> + form->number('score', $values, $errors, array('tabindex="6"')) ?>
    + + form->label(t('Due Date'), 'date_due') ?> + form->text('date_due', $values, $errors, array('placeholder="'.$this->text->in($date_format, $date_formats).'"', 'tabindex="7"'), 'form-date') ?>
    +
    +
    + +
    + + + + url->link(t('cancel'), 'board', 'show', array('project_id' => $task['project_id']), false, 'close-popover') ?> + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + +
    +
    +
    diff --git a/sources/app/Template/task_modification/edit_time.php b/sources/app/Template/task_modification/edit_time.php new file mode 100644 index 0000000..8e7f9b4 --- /dev/null +++ b/sources/app/Template/task_modification/edit_time.php @@ -0,0 +1,20 @@ +
    + + + url->link('', 'taskmodification', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-show-start-link', t('Set automatically the start date')) ?> + + + form->csrf() ?> + form->hidden('id', $values) ?> + + form->label(t('Start date'), 'date_started') ?> + form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-datetime') ?> + + form->label(t('Time estimated'), 'time_estimated') ?> + form->numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?> + + form->label(t('Time spent'), 'time_spent') ?> + form->numeric('time_spent', $values, array(), array('placeholder="'.t('hours').'"')) ?> + + +
    \ No newline at end of file diff --git a/sources/app/Template/task_status/close.php b/sources/app/Template/task_status/close.php new file mode 100644 index 0000000..4de3dcb --- /dev/null +++ b/sources/app/Template/task_status/close.php @@ -0,0 +1,15 @@ + + +
    +

    + e($task['title'])) ?> +

    + +
    + url->link(t('Yes'), 'taskstatus', 'close', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes', 'redirect' => $redirect), true, 'btn btn-red') ?> + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'close-popover') ?> +
    +
    \ No newline at end of file diff --git a/sources/app/Template/task_status/open.php b/sources/app/Template/task_status/open.php new file mode 100644 index 0000000..0043fda --- /dev/null +++ b/sources/app/Template/task_status/open.php @@ -0,0 +1,15 @@ + + +
    +

    + e($task['title'])) ?> +

    + +
    + url->link(t('Yes'), 'taskstatus', 'open', array('task_id' => $task['id'], 'project_id' => $task['project_id'], 'confirmation' => 'yes'), true, 'btn btn-red') ?> + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
    +
    \ No newline at end of file diff --git a/sources/app/Template/tasklink/edit.php b/sources/app/Template/tasklink/edit.php new file mode 100644 index 0000000..73b4327 --- /dev/null +++ b/sources/app/Template/tasklink/edit.php @@ -0,0 +1,34 @@ + + +
    + + form->csrf() ?> + form->hidden('id', $values) ?> + form->hidden('task_id', $values) ?> + form->hidden('opposite_task_id', $values) ?> + + form->label(t('Label'), 'link_id') ?> + form->select('link_id', $labels, $values, $errors) ?> + + form->label(t('Task'), 'title') ?> + 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') ?> + +
    + + + url->link(t('cancel'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> +
    +
    \ No newline at end of file diff --git a/sources/app/Template/twofactor/disable.php b/sources/app/Template/twofactor/disable.php new file mode 100644 index 0000000..36be4ef --- /dev/null +++ b/sources/app/Template/twofactor/disable.php @@ -0,0 +1,14 @@ + + +
    +

    + +

    + +
    + url->link(t('Yes'), 'twofactor', 'disable', array('user_id' => $user['id'], 'disable' => 'yes'), true, 'btn btn-red') ?> + url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> +
    +
    \ No newline at end of file diff --git a/sources/app/Template/user/authentication.php b/sources/app/Template/user/authentication.php new file mode 100644 index 0000000..20c3d37 --- /dev/null +++ b/sources/app/Template/user/authentication.php @@ -0,0 +1,35 @@ + +
    + + form->csrf() ?> + + form->hidden('id', $values) ?> + form->hidden('username', $values) ?> + + form->label(t('Google Id'), 'google_id') ?> + form->text('google_id', $values, $errors) ?> + + form->label(t('Github Id'), 'github_id') ?> + form->text('github_id', $values, $errors) ?> + + form->label(t('Gitlab Id'), 'gitlab_id') ?> + form->text('gitlab_id', $values, $errors) ?> + + form->checkbox('is_ldap_user', t('Remote user'), 1, isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1) ?> + form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?> + +
    + + + url->link(t('cancel'), 'user', 'show', array('user_id' => $user['id'])) ?> +
    + +
    + +
    +
    \ No newline at end of file diff --git a/sources/app/Template/user/create_local.php b/sources/app/Template/user/create_local.php new file mode 100644 index 0000000..3c8b43b --- /dev/null +++ b/sources/app/Template/user/create_local.php @@ -0,0 +1,52 @@ +
    + +
    +
    + + form->csrf() ?> + +
    + form->label(t('Username'), 'username') ?> + form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
    + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors) ?>
    + + form->label(t('Email'), 'email') ?> + form->email('email', $values, $errors) ?>
    + + form->label(t('Password'), 'password') ?> + form->password('password', $values, $errors, array('required')) ?>
    + + form->label(t('Confirmation'), 'confirmation') ?> + form->password('confirmation', $values, $errors, array('required')) ?>
    +
    + +
    + form->label(t('Add project member'), 'project_id') ?> + form->select('project_id', $projects, $values, $errors) ?>
    + + form->label(t('Timezone'), 'timezone') ?> + form->select('timezone', $timezones, $values, $errors) ?>
    + + form->label(t('Language'), 'language') ?> + form->select('language', $languages, $values, $errors) ?>
    + + form->checkbox('notifications_enabled', t('Enable notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?> + form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?> + form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?> +
    + +
    + + + url->link(t('cancel'), 'user', 'index') ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/sources/app/Template/user/create_remote.php b/sources/app/Template/user/create_remote.php new file mode 100644 index 0000000..1d04bc8 --- /dev/null +++ b/sources/app/Template/user/create_remote.php @@ -0,0 +1,61 @@ +
    + +
    + + form->csrf() ?> + form->hidden('is_ldap_user', array('is_ldap_user' => 1)) ?> + +
    + form->label(t('Username'), 'username') ?> + form->text('username', $values, $errors, array('autofocus', 'required', 'maxlength="50"')) ?>
    + + form->label(t('Name'), 'name') ?> + form->text('name', $values, $errors) ?>
    + + form->label(t('Email'), 'email') ?> + form->email('email', $values, $errors) ?>
    + + form->label(t('Google Id'), 'google_id') ?> + form->password('google_id', $values, $errors) ?>
    + + form->label(t('Github Id'), 'github_id') ?> + form->password('github_id', $values, $errors) ?>
    + + form->label(t('Gitlab Id'), 'gitlab_id') ?> + form->password('gitlab_id', $values, $errors) ?>
    +
    + +
    + form->label(t('Add project member'), 'project_id') ?> + form->select('project_id', $projects, $values, $errors) ?>
    + + form->label(t('Timezone'), 'timezone') ?> + form->select('timezone', $timezones, $values, $errors) ?>
    + + form->label(t('Language'), 'language') ?> + form->select('language', $languages, $values, $errors) ?>
    + + form->checkbox('notifications_enabled', t('Enable notifications'), 1, isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1 ? true : false) ?> + form->checkbox('is_admin', t('Administrator'), 1, isset($values['is_admin']) && $values['is_admin'] == 1 ? true : false) ?> + form->checkbox('is_project_admin', t('Project Administrator'), 1, isset($values['is_project_admin']) && $values['is_project_admin'] == 1 ? true : false) ?> + form->checkbox('disable_login_form', t('Disallow login form'), 1, isset($values['disable_login_form']) && $values['disable_login_form'] == 1) ?> +
    + +
    + + + url->link(t('cancel'), 'user', 'index') ?> +
    +
    +
    + +
    +
    \ No newline at end of file diff --git a/sources/app/Template/user/share.php b/sources/app/Template/user/share.php new file mode 100644 index 0000000..56dc867 --- /dev/null +++ b/sources/app/Template/user/share.php @@ -0,0 +1,18 @@ + + + + +
    + +
    + + url->link(t('Disable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'disable'), true, 'btn btn-red') ?> + + + url->link(t('Enable public access'), 'user', 'share', array('user_id' => $user['id'], 'switch' => 'enable'), true, 'btn btn-blue') ?> + diff --git a/sources/assets/css/print.css b/sources/assets/css/print.css new file mode 100644 index 0000000..db889a6 --- /dev/null +++ b/sources/assets/css/print.css @@ -0,0 +1,20 @@ +/*! jQuery UI - v1.11.4 - 2015-08-09 +* http://jqueryui.com +* Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, autocomplete.css, datepicker.css, menu.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url("images/ui-bg_flat_75_ffffff_40x100.png") 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_888888_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cd0a0a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px}/*! jQuery Timepicker Addon - v1.5.5 - 2015-05-24 +* http://trentrichardson.com/examples/timepicker +* Copyright (c) 2015 Trent Richardson; Licensed MIT */ + +.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:0 0;border:0;margin:0;padding:0}.ui-timepicker-div .ui_tpicker_unit_hide{display:none}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right;padding:0 5px 0 0}.ui-timepicker-rtl dl dt{float:right;clear:right}.ui-timepicker-rtl dl dd{margin:0 40% 10px 10px}.ui-timepicker-div.ui-timepicker-oneLine{padding-right:2px}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time,.ui-timepicker-div.ui-timepicker-oneLine dt{display:none}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label{display:block;padding-top:2px}.ui-timepicker-div.ui-timepicker-oneLine dl{text-align:right}.ui-timepicker-div.ui-timepicker-oneLine dl dd,.ui-timepicker-div.ui-timepicker-oneLine dl dd>div{display:inline-block;margin:0}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before{content:':';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before{content:'.';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide,.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{display:none}/* Chosen v1.1.0 | (c) 2011-2013 by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */ + +.chosen-container{position:relative;display:inline-block;vertical-align:middle;font-size:13px;zoom:1;*display:inline;-webkit-user-select:none;-moz-user-select:none;user-select:none}.chosen-container .chosen-drop{position:absolute;top:100%;left:-9999px;z-index:1010;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;width:100%;border:1px solid #aaa;border-top:0;background:#fff;box-shadow:0 4px 5px rgba(0,0,0,.15)}.chosen-container.chosen-with-drop .chosen-drop{left:0}.chosen-container a{cursor:pointer}.chosen-container-single .chosen-single{position:relative;display:block;overflow:hidden;padding:0 0 0 8px;height:23px;border:1px solid #aaa;border-radius:5px;background-color:#fff;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#fff),color-stop(50%,#f6f6f6),color-stop(52%,#eee),color-stop(100%,#f4f4f4));background:-webkit-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-moz-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-o-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-clip:padding-box;box-shadow:0 0 3px #fff inset,0 1px 1px rgba(0,0,0,.1);color:#444;text-decoration:none;white-space:nowrap;line-height:24px}.chosen-container-single .chosen-default{color:#999}.chosen-container-single .chosen-single span{display:block;overflow:hidden;margin-right:26px;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single-with-deselect span{margin-right:38px}.chosen-container-single .chosen-single abbr{position:absolute;top:6px;right:26px;display:block;width:12px;height:12px;background:url(../img/chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-single .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single.chosen-disabled .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single .chosen-single div{position:absolute;top:0;right:0;display:block;width:18px;height:100%}.chosen-container-single .chosen-single div b{display:block;width:100%;height:100%;background:url(../img/chosen-sprite.png) no-repeat 0 2px}.chosen-container-single .chosen-search{position:relative;z-index:1010;margin:0;padding:3px 4px;white-space:nowrap}.chosen-container-single .chosen-search input[type=text]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin:1px 0;padding:4px 20px 4px 5px;width:100%;height:auto;outline:0;border:1px solid #aaa;background:#fff url(../img/chosen-sprite.png) no-repeat 100% -20px;background:url(../img/chosen-sprite.png) no-repeat 100% -20px;font-size:1em;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-single .chosen-drop{margin-top:-1px;border-radius:0 0 4px 4px;background-clip:padding-box}.chosen-container-single.chosen-container-single-nosearch .chosen-search{position:absolute;left:-9999px}.chosen-container .chosen-results{position:relative;overflow-x:hidden;overflow-y:auto;margin:0 4px 4px 0;padding:0 0 0 4px;max-height:240px;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;margin:0;padding:5px 6px;list-style:none;line-height:15px;-webkit-touch-callout:none}.chosen-container .chosen-results li.active-result{display:list-item;cursor:pointer}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#ccc;cursor:default}.chosen-container .chosen-results li.highlighted{background-color:#3875d7;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#3875d7),color-stop(90%,#2a62bc));background-image:-webkit-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-moz-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-o-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:linear-gradient(#3875d7 20%,#2a62bc 90%);color:#fff}.chosen-container .chosen-results li.no-results{display:list-item;background:#f4f4f4}.chosen-container .chosen-results li.group-result{display:list-item;font-weight:700;cursor:default}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li em{font-style:normal;text-decoration:underline}.chosen-container-multi .chosen-choices{position:relative;overflow:hidden;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;width:100%;height:auto!important;height:1%;border:1px solid #aaa;background-color:#fff;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(1%,#eee),color-stop(15%,#fff));background-image:-webkit-linear-gradient(#eee 1%,#fff 15%);background-image:-moz-linear-gradient(#eee 1%,#fff 15%);background-image:-o-linear-gradient(#eee 1%,#fff 15%);background-image:linear-gradient(#eee 1%,#fff 15%);cursor:text}.chosen-container-multi .chosen-choices li{float:left;list-style:none}.chosen-container-multi .chosen-choices li.search-field{margin:0;padding:0;white-space:nowrap}.chosen-container-multi .chosen-choices li.search-field input[type=text]{margin:1px 0;padding:5px;height:15px;outline:0;border:0!important;background:transparent!important;box-shadow:none;color:#666;font-size:100%;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-multi .chosen-choices li.search-field .default{color:#999}.chosen-container-multi .chosen-choices li.search-choice{position:relative;margin:3px 0 3px 5px;padding:3px 20px 3px 5px;border:1px solid #aaa;border-radius:3px;background-color:#e4e4e4;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-clip:padding-box;box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);color:#333;line-height:13px;cursor:default}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close{position:absolute;top:4px;right:3px;display:block;width:12px;height:12px;background:url(../img/chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover{background-position:-42px -10px}.chosen-container-multi .chosen-choices li.search-choice-disabled{padding-right:5px;border:1px solid #ccc;background-color:#e4e4e4;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);color:#666}.chosen-container-multi .chosen-choices li.search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close{background-position:-42px -10px}.chosen-container-multi .chosen-results{margin:0;padding:0}.chosen-container-multi .chosen-drop .result-selected{display:list-item;color:#ccc;cursor:default}.chosen-container-active .chosen-single{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #aaa;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#eee),color-stop(80%,#fff));background-image:-webkit-linear-gradient(#eee 20%,#fff 80%);background-image:-moz-linear-gradient(#eee 20%,#fff 80%);background-image:-o-linear-gradient(#eee 20%,#fff 80%);background-image:linear-gradient(#eee 20%,#fff 80%);box-shadow:0 1px 0 #fff inset}.chosen-container-active.chosen-with-drop .chosen-single div{border-left:0;background:transparent}.chosen-container-active.chosen-with-drop .chosen-single div b{background-position:-18px 2px}.chosen-container-active .chosen-choices{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active .chosen-choices li.search-field input[type=text]{color:#111!important}.chosen-disabled{opacity:.5!important;cursor:default}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-rtl{text-align:right}.chosen-rtl .chosen-single{overflow:visible;padding:0 8px 0 0}.chosen-rtl .chosen-single span{margin-right:0;margin-left:26px;direction:rtl}.chosen-rtl .chosen-single-with-deselect span{margin-left:38px}.chosen-rtl .chosen-single div{right:auto;left:3px}.chosen-rtl .chosen-single abbr{right:auto;left:26px}.chosen-rtl .chosen-choices li{float:right}.chosen-rtl .chosen-choices li.search-field input[type=text]{direction:rtl}.chosen-rtl .chosen-choices li.search-choice{margin:3px 5px 3px 0;padding:3px 5px 3px 19px}.chosen-rtl .chosen-choices li.search-choice .search-choice-close{right:auto;left:4px}.chosen-rtl.chosen-container-single-nosearch .chosen-search,.chosen-rtl .chosen-drop{left:9999px}.chosen-rtl.chosen-container-single .chosen-results{margin:0 0 4px 4px;padding:0 4px 0 0}.chosen-rtl .chosen-results li.group-option{padding-right:15px;padding-left:0}.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div{border-right:0}.chosen-rtl .chosen-search input[type=text]{padding:4px 5px 4px 20px;background:#fff url(../img/chosen-sprite.png) no-repeat -30px -20px;background:url(../img/chosen-sprite.png) no-repeat -30px -20px;direction:rtl}.chosen-rtl.chosen-container-single .chosen-single div b{background-position:6px 2px}.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b{background-position:-12px 2px}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-resolution:144dppx){.chosen-rtl .chosen-search input[type=text],.chosen-container-single .chosen-single abbr,.chosen-container-single .chosen-single div b,.chosen-container-single .chosen-search input[type=text],.chosen-container-multi .chosen-choices .search-choice .search-choice-close,.chosen-container .chosen-results-scroll-down span,.chosen-container .chosen-results-scroll-up span{background-image:url(../img/chosen-sprite@2x.png)!important;background-size:52px 37px!important;background-repeat:no-repeat!important}}/*! + * FullCalendar v2.4.0 Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2015 Adam Shaw + */.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}body .fc{font-size:1em}.fc-unthemed .fc-divider,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed .fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1;opacity:.3;filter:alpha(opacity=30)}.fc-bgevent{background:#8fdf82;opacity:.3;filter:alpha(opacity=30)}.fc-nonbusiness{background:#d7d7d7}.fc-icon{display:inline-block;width:1em;height:1em;line-height:1em;font-size:1em;text-align:center;overflow:hidden;font-family:"Courier New",Courier,monospace;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative;margin:0 -1em}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700;font-size:200%;top:-7%;left:3%}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700;font-size:200%;top:-7%;left:-3%}.fc-icon-left-double-arrow:after{content:"\000AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\000BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%;left:-2%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%;left:2%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\000D7";font-size:200%;top:6%}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;white-space:nowrap;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-default{background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.fc-button-group{display:inline-block}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-popover .fc-header .fc-close{cursor:pointer}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:.9em;margin-top:2px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc-bg{bottom:0}.fc-bg table{height:100%}.fc table{width:100%;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{overflow-y:scroll;overflow-x:hidden}.fc-scroller>*{position:relative;width:100%;overflow:hidden}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;background-color:#3a87ad;font-weight:400}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-event.fc-draggable,.fc-event[href]{cursor:pointer}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25;filter:alpha(opacity=25)}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:3}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-h-event .fc-resizer{top:-1px;bottom:-1px;left:-1px;right:-1px;width:5px}.fc-ltr .fc-h-event .fc-start-resizer,.fc-ltr .fc-h-event .fc-start-resizer:after,.fc-ltr .fc-h-event .fc-start-resizer:before,.fc-rtl .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-end-resizer:after,.fc-rtl .fc-h-event .fc-end-resizer:before{right:auto;cursor:w-resize}.fc-ltr .fc-h-event .fc-end-resizer,.fc-ltr .fc-h-event .fc-end-resizer:after,.fc-ltr .fc-h-event .fc-end-resizer:before,.fc-rtl .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-start-resizer:after,.fc-rtl .fc-h-event .fc-start-resizer:before{left:auto;cursor:e-resize}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-day-grid-event .fc-resizer{left:-3px;right:-3px;width:7px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-toolbar{text-align:center;margin-bottom:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid{overflow:hidden}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:0 2px}.fc-basic-view td.fc-day-number,.fc-basic-view td.fc-week-number span{padding-top:2px;padding-bottom:2px}.fc-basic-view .fc-week-number{text-align:center}.fc-basic-view .fc-week-number span{display:inline-block;min-width:1.25em}.fc-ltr .fc-basic-view .fc-day-number{text-align:right}.fc-rtl .fc-basic-view .fc-day-number{text-align:left}.fc-day-number.fc-other-month{opacity:.3;filter:alpha(opacity=30)}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-bgevent-skeleton,.fc-time-grid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-time-grid .fc-bgevent-skeleton{z-index:3}.fc-time-grid .fc-highlight-skeleton{z-index:4}.fc-time-grid .fc-content-skeleton{z-index:5}.fc-time-grid .fc-helper-skeleton{z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight-container{position:relative}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-time-grid .fc-bgevent-container,.fc-time-grid .fc-event-container{position:relative}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event .fc-resizer:after{content:"="}/*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.3.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.3.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.3.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.3.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transform:translate(0, 0)}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-genderless:before,.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.c3 svg{font:10px sans-serif}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}header,.sidebar,.form-comment,.page-header{display:none}a{color:#36c;border:0}a:focus{outline:0;color:#df5353;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}table{width:100%;border-collapse:collapse;border-spacing:0;margin-bottom:20px;font-size:.95em}th,td{border:1px solid #eee;padding-top:.5em;padding-bottom:.5em;padding-left:3px;padding-right:3px}td{vertical-align:top}th{background:#fbfbfb;text-align:left}td li{margin-left:20px}.table-small{font-size:.8em}th a{text-decoration:none;color:#333}th a:focus,th a:hover{text-decoration:underline}.table-fixed{table-layout:fixed;white-space:nowrap}.table-fixed th{overflow:hidden}.table-fixed td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table-stripped tr:nth-child(odd) td{background:#fefefe}.column-3{width:3%}.column-5{width:5%}.column-8{width:7.5%}.column-10{width:10%}.column-12{width:12%}.column-15{width:15%}.column-18{width:18%}.column-20{width:20%}.column-25{width:25%}.column-30{width:30%}.column-35{width:35%}.column-40{width:40%}.column-50{width:50%}.column-60{width:60%}.column-70{width:70%}.public-board{margin-top:5px}.public-task{max-width:800px;margin:0 auto;margin-top:5px}#board-container{overflow-x:scroll}#board{table-layout:fixed}#board th.board-column-header{width:240px}#board td{vertical-align:top}.board-container-compact{overflow-x:initial}@media all and (-ms-high-contrast:active),(-ms-high-contrast:none){.board-container-compact #board{table-layout:auto}}#board th.board-column-header.board-column-compact{width:initial}.board-column-collapsed{display:none}td.board-column-task-collapsed{font-weight:bold;background-color:#fbfbfb}#board th.board-column-header-collapsed{width:28px;min-width:28px;text-align:center;overflow:hidden}.board-rotation-wrapper{position:relative;padding:8px 4px}.board-rotation{min-width:250px;-webkit-backface-visibility:hidden;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:0 100%;-moz-transform-origin:0 100%;-ms-transform-origin:0 100%;transform-origin:0 100%}.board-add-icon{float:left;padding:0 5px}.board-add-icon a{text-decoration:none;color:#36c;font-size:150%;line-height:70%}.board-add-icon a:focus,.board-add-icon a:hover{text-decoration:none;color:red}.board-column-header-task-count{color:#999;font-weight:normal}th.board-column-header-collapsed .board-column-header-task-count{font-size:.85em}th.board-swimlane-header{width:120px}a.board-swimlane-toggle{font-size:.95em}.board-swimlane-toggle-title{font-size:.85em;display:none}.board-swimlane-title{vertical-align:top}.board-task-list{overflow:auto}.board-task-list-limit{background-color:#df5353}.draggable-item{cursor:pointer;user-select:none}.draggable-placeholder{border:2px dashed #000;background:#fafafa;height:70px;margin-bottom:10px}div.draggable-item-selected{border:1px solid #000}.task-board-sort-handle{float:left;padding-right:5px}.task-board{position:relative;margin-bottom:2px;border:1px solid #000;padding:2px;font-size:.85em;word-wrap:break-word}div.task-board-recent{box-shadow:2px 2px 5px rgba(0,0,0,0.25)}div.task-board-status-closed{user-select:none;border:1px dotted #555}.task-table a,.task-board a{color:#000;text-decoration:none;font-weight:bold}.task-table a:focus,.task-table a:hover,.task-board a:focus,.task-board a:hover{text-decoration:underline}.task-board-collapsed{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}a.task-board-collapsed-title{font-weight:normal}.task-board .dropdown{float:left;margin-right:5px;font-size:1.1em}.task-board-title{margin-top:5px;margin-bottom:5px;font-size:1.1em}.task-board-title a{font-weight:normal}.task-board-user{font-size:.8em}.task-board-current-user a{text-decoration:underline}.task-board-current-user a:focus,.task-board-current-user a:hover{text-decoration:none}a.task-board-nobody{font-weight:normal;font-style:italic;color:#444}.task-board-category-container{text-align:right}.task-board-category{font-weight:bold;font-size:.9em;color:#000;border:1px solid #555;padding:2px;padding-right:5px;padding-left:5px}.task-board-icons{text-align:right;margin-top:8px}.task-board-icons a{opacity:.5}.task-board-icons span{opacity:.5;margin-left:2px}.task-board-icons a:hover,.task-board-icons span:hover{opacity:1.0}.task-board-date{font-weight:bold;color:#000}span.task-board-date-overdue{color:#d90000;opacity:1.0}.task-score{font-weight:bold}.task-board .task-score{font-size:1.1em}.task-show-details .task-score{position:absolute;bottom:5px;right:5px;font-size:2em}.task-board-closed,.task-board-days{position:absolute;right:5px;top:5px;opacity:.5;font-size:.8em}.task-board-days:hover{opacity:1.0}.task-days-age{border:#666 1px solid;padding:1px 4px 1px 2px;border-top-left-radius:3px;border-bottom-left-radius:3px}.task-days-incolumn{border:#666 1px solid;border-left:0;margin-left:-5px;padding:1px 2px 1px 4px;border-top-right-radius:3px;border-bottom-right-radius:3px}.board-container-compact .task-board-days{display:none}.task-show-details{position:relative;border-radius:5px;padding-bottom:10px}.task-show-details h2{font-size:1.8em;margin:0;margin-bottom:25px;padding:0;padding-left:10px;padding-right:10px}.task-show-details li{margin-left:25px;list-style-type:circle}.task-show-section{margin-top:30px;margin-bottom:20px}.task-show-files a{font-weight:bold;text-decoration:none}.task-show-files li{margin-left:25px;list-style-type:square;line-height:25px}.task-show-file-actions{font-size:.75em}.task-show-file-actions:before{content:" ["}.task-show-file-actions:after{content:"]"}.task-show-file-actions a{color:#333}.task-show-description{border-left:4px solid #333;padding-left:20px}.task-show-description-textarea{width:99%;max-width:99%;height:300px}.task-file-viewer{position:relative}.task-file-viewer img{max-width:95%;max-height:85%;margin-top:10px}.task-time-form{margin-top:10px;margin-bottom:25px;padding:3px}.task-link-closed{text-decoration:line-through}.task-show-images{list-style-type:none}.task-show-images li img{width:100%}.task-show-images li .img_container{width:250px;height:100px;overflow:hidden}.task-show-images li{padding:10px;overflow:auto;width:250px;min-height:120px;display:inline-block;vertical-align:top}.task-show-images li p{padding:5px;font-weight:bold}.task-show-images li:hover{background:#eee}.task-show-image-actions{margin-left:5px}.task-show-file-table{width:auto}.task-show-start-link{color:#000}.task-show-start-link:hover,.task-show-start-link:focus{color:red}.flag-milestone{color:green}.comment{margin-bottom:20px}.comment:hover{background:#f7f8e0}.comment-inner{border-left:4px solid #333;padding-bottom:10px;padding-left:20px;margin-left:20px;margin-right:10px}.comment-preview{border:2px solid #000;border-radius:3px;padding:10px}.comment-preview .comment-inner{border:0;padding:0;margin:0}.comment-title{margin-bottom:8px;padding-bottom:3px;border-bottom:1px dotted #aaa}.ui-tooltip .comment-title{font-size:80%}.ui-tooltip .comment-inner{padding-bottom:0}.comment-actions{font-size:.8em;padding:0;text-align:right}.comment-actions li{display:inline;padding-left:5px;padding-right:5px;border-right:1px dotted #000}.comment-actions li:last-child{padding-right:0;border:0}.comment-username{font-weight:bold}.comment-textarea{height:200px;width:80%;max-width:800px}#comments .comment-textarea{height:80px;width:500px}.subtasks-table{font-size:.85em}.subtasks-table td{vertical-align:middle}.markdown{line-height:1.4em;font-size:1.0}.markdown h1{margin-top:5px;margin-bottom:10px;font-size:1.5em;font-weight:bold;text-decoration:underline}.markdown h2{font-size:1.2em;font-weight:bold;text-decoration:underline}.markdown h3{font-size:1.1em;text-decoration:underline}.markdown h4{font-size:1.1em;text-decoration:underline}.markdown p{margin-bottom:10px}.markdown ol,.markdown ul{margin-left:25px;margin-top:10px;margin-bottom:10px}.markdown pre{background:#fbfbfb;padding:10px;border-radius:5px;border:1px solid #ddd;overflow:auto;color:#444}.markdown blockquote{font-style:italic;border-left:3px solid #ddd;padding-left:10px;margin-bottom:10px;margin-left:20px}.markdown img{display:block;max-width:80%;margin-top:10px}.documentation{margin:0 auto;padding:20px;max-width:850px;background:#fefefe;border:1px solid #ccc;border-radius:5px;font-size:1.1em;color:#555}.documentation img{border:1px solid #333}.documentation h1{text-decoration:none;font-size:1.8em;margin-bottom:30px}.documentation h2{font-size:1.3em;text-decoration:none;border-bottom:1px solid #ccc;margin-bottom:25px}.documentation li{line-height:30px} \ No newline at end of file diff --git a/sources/assets/css/src/filters.css b/sources/assets/css/src/filters.css new file mode 100644 index 0000000..c92b275 --- /dev/null +++ b/sources/assets/css/src/filters.css @@ -0,0 +1,68 @@ +.toolbar { + font-size: 0.9em; + padding-top: 5px; +} + +.views { + display: inline-block; + margin-right: 10px; +} + +.views li { + border: 1px solid #eee; + padding-left: 12px; + padding-right: 12px; + padding-top: 5px; + padding-bottom: 5px; + display: inline; +} + +.menu-inline li.active a, +.views li.active a { + font-weight: bold; + color: #000; + text-decoration: none; +} + +.views li:first-child { + border-right: none; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} + +.views li:last-child { + border-left: none; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + +.filters { + display: inline-block; + border: 1px solid #eee; + border-radius: 5px; + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; + margin-left: 10px; +} + +.filters ul { + font-size: 0.8em; +} + +.page-header .filters ul { + font-size: 0.9em; +} + +form.search { + display: inline; +} + +div.search { + margin-bottom: 20px; +} + +.filter-dropdowns { + display: inline-block; +} diff --git a/sources/assets/css/src/gantt.css b/sources/assets/css/src/gantt.css new file mode 100644 index 0000000..06349e8 --- /dev/null +++ b/sources/assets/css/src/gantt.css @@ -0,0 +1,143 @@ +/* Based on jQuery.ganttView v.0.8.0 Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com - MIT License */ +div.ganttview-hzheader-month, +div.ganttview-hzheader-day, +div.ganttview-vtheader, +div.ganttview-vtheader-item-name, +div.ganttview-vtheader-series, +div.ganttview-grid, +div.ganttview-grid-row-cell { + float: left; +} + +div.ganttview-hzheader-month, +div.ganttview-hzheader-day { + text-align: center; +} + +div.ganttview-grid-row-cell.last, +div.ganttview-hzheader-day.last, +div.ganttview-hzheader-month.last { + border-right: none; +} + +div.ganttview { + border: 1px solid #999; +} + +/* Horizontal Header */ +div.ganttview-hzheader-month { + width: 60px; + height: 20px; + border-right: 1px solid #d0d0d0; + line-height: 20px; + overflow: hidden; +} + +div.ganttview-hzheader-day { + width: 20px; + height: 20px; + border-right: 1px solid #f0f0f0; + border-top: 1px solid #d0d0d0; + line-height: 20px; + color: #777; +} + +/* Vertical Header */ +div.ganttview-vtheader { + margin-top: 41px; + width: 400px; + overflow: hidden; + background-color: #fff; +} + +div.ganttview-vtheader-item { + color: #666; +} + +div.ganttview-vtheader-series-name { + width: 400px; + height: 31px; + line-height: 31px; + padding-left: 3px; + border-top: 1px solid #d0d0d0; + font-size: 0.9em; + text-overflow: ellipsis; + overflow: auto; + white-space: nowrap; +} + +div.ganttview-vtheader-series-name a { + color: #666; + text-decoration: none; +} + +div.ganttview-vtheader-series-name a:hover { + color: #333; + text-decoration: underline; +} + +div.ganttview-vtheader-series-name a i { + color: #000; +} + +div.ganttview-vtheader-series-name a:hover i { + color: #666; +} + +/* Slider */ +div.ganttview-slide-container { + overflow: auto; + border-left: 1px solid #999; +} + +/* Grid */ +div.ganttview-grid-row-cell { + width: 20px; + height: 31px; + border-right: 1px solid #f0f0f0; + border-top: 1px solid #f0f0f0; +} + +div.ganttview-grid-row-cell.ganttview-weekend { + background-color: #fafafa; +} + +/* Blocks */ +div.ganttview-blocks { + margin-top: 40px; +} + +div.ganttview-block-container { + height: 28px; + padding-top: 4px; +} + +div.ganttview-block { + position: relative; + height: 25px; + background-color: #E5ECF9; + border: 1px solid #c0c0c0; + border-radius: 3px; +} + +.ganttview-block-movable { + cursor: move; +} + +div.ganttview-block-not-defined { + border-color: #000; + background-color: #000; +} + +div.ganttview-block-text { + position: absolute; + height: 12px; + font-size: 0.7em; + color: #999; + padding: 2px 3px; +} + +/* Adjustments for jQuery UI Styling */ +div.ganttview-block div.ui-resizable-handle.ui-resizable-s { + bottom: -0; +} diff --git a/sources/assets/css/src/print.css b/sources/assets/css/src/print.css new file mode 100644 index 0000000..6f71b42 --- /dev/null +++ b/sources/assets/css/src/print.css @@ -0,0 +1,6 @@ +header, +.sidebar, +.form-comment, +.page-header { + display: none; +} \ No newline at end of file diff --git a/sources/assets/css/src/screenshot.css b/sources/assets/css/src/screenshot.css new file mode 100644 index 0000000..4d91720 --- /dev/null +++ b/sources/assets/css/src/screenshot.css @@ -0,0 +1,19 @@ +#screenshot-zone { + position: relative; + border: 2px dashed #ccc; + width: 90%; + height: 250px; + overflow: auto; +} + +#screenshot-inner { + position: absolute; + left: 0; + bottom: 48%; + width: 100%; + text-align: center; +} + +#screenshot-zone.screenshot-pasted { + border: 2px solid #333; +} \ No newline at end of file diff --git a/sources/assets/css/vendor/c3.min.css b/sources/assets/css/vendor/c3.min.css new file mode 100644 index 0000000..08e5084 --- /dev/null +++ b/sources/assets/css/vendor/c3.min.css @@ -0,0 +1 @@ +.c3 svg{font:10px sans-serif}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000} \ No newline at end of file diff --git a/sources/assets/css/vendor/jquery-ui-timepicker-addon.min.css b/sources/assets/css/vendor/jquery-ui-timepicker-addon.min.css new file mode 100644 index 0000000..28f2d81 --- /dev/null +++ b/sources/assets/css/vendor/jquery-ui-timepicker-addon.min.css @@ -0,0 +1,5 @@ +/*! jQuery Timepicker Addon - v1.5.5 - 2015-05-24 +* http://trentrichardson.com/examples/timepicker +* Copyright (c) 2015 Trent Richardson; Licensed MIT */ + +.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:0 0;border:0;margin:0;padding:0}.ui-timepicker-div .ui_tpicker_unit_hide{display:none}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right;padding:0 5px 0 0}.ui-timepicker-rtl dl dt{float:right;clear:right}.ui-timepicker-rtl dl dd{margin:0 40% 10px 10px}.ui-timepicker-div.ui-timepicker-oneLine{padding-right:2px}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time,.ui-timepicker-div.ui-timepicker-oneLine dt{display:none}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label{display:block;padding-top:2px}.ui-timepicker-div.ui-timepicker-oneLine dl{text-align:right}.ui-timepicker-div.ui-timepicker-oneLine dl dd,.ui-timepicker-div.ui-timepicker-oneLine dl dd>div{display:inline-block;margin:0}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before{content:':';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before{content:'.';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide,.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{display:none} \ No newline at end of file diff --git a/sources/assets/img/gravatar-icon.png b/sources/assets/img/gravatar-icon.png new file mode 100644 index 0000000..d9b3e65 Binary files /dev/null and b/sources/assets/img/gravatar-icon.png differ diff --git a/sources/assets/img/jabber-icon.png b/sources/assets/img/jabber-icon.png new file mode 100644 index 0000000..0083227 Binary files /dev/null and b/sources/assets/img/jabber-icon.png differ diff --git a/sources/assets/img/mailgun-icon.png b/sources/assets/img/mailgun-icon.png new file mode 100644 index 0000000..41bb278 Binary files /dev/null and b/sources/assets/img/mailgun-icon.png differ diff --git a/sources/assets/img/postmark-icon.png b/sources/assets/img/postmark-icon.png new file mode 100644 index 0000000..87f0759 Binary files /dev/null and b/sources/assets/img/postmark-icon.png differ diff --git a/sources/assets/img/sendgrid-icon.png b/sources/assets/img/sendgrid-icon.png new file mode 100644 index 0000000..4247e27 Binary files /dev/null and b/sources/assets/img/sendgrid-icon.png differ diff --git a/sources/assets/js/src/App.js b/sources/assets/js/src/App.js new file mode 100644 index 0000000..2872baf --- /dev/null +++ b/sources/assets/js/src/App.js @@ -0,0 +1,201 @@ +function App() { + this.board = new Board(this); + this.markdown = new Markdown(); + this.sidebar = new Sidebar(); + this.search = new Search(this); + this.swimlane = new Swimlane(); + this.dropdown = new Dropdown(); + this.tooltip = new Tooltip(this); + this.popover = new Popover(this); + this.keyboardShortcuts(); + this.chosen(); + this.poll(); + + // Alert box fadeout + $(".alert-fade-out").delay(4000).fadeOut(800, function() { + $(this).remove(); + }); + + // Reload page when a destination project is changed + var reloading_project = false; + $("select.task-reload-project-destination").change(function() { + if (! reloading_project) { + $(".loading-icon").show(); + reloading_project = true; + window.location = $(this).data("redirect").replace(/PROJECT_ID/g, $(this).val()); + } + }); +} + +App.prototype.listen = function() { + this.popover.listen(); + this.markdown.listen(); + this.sidebar.listen(); + this.tooltip.listen(); + this.dropdown.listen(); + this.search.listen(); + this.search.focus(); + this.taskAutoComplete(); + this.datePicker(); + this.focus(); +}; + +App.prototype.refresh = function() { + $(document).off(); + this.listen(); +}; + +App.prototype.focus = function() { + + // Autofocus fields (html5 autofocus works only with page onload) + $("[autofocus]").each(function(index, element) { + $(this).focus(); + }) + + // Auto-select input fields + $(document).on('focus', '.auto-select', function() { + $(this).select(); + }); + + // Workaround for chrome + $(document).on('mouseup', '.auto-select', function(e) { + e.preventDefault(); + }); +}; + +App.prototype.poll = function() { + window.setInterval(this.checkSession, 60000); +}; + +App.prototype.keyboardShortcuts = function() { + var self = this; + + // Submit form + Mousetrap.bindGlobal("mod+enter", function() { + $("form").submit(); + }); + + // Open board selector + Mousetrap.bind("b", function(e) { + e.preventDefault(); + $('#board-selector').trigger('chosen:open'); + }); + + // Close popover and dropdown + Mousetrap.bindGlobal("esc", function() { + self.popover.close(); + self.dropdown.close(); + }); +}; + +App.prototype.checkSession = function() { + if (! $(".form-login").length) { + $.ajax({ + cache: false, + url: $("body").data("status-url"), + statusCode: { + 401: function() { + window.location = $("body").data("login-url"); + } + } + }); + } +}; + +App.prototype.datePicker = function() { + // Datepicker translation + $.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]); + + // Datepicker + $(".form-date").datepicker({ + showOtherMonths: true, + selectOtherMonths: true, + dateFormat: 'yy-mm-dd', + constrainInput: false + }); + + // Datetime picker + $(".form-datetime").datetimepicker({ + controlType: 'select', + oneLine: true, + dateFormat: 'yy-mm-dd', + // timeFormat: 'h:mm tt', + constrainInput: false + }); +}; + +App.prototype.taskAutoComplete = function() { + // Task auto-completion + if ($(".task-autocomplete").length) { + + if ($('.opposite_task_id').val() == '') { + $(".task-autocomplete").parent().find("input[type=submit]").attr('disabled','disabled'); + } + + $(".task-autocomplete").autocomplete({ + source: $(".task-autocomplete").data("search-url"), + minLength: 1, + select: function(event, ui) { + var field = $(".task-autocomplete").data("dst-field"); + $("input[name=" + field + "]").val(ui.item.id); + + $(".task-autocomplete").parent().find("input[type=submit]").removeAttr('disabled'); + } + }); + } +}; + +App.prototype.chosen = function() { + $(".chosen-select").chosen({ + width: "180px", + no_results_text: $(".chosen-select").data("notfound"), + disable_search_threshold: 10 + }); + + $(".select-auto-redirect").change(function() { + var regex = new RegExp($(this).data('redirect-regex'), 'g'); + window.location = $(this).data('redirect-url').replace(regex, $(this).val()); + }); +}; + +App.prototype.showLoadingIcon = function() { + $("body").append(' '); +}; + +App.prototype.hideLoadingIcon = function() { + $("#app-loading-icon").remove(); +}; + +App.prototype.isVisible = function() { + var property = ""; + + if (typeof document.hidden !== "undefined") { + property = "visibilityState"; + } else if (typeof document.mozHidden !== "undefined") { + property = "mozVisibilityState"; + } else if (typeof document.msHidden !== "undefined") { + property = "msVisibilityState"; + } else if (typeof document.webkitHidden !== "undefined") { + property = "webkitVisibilityState"; + } + + if (property != "") { + return document[property] == "visible"; + } + + return true; +}; + +App.prototype.formatDuration = function(d) { + if (d >= 86400) { + return Math.round(d/86400) + "d"; + } + else if (d >= 3600) { + return Math.round(d/3600) + "h"; + } + else if (d >= 60) { + return Math.round(d/60) + "m"; + } + + return d + "s"; +}; diff --git a/sources/assets/js/src/AvgTimeColumnChart.js b/sources/assets/js/src/AvgTimeColumnChart.js new file mode 100644 index 0000000..3fe02ea --- /dev/null +++ b/sources/assets/js/src/AvgTimeColumnChart.js @@ -0,0 +1,40 @@ +function AvgTimeColumnChart(app) { + this.app = app; +} + +AvgTimeColumnChart.prototype.execute = function() { + var metrics = $("#chart").data("metrics"); + var plots = [$("#chart").data("label")]; + var categories = []; + + for (var column_id in metrics) { + plots.push(metrics[column_id].average); + categories.push(metrics[column_id].title); + } + + c3.generate({ + data: { + columns: [plots], + type: 'bar' + }, + bar: { + width: { + ratio: 0.5 + } + }, + axis: { + x: { + type: 'category', + categories: categories + }, + y: { + tick: { + format: this.app.formatDuration + } + } + }, + legend: { + show: false + } + }); +}; diff --git a/sources/assets/js/src/Board.js b/sources/assets/js/src/Board.js new file mode 100644 index 0000000..8d732c5 --- /dev/null +++ b/sources/assets/js/src/Board.js @@ -0,0 +1,268 @@ +function Board(app) { + this.app = app; + this.checkInterval = null; +} + +Board.prototype.execute = function() { + this.app.swimlane.refresh(); + this.app.swimlane.listen(); + this.restoreColumnViewMode(); + this.compactView(); + this.poll(); + this.keyboardShortcuts(); + this.resizeColumnHeight(); + this.listen(); + this.dragAndDrop(); + + $(window).resize(this.resizeColumnHeight); +}; + +Board.prototype.poll = function() { + var interval = parseInt($("#board").attr("data-check-interval")); + + if (interval > 0) { + this.checkInterval = window.setInterval(this.check.bind(this), interval * 1000); + } +}; + +Board.prototype.reloadFilters = function(search) { + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: $("#board").data("reload-url"), + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify({ + search: search + }), + success: this.refresh.bind(this), + error: this.app.hideLoadingIcon.bind(this) + }); +}; + +Board.prototype.check = function() { + if (this.app.isVisible()) { + + var self = this; + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: $("#board").data("check-url"), + statusCode: { + 200: function(data) { self.refresh(data); }, + 304: function () { self.app.hideLoadingIcon(); } + } + }); + } +}; + +Board.prototype.save = function(taskId, columnId, position, swimlaneId) { + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: $("#board").data("save-url"), + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify({ + "task_id": taskId, + "column_id": columnId, + "swimlane_id": swimlaneId, + "position": position + }), + success: this.refresh.bind(this), + error: this.app.hideLoadingIcon.bind(this) + }); +}; + +Board.prototype.refresh = function(data) { + $("#board-container").replaceWith(data); + + this.app.refresh(); + this.app.swimlane.refresh(); + this.app.swimlane.listen(); + this.resizeColumnHeight(); + this.app.hideLoadingIcon(); + this.listen(); + this.dragAndDrop(); + this.compactView(); + this.restoreColumnViewMode(); +}; + +Board.prototype.resizeColumnHeight = function() { + if ($(".board-swimlane").length > 1) { + $(".board-task-list").each(function() { + if ($(this).height() > 500) { + $(this).height(500); + } + else { + $(this).css("min-height", 320); // Min height is the height of the menu dropdown + } + }); + } + else { + $(".board-task-list").height($(window).height() - 145); + } +}; + +Board.prototype.dragAndDrop = function() { + var self = this; + var params = { + forcePlaceholderSize: true, + delay: 300, + distance: 5, + connectWith: ".board-task-list", + placeholder: "draggable-placeholder", + items: ".draggable-item", + stop: function(event, ui) { + ui.item.removeClass("draggable-item-selected"); + self.save( + ui.item.attr('data-task-id'), + ui.item.parent().attr("data-column-id"), + ui.item.index() + 1, + ui.item.parent().attr('data-swimlane-id') + ); + }, + start: function(event, ui) { + ui.item.addClass("draggable-item-selected"); + ui.placeholder.height(ui.item.height()); + } + }; + + if ($.support.touch) { + $(".task-board-sort-handle").css("display", "inline"); + params["handle"] = ".task-board-sort-handle"; + } + + $(".board-task-list").sortable(params); +}; + +Board.prototype.listen = function() { + var self = this; + + $(document).on("click", ".task-board", function(e) { + if (e.target.tagName != "A") { + window.location = $(this).data("task-url"); + } + }); + + $(document).on('click', ".filter-toggle-scrolling", function(e) { + e.preventDefault(); + self.toggleCompactView(); + }); + + $(document).on("click", ".board-column-title", function() { + self.toggleColumnViewMode($(this).data("column-id")); + }); +}; + +Board.prototype.toggleCompactView = function() { + var scrolling = localStorage.getItem("horizontal_scroll") || 1; + localStorage.setItem("horizontal_scroll", scrolling == 0 ? 1 : 0); + this.compactView(); +}; + +Board.prototype.compactView = function() { + if (localStorage.getItem("horizontal_scroll") == 0) { + $(".filter-wide").show(); + $(".filter-compact").hide(); + + $("#board-container").addClass("board-container-compact"); + $("#board th:not(.board-column-header-collapsed)").addClass("board-column-compact"); + } + else { + $(".filter-wide").hide(); + $(".filter-compact").show(); + + $("#board-container").removeClass("board-container-compact"); + $("#board th").removeClass("board-column-compact"); + } +}; + +Board.prototype.toggleCollapsedMode = function() { + var self = this; + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: $('.filter-display-mode:not([style="display: none;"]) a').attr('href'), + success: function(data) { + $('.filter-display-mode').toggle(); + self.refresh(data); + } + }); +}; + +Board.prototype.restoreColumnViewMode = function() { + var self = this; + + $("tr:first th").each(function() { + var columnId = $(this).data('column-id'); + if (localStorage.getItem("hidden_column_" + columnId)) { + self.hideColumn(columnId); + } + }); +}; + +Board.prototype.toggleColumnViewMode = function(columnId) { + if (localStorage.getItem("hidden_column_" + columnId)) { + this.showColumn(columnId); + } + else { + this.hideColumn(columnId); + } +}; + +Board.prototype.hideColumn = function(columnId) { + $(".board-column-" + columnId + " .board-column-expanded").hide(); + $(".board-column-" + columnId + " .board-column-collapsed").show(); + $(".board-column-header-" + columnId + " .board-column-expanded").hide(); + $(".board-column-header-" + columnId + " .board-column-collapsed").show(); + + $(".board-column-header-" + columnId).each(function() { + $(this).removeClass("board-column-compact"); + $(this).addClass("board-column-header-collapsed"); + }); + + $(".board-column-" + columnId ).each(function() { + $(this).addClass("board-column-task-collapsed"); + }); + + $(".board-column-" + columnId + " .board-rotation").each(function() { + var position = $(".board-swimlane").position(); + $(this).css("width", $(".board-column-" + columnId + "").height()); + }); + + localStorage.setItem("hidden_column_" + columnId, 1); +}; + +Board.prototype.showColumn = function(columnId) { + $(".board-column-" + columnId + " .board-column-expanded").show(); + $(".board-column-" + columnId + " .board-column-collapsed").hide(); + $(".board-column-header-" + columnId + " .board-column-expanded").show(); + $(".board-column-header-" + columnId + " .board-column-collapsed").hide(); + + $(".board-column-header-" + columnId).removeClass("board-column-header-collapsed"); + $(".board-column-" + columnId).removeClass("board-column-task-collapsed"); + + if (localStorage.getItem("horizontal_scroll") == 0) { + $(".board-column-header-" + columnId).addClass("board-column-compact"); + } + + localStorage.removeItem("hidden_column_" + columnId); +}; + +Board.prototype.keyboardShortcuts = function() { + var self = this; + + Mousetrap.bind("c", function() { self.toggleCompactView(); }); + Mousetrap.bind("s", function() { self.toggleCollapsedMode(); }); + + Mousetrap.bind("n", function() { + self.app.popover.open($("#board").data("task-creation-url")); + }); +}; diff --git a/sources/assets/js/src/BudgetChart.js b/sources/assets/js/src/BudgetChart.js new file mode 100644 index 0000000..9ab0d5a --- /dev/null +++ b/sources/assets/js/src/BudgetChart.js @@ -0,0 +1,55 @@ +function BudgetChart() { +} + +BudgetChart.prototype.execute = function() { + var categories = []; + var metrics = $("#chart").data("metrics"); + var labels = $("#chart").data("labels"); + var inputFormat = d3.time.format("%Y-%m-%d"); + var outputFormat = d3.time.format($("#chart").data("date-format")); + + var columns = [ + [labels["in"]], + [labels["left"]], + [labels["out"]] + ]; + + var colors = {}; + colors[labels["in"]] = '#5858FA'; + colors[labels["left"]] = '#04B404'; + colors[labels["out"]] = '#DF3A01'; + + for (var i = 0; i < metrics.length; i++) { + categories.push(outputFormat(inputFormat.parse(metrics[i]["date"]))); + columns[0].push(metrics[i]["in"]); + columns[1].push(metrics[i]["left"]); + columns[2].push(metrics[i]["out"]); + } + + c3.generate({ + data: { + columns: columns, + colors: colors, + type : 'bar' + }, + bar: { + width: { + ratio: 0.25 + } + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + axis: { + x: { + type: 'category', + categories: categories + } + } + }); +}; diff --git a/sources/assets/js/src/BurndownChart.js b/sources/assets/js/src/BurndownChart.js new file mode 100644 index 0000000..79199b6 --- /dev/null +++ b/sources/assets/js/src/BurndownChart.js @@ -0,0 +1,48 @@ +function BurndownChart() { +} + +BurndownChart.prototype.execute = function() { + var metrics = $("#chart").data("metrics"); + var columns = [[$("#chart").data("label-total")]]; + var categories = []; + var inputFormat = d3.time.format("%Y-%m-%d"); + var outputFormat = d3.time.format($("#chart").data("date-format")); + + for (var i = 0; i < metrics.length; i++) { + + for (var j = 0; j < metrics[i].length; j++) { + + if (i == 0) { + columns.push([metrics[i][j]]); + } + else { + columns[j + 1].push(metrics[i][j]); + + if (j > 0) { + + if (columns[0][i] == undefined) { + columns[0].push(0); + } + + columns[0][i] += metrics[i][j]; + } + + if (j == 0) { + categories.push(outputFormat(inputFormat.parse(metrics[i][j]))); + } + } + } + } + + c3.generate({ + data: { + columns: columns + }, + axis: { + x: { + type: 'category', + categories: categories + } + } + }); +}; diff --git a/sources/assets/js/src/Calendar.js b/sources/assets/js/src/Calendar.js new file mode 100644 index 0000000..ffb00dc --- /dev/null +++ b/sources/assets/js/src/Calendar.js @@ -0,0 +1,49 @@ +function Calendar() { + +} + +Calendar.prototype.execute = function() { + var calendar = $('#calendar'); + + calendar.fullCalendar({ + lang: $("body").data("js-lang"), + editable: true, + eventLimit: true, + defaultView: "month", + header: { + left: 'prev,next today', + center: 'title', + right: 'month,agendaWeek,agendaDay' + }, + eventDrop: function(event) { + $.ajax({ + cache: false, + url: calendar.data("save-url"), + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify({ + "task_id": event.id, + "date_due": event.start.format() + }) + }); + }, + viewRender: function() { + var url = calendar.data("check-url"); + var params = { + "start": calendar.fullCalendar('getView').start.format(), + "end": calendar.fullCalendar('getView').end.format() + }; + + for (var key in params) { + url += "&" + key + "=" + params[key]; + } + + $.getJSON(url, function(events) { + calendar.fullCalendar('removeEvents'); + calendar.fullCalendar('addEventSource', events); + calendar.fullCalendar('rerenderEvents'); + }); + } + }); +}; diff --git a/sources/assets/js/src/CumulativeFlowDiagram.js b/sources/assets/js/src/CumulativeFlowDiagram.js new file mode 100644 index 0000000..61a0847 --- /dev/null +++ b/sources/assets/js/src/CumulativeFlowDiagram.js @@ -0,0 +1,48 @@ +function CumulativeFlowDiagram() { +} + +CumulativeFlowDiagram.prototype.execute = function() { + + var metrics = $("#chart").data("metrics"); + var columns = []; + var groups = []; + var categories = []; + var inputFormat = d3.time.format("%Y-%m-%d"); + var outputFormat = d3.time.format($("#chart").data("date-format")); + + for (var i = 0; i < metrics.length; i++) { + + for (var j = 0; j < metrics[i].length; j++) { + + if (i == 0) { + columns.push([metrics[i][j]]); + + if (j > 0) { + groups.push(metrics[i][j]); + } + } + else { + + columns[j].push(metrics[i][j]); + + if (j == 0) { + categories.push(outputFormat(inputFormat.parse(metrics[i][j]))); + } + } + } + } + + c3.generate({ + data: { + columns: columns, + type: 'area-spline', + groups: [groups] + }, + axis: { + x: { + type: 'category', + categories: categories + } + } + }); +}; diff --git a/sources/assets/js/src/Dropdown.js b/sources/assets/js/src/Dropdown.js new file mode 100644 index 0000000..6b40d1f --- /dev/null +++ b/sources/assets/js/src/Dropdown.js @@ -0,0 +1,36 @@ +function Dropdown() { +} + +Dropdown.prototype.listen = function() { + var self = this; + + $(document).on('click', function() { + self.close(); + }); + + $(document).on('click', '.dropdown-menu', function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + var submenu = $(this).next('ul'); + var submenuHeight = 240; + + if (! submenu.is(':visible')) { + self.close(); + + if ($(this).offset().top + submenuHeight - $(window).scrollTop() > $(window).height()) { + submenu.addClass('dropdown-submenu-open dropdown-submenu-top'); + } + else { + submenu.addClass('dropdown-submenu-open'); + } + } + else { + self.close(); + } + }); +}; + +Dropdown.prototype.close = function() { + $('.dropdown-submenu-open').removeClass('dropdown-submenu-open'); +}; diff --git a/sources/assets/js/src/Gantt.js b/sources/assets/js/src/Gantt.js new file mode 100644 index 0000000..cac5f29 --- /dev/null +++ b/sources/assets/js/src/Gantt.js @@ -0,0 +1,459 @@ +// Based on jQuery.ganttView v.0.8.8 Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com - MIT License +function Gantt(app) { + this.app = app; + this.data = []; + + this.options = { + container: "#gantt-chart", + showWeekends: true, + allowMoves: true, + allowResizes: true, + cellWidth: 21, + cellHeight: 31, + slideWidth: 1000, + vHeaderWidth: 200 + }; +} + +// Save record after a resize or move +Gantt.prototype.saveRecord = function(record) { + this.app.showLoadingIcon(); + + $.ajax({ + cache: false, + url: $(this.options.container).data("save-url"), + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify(record), + complete: this.app.hideLoadingIcon.bind(this) + }); +}; + +// Build the Gantt chart +Gantt.prototype.execute = function() { + this.data = this.prepareData($(this.options.container).data('records')); + + var minDays = Math.floor((this.options.slideWidth / this.options.cellWidth) + 5); + var range = this.getDateRange(minDays); + var startDate = range[0]; + var endDate = range[1]; + var container = $(this.options.container); + var chart = jQuery("
    ", { "class": "ganttview" }); + + chart.append(this.renderVerticalHeader()); + chart.append(this.renderSlider(startDate, endDate)); + container.append(chart); + + jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child", container).addClass("last"); + jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child", container).addClass("last"); + jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child", container).addClass("last"); + + if (! $(this.options.container).data('readonly')) { + this.listenForBlockResize(startDate); + this.listenForBlockMove(startDate); + } + else { + this.options.allowResizes = false; + this.options.allowMoves = false; + } +}; + +// Render record list on the left +Gantt.prototype.renderVerticalHeader = function() { + var headerDiv = jQuery("
    ", { "class": "ganttview-vtheader" }); + var itemDiv = jQuery("
    ", { "class": "ganttview-vtheader-item" }); + var seriesDiv = jQuery("
    ", { "class": "ganttview-vtheader-series" }); + + for (var i = 0; i < this.data.length; i++) { + var content = jQuery("") + .append(jQuery("", {"class": "fa fa-info-circle tooltip", "title": this.getVerticalHeaderTooltip(this.data[i])})) + .append(" "); + + if (this.data[i].type == "task") { + content.append(jQuery("", {"href": this.data[i].link, "target": "_blank"}).append(this.data[i].title)); + } + else { + content + .append(jQuery("", {"href": this.data[i].board_link, "target": "_blank", "title": $(this.options.container).data("label-board-link")}).append('')) + .append(" ") + .append(jQuery("", {"href": this.data[i].gantt_link, "target": "_blank", "title": $(this.options.container).data("label-gantt-link")}).append('')) + .append(" ") + .append(jQuery("", {"href": this.data[i].link, "target": "_blank"}).append(this.data[i].title)); + } + + seriesDiv.append(jQuery("
    ", {"class": "ganttview-vtheader-series-name"}).append(content)); + } + + itemDiv.append(seriesDiv); + headerDiv.append(itemDiv); + + return headerDiv; +}; + +// Render right part of the chart (top header + grid + bars) +Gantt.prototype.renderSlider = function(startDate, endDate) { + var slideDiv = jQuery("
    ", {"class": "ganttview-slide-container"}); + var dates = this.getDates(startDate, endDate); + + slideDiv.append(this.renderHorizontalHeader(dates)); + slideDiv.append(this.renderGrid(dates)); + slideDiv.append(this.addBlockContainers()); + this.addBlocks(slideDiv, startDate); + + return slideDiv; +}; + +// Render top header (days) +Gantt.prototype.renderHorizontalHeader = function(dates) { + var headerDiv = jQuery("
    ", { "class": "ganttview-hzheader" }); + var monthsDiv = jQuery("
    ", { "class": "ganttview-hzheader-months" }); + var daysDiv = jQuery("
    ", { "class": "ganttview-hzheader-days" }); + var totalW = 0; + + for (var y in dates) { + for (var m in dates[y]) { + var w = dates[y][m].length * this.options.cellWidth; + totalW = totalW + w; + + monthsDiv.append(jQuery("
    ", { + "class": "ganttview-hzheader-month", + "css": { "width": (w - 1) + "px" } + }).append($.datepicker.regional[$("body").data('js-lang')].monthNames[m] + " " + y)); + + for (var d in dates[y][m]) { + daysDiv.append(jQuery("
    ", { "class": "ganttview-hzheader-day" }).append(dates[y][m][d].getDate())); + } + } + } + + monthsDiv.css("width", totalW + "px"); + daysDiv.css("width", totalW + "px"); + headerDiv.append(monthsDiv).append(daysDiv); + + return headerDiv; +}; + +// Render grid +Gantt.prototype.renderGrid = function(dates) { + var gridDiv = jQuery("
    ", { "class": "ganttview-grid" }); + var rowDiv = jQuery("
    ", { "class": "ganttview-grid-row" }); + + for (var y in dates) { + for (var m in dates[y]) { + for (var d in dates[y][m]) { + var cellDiv = jQuery("
    ", { "class": "ganttview-grid-row-cell" }); + if (this.options.showWeekends && this.isWeekend(dates[y][m][d])) { + cellDiv.addClass("ganttview-weekend"); + } + rowDiv.append(cellDiv); + } + } + } + var w = jQuery("div.ganttview-grid-row-cell", rowDiv).length * this.options.cellWidth; + rowDiv.css("width", w + "px"); + gridDiv.css("width", w + "px"); + + for (var i = 0; i < this.data.length; i++) { + gridDiv.append(rowDiv.clone()); + } + + return gridDiv; +}; + +// Render bar containers +Gantt.prototype.addBlockContainers = function() { + var blocksDiv = jQuery("
    ", { "class": "ganttview-blocks" }); + + for (var i = 0; i < this.data.length; i++) { + blocksDiv.append(jQuery("
    ", { "class": "ganttview-block-container" })); + } + + return blocksDiv; +}; + +// Render bars +Gantt.prototype.addBlocks = function(slider, start) { + var rows = jQuery("div.ganttview-blocks div.ganttview-block-container", slider); + var rowIdx = 0; + + for (var i = 0; i < this.data.length; i++) { + var series = this.data[i]; + var size = this.daysBetween(series.start, series.end) + 1; + var offset = this.daysBetween(start, series.start); + var text = jQuery("
    ", {"class": "ganttview-block-text"}); + + var block = jQuery("
    ", { + "class": "ganttview-block tooltip" + (this.options.allowMoves ? " ganttview-block-movable" : ""), + "title": this.getBarTooltip(this.data[i]), + "css": { + "width": ((size * this.options.cellWidth) - 9) + "px", + "margin-left": (offset * this.options.cellWidth) + "px" + } + }).append(text); + + if (size >= 2) { + text.append(this.data[i].progress); + } + + block.data("record", this.data[i]); + this.setBarColor(block, this.data[i]); + jQuery(rows[rowIdx]).append(block); + rowIdx = rowIdx + 1; + } +}; + +// Get tooltip for vertical header +Gantt.prototype.getVerticalHeaderTooltip = function(record) { + var tooltip = ""; + + if (record.type == "task") { + tooltip = "" + record.column_title + " (" + record.progress + ")
    " + record.title; + } + else { + var types = ["managers", "members"]; + + for (var index in types) { + var type = types[index]; + if (! jQuery.isEmptyObject(record.users[type])) { + var list = jQuery("