diff options
69 files changed, 2018 insertions, 428 deletions
diff --git a/.travis.yml b/.travis.yml index 1596ec9a..398dda1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,20 @@ language: php php: - - "5.6" - - "5.5" - - "5.4" - - "5.3" + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 -before_script: wget https://phar.phpunit.de/phpunit.phar -script: +matrix: + fast_finish: true + allow_failures: + - php: 7.0 + +before_script: - composer install - - php phpunit.phar -c tests/units.sqlite.xml
\ No newline at end of file + +script: + - phpunit -c tests/units.sqlite.xml + diff --git a/README.markdown b/README.markdown index 52667d86..32a67c48 100644 --- a/README.markdown +++ b/README.markdown @@ -97,6 +97,8 @@ Documentation - [Bitbucket webhooks](docs/bitbucket-webhooks.markdown) - [Github webhooks](docs/github-webhooks.markdown) - [Gitlab webhooks](docs/gitlab-webhooks.markdown) +- [Hipchat](docs/hipchat.markdown) +- [Slack](docs/slack.markdown) #### More diff --git a/app/Console/Base.php b/app/Console/Base.php index aeafbefc..07243080 100644 --- a/app/Console/Base.php +++ b/app/Console/Base.php @@ -20,6 +20,7 @@ use Symfony\Component\Console\Command\Command; * @property \Model\Task $task * @property \Model\TaskExport $taskExport * @property \Model\TaskFinder $taskFinder + * @property \Model\Transition $transition */ abstract class Base extends Command { diff --git a/app/Console/TransitionExport.php b/app/Console/TransitionExport.php new file mode 100644 index 00000000..ad988c54 --- /dev/null +++ b/app/Console/TransitionExport.php @@ -0,0 +1,34 @@ +<?php + +namespace Console; + +use Core\Tool; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class TransitionExport extends Base +{ + protected function configure() + { + $this + ->setName('export:transitions') + ->setDescription('Task transitions CSV export') + ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') + ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') + ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $data = $this->transition->export( + $input->getArgument('project_id'), + $input->getArgument('start_date'), + $input->getArgument('end_date') + ); + + if (is_array($data)) { + Tool::csv($data); + } + } +} diff --git a/app/Controller/App.php b/app/Controller/App.php index 46731e7c..ebe39670 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -46,21 +46,21 @@ class App extends Base $project_ids = array_keys($projects); $task_paginator = $this->paginator - ->setUrl('app', $action, array('pagination' => 'tasks')) + ->setUrl('app', $action, array('pagination' => 'tasks', 'user_id' => $user_id)) ->setMax(10) ->setOrder('tasks.id') ->setQuery($this->taskFinder->getUserQuery($user_id)) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'tasks'); $subtask_paginator = $this->paginator - ->setUrl('app', $action, array('pagination' => 'subtasks')) + ->setUrl('app', $action, array('pagination' => 'subtasks', 'user_id' => $user_id)) ->setMax(10) ->setOrder('tasks.id') ->setQuery($this->subtask->getUserQuery($user_id, $status)) ->calculateOnlyIf($this->request->getStringParam('pagination') === 'subtasks'); $project_paginator = $this->paginator - ->setUrl('app', $action, array('pagination' => 'projects')) + ->setUrl('app', $action, array('pagination' => 'projects', 'user_id' => $user_id)) ->setMax(10) ->setOrder('name') ->setQuery($this->project->getQueryColumnStats($project_ids)) diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php index 6cfa2bad..49c7f56e 100644 --- a/app/Controller/Calendar.php +++ b/app/Controller/Calendar.php @@ -82,7 +82,9 @@ class Calendar extends Base $subtask_timeslots = $this->subtaskTimeTracking->getUserCalendarEvents($user_id, $start, $end); - $this->response->json(array_merge($due_tasks, $subtask_timeslots)); + $subtask_forcast = $this->config->get('subtask_forecast') == 1 ? $this->subtaskForecast->getCalendarEvents($user_id, $end) : array(); + + $this->response->json(array_merge($due_tasks, $subtask_timeslots, $subtask_forcast)); } /** diff --git a/app/Controller/Config.php b/app/Controller/Config.php index bee897be..bb6e860a 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -41,7 +41,10 @@ class Config extends Base $values = $this->request->getValues(); if ($redirect === 'board') { - $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0); + $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'subtask_forecast' => 0); + } + else if ($redirect === 'integrations') { + $values += array('integration_slack_webhook' => 0, 'integration_hipchat' => 0); } if ($this->config->save($values)) { @@ -102,6 +105,20 @@ class Config extends Base } /** + * Display the integration settings page + * + * @access public + */ + public function integrations() + { + $this->common('integrations'); + + $this->response->html($this->layout('config/integrations', array( + 'title' => t('Settings').' > '.t('Integrations'), + ))); + } + + /** * Display the webhook settings page * * @access public diff --git a/app/Controller/Currency.php b/app/Controller/Currency.php new file mode 100644 index 00000000..fac34a30 --- /dev/null +++ b/app/Controller/Currency.php @@ -0,0 +1,89 @@ +<?php + +namespace Controller; + +/** + * Currency controller + * + * @package controller + * @author Frederic Guillot + */ +class Currency extends Base +{ + /** + * Common layout for config views + * + * @access private + * @param string $template Template name + * @param array $params Template parameters + * @return string + */ + private function layout($template, array $params) + { + $params['board_selector'] = $this->projectPermission->getAllowedProjects($this->userSession->getId()); + $params['config_content_for_layout'] = $this->template->render($template, $params); + + return $this->template->layout('config/layout', $params); + } + + /** + * Display all currency rates and form + * + * @access public + */ + public function index(array $values = array(), array $errors = array()) + { + $this->response->html($this->layout('currency/index', array( + 'config_values' => array('application_currency' => $this->config->get('application_currency')), + 'values' => $values, + 'errors' => $errors, + 'rates' => $this->currency->getAll(), + 'currencies' => $this->config->getCurrencies(), + 'title' => t('Settings').' > '.t('Currency rates'), + ))); + } + + /** + * Validate and save a new currency rate + * + * @access public + */ + public function create() + { + $values = $this->request->getValues(); + list($valid, $errors) = $this->currency->validate($values); + + if ($valid) { + + if ($this->currency->create($values['currency'], $values['rate'])) { + $this->session->flash(t('The currency rate have been added successfully.')); + $this->response->redirect($this->helper->url('currency', 'index')); + } + else { + $this->session->flashError(t('Unable to add this currency rate.')); + } + } + + $this->index($values, $errors); + } + + /** + * Save reference currency + * + * @access public + */ + public function reference() + { + $values = $this->request->getValues(); + + if ($this->config->save($values)) { + $this->config->reload(); + $this->session->flash(t('Settings saved successfully.')); + } + else { + $this->session->flashError(t('Unable to save your settings.')); + } + + $this->response->redirect($this->helper->url('currency', 'index')); + } +} diff --git a/app/Controller/Export.php b/app/Controller/Export.php index 1997a4ea..b8f932c1 100644 --- a/app/Controller/Export.php +++ b/app/Controller/Export.php @@ -72,4 +72,14 @@ class Export extends Base { $this->common('projectDailySummary', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); } + + /** + * Transition export + * + * @access public + */ + public function transitions() + { + $this->common('transition', 'export', t('Transitions'), 'transitions', t('Task transitions export')); + } } diff --git a/app/Controller/Swimlane.php b/app/Controller/Swimlane.php index de2f1f12..e10d21f1 100644 --- a/app/Controller/Swimlane.php +++ b/app/Controller/Swimlane.php @@ -86,7 +86,7 @@ class Swimlane extends Base { $project = $this->getProject(); - $values = $this->request->getValues(); + $values = $this->request->getValues() + array('show_default_swimlane' => 0); list($valid,) = $this->swimlane->validateDefaultModification($values); if ($valid) { diff --git a/app/Controller/Task.php b/app/Controller/Task.php index ace40a01..64017582 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -526,4 +526,19 @@ class Task extends Base 'subtask_paginator' => $subtask_paginator, ))); } + + /** + * Display the task transitions + * + * @access public + */ + public function transitions() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('task/transitions', array( + 'task' => $task, + 'transitions' => $this->transition->getAllByTask($task['id']), + ))); + } } diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php new file mode 100644 index 00000000..e1d90858 --- /dev/null +++ b/app/Core/HttpClient.php @@ -0,0 +1,68 @@ +<?php + +namespace Core; + +/** + * HTTP client + * + * @package core + * @author Frederic Guillot + */ +class HttpClient +{ + /** + * HTTP connection timeout in seconds + * + * @var integer + */ + const HTTP_TIMEOUT = 2; + + /** + * Number of maximum redirections for the HTTP client + * + * @var integer + */ + const HTTP_MAX_REDIRECTS = 2; + + /** + * HTTP client user agent + * + * @var string + */ + const HTTP_USER_AGENT = 'Kanboard Webhook'; + + /** + * Send a POST HTTP request + * + * @static + * @access public + * @param string $url + * @param array $data + * @return string + */ + public static function post($url, array $data) + { + if (empty($url)) { + return ''; + } + + $headers = array( + 'User-Agent: '.self::HTTP_USER_AGENT, + 'Content-Type: application/json', + 'Connection: close', + ); + + $context = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'protocol_version' => 1.1, + 'timeout' => self::HTTP_TIMEOUT, + 'max_redirects' => self::HTTP_MAX_REDIRECTS, + 'header' => implode("\r\n", $headers), + 'content' => json_encode($data) + ) + )); + + return @file_get_contents(trim($url), false, $context); + } +} diff --git a/app/Integration/Hipchat.php b/app/Integration/Hipchat.php new file mode 100644 index 00000000..036925f7 --- /dev/null +++ b/app/Integration/Hipchat.php @@ -0,0 +1,53 @@ +<?php + +namespace Integration; + +/** + * Hipchat Webhook + * + * @package integration + * @author Frederic Guillot + */ +class Hipchat extends Base +{ + /** + * Send message to the Hipchat room + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $event_name Event name + * @param array $data Event data + */ + public function notify($project_id, $task_id, $event_name, array $event) + { + $project = $this->project->getbyId($project_id); + + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); + + $html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>'; + $html .= '<strong>'.$project['name'].'</strong><br/>'; + $html .= $this->projectActivity->getTitle($event); + + if ($this->config->get('application_url')) { + $html .= '<br/><a href="'.$this->config->get('application_url'); + $html .= $this->helper->u('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)).'">'; + $html .= t('view the task on Kanboard').'</a>'; + } + + $payload = array( + 'message' => $html, + 'color' => 'yellow', + ); + + $url = sprintf( + '%s/v2/room/%s/notification?auth_token=%s', + $this->config->get('integration_hipchat_api_url'), + $this->config->get('integration_hipchat_room_id'), + $this->config->get('integration_hipchat_room_token') + ); + + $this->httpClient->post($url, $payload); + } +} diff --git a/app/Integration/SlackWebhook.php b/app/Integration/SlackWebhook.php new file mode 100644 index 00000000..fc7daeb4 --- /dev/null +++ b/app/Integration/SlackWebhook.php @@ -0,0 +1,43 @@ +<?php + +namespace Integration; + +/** + * Slack Webhook + * + * @package integration + * @author Frederic Guillot + */ +class SlackWebhook extends Base +{ + /** + * Send message to the incoming Slack webhook + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $event_name Event name + * @param array $data Event data + */ + public function notify($project_id, $task_id, $event_name, array $event) + { + $project = $this->project->getbyId($project_id); + + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); + + $payload = array( + 'text' => '*['.$project['name'].']* '.str_replace('"', '"', $this->projectActivity->getTitle($event)), + 'username' => 'Kanboard', + 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', + ); + + if ($this->config->get('application_url')) { + $payload['text'] .= ' - <'.$this->config->get('application_url'); + $payload['text'] .= $this->helper->u('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)); + $payload['text'] .= '|'.t('view the task on Kanboard').'>'; + } + + $this->httpClient->post($this->config->get('integration_slack_webhook_url'), $payload); + } +} diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index c12e6d9b..a13aa3af 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 3a3d145e..2043215b 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -1,8 +1,8 @@ <?php return array( - // 'number.decimals_separator' => '', - // 'number.thousands_separator' => '', + 'number.decimals_separator' => ',', + 'number.thousands_separator' => '.', 'None' => 'Keines', 'edit' => 'Bearbeiten', 'Edit' => 'Bearbeiten', @@ -410,13 +410,13 @@ return array( 'Comment updated' => 'Kommentar wurde aktualisiert', 'New comment posted by %s' => 'Neuer Kommentar verfasst durch %s', 'List of due tasks for the project "%s"' => 'Liste der fälligen Aufgaben für das Projekt "%s"', - // 'New attachment' => '', - // 'New comment' => '', - // 'New subtask' => '', - // 'Subtask updated' => '', - // 'Task updated' => '', - // 'Task closed' => '', - // 'Task opened' => '', + 'New attachment' => 'Neuer Anhang', + 'New comment' => 'Neuer Kommentar', + 'New subtask' => 'Neue Teilaufgabe', + 'Subtask updated' => 'Teilaufgabe aktualisiert', + 'Task updated' => 'Aufgabe aktualisiert', + 'Task closed' => 'Aufgabe geschlossen', + 'Task opened' => 'Aufgabe geöffnet', '[%s][Due tasks]' => '[%s][Fällige Aufgaben]', '[Kanboard] Notification' => '[Kanboard] Benachrichtigung', 'I want to receive notifications only for those projects:' => 'Ich möchte nur für diese Projekte Benachrichtigungen erhalten:', @@ -500,9 +500,9 @@ return array( 'Task assignee change' => 'Zuständigkeit geändert', '%s change the assignee of the task #%d to %s' => '%s hat die Zusständigkeit der Aufgabe #%d geändert um %s', '%s changed the assignee of the task %s to %s' => '%s hat die Zuständigkeit der Aufgabe %s geändert um %s', - // 'Column Change' => '', - // 'Position Change' => '', - // 'Assignee Change' => '', + 'Column Change' => 'Spalte ändern', + 'Position Change' => 'Position ändern', + 'Assignee Change' => 'Zuordnung ändern', 'New password for the user "%s"' => 'Neues Passwort des Benutzers "%s"', 'Choose an event' => 'Aktion wählen', 'Github commit received' => 'Github commit empfangen', @@ -736,76 +736,100 @@ return array( 'Filter recently updated' => 'Zuletzt geänderte anzeigen', 'since %B %e, %Y at %k:%M %p' => 'seit %B %e, %Y um %k:%M %p', 'More filters' => 'Mehr Filter', - // 'Compact view' => '', - // 'Horizontal scrolling' => '', - // 'Compact/wide view' => '', - // 'No results match:' => '', - // 'Remove hourly rate' => '', - // 'Do you really want to remove this hourly rate?' => '', - // 'Hourly rates' => '', - // 'Hourly rate' => '', - // 'Currency' => '', - // 'Effective date' => '', - // 'Add new rate' => '', - // 'Rate removed successfully.' => '', - // 'Unable to remove this rate.' => '', - // 'Unable to save the hourly rate.' => '', - // 'Hourly rate created successfully.' => '', - // 'Start time' => '', - // 'End time' => '', - // 'Comment' => '', - // 'All day' => '', - // 'Day' => '', - // 'Manage timetable' => '', - // 'Overtime timetable' => '', - // 'Time off timetable' => '', - // 'Timetable' => '', - // 'Work timetable' => '', - // 'Week timetable' => '', - // 'Day timetable' => '', - // 'From' => '', - // 'To' => '', - // 'Time slot created successfully.' => '', - // 'Unable to save this time slot.' => '', - // 'Time slot removed successfully.' => '', - // 'Unable to remove this time slot.' => '', - // 'Do you really want to remove this time slot?' => '', - // 'Remove time slot' => '', - // 'Add new time slot' => '', - // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', - // 'Files' => '', - // 'Images' => '', - // 'Private project' => '', - // 'Amount' => '', + 'Compact view' => 'Kompaktansicht', + 'Horizontal scrolling' => 'Horizontales Scrollen', + 'Compact/wide view' => 'Kompakt/Breite-Ansicht', + 'No results match:' => 'Keine Ergebnisse:', + 'Remove hourly rate' => 'Stundensatz entfernen', + 'Do you really want to remove this hourly rate?' => 'Diesen Stundensatz wirklich entfernen?', + 'Hourly rates' => 'Stundensätze', + 'Hourly rate' => 'Stundensatz', + 'Currency' => 'Währung', + 'Effective date' => 'Inkraftsetzung', + 'Add new rate' => 'Neue Rate hinzufügen', + 'Rate removed successfully.' => 'Rate erfolgreich entfernt', + 'Unable to remove this rate.' => 'Nicht in der Lage, diese Rate zu entfernen.', + 'Unable to save the hourly rate.' => 'Nicht in der Lage, diese Rate zu speichern', + 'Hourly rate created successfully.' => 'Stundensatz erfolgreich angelegt.', + 'Start time' => 'Startzeit', + 'End time' => 'Endzeit', + 'Comment' => 'Kommentar', + 'All day' => 'ganztägig', + 'Day' => 'Tag', + 'Manage timetable' => 'Zeitplan verwalten', + 'Overtime timetable' => 'Überstunden Zeitplan', + 'Time off timetable' => 'Freizeit Zeitplan', + 'Timetable' => 'Zeitplan', + 'Work timetable' => 'Arbeitszeitplan', + 'Week timetable' => 'Wochenzeitplan', + 'Day timetable' => 'Tageszeitplan', + 'From' => 'von', + 'To' => 'bis', + 'Time slot created successfully.' => 'Zeitfenster erfolgreich erstellt.', + 'Unable to save this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu speichern.', + 'Time slot removed successfully.' => 'Zeitfenster erfolgreich entfernt.', + 'Unable to remove this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu entfernen', + 'Do you really want to remove this time slot?' => 'Soll diese Zeitfenster wirklich gelöscht werden?', + 'Remove time slot' => 'Zeitfenster entfernen', + 'Add new time slot' => 'Neues Zeitfenster hinzufügen', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Dieses Zeitfenster wird verwendet, wenn die Checkbox "gantägig" für Freizeit und Überstunden angeklickt ist.', + 'Files' => 'Dateien', + 'Images' => 'Bilder', + 'Private project' => 'privates Projekt', + 'Amount' => 'Betrag', // 'AUD - Australian Dollar' => '', - // 'Budget' => '', - // 'Budget line' => '', - // 'Budget line removed successfully.' => '', - // 'Budget lines' => '', + 'Budget' => 'Budget', + 'Budget line' => 'Budgetlinie', + 'Budget line removed successfully.' => 'Budgetlinie erfolgreich entfernt', + 'Budget lines' => 'Budgetlinien', // 'CAD - Canadian Dollar' => '', // 'CHF - Swiss Francs' => '', - // 'Cost' => '', - // 'Cost breakdown' => '', - // 'Custom Stylesheet' => '', - // 'download' => '', - // 'Do you really want to remove this budget line?' => '', + 'Cost' => 'Kosten', + 'Cost breakdown' => 'Kostenaufschlüsselung', + 'Custom Stylesheet' => 'benutzerdefiniertes Stylesheet', + 'download' => 'Download', + 'Do you really want to remove this budget line?' => 'Soll diese Budgetlinie wirklich entfernt werden?', // 'EUR - Euro' => '', - // 'Expenses' => '', + 'Expenses' => 'Kosten', // 'GBP - British Pound' => '', // 'INR - Indian Rupee' => '', // 'JPY - Japanese Yen' => '', - // 'New budget line' => '', + 'New budget line' => 'Neue Budgetlinie', // 'NZD - New Zealand Dollar' => '', - // 'Remove a budget line' => '', - // 'Remove budget line' => '', + 'Remove a budget line' => 'Budgetlinie entfernen', + 'Remove budget line' => 'Budgetlinie entfernen', // 'RSD - Serbian dinar' => '', - // 'The budget line have been created successfully.' => '', - // 'Unable to create the budget line.' => '', - // 'Unable to remove this budget line.' => '', + 'The budget line have been created successfully.' => 'Die Budgetlinie wurde erfolgreich angelegt.', + 'Unable to create the budget line.' => 'Budgetlinie konnte nicht erstellt werden.', + 'Unable to remove this budget line.' => 'Budgetlinie konnte nicht gelöscht werden.', // 'USD - US Dollar' => '', - // 'Remaining' => '', - // 'Destination column' => '', - // 'Move the task to another column when assigned to a user' => '', - // 'Move the task to another column when assignee is cleared' => '', - // 'Source column' => '', + 'Remaining' => 'Verbleibend', + 'Destination column' => 'Zielspalte', + 'Move the task to another column when assigned to a user' => 'Aufgabe in eine andere Spalte verschieben, wenn ein User zugeordnet wurde.', + 'Move the task to another column when assignee is cleared' => 'Aufgabe in eine andere Spalte verschieben, wenn die Zuordnung gelöscht wurde.', + 'Source column' => 'Quellspalte', + 'Show subtask estimates in the user calendar' => 'Teilaufgabenschätzung in Benutzerkalender anzeigen.', + 'Transitions' => 'Übergänge', + 'Executer' => 'Ausführender', + 'Time spent in the column' => 'Zeit in Spalte verbracht', + 'Task transitions' => 'Aufgaben Übergänge', + 'Task transitions export' => 'Aufgaben Übergänge exportieren', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Diese Auswertung enthält alle Spaltenbewegungen für jede Aufgabe mit Datum, Benutzer und Zeit vor jedem Wechsel.', + 'Currency rates' => 'Währungskurse', + 'Rate' => 'Kurse', + 'Change reference currency' => 'Referenzwährung ändern', + 'Add a new currency rate' => 'Neuen Währungskurs hinzufügen', + 'Currency rates are used to calculate project budget.' => 'Währungskurse werden verwendet um das Projektbudget zu berechnen.', + 'Reference currency' => 'Referenzwährung', + 'The currency rate have been added successfully.' => 'Der Währungskurs wurde erfolgreich hinzugefügt.', + 'Unable to add this currency rate.' => 'Währungskurs konnte nicht hinzugefügt werden', + 'Send notifications to a Slack channel' => 'Benachrichtigung an einen Slack-Kanal senden', + 'Webhook URL' => 'Webhook URL', + 'Help on Slack integration' => 'Hilfe für Slack integration.', + '%s remove the assignee of the task %s' => '%s Zuordnung für die Aufgabe %s entfernen', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 00b342dd..33c062c6 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index d1ae2412..856914ad 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 340d16b1..d389b0e1 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -810,4 +810,28 @@ return array( 'Move the task to another column when assigned to a user' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci est assignée à quelqu\'un', 'Move the task to another column when assignee is cleared' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci n\'est plus assignée', 'Source column' => 'Colonne d\'origine', + 'Show subtask estimates in the user calendar' => 'Afficher le temps estimé des sous-tâches dans le calendrier utilisateur', + 'Transitions' => 'Transitions', + 'Executer' => 'Exécutant', + 'Time spent in the column' => 'Temps passé dans la colonne', + 'Task transitions' => 'Transitions des tâches', + 'Task transitions export' => 'Export des transitions des tâches', + 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => 'Ce rapport contient tous les mouvements de colonne pour chaque tâche avec la date, l\'utilisateur et le temps passé pour chaque transition.', + 'Currency rates' => 'Taux de change des devises', + 'Rate' => 'Taux', + 'Change reference currency' => 'Changer la monnaie de référence', + 'Add a new currency rate' => 'Ajouter un nouveau taux pour une devise', + 'Currency rates are used to calculate project budget.' => 'Le cours des devises est utilisé pour calculer le budget des projets.', + 'Reference currency' => 'Devise de référence', + 'The currency rate have been added successfully.' => 'Le taux de change a été ajouté avec succès.', + 'Unable to add this currency rate.' => 'Impossible d\'ajouter ce taux de change', + 'Send notifications to a Slack channel' => 'Envoyer les notifications sur un salon de discussion Slack', + 'Webhook URL' => 'URL du webhook', + 'Help on Slack integration' => 'Aide sur l\'intégration avec Slack', + '%s remove the assignee of the task %s' => '%s a enlevé la personne assignée à la tâche %s', + 'Send notifications to Hipchat' => 'Envoyer les notifications vers Hipchat', + 'API URL' => 'URL de l\'api', + 'Room API ID or name' => 'Nom ou identifiant du salon de discussion', + 'Room notification token' => 'Jeton de sécurité du salon de discussion', + 'Help on Hipchat integration' => 'Aide sur l\'intégration avec Hipchat', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index a8d5b637..29792417 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -1,13 +1,13 @@ <?php return array( - // 'number.decimals_separator' => '', - // 'number.thousands_separator' => '', + 'number.decimals_separator' => ',', + 'number.thousands_separator' => ' ', 'None' => 'Nincs', 'edit' => 'szerkesztés', 'Edit' => 'Szerkesztés', - 'remove' => 'eltávolítás', - 'Remove' => 'Eltávolítás', + 'remove' => 'törlés', + 'Remove' => 'Törlés', 'Update' => 'Frissítés', 'Yes' => 'Igen', 'No' => 'Nem', @@ -437,7 +437,7 @@ return array( 'Move the task to another project' => 'Feladat áthelyezése másik projektbe', 'Move to another project' => 'Áthelyezés másik projektbe', 'Do you really want to duplicate this task?' => 'Tényleg szeretné megkettőzni ezt a feladatot?', - 'Duplicate a task' => 'Feladat megkettőzése', + 'Duplicate a task' => 'Feladat másolása', 'External accounts' => 'Külső fiókok', 'Account type' => 'Fiók típusa', 'Local' => 'Helyi', @@ -519,7 +519,7 @@ return array( 'Label' => 'Címke', 'Database' => 'Adatbázis', 'About' => 'Kanboard információ', - 'Database driver:' => 'Adatbázis driver:', + 'Database driver:' => 'Adatbázis motor:', 'Board settings' => 'Tábla beállítások', 'URL and token' => 'URL és tokenek', 'Webhook settings' => 'Webhook beállítások', @@ -576,8 +576,8 @@ return array( 'My subtasks' => 'Részfeladataim', 'User repartition' => 'Felhasználó újrafelosztás', 'User repartition for "%s"' => 'Felhasználó újrafelosztás: %s', - 'Clone this project' => 'Projekt megkettőzése', - 'Column removed successfully.' => 'Oszlop sikeresen eltávolítva.', + 'Clone this project' => 'Projekt másolása', + 'Column removed successfully.' => 'Oszlop sikeresen törölve.', 'Edit Project' => 'Projekt szerkesztése', 'Github Issue' => 'Github issue', 'Not enough data to show the graph.' => 'Nincs elég adat a grafikonhoz.', @@ -671,7 +671,7 @@ return array( 'There is nothing to show.' => 'Nincs megjelenítendő adat.', 'Time Tracking' => 'Idő követés', 'You already have one subtask in progress' => 'Már van egy folyamatban levő részfeladata', - 'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné duplikálni?', + 'Which parts of the project do you want to duplicate?' => 'A projekt mely részeit szeretné másolni?', 'Change dashboard view' => 'Vezérlőpult megjelenés változtatás', 'Show/hide activities' => 'Tevékenységek megjelenítése/elrejtése', 'Show/hide projects' => 'Projektek megjelenítése/elrejtése', @@ -730,82 +730,106 @@ return array( 'Close dialog box' => 'Ablak bezárása', 'Submit a form' => 'Űrlap beküldése', 'Board view' => 'Tábla nézet', - 'Keyboard shortcuts' => 'Billentyű kombináció', + 'Keyboard shortcuts' => 'Billentyű kombinációk', 'Open board switcher' => 'Tábla választó lenyitása', 'Application' => 'Alkalmazás', 'Filter recently updated' => 'Szűrés az utolsó módosítás ideje szerint', 'since %B %e, %Y at %k:%M %p' => '%Y. %m. %d. %H:%M óta', 'More filters' => 'További szűrők', - // 'Compact view' => '', - // 'Horizontal scrolling' => '', - // 'Compact/wide view' => '', - // 'No results match:' => '', - // 'Remove hourly rate' => '', - // 'Do you really want to remove this hourly rate?' => '', - // 'Hourly rates' => '', - // 'Hourly rate' => '', - // 'Currency' => '', - // 'Effective date' => '', - // 'Add new rate' => '', - // 'Rate removed successfully.' => '', - // 'Unable to remove this rate.' => '', - // 'Unable to save the hourly rate.' => '', - // 'Hourly rate created successfully.' => '', - // 'Start time' => '', - // 'End time' => '', - // 'Comment' => '', - // 'All day' => '', - // 'Day' => '', - // 'Manage timetable' => '', - // 'Overtime timetable' => '', - // 'Time off timetable' => '', - // 'Timetable' => '', - // 'Work timetable' => '', - // 'Week timetable' => '', - // 'Day timetable' => '', - // 'From' => '', - // 'To' => '', - // 'Time slot created successfully.' => '', - // 'Unable to save this time slot.' => '', - // 'Time slot removed successfully.' => '', - // 'Unable to remove this time slot.' => '', - // 'Do you really want to remove this time slot?' => '', - // 'Remove time slot' => '', - // 'Add new time slot' => '', - // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', - // 'Files' => '', - // 'Images' => '', - // 'Private project' => '', - // 'Amount' => '', - // 'AUD - Australian Dollar' => '', - // 'Budget' => '', - // 'Budget line' => '', - // 'Budget line removed successfully.' => '', - // 'Budget lines' => '', - // 'CAD - Canadian Dollar' => '', - // 'CHF - Swiss Francs' => '', - // 'Cost' => '', - // 'Cost breakdown' => '', - // 'Custom Stylesheet' => '', - // 'download' => '', - // 'Do you really want to remove this budget line?' => '', - // 'EUR - Euro' => '', - // 'Expenses' => '', - // 'GBP - British Pound' => '', - // 'INR - Indian Rupee' => '', - // 'JPY - Japanese Yen' => '', - // 'New budget line' => '', - // 'NZD - New Zealand Dollar' => '', - // 'Remove a budget line' => '', - // 'Remove budget line' => '', - // 'RSD - Serbian dinar' => '', - // 'The budget line have been created successfully.' => '', - // 'Unable to create the budget line.' => '', - // 'Unable to remove this budget line.' => '', - // 'USD - US Dollar' => '', - // 'Remaining' => '', - // 'Destination column' => '', - // 'Move the task to another column when assigned to a user' => '', - // 'Move the task to another column when assignee is cleared' => '', - // 'Source column' => '', + 'Compact view' => 'Kompakt nézet', + 'Horizontal scrolling' => 'Vízszintes görgetés', + 'Compact/wide view' => 'Kompakt/széles nézet', + 'No results match:' => 'Nincs találat:', + 'Remove hourly rate' => 'Órabér törlése', + 'Do you really want to remove this hourly rate?' => 'Valóban törölni kívánja az órabért?', + 'Hourly rates' => 'Órabérek', + 'Hourly rate' => 'Órabér', + 'Currency' => 'Pénznem', + 'Effective date' => 'Hatálybalépés ideje', + 'Add new rate' => 'Új bér', + 'Rate removed successfully.' => 'Bér sikeresen törölve.', + 'Unable to remove this rate.' => 'Bér törlése sikertelen.', + 'Unable to save the hourly rate.' => 'Órabér mentése sikertelen.', + 'Hourly rate created successfully.' => 'Órabér sikeresen mentve.', + 'Start time' => 'Kezdés ideje', + 'End time' => 'Végzés ideje', + 'Comment' => 'Megjegyzés', + 'All day' => 'Egész nap', + 'Day' => 'Nap', + 'Manage timetable' => 'Időbeosztás kezelése', + 'Overtime timetable' => 'Túlóra időbeosztás', + 'Time off timetable' => 'Szabadság időbeosztás', + 'Timetable' => 'Időbeosztás', + 'Work timetable' => 'Munka időbeosztás', + 'Week timetable' => 'Heti időbeosztás', + 'Day timetable' => 'Napi időbeosztás', + 'From' => 'Feladó:', + 'To' => 'Címzett:', + 'Time slot created successfully.' => 'Időszelet sikeresen létrehozva.', + 'Unable to save this time slot.' => 'Időszelet mentése sikertelen.', + 'Time slot removed successfully.' => 'Időszelet sikeresen törölve.', + 'Unable to remove this time slot.' => 'Időszelet törlése sikertelen.', + 'Do you really want to remove this time slot?' => 'Biztos törli ezt az időszeletet?', + 'Remove time slot' => 'Időszelet törlése', + 'Add new time slot' => 'Új Időszelet', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ez az időbeosztás van használatban ha az "egész nap" jelölőnégyzet be van jelölve a tervezett szabadságnál és túlóránál.', + 'Files' => 'Fájlok', + 'Images' => 'Képek', + 'Private project' => 'Privát projekt', + 'Amount' => 'Összeg', + 'AUD - Australian Dollar' => 'AUD - Ausztrál dollár', + 'Budget' => 'Költségvetés', + 'Budget line' => 'Költségvetési tétel', + 'Budget line removed successfully.' => 'Költségvetési tétel sikeresen törölve.', + 'Budget lines' => 'Költségvetési tételek', + 'CAD - Canadian Dollar' => 'CAD - Kanadai dollár', + 'CHF - Swiss Francs' => 'CHF - Svájci frank', + 'Cost' => 'Költség', + 'Cost breakdown' => 'Költség visszaszámlálás', + 'Custom Stylesheet' => 'Egyéni sítluslap', + 'download' => 'letöltés', + 'Do you really want to remove this budget line?' => 'Biztos törölni akarja ezt a költségvetési tételt?', + 'EUR - Euro' => 'EUR - Euro', + 'Expenses' => 'Kiadások', + 'GBP - British Pound' => 'GBP - Angol font', + 'INR - Indian Rupee' => 'INR - Indiai rúpia', + 'JPY - Japanese Yen' => 'JPY - Japán Yen', + 'New budget line' => 'Új költségvetési tétel', + 'NZD - New Zealand Dollar' => 'NZD - Új-Zélandi dollár', + 'Remove a budget line' => 'Költségvetési tétel törlése', + 'Remove budget line' => 'Költségvetési tétel törlése', + 'RSD - Serbian dinar' => 'RSD - Szerb dínár', + 'The budget line have been created successfully.' => 'Költségvetési tétel sikeresen létrehozva.', + 'Unable to create the budget line.' => 'Költségvetési tétel létrehozása sikertelen.', + 'Unable to remove this budget line.' => 'Költségvetési tétel törlése sikertelen.', + 'USD - US Dollar' => 'USD - Amerikai ollár', + 'Remaining' => 'Maradék', + 'Destination column' => 'Cél oszlop', + 'Move the task to another column when assigned to a user' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés után', + 'Move the task to another column when assignee is cleared' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés törlésekor', + 'Source column' => 'Forrás oszlop', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 6e704ca1..e8d4c4d1 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 43834145..52d5413d 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 00ef6023..b53f9c83 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 2f5be941..42a3dff7 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 6edf5d0d..b18f2143 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -1,8 +1,8 @@ <?php return array( - // 'number.decimals_separator' => '', - // 'number.thousands_separator' => '', + 'number.decimals_separator' => ',', + 'number.thousands_separator' => ' ', 'None' => 'Nenhum', 'edit' => 'editar', 'Edit' => 'Editar', @@ -721,58 +721,58 @@ return array( 'fixes' => 'corrige', 'is fixed by' => 'foi corrigido por', 'This task' => 'Esta tarefa', - // '<1h' => '', - // '%dh' => '', - // '%b %e' => '', + '<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' => '', + 'Close dialog box' => 'Fechar a caixa de diálogo', 'Submit a form' => 'Envia o formulário', - // 'Board view' => '', - // 'Keyboard shortcuts' => '', - // 'Open board switcher' => '', + 'Board view' => 'Página do painel', + 'Keyboard shortcuts' => 'Atalhos de teclado', + 'Open board switcher' => 'Abrir o comutador de painel', 'Application' => 'Aplicação', 'Filter recently updated' => 'Filtro recentemente atualizado', - // 'since %B %e, %Y at %k:%M %p' => '', + 'since %B %e, %Y at %k:%M %p' => 'desde o %d/%m/%Y às %H:%M', 'More filters' => 'Mais filtros', - // 'Compact view' => '', - // 'Horizontal scrolling' => '', - // 'Compact/wide view' => '', - // 'No results match:' => '', - // 'Remove hourly rate' => '', - // 'Do you really want to remove this hourly rate?' => '', - // 'Hourly rates' => '', - // 'Hourly rate' => '', - // 'Currency' => '', - // 'Effective date' => '', - // 'Add new rate' => '', - // 'Rate removed successfully.' => '', - // 'Unable to remove this rate.' => '', - // 'Unable to save the hourly rate.' => '', - // 'Hourly rate created successfully.' => '', - // 'Start time' => '', - // 'End time' => '', - // 'Comment' => '', - // 'All day' => '', - // 'Day' => '', - // 'Manage timetable' => '', - // 'Overtime timetable' => '', - // 'Time off timetable' => '', - // 'Timetable' => '', - // 'Work timetable' => '', - // 'Week timetable' => '', - // 'Day timetable' => '', - // 'From' => '', - // 'To' => '', - // 'Time slot created successfully.' => '', - // 'Unable to save this time slot.' => '', - // 'Time slot removed successfully.' => '', - // 'Unable to remove this time slot.' => '', - // 'Do you really want to remove this time slot?' => '', - // 'Remove time slot' => '', - // 'Add new time slot' => '', - // 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '', + 'Compact view' => 'Vista reduzida', + 'Horizontal scrolling' => 'Rolagem horizontal', + 'Compact/wide view' => 'Alternar entre a vista compacta e ampliada', + 'No results match:' => 'Nenhum resultado:', + 'Remove hourly rate' => 'Retirar taxa horária', + 'Do you really want to remove this hourly rate?' => 'Você deseja realmente remover esta taxa horária?', + 'Hourly rates' => 'Taxas horárias', + 'Hourly rate' => 'Taxa horária', + 'Currency' => 'Moeda', + 'Effective date' => 'Data efetiva', + 'Add new rate' => 'Adicionar nova taxa', + 'Rate removed successfully.' => 'Taxa removido com sucesso.', + 'Unable to remove this rate.' => 'Impossível de remover esta taxa.', + 'Unable to save the hourly rate.' => 'Impossível salvar a taxa horária.', + 'Hourly rate created successfully.' => 'Taxa horária criada com sucesso.', + 'Start time' => 'Horário de início', + 'End time' => 'Horário de término', + 'Comment' => 'comentário', + 'All day' => 'Dia inteiro', + 'Day' => 'Dia', + 'Manage timetable' => 'Gestão dos horários', + 'Overtime timetable' => 'Horas extras', + 'Time off timetable' => 'Horas de ausência', + 'Timetable' => 'Horários', + 'Work timetable' => 'Horas trabalhadas', + 'Week timetable' => 'Horário da semana', + 'Day timetable' => 'Horário de un dia', + 'From' => 'Desde', + 'To' => 'A', + 'Time slot created successfully.' => 'Intervalo de tempo criado com sucesso.', + 'Unable to save this time slot.' => 'Impossível de guardar este intervalo de tempo.', + 'Time slot removed successfully.' => 'Intervalo de tempo removido com sucesso.', + 'Unable to remove this time slot.' => 'Impossível de remover esse intervalo de tempo.', + 'Do you really want to remove this time slot?' => 'Você deseja realmente remover este intervalo de tempo?', + 'Remove time slot' => 'Remover um intervalo de tempo', + 'Add new time slot' => 'Adicionar um intervalo de tempo', + 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Esses horários são usados quando a caixa de seleção "Dia inteiro" está marcada para Horas de ausência ou Extras', // 'Files' => '', // 'Images' => '', // 'Private project' => '', @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 8f597adc..b77960d3 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -22,7 +22,7 @@ return array( 'Grey' => 'Серый', 'Save' => 'Сохранить', 'Login' => 'Вход', - 'Official website:' => 'Официальный сайт :', + 'Official website:' => 'Официальный сайт:', 'Unassigned' => 'Не назначена', 'View this task' => 'Посмотреть задачу', 'Remove user' => 'Удалить пользователя', @@ -65,7 +65,7 @@ return array( 'Disable' => 'Деактивировать', 'Enable' => 'Активировать', 'New project' => 'Новый проект', - 'Do you really want to remove this project: "%s"?' => 'Вы точно хотите удалить этот проект? : « %s » ?', + 'Do you really want to remove this project: "%s"?' => 'Вы точно хотите удалить проект: « %s » ?', 'Remove project' => 'Удалить проект', 'Boards' => 'Доски', 'Edit the board for "%s"' => 'Изменить доску для « %s »', @@ -80,7 +80,7 @@ return array( 'Remove a column' => 'Удалить колонку', 'Remove a column from a board' => 'Удалить колонку с доски', 'Unable to remove this column.' => 'Не удалось удалить колонку.', - 'Do you really want to remove this column: "%s"?' => 'Вы точно хотите удалить эту колонку : « %s » ?', + 'Do you really want to remove this column: "%s"?' => 'Вы точно хотите удалить эту колонку: « %s » ?', 'This action will REMOVE ALL TASKS associated to this column!' => 'Вы УДАЛИТЕ ВСЕ ЗАДАЧИ находящиеся в этой колонке !', 'Settings' => 'Настройки', 'Application settings' => 'Настройки приложения', @@ -94,9 +94,9 @@ return array( '(VACUUM command)' => '(Команда VACUUM)', '(Gzip compressed Sqlite file)' => '(Сжать GZip файл SQLite)', 'User settings' => 'Настройки пользователя', - 'My default project:' => 'Мой проект по умолчанию : ', + 'My default project:' => 'Мой проект по умолчанию:', 'Close a task' => 'Закрыть задачу', - 'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу : « %s » ?', + 'Do you really want to close this task: "%s"?' => 'Вы точно хотите закрыть задачу: « %s » ?', 'Edit a task' => 'Изменить задачу', 'Column' => 'Колонка', 'Color' => 'Цвет', @@ -104,7 +104,7 @@ return array( 'Create another task' => 'Создать другую задачу', 'New task' => 'Новая задача', 'Open a task' => 'Открыть задачу', - 'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу : « %s » ?', + 'Do you really want to open this task: "%s"?' => 'Вы уверены что хотите открыть задачу: « %s » ?', 'Back to the board' => 'Вернуться на доску', 'Created on %B %e, %Y at %k:%M %p' => 'Создано %d/%m/%Y в %H:%M', 'There is nobody assigned' => 'Никто не назначен', @@ -121,7 +121,7 @@ return array( 'The password is required' => 'Требуется пароль', 'This value must be an integer' => 'Это значение должно быть целым', 'The username must be unique' => 'Требуется уникальное имя пользователя', - 'The username must be alphanumeric' => 'Имя пользователя должно быть букво-цифровым', + 'The username must be alphanumeric' => 'Имя пользователя должно быть буквенно-цифровым', 'The user id is required' => 'Требуется ID пользователя', 'Passwords don\'t match' => 'Пароли не совпадают', 'The confirmation is required' => 'Требуется подтверждение', @@ -146,13 +146,13 @@ return array( 'Project removed successfully.' => 'Проект удален.', 'Project activated successfully.' => 'Проект активирован.', 'Unable to activate this project.' => 'Невозможно активировать проект.', - 'Project disabled successfully.' => 'Проект успешно выключен.', - 'Unable to disable this project.' => 'Не удалось выключить проект.', + 'Project disabled successfully.' => 'Проект успешно деактивирован.', + 'Unable to disable this project.' => 'Не удалось деактивировать проект.', 'Unable to open this task.' => 'Не удалось открыть задачу.', 'Task opened successfully.' => 'Задача открыта.', 'Unable to close this task.' => 'Не удалось закрыть задачу.', 'Task closed successfully.' => 'Задача закрыта.', - 'Unable to update your task.' => 'Не удалось обновить вашу задачу.', + 'Unable to update your task.' => 'Не удалось обновить задачу.', 'Task updated successfully.' => 'Задача обновлена.', 'Unable to create your task.' => 'Не удалось создать задачу.', 'Task created successfully.' => 'Задача создана.', @@ -162,12 +162,12 @@ return array( 'Unable to update your user.' => 'Не удалось обновить пользователя.', 'User removed successfully.' => 'Пользователь удален.', 'Unable to remove this user.' => 'Не удалось удалить пользователя.', - 'Board updated successfully.' => 'Доска обновлена.', + 'Board updated successfully.' => 'Доска успешно обновлена.', 'Ready' => 'Готовые', 'Backlog' => 'Ожидающие', 'Work in progress' => 'В процессе', 'Done' => 'Выполнена', - 'Application version:' => 'Версия приложения :', + 'Application version:' => 'Версия приложения:', 'Completed on %B %e, %Y at %k:%M %p' => 'Завершен %d/%m/%Y в %H:%M', '%B %e, %Y at %k:%M %p' => '%d/%m/%Y в %H:%M', 'Date created' => 'Дата создания', @@ -176,11 +176,11 @@ return array( 'No task' => 'Нет задачи', 'Completed tasks' => 'Завершенные задачи', 'List of projects' => 'Список проектов', - 'Completed tasks for "%s"' => 'Задачи завершенные для « %s »', + 'Completed tasks for "%s"' => 'Завершенные задачи для « %s »', '%d closed tasks' => '%d завершенных задач', - 'No task for this project' => 'нет задач для этого проекта', + 'No task for this project' => 'Нет задач для этого проекта', 'Public link' => 'Ссылка для просмотра', - 'There is no column in your project!' => 'Нет колонки в вашем проекте !', + 'There is no column in your project!' => 'Нет колонки в вашем проекте!', 'Change assignee' => 'Сменить назначенного', 'Change assignee for the task "%s"' => 'Сменить назначенного для задачи « %s »', 'Timezone' => 'Часовой пояс', @@ -194,19 +194,19 @@ return array( 'Edit project access list' => 'Изменить доступ к проекту', 'Edit users access' => 'Изменить доступ пользователей', 'Allow this user' => 'Разрешить этого пользователя', - 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту :', - 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет доступ ко всему.', - 'Revoke' => 'отозвать', + 'Only those users have access to this project:' => 'Только эти пользователи имеют доступ к проекту:', + 'Don\'t forget that administrators have access to everything.' => 'Помните, администратор имеет неограниченные права.', + 'Revoke' => 'Отозвать', 'List of authorized users' => 'Список авторизованных пользователей', 'User' => 'Пользователь', 'Nobody have access to this project.' => 'Ни у кого нет доступа к этому проекту', - 'You are not allowed to access to this project.' => 'Вам запрешен доступ к этому проекту.', + 'You are not allowed to access to this project.' => 'Вам запрещен доступ к этому проекту.', 'Comments' => 'Комментарии', 'Post comment' => 'Оставить комментарий', 'Write your text in Markdown' => 'Справка по синтаксису Markdown', 'Leave a comment' => 'Оставить комментарий 2', 'Comment is required' => 'Нужен комментарий', - 'Leave a description' => 'Оставьте описание', + 'Leave a description' => 'Напишите описание', 'Comment added successfully.' => 'Комментарий успешно добавлен.', 'Unable to create your comment.' => 'Невозможно создать комментарий.', 'The description is required' => 'Требуется описание', @@ -217,7 +217,7 @@ return array( '%B %e, %Y' => '%d/%m/%Y', // '%b %e, %Y' => '', 'Automatic actions' => 'Автоматические действия', - 'Your automatic action have been created successfully.' => 'Автоматика настроена.', + 'Your automatic action have been created successfully.' => 'Автоматика успешно настроена.', 'Unable to create your automatic action.' => 'Не удалось создать автоматизированное действие.', 'Remove an action' => 'Удалить действие', 'Unable to remove this action.' => 'Не удалось удалить действие', @@ -260,8 +260,8 @@ return array( 'Remove a comment' => 'Удалить комментарий', 'Comment removed successfully.' => 'Комментарий удален.', 'Unable to remove this comment.' => 'Не удалось удалить этот комментарий.', - 'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий ?', - 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор или автор комментарий могут получить доступ.', + 'Do you really want to remove this comment?' => 'Вы точно хотите удалить этот комментарий?', + 'Only administrators or the creator of the comment can access to this page.' => 'Только администратор и автор комментария имеют доступ к этой странице.', 'Details' => 'Подробности', 'Current password for the user "%s"' => 'Текущий пароль для пользователя « %s »', 'The current password is required' => 'Требуется текущий пароль', @@ -280,7 +280,7 @@ return array( 'Remember Me' => 'Запомнить меня', 'Creation date' => 'Дата создания', 'Filter by user' => 'Фильтр по пользователям', - 'Filter by due date' => 'Фильтр по сроку', + 'Filter by due date' => 'Фильтр по дате', 'Everybody' => 'Все', 'Open' => 'Открытый', 'Closed' => 'Закрытый', @@ -288,17 +288,17 @@ return array( 'Nothing found.' => 'Ничего не найдено.', 'Search in the project "%s"' => 'Искать в проекте « %s »', 'Due date' => 'Срок', - 'Others formats accepted: %s and %s' => 'Другой формат приемлем : %s и %s', + 'Others formats accepted: %s and %s' => 'Другой формат приемлем: %s и %s', 'Description' => 'Описание', '%d comments' => '%d комментариев', '%d comment' => '%d комментарий', - 'Email address invalid' => 'Adresse email invalide', + 'Email address invalid' => 'Некорректный e-mail адрес', 'Your Google Account is not linked anymore to your profile.' => 'Ваш аккаунт в Google больше не привязан к вашему профилю.', 'Unable to unlink your Google Account.' => 'Не удалось отвязать ваш профиль от Google.', 'Google authentication failed' => 'Аутентификация Google не удалась', 'Unable to link your Google Account.' => 'Не удалось привязать ваш профиль к Google.', 'Your Google Account is linked to your profile successfully.' => 'Ваш профиль успешно привязан к Google.', - 'Email' => 'Email', + 'Email' => 'E-mail', 'Link my Google Account' => 'Привязать мой профиль к Google', 'Unlink my Google Account' => 'Отвязать мой профиль от Google', 'Login with my Google Account' => 'Аутентификация через Google', @@ -309,10 +309,10 @@ return array( 'Remove a task' => 'Удалить задачу', 'Do you really want to remove this task: "%s"?' => 'Вы точно хотите удалить эту задачу « %s » ?', 'Assign automatically a color based on a category' => 'Автоматически назначать цвет по категории', - 'Assign automatically a category based on a color' => 'Автоматически назначать категорию по цвету ', + 'Assign automatically a category based on a color' => 'Автоматически назначать категорию по цвету', 'Task creation or modification' => 'Создание или изменение задачи', 'Category' => 'Категория', - 'Category:' => 'Категория :', + 'Category:' => 'Категория:', 'Categories' => 'Категории', 'Category not found.' => 'Категория не найдена', 'Your category have been created successfully.' => 'Категория создана.', @@ -344,10 +344,10 @@ return array( 'Edit a comment' => 'Изменить комментарий', 'Summary' => 'Сводка', 'Time tracking' => 'Отслеживание времени', - 'Estimate:' => 'Приблизительно :', - 'Spent:' => 'Затрачено :', - 'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу ?', - 'Remaining:' => 'Осталось :', + 'Estimate:' => 'Приблизительно:', + 'Spent:' => 'Затрачено:', + 'Do you really want to remove this sub-task?' => 'Вы точно хотите удалить подзадачу?', + 'Remaining:' => 'Осталось:', 'hours' => 'часов', 'spent' => 'затрачено', 'estimated' => 'расчетное', @@ -367,11 +367,11 @@ return array( 'Unable to update your sub-task.' => 'Не удалось обновить подзадачу.', 'Unable to create your sub-task.' => 'Не удалось создать подзадачу.', 'Sub-task added successfully.' => 'Подзадача добавлена.', - 'Maximum size: ' => 'Максимальный размер : ', + 'Maximum size: ' => 'Максимальный размер: ', 'Unable to upload the file.' => 'Не удалось загрузить файл.', 'Display another project' => 'Показать другой проект', 'Your GitHub account was successfully linked to your profile.' => 'Ваш GitHub привязан к вашему профилю.', - 'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к Github.', + 'Unable to link your GitHub Account.' => 'Не удалось привязать ваш профиль к GitHub.', 'GitHub authentication failed' => 'Аутентификация в GitHub не удалась', 'Your GitHub account is no longer linked to your profile.' => 'Ваш GitHub отвязан от вашего профиля.', 'Unable to unlink your GitHub Account.' => 'Не удалось отвязать ваш профиль от GitHub.', @@ -395,16 +395,16 @@ return array( 'Clone Project' => 'Клонировать проект', 'Project cloned successfully.' => 'Проект клонирован.', 'Unable to clone this project.' => 'Не удалось клонировать проект.', - 'Email notifications' => 'Уведомления по email', - 'Enable email notifications' => 'Включить уведомления по email', - 'Task position:' => 'Позиция задачи :', + 'Email notifications' => 'Уведомления по e-mail', + 'Enable email notifications' => 'Включить уведомления по e-mail', + 'Task position:' => 'Позиция задачи:', 'The task #%d have been opened.' => 'Задача #%d была открыта.', 'The task #%d have been closed.' => 'Задача #%d была закрыта.', 'Sub-task updated' => 'Подзадача обновлена', - 'Title:' => 'Название :', - 'Status:' => 'Статус :', - 'Assignee:' => 'Назначена :', - 'Time tracking:' => 'Отслеживание времени :', + 'Title:' => 'Название:', + 'Status:' => 'Статус:', + 'Assignee:' => 'Назначена:', + 'Time tracking:' => 'Отслеживание времени:', 'New sub-task' => 'Новая подзадача', 'New attachment added "%s"' => 'Добавлено вложение « %s »', 'Comment updated' => 'Комментарий обновлен', @@ -419,7 +419,7 @@ return array( // 'Task opened' => '', '[%s][Due tasks]' => '[%s][Текущие задачи]', '[Kanboard] Notification' => '[Kanboard] Оповещение', - 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам :', + 'I want to receive notifications only for those projects:' => 'Я хочу получать уведомления только по этим проектам:', 'view the task on Kanboard' => 'посмотреть задачу на Kanboard', 'Public access' => 'Общий доступ', 'Category management' => 'Управление категориями', @@ -430,9 +430,9 @@ return array( 'Active projects' => 'Активные проекты', 'Inactive projects' => 'Неактивные проекты', 'Public access disabled' => 'Общий доступ отключен', - 'Do you really want to disable this project: "%s"?' => 'Вы точно хотите отключить проект: "%s"?', + 'Do you really want to disable this project: "%s"?' => 'Вы точно хотите деактивировать проект: "%s"?', 'Do you really want to duplicate this project: "%s"?' => 'Вы точно хотите клонировать проект: "%s"?', - 'Do you really want to enable this project: "%s"?' => 'Вы точно хотите включить проект: "%s"?', + 'Do you really want to enable this project: "%s"?' => 'Вы точно хотите активировать проект: "%s"?', 'Project activation' => 'Активация проекта', 'Move the task to another project' => 'Переместить задачу в другой проект', 'Move to another project' => 'Переместить в другой проект', @@ -448,7 +448,7 @@ return array( 'Github account linked' => 'Профиль GitHub связан', 'Username:' => 'Имя пользователя:', 'Name:' => 'Имя:', - 'Email:' => 'Email:', + 'Email:' => 'E-mail:', 'Default project:' => 'Проект по умолчанию:', 'Notifications:' => 'Уведомления:', 'Notifications' => 'Уведомления', @@ -485,7 +485,7 @@ return array( 'No activity.' => 'Нет активности', 'RSS feed' => 'RSS лента', '%s updated a comment on the task #%d' => '%s обновил комментарий задачи #%d', - '%s commented on the task #%d' => '%s откомментировал задачу #%d', + '%s commented on the task #%d' => '%s прокомментировал задачу #%d', '%s updated a subtask for the task #%d' => '%s обновил подзадачу задачи #%d', '%s created a subtask for the task #%d' => '%s создал подзадачу для задачи #%d', '%s updated the task #%d' => '%s обновил задачу #%d', @@ -503,14 +503,14 @@ return array( // 'Column Change' => '', // 'Position Change' => '', // 'Assignee Change' => '', - 'New password for the user "%s"' => 'Новый пароль для пользователя %s"', + 'New password for the user "%s"' => 'Новый пароль для пользователя "%s"', 'Choose an event' => 'Выберите событие', - 'Github commit received' => 'Github: коммит получен', - 'Github issue opened' => 'Github: новая проблема', - 'Github issue closed' => 'Github: проблема закрыта', - 'Github issue reopened' => 'Github: проблема переоткрыта', - 'Github issue assignee change' => 'Github: сменить ответственного за проблему', - 'Github issue label change' => 'Github: ярлык проблемы изменен', + 'Github commit received' => 'GitHub: коммит получен', + 'Github issue opened' => 'GitHub: новая проблема', + 'Github issue closed' => 'GitHub: проблема закрыта', + 'Github issue reopened' => 'GitHub: проблема переоткрыта', + 'Github issue assignee change' => 'GitHub: сменить ответственного за проблему', + 'Github issue label change' => 'GitHub: ярлык проблемы изменен', 'Create a task from an external provider' => 'Создать задачу из внешнего источника', 'Change the assignee based on an external username' => 'Изменить назначенного основываясь на внешнем имени пользователя', 'Change the category based on an external label' => 'Изменить категорию основываясь на внешнем ярлыке', @@ -562,9 +562,9 @@ return array( // 'Github issue comment created' => '', // 'Configure' => '', // 'Project management' => '', - // 'My projects' => '', - // 'Columns' => '', - // 'Task' => '', + 'My projects' => 'Мои проекты', + 'Columns' => 'Колонки', + 'Task' => 'Задача', // 'Your are not member of any project.' => '', // 'Percentage' => '', // 'Number of tasks' => '', @@ -572,8 +572,8 @@ return array( // 'Reportings' => '', // 'Task repartition for "%s"' => '', // 'Analytics' => '', - // 'Subtask' => '', - // 'My subtasks' => '', + 'Subtask' => 'Подзадача', + 'My subtasks' => 'Мои подзадачи', // 'User repartition' => '', // 'User repartition for "%s"' => '', // 'Clone this project' => '', @@ -648,10 +648,10 @@ return array( // 'Language:' => '', // 'Timezone:' => '', // 'All columns' => '', - // 'Calendar for "%s"' => '', - // 'Filter by column' => '', - // 'Filter by status' => '', - // 'Calendar' => '', + 'Calendar for "%s"' => 'Календарь для "%s"', + 'Filter by column' => 'Фильтр по колонке', + 'Filter by status' => 'Фильтр по статусу', + 'Calendar' => 'Календарь', // 'Next' => '', // '#%d' => '', // 'Filter by color' => '', @@ -688,18 +688,18 @@ return array( // 'Task age in days' => '', // 'Days in this column' => '', // '%dd' => '', - // 'Add a link' => '', - // 'Add a new link' => '', - // 'Do you really want to remove this link: "%s"?' => '', - // 'Do you really want to remove this link with task #%d?' => '', - // 'Field required' => '', - // 'Link added successfully.' => '', - // 'Link updated successfully.' => '', - // 'Link removed successfully.' => '', - // 'Link labels' => '', - // 'Link modification' => '', - // 'Links' => '', - // 'Link settings' => '', + 'Add a link' => 'Добавить ссылку на другие задачи', + 'Add a new link' => 'Добавление новой ссылки', + 'Do you really want to remove this link: "%s"?' => 'Вы уверены что хотите удалить ссылку: "%s"?', + 'Do you really want to remove this link with task #%d?' => 'Вы уверены что хотите удалить ссылку вместе с задачей #%d?', + 'Field required' => 'Поле обязательно для заполнения', + 'Link added successfully.' => 'Ссылка успешно добавлена', + 'Link updated successfully.' => 'Ссылка успешно обновлена', + 'Link removed successfully.' => 'Ссылка успешно удалена', + 'Link labels' => 'Метки для ссылки', + 'Link modification' => 'Обновление ссылки', + 'Links' => 'Ссылки', + 'Link settings' => 'Настройки ссылки', // 'Opposite label' => '', // 'Remove a link' => '', // 'Task\'s links' => '', @@ -709,37 +709,37 @@ return array( // 'Unable to create your link.' => '', // 'Unable to update your link.' => '', // 'Unable to remove this link.' => '', - // 'relates to' => '', - // 'blocks' => '', - // 'is blocked by' => '', - // 'duplicates' => '', - // 'is duplicated by' => '', - // 'is a child of' => '', - // 'is a parent of' => '', - // 'targets milestone' => '', - // 'is a milestone of' => '', - // 'fixes' => '', - // 'is fixed by' => '', - // 'This task' => '', + 'relates to' => 'связана с', + 'blocks' => 'блокирует', + 'is blocked by' => 'заблокирована в', + 'duplicates' => 'дублирует', + 'is duplicated by' => 'дублирована в', + 'is a child of' => 'наследник', + 'is a parent of' => 'родитель', + 'targets milestone' => 'часть этапа', + 'is a milestone of' => '', + 'fixes' => 'исправляет', + 'is fixed by' => 'исправлено в', + 'This task' => 'Эта задача', // '<1h' => '', // '%dh' => '', // '%b %e' => '', - // 'Expand tasks' => '', - // 'Collapse tasks' => '', - // 'Expand/collapse tasks' => '', - // 'Close dialog box' => '', - // 'Submit a form' => '', - // 'Board view' => '', - // 'Keyboard shortcuts' => '', - // 'Open board switcher' => '', - // 'Application' => '', - // 'Filter recently updated' => '', + 'Expand tasks' => 'Развернуть задачи', + 'Collapse tasks' => 'Свернуть задачи', + 'Expand/collapse tasks' => 'Развернуть/свернуть задачи', + 'Close dialog box' => 'Закрыть диалог', + 'Submit a form' => 'Отправить форму', + 'Board view' => 'Просмотр доски', + 'Keyboard shortcuts' => 'Горячие клавиши', + 'Open board switcher' => 'Открыть переключатель доски', + 'Application' => 'Приложение', + 'Filter recently updated' => 'Сортировать по дате обновления', // 'since %B %e, %Y at %k:%M %p' => '', - // 'More filters' => '', - // 'Compact view' => '', - // 'Horizontal scrolling' => '', - // 'Compact/wide view' => '', - // 'No results match:' => '', + 'More filters' => 'Использовать фильтры', + 'Compact view' => 'Компактный вид', + 'Horizontal scrolling' => 'Горизонтальная прокрутка', + 'Compact/wide view' => 'Компактный/широкий вид', + 'No results match:' => 'Отсутствуют результаты:', // 'Remove hourly rate' => '', // 'Do you really want to remove this hourly rate?' => '', // 'Hourly rates' => '', @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 9c7f63a8..0514faa4 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 212440a7..dede7364 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index bc94adf4..86a4f79c 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 0272a7f8..eb971798 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index e99fedab..70175e5f 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -808,4 +808,28 @@ return array( // 'Move the task to another column when assigned to a user' => '', // 'Move the task to another column when assignee is cleared' => '', // 'Source column' => '', + // 'Show subtask estimates in the user calendar' => '', + // 'Transitions' => '', + // 'Executer' => '', + // 'Time spent in the column' => '', + // 'Task transitions' => '', + // 'Task transitions export' => '', + // 'This report contains all column moves for each task with the date, the user and the time spent for each transition.' => '', + // 'Currency rates' => '', + // 'Rate' => '', + // 'Change reference currency' => '', + // 'Add a new currency rate' => '', + // 'Currency rates are used to calculate project budget.' => '', + // 'Reference currency' => '', + // 'The currency rate have been added successfully.' => '', + // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', + // 'Send notifications to Hipchat' => '', + // 'API URL' => '', + // 'Room API ID or name' => '', + // 'Room notification token' => '', + // 'Help on Hipchat integration' => '', ); diff --git a/app/Model/Acl.php b/app/Model/Acl.php index b52a7864..403c45d0 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -72,6 +72,7 @@ class Acl extends Base 'link' => '*', 'project' => array('remove'), 'hourlyrate' => '*', + 'currency' => '*', ); /** diff --git a/app/Model/Budget.php b/app/Model/Budget.php index 84cadf6e..d74dd870 100644 --- a/app/Model/Budget.php +++ b/app/Model/Budget.php @@ -111,15 +111,19 @@ class Budget extends Base $date = $today->format('Y-m-d'); $today_in = isset($in[$date]) ? (int) $in[$date] : 0; $today_out = isset($out[$date]) ? (int) $out[$date] : 0; - $left += $today_in; - $left -= $today_out; - - $serie[] = array( - 'date' => $date, - 'in' => $today_in, - 'out' => -$today_out, - 'left' => $left, - ); + + if ($today_in > 0 || $today_out > 0) { + + $left += $today_in; + $left -= $today_out; + + $serie[] = array( + 'date' => $date, + 'in' => $today_in, + 'out' => -$today_out, + 'left' => $left, + ); + } } return $serie; @@ -143,7 +147,7 @@ class Budget extends Base foreach ($rates as $rate) { if ($rate['user_id'] == $record['user_id'] && date('Y-m-d', $rate['date_effective']) <= date('Y-m-d', $record['start'])) { - $hourly_price = $rate['rate']; + $hourly_price = $this->currency->getPrice($rate['currency'], $rate['rate']); break; } } diff --git a/app/Model/Currency.php b/app/Model/Currency.php new file mode 100644 index 00000000..bc423337 --- /dev/null +++ b/app/Model/Currency.php @@ -0,0 +1,104 @@ +<?php + +namespace Model; + +use SimpleValidator\Validator; +use SimpleValidator\Validators; + +/** + * Currency + * + * @package model + * @author Frederic Guillot + */ +class Currency extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'currencies'; + + /** + * Get all currency rates + * + * @access public + * @return array + */ + public function getAll() + { + return $this->db->table(self::TABLE)->findAll(); + } + + /** + * Calculate the price for the reference currency + * + * @access public + * @return array + */ + public function getPrice($currency, $price) + { + static $rates = null; + $reference = $this->config->get('application_currency', 'USD'); + + if ($reference !== $currency) { + $rates = $rates === null ? $this->db->hashtable(self::TABLE)->getAll('currency', 'rate') : array(); + $rate = isset($rates[$currency]) ? $rates[$currency] : 1; + + return $rate * $price; + } + + return $price; + } + + /** + * Add a new currency rate + * + * @access public + * @param string $currency + * @param float $rate + * @return boolean|integer + */ + public function create($currency, $rate) + { + if ($this->db->table(self::TABLE)->eq('currency', $currency)->count() === 1) { + return $this->update($currency, $rate); + } + + return $this->persist(self::TABLE, compact('currency', 'rate')); + } + + /** + * Update a currency rate + * + * @access public + * @param string $currency + * @param float $rate + * @return boolean + */ + public function update($currency, $rate) + { + return $this->db->table(self::TABLE)->eq('currency', $currency)->update(array('rate' => $rate)); + } + + /** + * Validate + * + * @access public + * @param array $values Form values + * @return array $valid, $errors [0] = Success or not, [1] = List of errors + */ + public function validate(array $values) + { + $v = new Validator($values, array( + new Validators\Required('currency', t('Field required')), + new Validators\Required('rate', t('Field required')), + )); + + return array( + $v->execute(), + $v->getErrors() + ); + } +} diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 652cc842..c5fbbd38 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -162,7 +162,13 @@ class ProjectActivity extends Base { switch ($event['event_name']) { case Task::EVENT_ASSIGNEE_CHANGE: - return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $event['task']['assignee_name'] ?: $event['task']['assignee_username']); + $assignee = $event['task']['assignee_name'] ?: $event['task']['assignee_username']; + + if (! empty($assignee)) { + return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $assignee); + } + + return t('%s remove the assignee of the task %s', $event['author'], e('#%d', $event['task']['id'])); case Task::EVENT_UPDATE: return t('%s updated the task #%d', $event['author'], $event['task']['id']); case Task::EVENT_CREATE: diff --git a/app/Model/Subtask.php b/app/Model/Subtask.php index e33373e0..492f3a77 100644 --- a/app/Model/Subtask.php +++ b/app/Model/Subtask.php @@ -98,6 +98,7 @@ class Subtask extends Base Subtask::TABLE.'.*', Task::TABLE.'.project_id', Task::TABLE.'.color_id', + Task::TABLE.'.title AS task_name', Project::TABLE.'.name AS project_name' ) ->eq('user_id', $user_id) diff --git a/app/Model/SubtaskForecast.php b/app/Model/SubtaskForecast.php new file mode 100644 index 00000000..cb86f6d7 --- /dev/null +++ b/app/Model/SubtaskForecast.php @@ -0,0 +1,118 @@ +<?php + +namespace Model; + +use DateTime; +use DateInterval; + +/** + * Subtask Forecast + * + * @package model + * @author Frederic Guillot + */ +class SubtaskForecast extends Base +{ + /** + * Get not completed subtasks with an estimate sorted by postition + * + * @access public + * @param integer $user_id + * @return array + */ + public function getSubtasks($user_id) + { + return $this->db + ->table(Subtask::TABLE) + ->columns(Subtask::TABLE.'.id', Task::TABLE.'.project_id', Subtask::TABLE.'.task_id', Subtask::TABLE.'.title', Subtask::TABLE.'.time_estimated') + ->join(Task::TABLE, 'id', 'task_id') + ->asc(Task::TABLE.'.position') + ->asc(Subtask::TABLE.'.position') + ->gt(Subtask::TABLE.'.time_estimated', 0) + ->eq(Subtask::TABLE.'.status', Subtask::STATUS_TODO) + ->eq(Subtask::TABLE.'.user_id', $user_id) + ->findAll(); + } + + /** + * Get the start date for the forecast + * + * @access public + * @param integer $user_id + * @return array + */ + public function getStartDate($user_id) + { + $subtask = $this->db->table(Subtask::TABLE) + ->columns(Subtask::TABLE.'.time_estimated', SubtaskTimeTracking::TABLE.'.start') + ->eq(SubtaskTimeTracking::TABLE.'.user_id', $user_id) + ->eq(SubtaskTimeTracking::TABLE.'.end', 0) + ->status('status', Subtask::STATUS_INPROGRESS) + ->join(SubtaskTimeTracking::TABLE, 'subtask_id', 'id') + ->findOne(); + + if ($subtask && $subtask['time_estimated'] && $subtask['start']) { + return date('Y-m-d H:i', $subtask['start'] + $subtask['time_estimated'] * 3600); + } + + return date('Y-m-d H:i'); + } + + /** + * Get all calendar events according to the user timetable and the subtasks estimates + * + * @access public + * @param integer $user_id + * @param string $end End date of the calendar + * @return array + */ + public function getCalendarEvents($user_id, $end) + { + $events = array(); + $start_date = new DateTime($this->getStartDate($user_id)); + $timetable = $this->timetable->calculate($user_id, $start_date, new DateTime($end)); + $subtasks = $this->getSubtasks($user_id); + $total = count($subtasks); + $offset = 0; + + foreach ($timetable as $slot) { + + $interval = $this->dateParser->getHours($slot[0], $slot[1]); + $start = $slot[0]->getTimestamp(); + + if ($slot[0] < $start_date) { + continue; + } + + while ($offset < $total) { + + $event = array( + 'id' => $subtasks[$offset]['id'].'-'.$subtasks[$offset]['task_id'].'-'.$offset, + 'subtask_id' => $subtasks[$offset]['id'], + 'title' => t('#%d', $subtasks[$offset]['task_id']).' '.$subtasks[$offset]['title'], + 'url' => $this->helper->url('task', 'show', array('task_id' => $subtasks[$offset]['task_id'], 'project_id' => $subtasks[$offset]['project_id'])), + 'editable' => false, + 'start' => date('Y-m-d\TH:i:s', $start), + ); + + if ($subtasks[$offset]['time_estimated'] <= $interval) { + + $start += $subtasks[$offset]['time_estimated'] * 3600; + $interval -= $subtasks[$offset]['time_estimated']; + $offset++; + + $event['end'] = date('Y-m-d\TH:i:s', $start); + $events[] = $event; + } + else { + $subtasks[$offset]['time_estimated'] -= $interval; + $event['end'] = $slot[1]->format('Y-m-d\TH:i:s'); + $events[] = $event; + break; + } + } + } + + return $events; + } +} diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php index 6dd10b02..ab5fe43b 100644 --- a/app/Model/TaskPosition.php +++ b/app/Model/TaskPosition.php @@ -143,6 +143,9 @@ class TaskPosition extends Base 'position' => $new_position, 'column_id' => $new_column_id, 'swimlane_id' => $new_swimlane_id, + 'src_column_id' => $task['column_id'], + 'dst_column_id' => $new_column_id, + 'date_moved' => $task['date_moved'], ); if ($task['swimlane_id'] != $new_swimlane_id) { diff --git a/app/Model/Transition.php b/app/Model/Transition.php new file mode 100644 index 00000000..cb759e4a --- /dev/null +++ b/app/Model/Transition.php @@ -0,0 +1,170 @@ +<?php + +namespace Model; + +/** + * Transition model + * + * @package model + * @author Frederic Guillot + */ +class Transition extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'transitions'; + + /** + * Save transition event + * + * @access public + * @param integer $user_id + * @param array $task + * @return boolean + */ + public function save($user_id, array $task) + { + return $this->db->table(self::TABLE)->insert(array( + 'user_id' => $user_id, + 'project_id' => $task['project_id'], + 'task_id' => $task['task_id'], + 'src_column_id' => $task['src_column_id'], + 'dst_column_id' => $task['dst_column_id'], + 'date' => time(), + 'time_spent' => time() - $task['date_moved'] + )); + } + + /** + * Get all transitions by task + * + * @access public + * @param integer $task_id + * @return array + */ + public function getAllByTask($task_id) + { + return $this->db->table(self::TABLE) + ->columns( + 'src.title as src_column', + 'dst.title as dst_column', + User::TABLE.'.name', + User::TABLE.'.username', + self::TABLE.'.user_id', + self::TABLE.'.date', + self::TABLE.'.time_spent' + ) + ->eq('task_id', $task_id) + ->desc('date') + ->join(User::TABLE, 'id', 'user_id') + ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->findAll(); + } + + /** + * Get all transitions by project + * + * @access public + * @param integer $project_id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function getAllByProjectAndDate($project_id, $from, $to) + { + if (! is_numeric($from)) { + $from = $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($from)); + } + + if (! is_numeric($to)) { + $to = $this->dateParser->removeTimeFromTimestamp(strtotime('+1 day', $this->dateParser->getTimestamp($to))); + } + + return $this->db->table(self::TABLE) + ->columns( + Task::TABLE.'.id', + Task::TABLE.'.title', + 'src.title as src_column', + 'dst.title as dst_column', + User::TABLE.'.name', + User::TABLE.'.username', + self::TABLE.'.user_id', + self::TABLE.'.date', + self::TABLE.'.time_spent' + ) + ->gte('date', $from) + ->lte('date', $to) + ->eq(self::TABLE.'.project_id', $project_id) + ->desc('date') + ->join(Task::TABLE, 'id', 'task_id') + ->join(User::TABLE, 'id', 'user_id') + ->join(Board::TABLE.' as src', 'id', 'src_column_id', self::TABLE, 'src') + ->join(Board::TABLE.' as dst', 'id', 'dst_column_id', self::TABLE, 'dst') + ->findAll(); + } + + /** + * Get project export + * + * @access public + * @param integer $project_id Project id + * @param mixed $from Start date (timestamp or user formatted date) + * @param mixed $to End date (timestamp or user formatted date) + * @return array + */ + public function export($project_id, $from, $to) + { + $results = array($this->getColumns()); + $transitions = $this->getAllByProjectAndDate($project_id, $from, $to); + + foreach ($transitions as $transition) { + $results[] = $this->format($transition); + } + + return $results; + } + + /** + * Get column titles + * + * @access public + * @return string[] + */ + public function getColumns() + { + return array( + e('Id'), + e('Task Title'), + e('Source column'), + e('Destination column'), + e('Executer'), + e('Date'), + e('Time spent'), + ); + } + + /** + * Format the output of a transition array + * + * @access public + * @param array $transition + * @return array + */ + public function format(array $transition) + { + $values = array(); + $values[] = $transition['id']; + $values[] = $transition['title']; + $values[] = $transition['src_column']; + $values[] = $transition['dst_column']; + $values[] = $transition['name'] ?: $transition['username']; + $values[] = date('Y-m-d H:i', $transition['date']); + $values[] = round($transition['time_spent'] / 3600, 2); + + return $values; + } +} diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php index 7edffa6e..b3603818 100644 --- a/app/Model/Webhook.php +++ b/app/Model/Webhook.php @@ -11,27 +11,6 @@ namespace Model; class Webhook extends Base { /** - * HTTP connection timeout in seconds - * - * @var integer - */ - const HTTP_TIMEOUT = 1; - - /** - * Number of maximum redirections for the HTTP client - * - * @var integer - */ - const HTTP_MAX_REDIRECTS = 3; - - /** - * HTTP client user agent - * - * @var string - */ - const HTTP_USER_AGENT = 'Kanboard Webhook'; - - /** * Call the external URL * * @access public @@ -42,22 +21,6 @@ class Webhook extends Base { $token = $this->config->get('webhook_token'); - $headers = array( - 'Connection: close', - 'User-Agent: '.self::HTTP_USER_AGENT, - ); - - $context = stream_context_create(array( - 'http' => array( - 'method' => 'POST', - 'protocol_version' => 1.1, - 'timeout' => self::HTTP_TIMEOUT, - 'max_redirects' => self::HTTP_MAX_REDIRECTS, - 'header' => implode("\r\n", $headers), - 'content' => json_encode($task) - ) - )); - if (strpos($url, '?') !== false) { $url .= '&token='.$token; } @@ -65,6 +28,6 @@ class Webhook extends Base $url .= '?token='.$token; } - @file_get_contents($url, false, $context); + return $this->httpClient->post($url, $task); } } diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index a78ffacf..f0e0d6b2 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,61 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 54; +const VERSION = 59; + +function version_59($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} + +function version_58($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} + +function version_57($pdo) +{ + $pdo->exec('CREATE TABLE currencies (`currency` CHAR(3) NOT NULL UNIQUE, `rate` FLOAT DEFAULT 0) ENGINE=InnoDB CHARSET=utf8'); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_currency', 'USD')); +} + +function version_56($pdo) +{ + $pdo->exec('CREATE TABLE transitions ( + `id` INT NOT NULL AUTO_INCREMENT, + `user_id` INT NOT NULL, + `project_id` INT NOT NULL, + `task_id` INT NOT NULL, + `src_column_id` INT NOT NULL, + `dst_column_id` INT NOT NULL, + `date` INT NOT NULL, + `time_spent` INT DEFAULT 0, + FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8'); + + $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)"); + $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)"); + $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)"); +} + +function version_55($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_forecast', '0')); +} function version_54($pdo) { @@ -85,7 +139,7 @@ function version_50($pdo) user_id INT NOT NULL, rate FLOAT DEFAULT 0, date_effective INTEGER NOT NULL, - currency TEXT NOT NULL, + currency CHAR(3) NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, PRIMARY KEY(id) ) ENGINE=InnoDB CHARSET=utf8"); diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 2396000f..f7a0453d 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,60 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 35; +const VERSION = 40; + +function version_40($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} + +function version_39($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} + +function version_38($pdo) +{ + $pdo->exec('CREATE TABLE currencies ("currency" CHAR(3) NOT NULL UNIQUE, "rate" REAL DEFAULT 0)'); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_currency', 'USD')); +} + +function version_37($pdo) +{ + $pdo->exec('CREATE TABLE transitions ( + "id" SERIAL PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "project_id" INTEGER NOT NULL, + "task_id" INTEGER NOT NULL, + "src_column_id" INTEGER NOT NULL, + "dst_column_id" INTEGER NOT NULL, + "date" INTEGER NOT NULL, + "time_spent" INTEGER DEFAULT 0, + FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + )'); + + $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)"); + $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)"); + $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)"); +} + +function version_36($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_forecast', '0')); +} function version_35($pdo) { @@ -80,7 +133,7 @@ function version_31($pdo) user_id INTEGER NOT NULL, rate REAL DEFAULT 0, date_effective INTEGER NOT NULL, - currency TEXT NOT NULL, + currency CHAR(3) NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )"); } diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 0e0512d0..3ad045e6 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,60 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 53; +const VERSION = 58; + +function version_58($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_hipchat', '0')); + $rq->execute(array('integration_hipchat_api_url', 'https://api.hipchat.com')); + $rq->execute(array('integration_hipchat_room_id', '')); + $rq->execute(array('integration_hipchat_room_token', '')); +} + +function version_57($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} + +function version_56($pdo) +{ + $pdo->exec('CREATE TABLE currencies ("currency" TEXT NOT NULL UNIQUE, "rate" REAL DEFAULT 0)'); + + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('application_currency', 'USD')); +} + +function version_55($pdo) +{ + $pdo->exec('CREATE TABLE transitions ( + "id" INTEGER PRIMARY KEY, + "user_id" INTEGER NOT NULL, + "project_id" INTEGER NOT NULL, + "task_id" INTEGER NOT NULL, + "src_column_id" INTEGER NOT NULL, + "dst_column_id" INTEGER NOT NULL, + "date" INTEGER NOT NULL, + "time_spent" INTEGER DEFAULT 0, + FOREIGN KEY(src_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(dst_column_id) REFERENCES columns(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE + )'); + + $pdo->exec("CREATE INDEX transitions_task_index ON transitions(task_id)"); + $pdo->exec("CREATE INDEX transitions_project_index ON transitions(project_id)"); + $pdo->exec("CREATE INDEX transitions_user_index ON transitions(user_id)"); +} + +function version_54($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('subtask_forecast', '0')); +} function version_53($pdo) { diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index fc71ebf9..6a12ea5a 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -22,6 +22,7 @@ class ClassProvider implements ServiceProviderInterface 'Color', 'Comment', 'Config', + 'Currency', 'DateParser', 'File', 'HourlyRate', @@ -36,6 +37,7 @@ class ClassProvider implements ServiceProviderInterface 'ProjectPermission', 'Subtask', 'SubtaskExport', + 'SubtaskForecast', 'SubtaskTimeTracking', 'Swimlane', 'Task', @@ -55,6 +57,7 @@ class ClassProvider implements ServiceProviderInterface 'TimetableWeek', 'TimetableOff', 'TimetableExtra', + 'Transition', 'User', 'UserSession', 'Webhook', @@ -66,11 +69,14 @@ class ClassProvider implements ServiceProviderInterface 'MemoryCache', 'FileCache', 'Request', + 'HttpClient', ), 'Integration' => array( 'GitlabWebhook', 'GithubWebhook', 'BitbucketWebhook', + 'Hipchat', + 'SlackWebhook', ) ); diff --git a/app/ServiceProvider/EventDispatcherProvider.php b/app/ServiceProvider/EventDispatcherProvider.php index ec382206..f002db74 100644 --- a/app/ServiceProvider/EventDispatcherProvider.php +++ b/app/ServiceProvider/EventDispatcherProvider.php @@ -14,6 +14,7 @@ use Subscriber\ProjectModificationDateSubscriber; use Subscriber\WebhookSubscriber; use Subscriber\SubtaskTimesheetSubscriber; use Subscriber\TaskMovedDateSubscriber; +use Subscriber\TransitionSubscriber; class EventDispatcherProvider implements ServiceProviderInterface { @@ -29,6 +30,7 @@ class EventDispatcherProvider implements ServiceProviderInterface $container['dispatcher']->addSubscriber(new NotificationSubscriber($container)); $container['dispatcher']->addSubscriber(new SubtaskTimesheetSubscriber($container)); $container['dispatcher']->addSubscriber(new TaskMovedDateSubscriber($container)); + $container['dispatcher']->addSubscriber(new TransitionSubscriber($container)); // Automatic actions $container['action']->attachEvents(); diff --git a/app/Subscriber/ProjectActivitySubscriber.php b/app/Subscriber/ProjectActivitySubscriber.php index 00f5b044..42314637 100644 --- a/app/Subscriber/ProjectActivitySubscriber.php +++ b/app/Subscriber/ProjectActivitySubscriber.php @@ -41,6 +41,33 @@ class ProjectActivitySubscriber extends Base implements EventSubscriberInterface $event_name, $values ); + + $this->sendSlackNotification($event_name, $values); + $this->sendHipchatNotification($event_name, $values); + } + } + + private function sendSlackNotification($event_name, array $values) + { + if ($this->config->get('integration_slack_webhook') == 1) { + $this->slackWebhook->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); + } + } + + private function sendHipchatNotification($event_name, array $values) + { + if ($this->config->get('integration_hipchat') == 1) { + $this->hipchat->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); } } diff --git a/app/Subscriber/TransitionSubscriber.php b/app/Subscriber/TransitionSubscriber.php new file mode 100644 index 00000000..347dd37d --- /dev/null +++ b/app/Subscriber/TransitionSubscriber.php @@ -0,0 +1,26 @@ +<?php + +namespace Subscriber; + +use Event\TaskEvent; +use Model\Task; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class TransitionSubscriber extends Base implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + Task::EVENT_MOVE_COLUMN => array('execute', 0), + ); + } + + public function execute(TaskEvent $event) + { + $user_id = $this->userSession->getId(); + + if (! empty($user_id)) { + $this->transition->save($user_id, $event->getAll()); + } + } +}
\ No newline at end of file diff --git a/app/Template/app/subtasks.php b/app/Template/app/subtasks.php index fdfbdf2f..487b66fc 100644 --- a/app/Template/app/subtasks.php +++ b/app/Template/app/subtasks.php @@ -6,6 +6,7 @@ <tr> <th class="column-10"><?= $paginator->order('Id', 'tasks.id') ?></th> <th class="column-20"><?= $paginator->order(t('Project'), 'project_name') ?></th> + <th><?= $paginator->order(t('Task'), 'task_name') ?></th> <th><?= $paginator->order(t('Subtask'), 'title') ?></th> <th class="column-20"><?= t('Time tracking') ?></th> </tr> @@ -18,6 +19,9 @@ <?= $this->a($this->e($subtask['project_name']), 'board', 'show', array('project_id' => $subtask['project_id'])) ?> </td> <td> + <?= $this->a($this->e($subtask['task_name']), 'task', 'show', array('task_id' => $subtask['task_id'], 'project_id' => $subtask['project_id'])) ?> + </td> + <td> <?= $this->toggleSubtaskStatus($subtask, 'dashboard') ?> </td> <td> diff --git a/app/Template/budget/breakdown.php b/app/Template/budget/breakdown.php index d4168406..0a3c63d7 100644 --- a/app/Template/budget/breakdown.php +++ b/app/Template/budget/breakdown.php @@ -22,7 +22,7 @@ <tr> <td><?= $this->a($this->e($record['task_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td> <td><?= $this->a($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td> - <td><?= $this->e($record['name'] ?: $record['username']) ?></td> + <td><?= $this->a($this->e($record['name'] ?: $record['username']), 'user', 'show', array('user_id' => $record['user_id'])) ?></td> <td><?= n($record['cost']) ?></td> <td><?= n($record['time_spent']).' '.t('hours') ?></td> <td><?= dt('%B %e, %Y', $record['start']) ?></td> diff --git a/app/Template/config/board.php b/app/Template/config/board.php index 57efcd08..15e2b422 100644 --- a/app/Template/config/board.php +++ b/app/Template/config/board.php @@ -28,6 +28,7 @@ <?= $this->formCheckbox('subtask_restriction', t('Allow only one subtask in progress at the same time for a user'), 1, $values['subtask_restriction'] == 1) ?> <?= $this->formCheckbox('subtask_time_tracking', t('Enable time tracking for subtasks'), 1, $values['subtask_time_tracking'] == 1) ?> + <?= $this->formCheckbox('subtask_forecast', t('Show subtask estimates in the user calendar'), 1, $values['subtask_forecast'] == 1) ?> <div class="form-actions"> <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php new file mode 100644 index 00000000..6f90e0ab --- /dev/null +++ b/app/Template/config/integrations.php @@ -0,0 +1,38 @@ +<div class="page-header"> + <h2><?= t('Integration with third-party services') ?></h2> +</div> + +<form method="post" action="<?= $this->u('config', 'integrations') ?>" autocomplete="off"> + + <?= $this->formCsrf() ?> + + <h3><img src="assets/img/hipchat-icon.png"/> <?= t('Hipchat') ?></h3> + <div class="listing"> + <?= $this->formCheckbox('integration_hipchat', t('Send notifications to Hipchat'), 1, $values['integration_hipchat'] == 1) ?> + + <?= $this->formLabel(t('API URL'), 'integration_hipchat_api_url') ?> + <?= $this->formText('integration_hipchat_api_url', $values, $errors) ?> + + <?= $this->formLabel(t('Room API ID or name'), 'integration_hipchat_room_id') ?> + <?= $this->formText('integration_hipchat_room_id', $values, $errors) ?> + + <?= $this->formLabel(t('Room notification token'), 'integration_hipchat_room_token') ?> + <?= $this->formText('integration_hipchat_room_token', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/hipchat" target="_blank"><?= t('Help on Hipchat integration') ?></a></p> + </div> + + <h3><i class="fa fa-slack fa-fw"></i> <?= t('Slack') ?></h3> + <div class="listing"> + <?= $this->formCheckbox('integration_slack_webhook', t('Send notifications to a Slack channel'), 1, $values['integration_slack_webhook'] == 1) ?> + + <?= $this->formLabel(t('Webhook URL'), 'integration_slack_webhook_url') ?> + <?= $this->formText('integration_slack_webhook_url', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php index 89f2c203..a4f9d8e3 100644 --- a/app/Template/config/sidebar.php +++ b/app/Template/config/sidebar.php @@ -14,6 +14,12 @@ <?= $this->a(t('Link settings'), 'link', 'index') ?> </li> <li> + <?= $this->a(t('Currency rates'), 'currency', 'index') ?> + </li> + <li> + <?= $this->a(t('Integrations'), 'config', 'integrations') ?> + </li> + <li> <?= $this->a(t('Webhooks'), 'config', 'webhook') ?> </li> <li> diff --git a/app/Template/currency/index.php b/app/Template/currency/index.php new file mode 100644 index 00000000..7839a142 --- /dev/null +++ b/app/Template/currency/index.php @@ -0,0 +1,56 @@ +<div class="page-header"> + <h2><?= t('Currency rates') ?></h2> +</div> + +<?php if (! empty($rates)): ?> + +<table class="table-stripped"> + <tr> + <th class="column-35"><?= t('Currency') ?></th> + <th><?= t('Rate') ?></th> + </tr> + <?php foreach ($rates as $rate): ?> + <tr> + <td> + <strong><?= $this->e($rate['currency']) ?></strong> + </td> + <td> + <?= n($rate['rate']) ?> + </td> + </tr> + <?php endforeach ?> +</table> + +<hr/> +<h3><?= t('Change reference currency') ?></h3> +<?php endif ?> +<form method="post" action="<?= $this->u('currency', 'reference') ?>" autocomplete="off"> + + <?= $this->formCsrf() ?> + + <?= $this->formLabel(t('Reference currency'), 'application_currency') ?> + <?= $this->formSelect('application_currency', $currencies, $config_values, $errors) ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<hr/> +<h3><?= t('Add a new currency rate') ?></h3> +<form method="post" action="<?= $this->u('currency', 'create') ?>" autocomplete="off"> + + <?= $this->formCsrf() ?> + + <?= $this->formLabel(t('Currency'), 'currency') ?> + <?= $this->formSelect('currency', $currencies, $values, $errors) ?><br/> + + <?= $this->formLabel(t('Rate'), 'rate') ?> + <?= $this->formText('rate', $values, $errors, array(), 'form-numeric') ?><br/> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form> + +<p class="alert alert-info"><?= t('Currency rates are used to calculate project budget.') ?></p> diff --git a/app/Template/event/task_assignee_change.php b/app/Template/event/task_assignee_change.php index 6eac412b..22ed936b 100644 --- a/app/Template/event/task_assignee_change.php +++ b/app/Template/event/task_assignee_change.php @@ -1,9 +1,15 @@ <p class="activity-title"> - <?= e('%s changed the assignee of the task %s to %s', - $this->e($author), - $this->a(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), - $this->e($task['assignee_name'] ?: $task['assignee_username']) - ) ?> + <?php $assignee = $task['assignee_name'] ?: $task['assignee_username'] ?> + + <?php if (! empty($assignee)): ?> + <?= e('%s changed the assignee of the task %s to %s', + $this->e($author), + $this->a(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->e($assignee) + ) ?> + <?php else: ?> + <?= e('%s remove the assignee of the task %s', $this->e($author), $this->a(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> + <?php endif ?> </p> <p class="activity-description"> <em><?= $this->e($task['title']) ?></em> diff --git a/app/Template/export/transitions.php b/app/Template/export/transitions.php new file mode 100644 index 00000000..7cd190e0 --- /dev/null +++ b/app/Template/export/transitions.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2> + <?= t('Task transitions export') ?> + </h2> +</div> + +<p class="alert alert-info"><?= t('This report contains all column moves for each task with the date, the user and the time spent for each transition.') ?></p> + +<form method="get" action="?" autocomplete="off"> + + <?= $this->formHidden('controller', $values) ?> + <?= $this->formHidden('action', $values) ?> + <?= $this->formHidden('project_id', $values) ?> + + <?= $this->formLabel(t('Start Date'), 'from') ?> + <?= $this->formText('from', $values, $errors, array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?><br/> + + <?= $this->formLabel(t('End Date'), 'to') ?> + <?= $this->formText('to', $values, $errors, array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?> + + <div class="form-help"><?= t('Others formats accepted: %s and %s', date('Y-m-d'), date('Y_m_d')) ?></div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/project/integrations.php b/app/Template/project/integrations.php index 194bd672..4f6553ad 100644 --- a/app/Template/project/integrations.php +++ b/app/Template/project/integrations.php @@ -8,7 +8,7 @@ <p class="form-help"><a href="http://kanboard.net/documentation/github-webhooks" target="_blank"><?= t('Help on Github webhooks') ?></a></p> </div> -<h3><i class="fa fa-git fa-fw"></i> <?= t('Gitlab webhooks') ?></h3> +<h3><img src="assets/img/gitlab-icon.png"/> <?= t('Gitlab webhooks') ?></h3> <div class="listing"> <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'gitlab', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> <p class="form-help"><a href="http://kanboard.net/documentation/gitlab-webhooks" target="_blank"><?= t('Help on Gitlab webhooks') ?></a></p> diff --git a/app/Template/project/sidebar.php b/app/Template/project/sidebar.php index 4afc8ba9..47458144 100644 --- a/app/Template/project/sidebar.php +++ b/app/Template/project/sidebar.php @@ -63,6 +63,9 @@ <?= $this->a(t('Subtasks'), 'export', 'subtasks', array('project_id' => $project['id'])) ?> </li> <li> + <?= $this->a(t('Task transitions'), 'export', 'transitions', array('project_id' => $project['id'])) ?> + </li> + <li> <?= $this->a(t('Daily project summary'), 'export', 'summary', array('project_id' => $project['id'])) ?> </li> </ul> diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index f41be14d..cb3b3c69 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -4,6 +4,9 @@ <li> <?= $this->a(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> + <li> + <?= $this->a(t('Transitions'), 'task', 'transitions', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> <?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?> <li> <?= $this->a(t('Time tracking'), 'task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> diff --git a/app/Template/task/transitions.php b/app/Template/task/transitions.php new file mode 100644 index 00000000..2f45eb39 --- /dev/null +++ b/app/Template/task/transitions.php @@ -0,0 +1,26 @@ +<div class="page-header"> + <h2><?= t('Transitions') ?></h2> +</div> + +<?php if (empty($transitions)): ?> + <p class="alert"><?= t('There is nothing to show.') ?></p> +<?php else: ?> + <table class="table-stripped"> + <tr> + <th><?= t('Date') ?></th> + <th><?= t('Source column') ?></th> + <th><?= t('Destination column') ?></th> + <th><?= t('Executer') ?></th> + <th><?= t('Time spent in the column') ?></th> + </tr> + <?php foreach ($transitions as $transition): ?> + <tr> + <td><?= dt('%B %e, %Y at %k:%M %p', $transition['date']) ?></td> + <td><?= $this->e($transition['src_column']) ?></td> + <td><?= $this->e($transition['dst_column']) ?></td> + <td><?= $this->a($this->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td> + <td><?= n(round($transition['time_spent'] / 3600, 2)).' '.t('hours') ?></td> + </tr> + <?php endforeach ?> + </table> +<?php endif ?>
\ No newline at end of file diff --git a/assets/img/gitlab-icon.png b/assets/img/gitlab-icon.png Binary files differnew file mode 100644 index 00000000..7e1eaa5c --- /dev/null +++ b/assets/img/gitlab-icon.png diff --git a/assets/img/hipchat-icon.png b/assets/img/hipchat-icon.png Binary files differnew file mode 100644 index 00000000..1b0a825f --- /dev/null +++ b/assets/img/hipchat-icon.png diff --git a/assets/js/app.js b/assets/js/app.js index a1fc5d56..52152a87 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -141,9 +141,9 @@ $("#popover-content").click(function(a){a.stopPropagation()});$(".close-popover" return""!=a?"visible"==document[a]:!0},SetStorageItem:function(a,c){"undefined"!==typeof Storage&&localStorage.setItem(a,c)},GetStorageItem:function(a){return"undefined"!==typeof Storage?localStorage.getItem(a):""},MarkdownPreview:function(a){a.preventDefault();var c=$(this),b=$(this).closest("ul"),d=$(".write-area"),e=$(".preview-area"),f=$("textarea");$.ajax({url:"?controller=app&action=preview",contentType:"application/json",type:"POST",processData:!1,dataType:"html",data:JSON.stringify({text:f.val()})}).done(function(a){b.find("li").removeClass("form-tab-selected"); c.parent().addClass("form-tab-selected");e.find(".markdown").html(a);e.css("height",f.css("height"));e.css("width",f.css("width"));d.hide();e.show()})},MarkdownWriter:function(a){a.preventDefault();$(this).closest("ul").find("li").removeClass("form-tab-selected");$(this).parent().addClass("form-tab-selected");$(".write-area").show();$(".preview-area").hide()},CheckSession:function(){$(".form-login").length||$.ajax({cache:!1,url:$("body").data("status-url"),statusCode:{401:function(){window.location= $("body").data("login-url")}}})},Init:function(){$("#board-selector").chosen({width:180,no_results_text:$("#board-selector").data("notfound")});$("#board-selector").change(function(){window.location=$(this).attr("data-board-url").replace(/PROJECT_ID/g,$(this).val())});window.setInterval(Kanboard.CheckSession,6E4);Mousetrap.bindGlobal("mod+enter",function(){$("form").submit()});Mousetrap.bind("b",function(a){a.preventDefault();$("#board-selector").trigger("chosen:open")});$(".column-tooltip").tooltip({content:function(){return'<div class="markdown">'+ -$(this).attr("title")+"</div>"},position:{my:"left-20 top",at:"center bottom+9",using:function(a,c){$(this).css(a);var b=c.target.left+c.target.width/2-c.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(c.vertical).addClass(0==b?"align-left":"align-right").appendTo(this)}}});$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);Kanboard.InitAfterAjax()},InitAfterAjax:function(){$(document).on("click",".popover",Kanboard.Popover);$(".form-date").datepicker({showOtherMonths:!0, -selectOtherMonths:!0,dateFormat:"yy-mm-dd",constrainInput:!1});$("#markdown-preview").click(Kanboard.MarkdownPreview);$("#markdown-write").click(Kanboard.MarkdownWriter);$(".auto-select").focus(function(){$(this).select()});$(".dropit-submenu").hide();$(".dropdown").not(".dropit").dropit({triggerParentEl:"span"});$(".task-autocomplete").length&&($(".task-autocomplete").parent().find("input[type=submit]").attr("disabled","disabled"),$(".task-autocomplete").autocomplete({source:$(".task-autocomplete").data("search-url"), -minLength:2,select:function(a,c){var b=$(".task-autocomplete").data("dst-field");$("input[name="+b+"]").val(c.item.id);$(".task-autocomplete").parent().find("input[type=submit]").removeAttr("disabled")}}))}}}(); +$(this).attr("title")+"</div>"},position:{my:"left-20 top",at:"center bottom+9",using:function(a,c){$(this).css(a);var b=c.target.left+c.target.width/2-c.element.left-20;$("<div>").addClass("tooltip-arrow").addClass(c.vertical).addClass(0==b?"align-left":"align-right").appendTo(this)}}});$.datepicker.setDefaults($.datepicker.regional[$("body").data("js-lang")]);Kanboard.InitAfterAjax()},InitAfterAjax:function(){$(document).on("click",".popover",Kanboard.Popover);$("input[autofocus]").each(function(a, +c){$(this).focus()});$(".form-date").datepicker({showOtherMonths:!0,selectOtherMonths:!0,dateFormat:"yy-mm-dd",constrainInput:!1});$("#markdown-preview").click(Kanboard.MarkdownPreview);$("#markdown-write").click(Kanboard.MarkdownWriter);$(".auto-select").focus(function(){$(this).select()});$(".dropit-submenu").hide();$(".dropdown").not(".dropit").dropit({triggerParentEl:"span"});$(".task-autocomplete").length&&($(".task-autocomplete").parent().find("input[type=submit]").attr("disabled","disabled"), +$(".task-autocomplete").autocomplete({source:$(".task-autocomplete").data("search-url"),minLength:2,select:function(a,c){var b=$(".task-autocomplete").data("dst-field");$("input[name="+b+"]").val(c.item.id);$(".task-autocomplete").parent().find("input[type=submit]").removeAttr("disabled")}}))}}}(); Kanboard.Board=function(){function a(a){a.preventDefault();a.stopPropagation();Kanboard.Popover(a,Kanboard.InitAfterAjax)}function c(){Mousetrap.bind("n",function(){Kanboard.OpenPopover($("#board").data("task-creation-url"),Kanboard.InitAfterAjax)});Mousetrap.bind("s",function(){"expanded"===(Kanboard.GetStorageItem(d())||"expanded")?(e(),Kanboard.SetStorageItem(d(),"collapsed")):(f(),Kanboard.SetStorageItem(d(),"expanded"))});Mousetrap.bind("c",function(){p()})}function b(){$(".filter-expand-link").click(function(a){a.preventDefault(); f();Kanboard.SetStorageItem(d(),"expanded")});$(".filter-collapse-link").click(function(a){a.preventDefault();e();Kanboard.SetStorageItem(d(),"collapsed")});g()}function d(){return"board_stacking_"+$("#board").data("project-id")}function e(){$(".filter-collapse").hide();$(".task-board-collapsed").show();$(".filter-expand").show();$(".task-board-expanded").hide()}function f(){$(".filter-collapse").show();$(".task-board-collapsed").hide();$(".filter-expand").hide();$(".task-board-expanded").show()} function g(){"expanded"===(Kanboard.GetStorageItem(d())||"expanded")?f():e()}function h(){$(".column").sortable({delay:300,distance:5,connectWith:".column",placeholder:"draggable-placeholder",stop:function(a,b){q(b.item.attr("data-task-id"),b.item.parent().attr("data-column-id"),b.item.index()+1,b.item.parent().attr("data-swimlane-id"))}});$("#board").on("click",".task-board-popover",a);$("#board").on("click",".task-board",function(){window.location=$(this).data("task-url")});$(".task-board-tooltip").tooltip({track:!1, diff --git a/assets/js/src/base.js b/assets/js/src/base.js index 05846ab8..0cd0e7df 100644 --- a/assets/js/src/base.js +++ b/assets/js/src/base.js @@ -222,6 +222,11 @@ var Kanboard = (function() { // Popover $(document).on("click", ".popover", Kanboard.Popover); + // Autofocus fields (html5 autofocus works only with page onload) + $("input[autofocus]").each(function(index, element) { + $(this).focus(); + }) + // Datepicker $(".form-date").datepicker({ showOtherMonths: true, @@ -243,6 +248,7 @@ var Kanboard = (function() { $(".dropit-submenu").hide(); $('.dropdown').not(".dropit").dropit({ triggerParentEl : "span" }); + // Task auto-completion if ($(".task-autocomplete").length) { $(".task-autocomplete").parent().find("input[type=submit]").attr('disabled','disabled'); diff --git a/composer.json b/composer.json index 0ab8f511..8e78ab39 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "require" : { + "php": ">=5.3", "ext-mbstring" : "*", "fguillot/simple-validator" : "dev-master", "swiftmailer/swiftmailer" : "@stable", @@ -24,4 +25,4 @@ "require-dev" : { "symfony/stopwatch" : "~2.6" } -}
\ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 3d68e4eb..3331ecd1 100644 --- a/composer.lock +++ b/composer.lock @@ -88,12 +88,12 @@ "source": { "type": "git", "url": "https://github.com/fguillot/picoDb.git", - "reference": "d7ef5561d6d76c50717492822813125f9699700a" + "reference": "cd6a571d2de5c0b30d538d7cd6603dc16b25b844" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/picoDb/zipball/d7ef5561d6d76c50717492822813125f9699700a", - "reference": "d7ef5561d6d76c50717492822813125f9699700a", + "url": "https://api.github.com/repos/fguillot/picoDb/zipball/cd6a571d2de5c0b30d538d7cd6603dc16b25b844", + "reference": "cd6a571d2de5c0b30d538d7cd6603dc16b25b844", "shasum": "" }, "require": { @@ -117,7 +117,7 @@ ], "description": "Minimalist database query builder", "homepage": "https://github.com/fguillot/picoDb", - "time": "2015-03-15 21:03:40" + "time": "2015-03-27 02:21:18" }, { "name": "fguillot/simple-validator", @@ -393,17 +393,17 @@ }, { "name": "symfony/console", - "version": "v2.6.4", + "version": "v2.6.5", "target-dir": "Symfony/Component/Console", "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "e44154bfe3e41e8267d7a3794cd9da9a51cfac34" + "reference": "53f86497ccd01677e22435cfb7262599450a90d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/e44154bfe3e41e8267d7a3794cd9da9a51cfac34", - "reference": "e44154bfe3e41e8267d7a3794cd9da9a51cfac34", + "url": "https://api.github.com/repos/symfony/Console/zipball/53f86497ccd01677e22435cfb7262599450a90d1", + "reference": "53f86497ccd01677e22435cfb7262599450a90d1", "shasum": "" }, "require": { @@ -412,6 +412,7 @@ "require-dev": { "psr/log": "~1.0", "symfony/event-dispatcher": "~2.1", + "symfony/phpunit-bridge": "~2.7", "symfony/process": "~2.1" }, "suggest": { @@ -446,21 +447,21 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2015-01-25 04:39:26" + "time": "2015-03-13 17:37:22" }, { "name": "symfony/event-dispatcher", - "version": "v2.6.4", + "version": "v2.6.5", "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "f75989f3ab2743a82fe0b03ded2598a2b1546813" + "reference": "70f7c8478739ad21e3deef0d977b38c77f1fb284" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/f75989f3ab2743a82fe0b03ded2598a2b1546813", - "reference": "f75989f3ab2743a82fe0b03ded2598a2b1546813", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/70f7c8478739ad21e3deef0d977b38c77f1fb284", + "reference": "70f7c8478739ad21e3deef0d977b38c77f1fb284", "shasum": "" }, "require": { @@ -471,6 +472,7 @@ "symfony/config": "~2.0,>=2.0.5", "symfony/dependency-injection": "~2.6", "symfony/expression-language": "~2.6", + "symfony/phpunit-bridge": "~2.7", "symfony/stopwatch": "~2.3" }, "suggest": { @@ -504,28 +506,31 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2015-02-01 16:10:57" + "time": "2015-03-13 17:37:22" } ], "packages-dev": [ { "name": "symfony/stopwatch", - "version": "v2.6.4", + "version": "v2.6.5", "target-dir": "Symfony/Component/Stopwatch", "source": { "type": "git", "url": "https://github.com/symfony/Stopwatch.git", - "reference": "e8da5286132ba75ce4b4275fbf0f4cd369bfd71c" + "reference": "ba4e774f71e2ce3e3f65cabac4031b9029972af5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/e8da5286132ba75ce4b4275fbf0f4cd369bfd71c", - "reference": "e8da5286132ba75ce4b4275fbf0f4cd369bfd71c", + "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/ba4e774f71e2ce3e3f65cabac4031b9029972af5", + "reference": "ba4e774f71e2ce3e3f65cabac4031b9029972af5", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "symfony/phpunit-bridge": "~2.7" + }, "type": "library", "extra": { "branch-alias": { @@ -553,7 +558,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "http://symfony.com", - "time": "2015-01-03 08:01:59" + "time": "2015-02-24 11:52:21" } ], "aliases": [], diff --git a/docs/cli.markdown b/docs/cli.markdown index 0e2a0203..b742e0ac 100644 --- a/docs/cli.markdown +++ b/docs/cli.markdown @@ -17,16 +17,16 @@ $ ./kanboard Kanboard version master Usage: - [options] command [arguments] + command [options] [arguments] Options: - --help (-h) Display this help message. - --quiet (-q) Do not output any message. - --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug. - --version (-V) Display this application version. - --ansi Force ANSI output. - --no-ansi Disable ANSI output. - --no-interaction (-n) Do not ask any interactive question. + --help (-h) Display this help message + --quiet (-q) Do not output any message + --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + --version (-V) Display this application version + --ansi Force ANSI output + --no-ansi Disable ANSI output + --no-interaction (-n) Do not ask any interactive question Available commands: help Displays help for a command @@ -35,6 +35,7 @@ export export:daily-project-summary Daily project summary CSV export (number of tasks per column and per day) export:subtasks Subtasks CSV export export:tasks Tasks CSV export + export:transitions Task transitions CSV export notification notification:overdue-tasks Send notifications for overdue tasks projects @@ -44,6 +45,22 @@ projects Available commands ------------------ +### Tasks CSV export + +Usage: + +```bash +./kanboard export:tasks <project_id> <start_date> <end_date> +``` + +Example: + +```bash +./kanboard export:tasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv +``` + +CSV data are sent to `stdout`. + ### Subtasks CSV export Usage: @@ -58,21 +75,33 @@ Example: ./kanboard export:subtasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv ``` -### Tasks CSV export +### Task transitions CSV export Usage: ```bash -./kanboard export:tasks <project_id> <start_date> <end_date> +./kanboard export:transitions <project_id> <start_date> <end_date> ``` Example: ```bash -./kanboard export:tasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv +./kanboard export:transitions 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv ``` -CSV data are sent to stdout. +### Export daily summaries data in CSV + +The exported data will be printed on the standard output: + +```bash +./kanboard export:daily-project-summary <project_id> <start_date> <end_date> +``` + +Example: + +```bash +./kanboard export:daily-project-summary 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv +``` ### Send notifications for overdue tasks @@ -111,17 +140,3 @@ Run calculation for Project #0 Run calculation for Project #1 Run calculation for Project #10 ``` - -### Export daily summaries data in CSV - -The exported data will be printed on the standard output: - -```bash -./kanboard export:daily-project-summary <project_id> <start_date> <end_date> -``` - -Example: - -```bash -./kanboard export:daily-project-summary 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv -``` diff --git a/docs/hipchat.markdown b/docs/hipchat.markdown new file mode 100644 index 00000000..45d93eb2 --- /dev/null +++ b/docs/hipchat.markdown @@ -0,0 +1,31 @@ +Hipchat integration +=================== + +Send notifications to a room +----------------------------- + +Example of notifications: + +![Hipchat notification](http://kanboard.net/screenshots/documentation/hipchat-notification.png) + +This feature use the room notification token system of Hipchat. + +### Hipchat configuration + +![Hipchat room token](http://kanboard.net/screenshots/documentation/hipchat-room-token.png) + +1. Go to to **My account** +2. Click on the tab **Rooms** and select the room you want to send the notifications +3. On the left, choose **Tokens** +4. Enter a label, by example "Kanboard" and save + +### Kanboard configuration + +![Hipchat settings](http://kanboard.net/screenshots/documentation/hipchat-settings.png) + +1. Go to **Settings > Integrations > Hipchat** +2. Replace the API url if you use the self-hosted version of Hipchat +3. Set the room name or the room API ID +4. Copy and paste the token generated previously + +Now, all Kanboard events will be sent to the Hipchat room. diff --git a/docs/slack.markdown b/docs/slack.markdown new file mode 100644 index 00000000..89e3006b --- /dev/null +++ b/docs/slack.markdown @@ -0,0 +1,21 @@ +Slack integration +================= + +Send notifications to a channel +------------------------------- + +Example of notifications: + +![Slack notification](http://kanboard.net/screenshots/documentation/slack-notification.png) + +This feature use the [Incoming webhook](https://api.slack.com/incoming-webhooks) system of Slack. + +### Slack configuration + +![Slack webhook creation](http://kanboard.net/screenshots/documentation/slack-add-incoming-webhook.png) + +1. Click on the Team dropdown and choose **Configure Integrations** +2. On the list of services, scroll-down and choose **DIY Integrations & Customizations > Incoming WebHooks** +3. Copy the webhook url to the Kanboard settings page: **Settings > Integrations > Slack** + +Now, all Kanboard events will be sent to the Slack channel. @@ -14,4 +14,5 @@ $application->add(new Console\SubtaskExport($container)); $application->add(new Console\TaskExport($container)); $application->add(new Console\ProjectDailySummaryCalculation($container)); $application->add(new Console\ProjectDailySummaryExport($container)); +$application->add(new Console\TransitionExport($container)); $application->run(); |