diff options
Diffstat (limited to 'app')
58 files changed, 1257 insertions, 481 deletions
diff --git a/app/Api/Project.php b/app/Api/Project.php index 2451cd9c..faf2a3da 100644 --- a/app/Api/Project.php +++ b/app/Api/Project.php @@ -12,17 +12,17 @@ class Project extends Base { public function getProjectById($project_id) { - return $this->project->getById($project_id); + return $this->formatProject($this->project->getById($project_id)); } public function getProjectByName($name) { - return $this->project->getByName($name); + return $this->formatProject($this->project->getByName($name)); } public function getAllProjects() { - return $this->project->getAll(); + return $this->formatProjects($this->project->getAll()); } public function removeProject($project_id) @@ -82,4 +82,28 @@ class Project extends Base list($valid,) = $this->project->validateModification($values); return $valid && $this->project->update($values); } + + private function formatProject($project) + { + if (! empty($project)) { + $project['url'] = array( + 'board' => $this->helper->url->base().$this->helper->url->to('board', 'show', array('project_id' => $project['id'])), + 'calendar' => $this->helper->url->base().$this->helper->url->to('calendar', 'show', array('project_id' => $project['id'])), + 'list' => $this->helper->url->base().$this->helper->url->to('listing', 'show', array('project_id' => $project['id'])), + ); + } + + return $project; + } + + private function formatProjects($projects) + { + if (! empty($projects)) { + foreach ($projects as &$project) { + $project = $this->formatProject($project); + } + } + + return $projects; + } } diff --git a/app/Api/Task.php b/app/Api/Task.php index e06c012b..ade49a6d 100644 --- a/app/Api/Task.php +++ b/app/Api/Task.php @@ -14,17 +14,17 @@ class Task extends Base { public function getTask($task_id) { - return $this->taskFinder->getById($task_id); + return $this->formatTask($this->taskFinder->getById($task_id)); } public function getTaskByReference($project_id, $reference) { - return $this->taskFinder->getByReference($project_id, $reference); + return $this->formatTask($this->taskFinder->getByReference($project_id, $reference)); } public function getAllTasks($project_id, $status_id = TaskModel::STATUS_OPEN) { - return $this->taskFinder->getAll($project_id, $status_id); + return $this->formatTasks($this->taskFinder->getAll($project_id, $status_id)); } public function getOverdueTasks() @@ -115,4 +115,24 @@ class Task extends Base list($valid) = $this->taskValidator->validateApiModification($values); return $valid && $this->taskModification->update($values); } + + private function formatTask($task) + { + if (! empty($task)) { + $task['url'] = $this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])); + } + + return $task; + } + + private function formatTasks($tasks) + { + if (! empty($tasks)) { + foreach ($tasks as &$task) { + $task = $this->formatTask($task); + } + } + + return $tasks; + } } diff --git a/app/Auth/RememberMe.php b/app/Auth/RememberMe.php index e8b20f37..eebf4f4b 100644 --- a/app/Auth/RememberMe.php +++ b/app/Auth/RememberMe.php @@ -119,31 +119,6 @@ class RememberMe extends Base } /** - * Update the database and the cookie with a new sequence - * - * @access public - */ - public function refresh() - { - $credentials = $this->readCookie(); - - if ($credentials !== false) { - - $record = $this->find($credentials['token'], $credentials['sequence']); - - if ($record) { - - // Update the sequence - $this->writeCookie( - $record['token'], - $this->update($record['token']), - $record['expiration'] - ); - } - } - } - - /** * Remove a session record * * @access public @@ -197,9 +172,10 @@ class RememberMe extends Base $this->cleanup($user_id); - $this->db - ->table(self::TABLE) - ->insert(array( + $this + ->db + ->table(self::TABLE) + ->insert(array( 'user_id' => $user_id, 'ip' => $ip, 'user_agent' => $user_agent, @@ -207,7 +183,7 @@ class RememberMe extends Base 'sequence' => $sequence, 'expiration' => $expiration, 'date_creation' => time(), - )); + )); return array( 'token' => $token, diff --git a/app/Console/Base.php b/app/Console/Base.php index 07243080..86d78ab3 100644 --- a/app/Console/Base.php +++ b/app/Console/Base.php @@ -11,16 +11,17 @@ use Symfony\Component\Console\Command\Command; * @package console * @author Frederic Guillot * - * @property \Model\Notification $notification - * @property \Model\Project $project - * @property \Model\ProjectPermission $projectPermission - * @property \Model\ProjectAnalytic $projectAnalytic - * @property \Model\ProjectDailySummary $projectDailySummary - * @property \Model\SubtaskExport $subtaskExport - * @property \Model\Task $task - * @property \Model\TaskExport $taskExport - * @property \Model\TaskFinder $taskFinder - * @property \Model\Transition $transition + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\ProjectPermission $projectPermission + * @property \Model\ProjectAnalytic $projectAnalytic + * @property \Model\ProjectDailyColumnStats $projectDailyColumnStats + * @property \Model\ProjectDailyStats $projectDailyColumnStats + * @property \Model\SubtaskExport $subtaskExport + * @property \Model\Task $task + * @property \Model\TaskExport $taskExport + * @property \Model\TaskFinder $taskFinder + * @property \Model\Transition $transition */ abstract class Base extends Command { diff --git a/app/Console/ProjectDailySummaryExport.php b/app/Console/ProjectDailyColumnStatsExport.php index 07841d52..b9830662 100644 --- a/app/Console/ProjectDailySummaryExport.php +++ b/app/Console/ProjectDailyColumnStatsExport.php @@ -7,13 +7,13 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ProjectDailySummaryExport extends Base +class ProjectDailyColumnStatsExport extends Base { protected function configure() { $this - ->setName('export:daily-project-summary') - ->setDescription('Daily project summary CSV export (number of tasks per column and per day)') + ->setName('export:daily-project-column-stats') + ->setDescription('Daily project column stats CSV export (number of tasks per column and per day)') ->addArgument('project_id', InputArgument::REQUIRED, 'Project id') ->addArgument('start_date', InputArgument::REQUIRED, 'Start date (YYYY-MM-DD)') ->addArgument('end_date', InputArgument::REQUIRED, 'End date (YYYY-MM-DD)'); @@ -21,7 +21,7 @@ class ProjectDailySummaryExport extends Base protected function execute(InputInterface $input, OutputInterface $output) { - $data = $this->projectDailySummary->getAggregatedMetrics( + $data = $this->projectDailyColumnStats->getAggregatedMetrics( $input->getArgument('project_id'), $input->getArgument('start_date'), $input->getArgument('end_date') diff --git a/app/Console/ProjectDailySummaryCalculation.php b/app/Console/ProjectDailyStatsCalculation.php index b2ada1b6..4b77c556 100644 --- a/app/Console/ProjectDailySummaryCalculation.php +++ b/app/Console/ProjectDailyStatsCalculation.php @@ -6,13 +6,13 @@ use Model\Project; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ProjectDailySummaryCalculation extends Base +class ProjectDailyStatsCalculation extends Base { protected function configure() { $this - ->setName('projects:daily-summary') - ->setDescription('Calculate daily summary data for all projects'); + ->setName('projects:daily-stats') + ->setDescription('Calculate daily statistics for all projects'); } protected function execute(InputInterface $input, OutputInterface $output) @@ -21,7 +21,8 @@ class ProjectDailySummaryCalculation extends Base foreach ($projects as $project) { $output->writeln('Run calculation for '.$project['name']); - $this->projectDailySummary->updateTotals($project['id'], date('Y-m-d')); + $this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d')); + $this->projectDailyStats->updateTotals($project['id'], date('Y-m-d')); } } } diff --git a/app/Controller/Activity.php b/app/Controller/Activity.php index 2276b3b8..234e4be4 100644 --- a/app/Controller/Activity.php +++ b/app/Controller/Activity.php @@ -26,4 +26,20 @@ class Activity extends Base 'title' => t('%s\'s activity', $project['name']) ))); } + + /** + * Display task activities + * + * @access public + */ + public function task() + { + $task = $this->getTask(); + + $this->response->html($this->taskLayout('activity/task', array( + 'title' => $task['title'], + 'task' => $task, + 'events' => $this->projectActivity->getTask($task['id']), + ))); + } } diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php index 2413ba92..ca2146ed 100644 --- a/app/Controller/Analytic.php +++ b/app/Controller/Analytic.php @@ -3,7 +3,7 @@ namespace Controller; /** - * Project Anaytic controller + * Project Analytic controller * * @package controller * @author Frederic Guillot @@ -27,6 +27,56 @@ class Analytic extends Base } /** + * Show average Lead and Cycle time + * + * @access public + */ + public function leadAndCycleTime() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $this->projectDailyStats->updateTotals($project['id'], date('Y-m-d')); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + $this->response->html($this->layout('analytic/lead_cycle_time', array( + 'values' => array( + 'from' => $from, + 'to' => $to, + ), + 'project' => $project, + 'average' => $this->projectAnalytic->getAverageLeadAndCycleTime($project['id']), + 'metrics' => $this->projectDailyStats->getRawMetrics($project['id'], $from, $to), + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Lead and Cycle time for "%s"', $project['name']), + ))); + } + + /** + * Show average time spent by column + * + * @access public + */ + public function averageTimeByColumn() + { + $project = $this->getProject(); + + $this->response->html($this->layout('analytic/avg_time_columns', array( + 'project' => $project, + 'metrics' => $this->projectAnalytic->getAverageTimeSpentByColumn($project['id']), + 'title' => t('Average time spent into each column for "%s"', $project['name']), + ))); + } + + /** * Show tasks distribution graph * * @access public @@ -88,6 +138,8 @@ class Analytic extends Base $project = $this->getProject(); $values = $this->request->getValues(); + $this->projectDailyColumnStats->updateTotals($project['id'], date('Y-m-d')); + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); $to = $this->request->getStringParam('to', date('Y-m-d')); @@ -96,7 +148,7 @@ class Analytic extends Base $to = $values['to']; } - $display_graph = $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2; + $display_graph = $this->projectDailyColumnStats->countDays($project['id'], $from, $to) >= 2; $this->response->html($this->layout($template, array( 'values' => array( @@ -104,7 +156,7 @@ class Analytic extends Base 'to' => $to, ), 'display_graph' => $display_graph, - 'metrics' => $display_graph ? $this->projectDailySummary->getAggregatedMetrics($project['id'], $from, $to, $column) : array(), + 'metrics' => $display_graph ? $this->projectDailyColumnStats->getAggregatedMetrics($project['id'], $from, $to, $column) : array(), 'project' => $project, 'date_format' => $this->config->get('application_date_format'), 'date_formats' => $this->dateParser->getAvailableFormats(), diff --git a/app/Controller/Board.php b/app/Controller/Board.php index caaa38ef..ac80a192 100644 --- a/app/Controller/Board.php +++ b/app/Controller/Board.php @@ -310,4 +310,28 @@ class Board extends Base 'recurrence_basedate_list' => $this->task->getRecurrenceBasedateList(), ))); } + + /** + * Enable collapsed mode + * + * @access public + */ + public function collapse() + { + $project_id = $this->request->getIntegerParam('project_id'); + $this->userSession->setBoardDisplayMode($project_id, true); + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project_id))); + } + + /** + * Enable expanded mode + * + * @access public + */ + public function expand() + { + $project_id = $this->request->getIntegerParam('project_id'); + $this->userSession->setBoardDisplayMode($project_id, false); + $this->response->redirect($this->helper->url->to('board', 'show', array('project_id' => $project_id))); + } } diff --git a/app/Controller/Export.php b/app/Controller/Export.php index 117fb5ee..8b558c0a 100644 --- a/app/Controller/Export.php +++ b/app/Controller/Export.php @@ -70,7 +70,7 @@ class Export extends Base */ public function summary() { - $this->common('projectDailySummary', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); + $this->common('ProjectDailyColumnStats', 'getAggregatedMetrics', t('Summary'), 'summary', t('Daily project summary export')); } /** diff --git a/app/Controller/Ical.php b/app/Controller/Ical.php index 8a7ed8b5..0129915e 100644 --- a/app/Controller/Ical.php +++ b/app/Controller/Ical.php @@ -78,8 +78,8 @@ class Ical extends Base */ private function renderCalendar(TaskFilter $filter, iCalendar $calendar) { - $start = $this->request->getStringParam('start', strtotime('-1 month')); - $end = $this->request->getStringParam('end', strtotime('+2 months')); + $start = $this->request->getStringParam('start', strtotime('-2 month')); + $end = $this->request->getStringParam('end', strtotime('+6 months')); // Tasks if ($this->config->get('calendar_project_tasks', 'date_started') === 'date_creation') { diff --git a/app/Controller/Task.php b/app/Controller/Task.php index dc83f7b1..0d85f411 100644 --- a/app/Controller/Task.php +++ b/app/Controller/Task.php @@ -64,7 +64,7 @@ class Task extends Base 'time_spent' => $task['time_spent'] ?: '', ); - $this->dateParser->format($values, array('date_started')); + $this->dateParser->format($values, array('date_started'), 'Y-m-d H:i'); $this->response->html($this->taskLayout('task/show', array( 'project' => $this->project->getById($task['project_id']), @@ -88,19 +88,20 @@ class Task extends Base } /** - * Display task activities + * Display task analytics * * @access public */ - public function activites() + public function analytics() { $task = $this->getTask(); - $this->response->html($this->taskLayout('task/activity', array( + $this->response->html($this->taskLayout('task/analytics', array( 'title' => $task['title'], 'task' => $task, - 'ajax' => $this->request->isAjax(), - 'events' => $this->projectActivity->getTask($task['id']), + 'lead_time' => $this->taskAnalytic->getLeadTime($task), + 'cycle_time' => $this->taskAnalytic->getCycleTime($task), + 'time_spent_columns' => $this->taskAnalytic->getTimeSpentByColumn($task), ))); } @@ -151,7 +152,6 @@ class Task extends Base { $project = $this->getProject(); $values = $this->request->getValues(); - $values['creator_id'] = $this->userSession->getId(); list($valid, $errors) = $this->taskValidator->validateCreation($values); @@ -618,4 +618,16 @@ class Task extends Base 'transitions' => $this->transition->getAllByTask($task['id']), ))); } + + /** + * Set automatically the start date + * + * @access public + */ + public function start() + { + $task = $this->getTask(); + $this->taskModification->update(array('id' => $task['id'], 'date_started' => time())); + $this->response->redirect($this->helper->url->to('task', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id']))); + } } diff --git a/app/Core/Base.php b/app/Core/Base.php index d4d7faa3..14466d5c 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -48,7 +48,8 @@ use Pimple\Container; * @property \Model\ProjectActivity $projectActivity * @property \Model\ProjectAnalytic $projectAnalytic * @property \Model\ProjectDuplication $projectDuplication - * @property \Model\ProjectDailySummary $projectDailySummary + * @property \Model\ProjectDailyColumnStats $projectDailyColumnStats + * @property \Model\ProjectDailyStats $projectDailyStats * @property \Model\ProjectIntegration $projectIntegration * @property \Model\ProjectPermission $projectPermission * @property \Model\Subtask $subtask diff --git a/app/Core/Helper.php b/app/Core/Helper.php index 53084a7e..64eaed23 100644 --- a/app/Core/Helper.php +++ b/app/Core/Helper.php @@ -2,6 +2,8 @@ namespace Core; +use Pimple\Container; + /** * Helper base class * @@ -10,7 +12,7 @@ namespace Core; * * @property \Helper\App $app * @property \Helper\Asset $asset - * @property \Helper\Datetime $datetime + * @property \Helper\Dt $dt * @property \Helper\File $file * @property \Helper\Form $form * @property \Helper\Subtask $subtask @@ -19,16 +21,34 @@ namespace Core; * @property \Helper\Url $url * @property \Helper\User $user */ -class Helper extends Base +class Helper { /** * Helper instances * - * @static * @access private * @var array */ - private static $helpers = array(); + private $helpers = array(); + + /** + * Container instance + * + * @access protected + * @var \Pimple\Container + */ + protected $container; + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + } /** * Load automatically helpers @@ -39,12 +59,12 @@ class Helper extends Base */ public function __get($name) { - if (! isset(self::$helpers[$name])) { + if (! isset($this->helpers[$name])) { $class = '\Helper\\'.ucfirst($name); - self::$helpers[$name] = new $class($this->container); + $this->helpers[$name] = new $class($this->container); } - return self::$helpers[$name]; + return $this->helpers[$name]; } /** diff --git a/app/Helper/Board.php b/app/Helper/Board.php new file mode 100644 index 00000000..452a3b70 --- /dev/null +++ b/app/Helper/Board.php @@ -0,0 +1,24 @@ +<?php + +namespace Helper; + +/** + * Board Helper + * + * @package helper + * @author Frederic Guillot + */ +class Board extends \Core\Base +{ + /** + * Return true if tasks are collapsed + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isCollapsed($project_id) + { + return $this->userSession->isBoardCollapsed($project_id); + } +} diff --git a/app/Helper/Datetime.php b/app/Helper/Dt.php index 74ea9bdd..b338fdc8 100644 --- a/app/Helper/Datetime.php +++ b/app/Helper/Dt.php @@ -2,15 +2,35 @@ namespace Helper; +use DateTime; + /** * DateTime helpers * * @package helper * @author Frederic Guillot */ -class Datetime extends \Core\Base +class Dt extends \Core\Base { /** + * Get duration in seconds into human format + * + * @access public + * @param integer $seconds + * @return string + */ + public function duration($seconds) + { + if ($seconds == 0) { + return 0; + } + + $dtF = new DateTime("@0"); + $dtT = new DateTime("@$seconds"); + return $dtF->diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds'); + } + + /** * Get the age of an item in quasi human readable format. * It's in this format: <1h , NNh, NNd * diff --git a/app/Helper/Url.php b/app/Helper/Url.php index e133f195..8de63f8d 100644 --- a/app/Helper/Url.php +++ b/app/Helper/Url.php @@ -99,6 +99,10 @@ class Url extends \Core\Base */ public function server() { + if (empty($_SERVER['SERVER_NAME'])) { + return 'http://localhost/'; + } + $self = str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])); $url = Request::isHTTPS() ? 'https://' : 'http://'; diff --git a/app/Integration/SlackWebhook.php b/app/Integration/SlackWebhook.php index 975ea21f..498cea09 100644 --- a/app/Integration/SlackWebhook.php +++ b/app/Integration/SlackWebhook.php @@ -40,6 +40,25 @@ class SlackWebhook extends \Core\Base } /** + * Get optional Slack channel + * + * @access public + * @param integer $project_id + * @return string + */ + public function getChannel($project_id) + { + $channel = $this->config->get('integration_slack_webhook_channel'); + + if (! empty($channel)) { + return $channel; + } + + $options = $this->projectIntegration->getParameters($project_id); + return $options['slack_webhook_channel']; + } + + /** * Send message to the incoming Slack webhook * * @access public @@ -69,6 +88,11 @@ class SlackWebhook extends \Core\Base $payload['text'] .= '|'.t('view the task on Kanboard').'>'; } + $channel = $this->getChannel($project_id); + if (! empty($channel)) { + $payload['channel'] = $channel; + } + $this->httpClient->postJson($this->getWebhookUrl($project_id), $payload); } } diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 33c14c02..4690edea 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -20,15 +20,15 @@ return array( 'Red' => 'Röd', 'Orange' => 'Orange', 'Grey' => 'Grå', - // 'Brown' => '', - // 'Deep Orange' => '', - // 'Dark Grey' => '', - // 'Pink' => '', - // 'Teal' => '', - // 'Cyan' => '', - // 'Lime' => '', - // 'Light Green' => '', - // 'Amber' => '', + 'Brown' => 'Brun', + 'Deep Orange' => 'Mörkorange', + 'Dark Grey' => 'Mörkgrå', + 'Pink' => 'Rosa', + 'Teal' => 'Grönblå', + 'Cyan' => 'Cyan', + 'Lime' => 'Lime', + 'Light Green' => 'Ljusgrön', + 'Amber' => 'Bärnsten', 'Save' => 'Spara', 'Login' => 'Login', 'Official website:' => 'Officiell webbsida:', @@ -743,7 +743,7 @@ return array( 'Move the task to another column when assigned to a user' => 'Flytta uppgiften till en annan kolumn när den tilldelats en användare', 'Move the task to another column when assignee is cleared' => 'Flytta uppgiften till en annan kolumn när tilldelningen tas bort.', 'Source column' => 'Källkolumn', - // 'Show subtask estimates (forecast of future work)' => '', + 'Show subtask estimates (forecast of future work)' => 'Visa uppskattningar för deluppgifter (prognos för framtida arbete)', 'Transitions' => 'Övergångar', 'Executer' => 'Verkställare', 'Time spent in the column' => 'Tid i kolumnen.', @@ -788,187 +788,187 @@ return array( 'Burndown chart for "%s"' => 'Burndown diagram för "%s"', 'Burndown chart' => 'Burndown diagram', 'This chart show the task complexity over the time (Work Remaining).' => 'Diagrammet visar uppgiftens svårighet över tid (återstående arbete).', - // 'Screenshot taken %s' => '', - // 'Add a screenshot' => '', - // 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '', - // 'Screenshot uploaded successfully.' => '', + 'Screenshot taken %s' => 'Skärmdump tagen %s', + 'Add a screenshot' => 'Lägg till en skärmdump', + 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Ta en skärmdump och tryck CTRL+V för att klistra in här.', + 'Screenshot uploaded successfully.' => 'Skärmdumpen laddades upp.', 'SEK - Swedish Krona' => 'SEK - Svensk Krona', - // 'The project identifier is an optional alphanumeric code used to identify your project.' => '', - // 'Identifier' => '', - // 'Postmark (incoming emails)' => '', - // 'Help on Postmark integration' => '', - // 'Mailgun (incoming emails)' => '', - // 'Help on Mailgun integration' => '', + 'The project identifier is an optional alphanumeric code used to identify your project.' => 'Projektidentifieraren är en valbar alfanumerisk kod som används för att identifiera ditt projekt.', + 'Identifier' => 'Identifierare', + 'Postmark (incoming emails)' => 'Postmark (inkommande e-post)', + 'Help on Postmark integration' => 'Hjälp för Postmark integration', + 'Mailgun (incoming emails)' => 'Mailgrun (inkommande e-post)', + 'Help on Mailgun integration' => 'Hjälp för Mailgrun integration', // 'Sendgrid (incoming emails)' => '', - // 'Help on Sendgrid integration' => '', - // 'Disable two factor authentication' => '', - // 'Do you really want to disable the two factor authentication for this user: "%s"?' => '', - // 'Edit link' => '', - // 'Start to type task title...' => '', - // 'A task cannot be linked to itself' => '', - // 'The exact same link already exists' => '', - // 'Recurrent task is scheduled to be generated' => '', - // 'Recurring information' => '', - // 'Score' => '', - // 'The identifier must be unique' => '', - // 'This linked task id doesn\'t exists' => '', - // 'This value must be alphanumeric' => '', - // 'Edit recurrence' => '', - // 'Generate recurrent task' => '', - // 'Trigger to generate recurrent task' => '', - // 'Factor to calculate new due date' => '', - // 'Timeframe to calculate new due date' => '', - // 'Base date to calculate new due date' => '', - // 'Action date' => '', - // 'Base date to calculate new due date: ' => '', - // 'This task has created this child task: ' => '', - // 'Day(s)' => '', - // 'Existing due date' => '', - // 'Factor to calculate new due date: ' => '', - // 'Month(s)' => '', - // 'Recurrence' => '', - // 'This task has been created by: ' => '', - // 'Recurrent task has been generated:' => '', - // 'Timeframe to calculate new due date: ' => '', - // 'Trigger to generate recurrent task: ' => '', - // 'When task is closed' => '', - // 'When task is moved from first column' => '', - // 'When task is moved to last column' => '', - // 'Year(s)' => '', - // 'Jabber (XMPP)' => '', - // 'Send notifications to Jabber' => '', - // 'XMPP server address' => '', - // 'Jabber domain' => '', - // 'Jabber nickname' => '', - // 'Multi-user chat room' => '', - // 'Help on Jabber integration' => '', - // 'The server address must use this format: "tcp://hostname:5222"' => '', - // 'Calendar settings' => '', - // 'Project calendar view' => '', - // 'Project settings' => '', - // 'Show subtasks based on the time tracking' => '', - // 'Show tasks based on the creation date' => '', - // 'Show tasks based on the start date' => '', - // 'Subtasks time tracking' => '', - // 'User calendar view' => '', - // 'Automatically update the start date' => '', - // 'iCal feed' => '', - // 'Preferences' => '', - // 'Security' => '', - // 'Two factor authentication disabled' => '', - // 'Two factor authentication enabled' => '', - // 'Unable to update this user.' => '', - // 'There is no user management for private projects.' => '', - // 'User that will receive the email' => '', - // 'Email subject' => '', - // 'Date' => '', - // 'By @%s on Bitbucket' => '', - // 'Bitbucket Issue' => '', - // 'Commit made by @%s on Bitbucket' => '', - // 'Commit made by @%s on Github' => '', - // 'By @%s on Github' => '', - // 'Commit made by @%s on Gitlab' => '', - // 'Add a comment log when moving the task between columns' => '', - // 'Move the task to another column when the category is changed' => '', - // 'Send a task by email to someone' => '', - // 'Reopen a task' => '', - // 'Bitbucket issue opened' => '', - // 'Bitbucket issue closed' => '', - // 'Bitbucket issue reopened' => '', - // 'Bitbucket issue assignee change' => '', - // 'Bitbucket issue comment created' => '', - // 'Column change' => '', - // 'Position change' => '', - // 'Swimlane change' => '', - // 'Assignee change' => '', - // '[%s] Overdue tasks' => '', - // 'Notification' => '', - // '%s moved the task #%d to the first swimlane' => '', - // '%s moved the task #%d to the swimlane "%s"' => '', - // 'Swimlane' => '', - // 'Budget overview' => '', - // 'Type' => '', - // 'There is not enough data to show something.' => '', - // 'Gravatar' => '', - // 'Hipchat' => '', - // 'Slack' => '', - // '%s moved the task %s to the first swimlane' => '', - // '%s moved the task %s to the swimlane "%s"' => '', - // 'This report contains all subtasks information for the given date range.' => '', - // 'This report contains all tasks information for the given date range.' => '', - // 'Project activities for %s' => '', - // 'view the board on Kanboard' => '', - // 'The task have been moved to the first swimlane' => '', - // 'The task have been moved to another swimlane:' => '', - // 'Overdue tasks for the project "%s"' => '', - // 'New title: %s' => '', - // 'The task is not assigned anymore' => '', - // 'New assignee: %s' => '', - // 'There is no category now' => '', - // 'New category: %s' => '', - // 'New color: %s' => '', - // 'New complexity: %d' => '', - // 'The due date have been removed' => '', - // 'There is no description anymore' => '', - // 'Recurrence settings have been modified' => '', - // 'Time spent changed: %sh' => '', - // 'Time estimated changed: %sh' => '', - // 'The field "%s" have been updated' => '', - // 'The description have been modified' => '', - // 'Do you really want to close the task "%s" as well as all subtasks?' => '', - // 'Swimlane: %s' => '', - // 'I want to receive notifications for:' => '', - // 'All tasks' => '', - // 'Only for tasks assigned to me' => '', - // 'Only for tasks created by me' => '', - // 'Only for tasks created by me and assigned to me' => '', - // '%A' => '', - // '%b %e, %Y, %k:%M %p' => '', - // 'New due date: %B %e, %Y' => '', - // 'Start date changed: %B %e, %Y' => '', - // '%k:%M %p' => '', - // '%%Y-%%m-%%d' => '', - // 'Total for all columns' => '', - // 'You need at least 2 days of data to show the chart.' => '', - // '<15m' => '', - // '<30m' => '', - // 'Stop timer' => '', - // 'Start timer' => '', - // 'Add project member' => '', - // 'Enable notifications' => '', - // 'My activity stream' => '', - // 'My calendar' => '', - // 'Search tasks' => '', - // 'Back to the calendar' => '', - // 'Filters' => '', - // 'Reset filters' => '', - // 'My tasks due tomorrow' => '', - // 'Tasks due today' => '', - // 'Tasks due tomorrow' => '', - // 'Tasks due yesterday' => '', - // 'Closed tasks' => '', - // 'Open tasks' => '', - // 'Not assigned' => '', - // 'View advanced search syntax' => '', - // 'Overview' => '', - // '%b %e %Y' => '', - // 'Board/Calendar/List view' => '', - // 'Switch to the board view' => '', - // 'Switch to the calendar view' => '', - // 'Switch to the list view' => '', - // 'Go to the search/filter box' => '', - // 'There is no activity yet.' => '', - // 'No tasks found.' => '', - // 'Keyboard shortcut: "%s"' => '', - // 'List' => '', - // 'Filter' => '', - // 'Advanced search' => '', - // 'Example of query: ' => '', - // 'Search by project: ' => '', - // 'Search by column: ' => '', - // 'Search by assignee: ' => '', - // 'Search by color: ' => '', - // 'Search by category: ' => '', - // 'Search by description: ' => '', - // 'Search by due date: ' => '', + 'Help on Sendgrid integration' => 'Hjälp för Sendgrid integration', + 'Disable two factor authentication' => 'Inaktivera två-faktors autentisering', + 'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Vill du verkligen inaktivera två-faktors autentisering för denna användare: "%s"?', + 'Edit link' => 'Ändra länk', + 'Start to type task title...' => 'Börja skriv uppgiftstitel...', + 'A task cannot be linked to itself' => 'En uppgift kan inte länkas till sig själv', + 'The exact same link already exists' => 'Länken existerar redan', + 'Recurrent task is scheduled to be generated' => 'Återkommande uppgift är schemalagd att genereras', + 'Recurring information' => 'Återkommande information', + 'Score' => 'Poäng', + 'The identifier must be unique' => 'Identifieraren måste vara unik', + 'This linked task id doesn\'t exists' => 'Denna länkade uppgifts id existerar inte', + 'This value must be alphanumeric' => 'Värdet måste vara alfanumeriskt', + 'Edit recurrence' => 'Ändra återkommande', + 'Generate recurrent task' => 'Generera återkommande uppgift', + 'Trigger to generate recurrent task' => 'Aktivera att generera återkommande uppgift', + 'Factor to calculate new due date' => 'Faktor för att beräkna nytt datum', + 'Timeframe to calculate new due date' => 'Tidsram för att beräkna nytt datum', + 'Base date to calculate new due date' => 'Basdatum för att beräkna nytt datum', + 'Action date' => 'Händelsedatum', + 'Base date to calculate new due date: ' => 'Basdatum för att beräkna nytt förfallodatum: ', + 'This task has created this child task: ' => 'Uppgiften har skapat denna underliggande uppgift: ', + 'Day(s)' => 'Dag(ar)', + 'Existing due date' => 'Existerande förfallodatum', + 'Factor to calculate new due date: ' => 'Faktor för att beräkna nytt förfallodatum: ', + 'Month(s)' => 'Månad(er)', + 'Recurrence' => 'Återkommande', + 'This task has been created by: ' => 'Uppgiften har skapats av: ', + 'Recurrent task has been generated:' => 'Återkommande uppgift har genererats:', + 'Timeframe to calculate new due date: ' => 'Tidsram för att beräkna nytt förfallodatum: ', + 'Trigger to generate recurrent task: ' => 'Aktivera att generera återkommande uppgift: ', + 'When task is closed' => 'När uppgiften är stängd', + 'When task is moved from first column' => 'När uppgiften flyttas från första kolumnen', + 'When task is moved to last column' => 'När uppgiften flyttas till sista kolumnen', + 'Year(s)' => 'År', + 'Jabber (XMPP)' => 'Jabber (XMPP)', + 'Send notifications to Jabber' => 'Skicka notiser till Jabber', + 'XMPP server address' => 'XMPP serveradress', + 'Jabber domain' => 'Jabber domän', + 'Jabber nickname' => 'Jabber smeknamn', + 'Multi-user chat room' => 'Multi-user chatrum', + 'Help on Jabber integration' => 'Hjälp för Jabber integration', + 'The server address must use this format: "tcp://hostname:5222"' => 'Serveradressen måste använda detta format: "tcp://hostname:5222"', + 'Calendar settings' => 'Inställningar för kalendern', + 'Project calendar view' => 'Projektkalendervy', + 'Project settings' => 'Projektinställningar', + 'Show subtasks based on the time tracking' => 'Visa deluppgifter baserade på tidsspårning', + 'Show tasks based on the creation date' => 'Visa uppgifter baserade på skapat datum', + 'Show tasks based on the start date' => 'Visa uppgifter baserade på startdatum', + 'Subtasks time tracking' => 'Deluppgifter tidsspårning', + 'User calendar view' => 'Användarkalendervy', + 'Automatically update the start date' => 'Automatisk uppdatering av startdatum', + 'iCal feed' => 'iCal flöde', + 'Preferences' => 'Preferenser', + 'Security' => 'Säkerhet', + 'Two factor authentication disabled' => 'Tvåfaktorsverifiering inaktiverad', + 'Two factor authentication enabled' => 'Tvåfaktorsverifiering aktiverad', + 'Unable to update this user.' => 'Kunde inte uppdatera användaren.', + 'There is no user management for private projects.' => 'Det finns ingen användarhantering för privata projekt.', + 'User that will receive the email' => 'Användare som kommer att ta emot mailet', + 'Email subject' => 'E-post ämne', + 'Date' => 'Datum', + 'By @%s on Bitbucket' => 'Av @%s på Bitbucket', + 'Bitbucket Issue' => 'Bitbucket fråga', + 'Commit made by @%s on Bitbucket' => 'Bidrag gjort av @%s på Bitbucket', + 'Commit made by @%s on Github' => 'Bidrag gjort av @%s på GitHub', + 'By @%s on Github' => 'Av @%s på GitHub', + 'Commit made by @%s on Gitlab' => 'Bidrag gjort av @%s på Gitlab', + 'Add a comment log when moving the task between columns' => 'Lägg till en kommentarslogg när en uppgift flyttas mellan kolumner', + 'Move the task to another column when the category is changed' => 'Flyttas uppgiften till en annan kolumn när kategorin ändras', + 'Send a task by email to someone' => 'Skicka en uppgift med e-post till någon', + 'Reopen a task' => 'Återöppna en uppgift', + 'Bitbucket issue opened' => 'Bitbucketfråga öppnad', + 'Bitbucket issue closed' => 'Bitbucketfråga stängd', + 'Bitbucket issue reopened' => 'Bitbucketfråga återöppnad', + 'Bitbucket issue assignee change' => 'Bitbucketfråga tilldelningsändring', + 'Bitbucket issue comment created' => 'Bitbucketfråga kommentar skapad', + 'Column change' => 'Kolumnändring', + 'Position change' => 'Positionsändring', + 'Swimlane change' => 'Swimlaneändring', + 'Assignee change' => 'Tilldelningsändring', + '[%s] Overdue tasks' => '[%s] Försenade uppgifter', + 'Notification' => 'Notis', + '%s moved the task #%d to the first swimlane' => '%s flyttade uppgiften #%d till första swimlane', + '%s moved the task #%d to the swimlane "%s"' => '%s flyttade uppgiften #%d till swimlane "%s"', + 'Swimlane' => 'Swimlane', + 'Budget overview' => 'Budgetöversikt', + 'Type' => 'Typ', + 'There is not enough data to show something.' => 'Det finns inte tillräckligt mycket data för att visa något.', + 'Gravatar' => 'Gravatar', + 'Hipchat' => 'Hipchat', + 'Slack' => 'Slack', + '%s moved the task %s to the first swimlane' => '%s flyttade uppgiften %s till första swimlane', + '%s moved the task %s to the swimlane "%s"' => '%s flyttade uppgiften %s till swimlane "%s"', + 'This report contains all subtasks information for the given date range.' => 'Denna rapport innehåller all deluppgiftsinformation för det givna datumintervallet.', + 'This report contains all tasks information for the given date range.' => 'Denna rapport innehåller all uppgiftsinformation för det givna datumintervallet.', + 'Project activities for %s' => 'Projektaktiviteter för %s', + 'view the board on Kanboard' => 'visa tavlan på Kanboard', + 'The task have been moved to the first swimlane' => 'Uppgiften har flyttats till första swimlane', + 'The task have been moved to another swimlane:' => 'Uppgiften har flyttats till en annan swimlane:', + 'Overdue tasks for the project "%s"' => 'Försenade uppgifter för projektet "%s"', + 'New title: %s' => 'Ny titel: %s', + 'The task is not assigned anymore' => 'Uppgiften är inte länge tilldelad', + 'New assignee: %s' => 'Ny tilldelning: %s', + 'There is no category now' => 'Det finns ingen kategori nu', + 'New category: %s' => 'Ny kategori: %s', + 'New color: %s' => 'Ny färg: %s', + 'New complexity: %d' => 'Ny komplexitet: %d', + 'The due date have been removed' => 'Förfallodatumet har tagits bort', + 'There is no description anymore' => 'Det finns ingen beskrivning längre', + 'Recurrence settings have been modified' => 'Återkommande inställning har ändrats', + 'Time spent changed: %sh' => 'Spenderad tid har ändrats: %sh', + 'Time estimated changed: %sh' => 'Tidsuppskattning ändrad: %sh', + 'The field "%s" have been updated' => 'Fältet "%s" har uppdaterats', + 'The description have been modified' => 'Beskrivningen har modifierats', + 'Do you really want to close the task "%s" as well as all subtasks?' => 'Vill du verkligen stänga uppgiften "%s" och alla deluppgifter?', + 'Swimlane: %s' => 'Swimlane: %s', + 'I want to receive notifications for:' => 'Jag vill få notiser för:', + 'All tasks' => 'Alla uppgifter', + 'Only for tasks assigned to me' => 'Bara för uppgifter tilldelade mig', + 'Only for tasks created by me' => 'Bara för uppgifter skapade av mig', + 'Only for tasks created by me and assigned to me' => 'Bara för uppgifter skapade av mig och tilldelade till mig', + '%A' => '%A', + '%b %e, %Y, %k:%M %p' => '%b %e, %Y, %k:%M %p', + 'New due date: %B %e, %Y' => 'Nytt förfallodatum: %B %e, %Y', + 'Start date changed: %B %e, %Y' => 'Startdatum ändrat: %B %e, %Y', + '%k:%M %p' => '%k:%M %p', + '%%Y-%%m-%%d' => '%%Y-%%m-%%d', + 'Total for all columns' => 'Totalt för alla kolumner', + 'You need at least 2 days of data to show the chart.' => 'Du behöver minst två dagars data för att visa diagrammet.', + '<15m' => '<15m', + '<30m' => '<30m', + 'Stop timer' => 'Stoppa timer', + 'Start timer' => 'Starta timer', + 'Add project member' => 'Lägg till projektmedlem', + 'Enable notifications' => 'Aktivera notiser', + 'My activity stream' => 'Min aktivitetsström', + 'My calendar' => 'Min kalender', + 'Search tasks' => 'Sök uppgifter', + 'Back to the calendar' => 'Tillbaka till kalendern', + 'Filters' => 'Filter', + 'Reset filters' => 'Återställ filter', + 'My tasks due tomorrow' => 'Mina uppgifter förfaller imorgon', + 'Tasks due today' => 'Uppgifter förfaller idag', + 'Tasks due tomorrow' => 'Uppgifter förfaller imorgon', + 'Tasks due yesterday' => 'Uppgifter förföll igår', + 'Closed tasks' => 'Stängda uppgifter', + 'Open tasks' => 'Öppna uppgifter', + 'Not assigned' => 'Inte tilldelad', + 'View advanced search syntax' => 'Visa avancerad söksyntax', + 'Overview' => 'Översikts', + '%b %e %Y' => '%b %e %Y', + 'Board/Calendar/List view' => 'Tavla/Kalender/Listvy', + 'Switch to the board view' => 'Växla till tavelvy', + 'Switch to the calendar view' => 'Växla till kalendervy', + 'Switch to the list view' => 'Växla till listvy', + 'Go to the search/filter box' => 'Gå till sök/filter box', + 'There is no activity yet.' => 'Det finns ingen aktivitet ännu.', + 'No tasks found.' => 'Inga uppgifter hittades.', + 'Keyboard shortcut: "%s"' => 'Tangentbordsgenväg: "%s"', + 'List' => 'Lista', + 'Filter' => 'Filter', + 'Advanced search' => 'Avancerad sök', + 'Example of query: ' => 'Exempel på fråga', + 'Search by project: ' => 'Sök efter projekt:', + 'Search by column: ' => 'Sök efter kolumn:', + 'Search by assignee: ' => 'Sök efter tilldelad:', + 'Search by color: ' => 'Sök efter färg:', + 'Search by category: ' => 'Sök efter kategori:', + 'Search by description: ' => 'Sök efter beskrivning', + 'Search by due date: ' => 'Sök efter förfallodatum', ); diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php index 86c1c43f..31969b57 100644 --- a/app/Model/Authentication.php +++ b/app/Model/Authentication.php @@ -49,11 +49,6 @@ class Authentication extends Base return false; } - // We update each time the RememberMe cookie tokens - if ($this->backend('rememberMe')->hasCookie()) { - $this->backend('rememberMe')->refresh(); - } - return true; } diff --git a/app/Model/DateParser.php b/app/Model/DateParser.php index be79a92e..79a8385c 100644 --- a/app/Model/DateParser.php +++ b/app/Model/DateParser.php @@ -85,8 +85,7 @@ class DateParser extends Base */ public function getTimestamp($value) { - foreach ($this->getDateFormats() as $format) { - + foreach ($this->getAllFormats() as $format) { $timestamp = $this->getValidDate($value, $format); if ($timestamp !== 0) { @@ -98,6 +97,25 @@ class DateParser extends Base } /** + * Get all combinations of date/time formats + * + * @access public + * @return []string + */ + public function getAllFormats() + { + $formats = array(); + + foreach ($this->getDateFormats() as $date) { + foreach ($this->getTimeFormats() as $time) { + $formats[] = $date.' '.$time; + } + } + + return array_merge($formats, $this->getDateFormats()); + } + + /** * Return the list of supported date formats (for the parser) * * @access public @@ -113,6 +131,21 @@ class DateParser extends Base } /** + * Return the list of supported time formats (for the parser) + * + * @access public + * @return string[] + */ + public function getTimeFormats() + { + return array( + 'H:i', + 'g:i A', + 'g:iA', + ); + } + + /** * Return the list of available date formats (for the config page) * * @access public @@ -143,7 +176,7 @@ class DateParser extends Base * Get a timetstamp from an ISO date format * * @access public - * @param string $date Date format + * @param string $date * @return integer */ public function getTimestampFromIsoFormat($date) @@ -166,7 +199,6 @@ class DateParser extends Base } foreach ($fields as $field) { - if (! empty($values[$field])) { $values[$field] = date($format, $values[$field]); } @@ -180,15 +212,16 @@ class DateParser extends Base * Convert date (form input data) * * @access public - * @param array $values Database values - * @param string[] $fields Date fields + * @param array $values Database values + * @param string[] $fields Date fields + * @param boolean $keep_time Keep time or not */ - public function convert(array &$values, array $fields) + public function convert(array &$values, array $fields, $keep_time = false) { foreach ($fields as $field) { - if (! empty($values[$field]) && ! is_numeric($values[$field])) { - $values[$field] = $this->removeTimeFromTimestamp($this->getTimestamp($values[$field])); + $timestamp = $this->getTimestamp($values[$field]); + $values[$field] = $keep_time ? $timestamp : $this->removeTimeFromTimestamp($timestamp); } } } diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php index a663f921..1ee8a405 100644 --- a/app/Model/ProjectAnalytic.php +++ b/app/Model/ProjectAnalytic.php @@ -49,7 +49,7 @@ class ProjectAnalytic extends Base * Get users repartition * * @access public - * @param integer $project_id Project id + * @param integer $project_id * @return array */ public function getUserRepartition($project_id) @@ -87,4 +87,96 @@ class ProjectAnalytic extends Base return array_values($metrics); } + + /** + * Get the average lead and cycle time + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAverageLeadAndCycleTime($project_id) + { + $stats = array( + 'count' => 0, + 'total_lead_time' => 0, + 'total_cycle_time' => 0, + 'avg_lead_time' => 0, + 'avg_cycle_time' => 0, + ); + + $tasks = $this->db + ->table(Task::TABLE) + ->columns('date_completed', 'date_creation', 'date_started') + ->eq('project_id', $project_id) + ->desc('id') + ->limit(1000) + ->findAll(); + + foreach ($tasks as &$task) { + $stats['count']++; + $stats['total_lead_time'] += ($task['date_completed'] ?: time()) - $task['date_creation']; + $stats['total_cycle_time'] += empty($task['date_started']) ? 0 : ($task['date_completed'] ?: time()) - $task['date_started']; + } + + $stats['avg_lead_time'] = (int) ($stats['total_lead_time'] / $stats['count']); + $stats['avg_cycle_time'] = (int) ($stats['total_cycle_time'] / $stats['count']); + + return $stats; + } + + /** + * Get the average time spent into each column + * + * @access public + * @param integer $project_id + * @return array + */ + public function getAverageTimeSpentByColumn($project_id) + { + $stats = array(); + $columns = $this->board->getColumnsList($project_id); + + // Get the time spent of the last move for each tasks + $tasks = $this->db + ->table(Task::TABLE) + ->columns('id', 'date_completed', 'date_moved', 'column_id') + ->eq('project_id', $project_id) + ->desc('id') + ->limit(1000) + ->findAll(); + + // Init values + foreach ($columns as $column_id => $column_title) { + $stats[$column_id] = array( + 'count' => 0, + 'time_spent' => 0, + 'average' => 0, + 'title' => $column_title, + ); + } + + // Get time spent foreach task/column and take into account the last move + foreach ($tasks as &$task) { + $sums = $this->transition->getTimeSpentByTask($task['id']); + + if (! isset($sums[$task['column_id']])) { + $sums[$task['column_id']] = 0; + } + + $sums[$task['column_id']] += ($task['date_completed'] ?: time()) - $task['date_moved']; + + foreach ($sums as $column_id => $time_spent) { + $stats[$column_id]['count']++; + $stats[$column_id]['time_spent'] += $time_spent; + } + } + + // Calculate average for each column + foreach ($columns as $column_id => $column_title) { + $stats[$column_id]['average'] = (int) ($stats[$column_id]['time_spent'] / $stats[$column_id]['count']); + } + + return $stats; + } } diff --git a/app/Model/ProjectDailySummary.php b/app/Model/ProjectDailyColumnStats.php index 04dc5629..26e9d8b7 100644 --- a/app/Model/ProjectDailySummary.php +++ b/app/Model/ProjectDailyColumnStats.php @@ -3,22 +3,22 @@ namespace Model; /** - * Project daily summary + * Project Daily Column Stats * * @package model * @author Frederic Guillot */ -class ProjectDailySummary extends Base +class ProjectDailyColumnStats extends Base { /** * SQL table name * * @var string */ - const TABLE = 'project_daily_summaries'; + const TABLE = 'project_daily_column_stats'; /** - * Update daily totals for the project + * Update daily totals for the project and foreach column * * "total" is the number open of tasks in the column * "score" is the sum of tasks score in the column @@ -38,7 +38,7 @@ class ProjectDailySummary extends Base // This call will fail if the record already exists // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE) - $db->table(ProjectDailySummary::TABLE)->insert(array( + $db->table(ProjectDailyColumnStats::TABLE)->insert(array( 'day' => $date, 'project_id' => $project_id, 'column_id' => $column_id, @@ -46,7 +46,7 @@ class ProjectDailySummary extends Base 'score' => 0, )); - $db->table(ProjectDailySummary::TABLE) + $db->table(ProjectDailyColumnStats::TABLE) ->eq('project_id', $project_id) ->eq('column_id', $column_id) ->eq('day', $date) @@ -95,19 +95,19 @@ class ProjectDailySummary extends Base */ public function getRawMetrics($project_id, $from, $to) { - return $this->db->table(ProjectDailySummary::TABLE) + return $this->db->table(ProjectDailyColumnStats::TABLE) ->columns( - ProjectDailySummary::TABLE.'.column_id', - ProjectDailySummary::TABLE.'.day', - ProjectDailySummary::TABLE.'.total', - ProjectDailySummary::TABLE.'.score', + ProjectDailyColumnStats::TABLE.'.column_id', + ProjectDailyColumnStats::TABLE.'.day', + ProjectDailyColumnStats::TABLE.'.total', + ProjectDailyColumnStats::TABLE.'.score', Board::TABLE.'.title AS column_title' ) ->join(Board::TABLE, 'id', 'column_id') - ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id) + ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) ->gte('day', $from) ->lte('day', $to) - ->asc(ProjectDailySummary::TABLE.'.day') + ->asc(ProjectDailyColumnStats::TABLE.'.day') ->findAll(); } @@ -122,17 +122,17 @@ class ProjectDailySummary extends Base */ public function getRawMetricsByDay($project_id, $from, $to) { - return $this->db->table(ProjectDailySummary::TABLE) + return $this->db->table(ProjectDailyColumnStats::TABLE) ->columns( - ProjectDailySummary::TABLE.'.day', - 'SUM('.ProjectDailySummary::TABLE.'.total) AS total', - 'SUM('.ProjectDailySummary::TABLE.'.score) AS score' + ProjectDailyColumnStats::TABLE.'.day', + 'SUM('.ProjectDailyColumnStats::TABLE.'.total) AS total', + 'SUM('.ProjectDailyColumnStats::TABLE.'.score) AS score' ) - ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id) + ->eq(ProjectDailyColumnStats::TABLE.'.project_id', $project_id) ->gte('day', $from) ->lte('day', $to) - ->asc(ProjectDailySummary::TABLE.'.day') - ->groupBy(ProjectDailySummary::TABLE.'.day') + ->asc(ProjectDailyColumnStats::TABLE.'.day') + ->groupBy(ProjectDailyColumnStats::TABLE.'.day') ->findAll(); } @@ -160,7 +160,7 @@ class ProjectDailySummary extends Base $aggregates = array(); // Fetch metrics for the project - $records = $this->db->table(ProjectDailySummary::TABLE) + $records = $this->db->table(ProjectDailyColumnStats::TABLE) ->eq('project_id', $project_id) ->gte('day', $from) ->lte('day', $to) diff --git a/app/Model/ProjectDailyStats.php b/app/Model/ProjectDailyStats.php new file mode 100644 index 00000000..56a51730 --- /dev/null +++ b/app/Model/ProjectDailyStats.php @@ -0,0 +1,72 @@ +<?php + +namespace Model; + +/** + * Project Daily Stats + * + * @package model + * @author Frederic Guillot + */ +class ProjectDailyStats extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_daily_stats'; + + /** + * Update daily totals for the project + * + * @access public + * @param integer $project_id Project id + * @param string $date Record date (YYYY-MM-DD) + * @return boolean + */ + public function updateTotals($project_id, $date) + { + $lead_cycle_time = $this->projectAnalytic->getAverageLeadAndCycleTime($project_id); + + return $this->db->transaction(function($db) use ($project_id, $date, $lead_cycle_time) { + + // This call will fail if the record already exists + // (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE) + $db->table(ProjectDailyStats::TABLE)->insert(array( + 'day' => $date, + 'project_id' => $project_id, + 'avg_lead_time' => 0, + 'avg_cycle_time' => 0, + )); + + $db->table(ProjectDailyStats::TABLE) + ->eq('project_id', $project_id) + ->eq('day', $date) + ->update(array( + 'avg_lead_time' => $lead_cycle_time['avg_lead_time'], + 'avg_cycle_time' => $lead_cycle_time['avg_cycle_time'], + )); + }); + } + + /** + * Get raw metrics for the project within a data range + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return array + */ + public function getRawMetrics($project_id, $from, $to) + { + return $this->db->table(self::TABLE) + ->columns('day', 'avg_lead_time', 'avg_cycle_time') + ->eq(self::TABLE.'.project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(self::TABLE.'.day') + ->findAll(); + } +} diff --git a/app/Model/TaskAnalytic.php b/app/Model/TaskAnalytic.php new file mode 100644 index 00000000..33a645c1 --- /dev/null +++ b/app/Model/TaskAnalytic.php @@ -0,0 +1,71 @@ +<?php + +namespace Model; + +/** + * Task Analytic + * + * @package model + * @author Frederic Guillot + */ +class TaskAnalytic extends Base +{ + /** + * Get the time between date_creation and date_completed or now if empty + * + * @access public + * @param array $task + * @return integer + */ + public function getLeadTime(array $task) + { + return ($task['date_completed'] ?: time()) - $task['date_creation']; + } + + /** + * Get the time between date_started and date_completed or now if empty + * + * @access public + * @param array $task + * @return integer + */ + public function getCycleTime(array $task) + { + if (empty($task['date_started'])) { + return 0; + } + + return ($task['date_completed'] ?: time()) - $task['date_started']; + } + + /** + * Get the average time spent in each column + * + * @access public + * @param array $task + * @return array + */ + public function getTimeSpentByColumn(array $task) + { + $result = array(); + $columns = $this->board->getColumnsList($task['project_id']); + $sums = $this->transition->getTimeSpentByTask($task['id']); + + foreach ($columns as $column_id => $column_title) { + + $time_spent = isset($sums[$column_id]) ? $sums[$column_id] : 0; + + if ($task['column_id'] == $column_id) { + $time_spent += ($task['date_completed'] ?: time()) - $task['date_moved']; + } + + $result[] = array( + 'id' => $column_id, + 'title' => $column_title, + 'time_spent' => $time_spent, + ); + } + + return $result; + } +} diff --git a/app/Model/TaskCreation.php b/app/Model/TaskCreation.php index 893cbc43..e530da13 100644 --- a/app/Model/TaskCreation.php +++ b/app/Model/TaskCreation.php @@ -43,9 +43,10 @@ class TaskCreation extends Base */ public function prepare(array &$values) { - $this->dateParser->convert($values, array('date_due', 'date_started')); + $this->dateParser->convert($values, array('date_due')); + $this->dateParser->convert($values, array('date_started'), true); $this->removeFields($values, array('another_task')); - $this->resetFields($values, array('owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); + $this->resetFields($values, array('creator_id', 'owner_id', 'swimlane_id', 'date_due', 'score', 'category_id', 'time_estimated')); if (empty($values['column_id'])) { $values['column_id'] = $this->board->getFirstColumn($values['project_id']); @@ -59,6 +60,10 @@ class TaskCreation extends Base $values['title'] = t('Untitled'); } + if ($this->userSession->isLogged()) { + $values['creator_id'] = $this->userSession->getId(); + } + $values['swimlane_id'] = empty($values['swimlane_id']) ? 0 : $values['swimlane_id']; $values['date_creation'] = time(); $values['date_modification'] = $values['date_creation']; diff --git a/app/Model/TaskFilter.php b/app/Model/TaskFilter.php index 6cf10a17..dfd97d9d 100644 --- a/app/Model/TaskFilter.php +++ b/app/Model/TaskFilter.php @@ -118,7 +118,7 @@ class TaskFilter extends Base * Exclude a list of task_id * * @access public - * @param array $task_ids + * @param integer[] $task_ids * @return TaskFilter */ public function excludeTasks(array $task_ids) @@ -641,10 +641,10 @@ class TaskFilter extends Base * Transform results to ical events * * @access public - * @param string $start_column Column name for the start date - * @param string $end_column Column name for the end date - * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object - * @return Eluceo\iCal\Component\Calendar + * @param string $start_column Column name for the start date + * @param string $end_column Column name for the end date + * @param Calendar $vCalendar Calendar object + * @return Calendar */ public function addDateTimeIcalEvents($start_column, $end_column, Calendar $vCalendar = null) { @@ -674,9 +674,9 @@ class TaskFilter extends Base * Transform results to all day ical events * * @access public - * @param string $column Column name for the date - * @param Eluceo\iCal\Component\Calendar $vCalendar Calendar object - * @return Eluceo\iCal\Component\Calendar + * @param string $column Column name for the date + * @param Calendar $vCalendar Calendar object + * @return Calendar */ public function addAllDayIcalEvents($column = 'date_due', Calendar $vCalendar = null) { @@ -706,7 +706,7 @@ class TaskFilter extends Base * @access protected * @param array $task * @param string $uid - * @return Eluceo\iCal\Component\Event + * @return Event */ protected function getTaskIcalEvent(array &$task, $uid) { @@ -723,11 +723,11 @@ class TaskFilter extends Base $vEvent->setSummary(t('#%d', $task['id']).' '.$task['title']); $vEvent->setUrl($this->helper->url->base().$this->helper->url->to('task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))); - if (! empty($task['creator_id'])) { - $vEvent->setOrganizer('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); + if (! empty($task['owner_id'])) { + $vEvent->setOrganizer('MAILTO:'.($task['assignee_email'] ?: $task['assignee_username'].'@kanboard.local')); } - if (! empty($task['owner_id'])) { + if (! empty($task['creator_id'])) { $attendees = new Attendees; $attendees->add('MAILTO:'.($task['creator_email'] ?: $task['creator_username'].'@kanboard.local')); $vEvent->setAttendees($attendees); diff --git a/app/Model/TaskLink.php b/app/Model/TaskLink.php index 7d3a8918..3fdbd04b 100644 --- a/app/Model/TaskLink.php +++ b/app/Model/TaskLink.php @@ -4,7 +4,6 @@ namespace Model; use SimpleValidator\Validator; use SimpleValidator\Validators; -use PicoDb\Table; /** * TaskLink model diff --git a/app/Model/TaskModification.php b/app/Model/TaskModification.php index 4691ce81..b67106e1 100644 --- a/app/Model/TaskModification.php +++ b/app/Model/TaskModification.php @@ -83,7 +83,8 @@ class TaskModification extends Base */ public function prepare(array &$values) { - $this->dateParser->convert($values, array('date_due', 'date_started')); + $this->dateParser->convert($values, array('date_due')); + $this->dateParser->convert($values, array('date_started'), true); $this->removeFields($values, array('another_task', 'id')); $this->resetFields($values, array('date_due', 'date_started', 'score', 'category_id', 'time_estimated', 'time_spent')); $this->convertIntegerFields($values, array('is_active', 'recurrence_status', 'recurrence_trigger', 'recurrence_factor', 'recurrence_timeframe', 'recurrence_basedate')); diff --git a/app/Model/TaskPosition.php b/app/Model/TaskPosition.php index a33a4029..874633b1 100644 --- a/app/Model/TaskPosition.php +++ b/app/Model/TaskPosition.php @@ -26,109 +26,176 @@ class TaskPosition extends Base */ public function movePosition($project_id, $task_id, $column_id, $position, $swimlane_id = 0, $fire_events = true) { - $original_task = $this->taskFinder->getById($task_id); + if ($position < 1) { + return false; + } + + $task = $this->taskFinder->getById($task_id); // Ignore closed tasks - if ($original_task['is_active'] == Task::STATUS_CLOSED) { + if ($task['is_active'] == Task::STATUS_CLOSED) { return true; } - $result = $this->calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id); - - if ($result) { + $result = false; - if ($original_task['swimlane_id'] != $swimlane_id) { - $this->calculateAndSave($project_id, 0, $column_id, 1, $original_task['swimlane_id']); - } + if ($task['swimlane_id'] != $swimlane_id) { + $result = $this->saveSwimlaneChange($project_id, $task_id, $position, $task['column_id'], $column_id, $task['swimlane_id'], $swimlane_id); + } + else if ($task['column_id'] != $column_id) { + $result = $this->saveColumnChange($project_id, $task_id, $position, $swimlane_id, $task['column_id'], $column_id); + } + else if ($task['position'] != $position) { + $result = $this->savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id); + } - if ($fire_events) { - $this->fireEvents($original_task, $column_id, $position, $swimlane_id); - } + if ($result && $fire_events) { + $this->fireEvents($task, $column_id, $position, $swimlane_id); } return $result; } /** - * Calculate the new position of all tasks + * Move a task to another swimlane * - * @access public - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param integer $column_id Column id - * @param integer $position Position (must be >= 1) - * @param integer $swimlane_id Swimlane id - * @return array|boolean + * @access private + * @param integer $project_id + * @param integer $task_id + * @param integer $position + * @param integer $original_column_id + * @param integer $new_column_id + * @param integer $original_swimlane_id + * @param integer $new_swimlane_id + * @return boolean */ - public function calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id = 0) + private function saveSwimlaneChange($project_id, $task_id, $position, $original_column_id, $new_column_id, $original_swimlane_id, $new_swimlane_id) { - // The position can't be lower than 1 - if ($position < 1) { - return false; - } + $this->db->startTransaction(); + $r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $original_swimlane_id); + $r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $new_swimlane_id); + $this->db->closeTransaction(); - $board = $this->db->table(Board::TABLE)->eq('project_id', $project_id)->asc('position')->findAllByColumn('id'); - $columns = array(); - - // For each column fetch all tasks ordered by position - foreach ($board as $board_column_id) { - - $columns[$board_column_id] = $this->db->table(Task::TABLE) - ->eq('is_active', 1) - ->eq('swimlane_id', $swimlane_id) - ->eq('project_id', $project_id) - ->eq('column_id', $board_column_id) - ->neq('id', $task_id) - ->asc('position') - ->asc('id') // Fix Postgresql unit test - ->findAllByColumn('id'); - } + return $r1 && $r2; + } - // The column must exists - if (! isset($columns[$column_id])) { - return false; - } + /** + * Move a task to another column + * + * @access private + * @param integer $project_id + * @param integer $task_id + * @param integer $position + * @param integer $swimlane_id + * @param integer $original_column_id + * @param integer $new_column_id + * @return boolean + */ + private function saveColumnChange($project_id, $task_id, $position, $swimlane_id, $original_column_id, $new_column_id) + { + $this->db->startTransaction(); + $r1 = $this->saveTaskPositions($project_id, $task_id, 0, $original_column_id, $swimlane_id); + $r2 = $this->saveTaskPositions($project_id, $task_id, $position, $new_column_id, $swimlane_id); + $this->db->closeTransaction(); - // We put our task to the new position - if ($task_id) { - array_splice($columns[$column_id], $position - 1, 0, $task_id); - } + return $r1 && $r2; + } + + /** + * Move a task to another position in the same column + * + * @access private + * @param integer $project_id + * @param integer $task_id + * @param integer $position + * @param integer $column_id + * @param integer $swimlane_id + * @return boolean + */ + private function savePositionChange($project_id, $task_id, $position, $column_id, $swimlane_id) + { + $this->db->startTransaction(); + $result = $this->saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id); + $this->db->closeTransaction(); - return $columns; + return $result; } /** - * Save task positions + * Save all task positions for one column * * @access private - * @param array $columns Sorted tasks - * @param integer $swimlane_id Swimlane id + * @param integer $project_id + * @param integer $task_id + * @param integer $position + * @param integer $column_id + * @param integer $swimlane_id * @return boolean */ - private function savePositions(array $columns, $swimlane_id) + private function saveTaskPositions($project_id, $task_id, $position, $column_id, $swimlane_id) { - return $this->db->transaction(function ($db) use ($columns, $swimlane_id) { + $tasks_ids = $this->db->table(Task::TABLE) + ->eq('is_active', 1) + ->eq('swimlane_id', $swimlane_id) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->neq('id', $task_id) + ->asc('position') + ->asc('id') + ->findAllByColumn('id'); + + $offset = 1; + + foreach ($tasks_ids as $current_task_id) { + + // Insert the new task + if ($position == $offset) { + if (! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) { + return false; + } + $offset++; + } - foreach ($columns as $column_id => $column) { + // Rewrite other tasks position + if (! $this->saveTaskPosition($current_task_id, $offset, $column_id, $swimlane_id)) { + return false; + } - $position = 1; + $offset++; + } - foreach ($column as $task_id) { + // Insert the new task at the bottom and normalize bad position + if ($position >= $offset && ! $this->saveTaskPosition($task_id, $offset, $column_id, $swimlane_id)) { + return false; + } - $result = $db->table(Task::TABLE)->eq('id', $task_id)->update(array( - 'position' => $position, - 'column_id' => $column_id, - 'swimlane_id' => $swimlane_id, - )); + return true; + } - if (! $result) { - return false; - } + /** + * Save new task position + * + * @access private + * @param integer $task_id + * @param integer $position + * @param integer $column_id + * @param integer $swimlane_id + * @return boolean + */ + private function saveTaskPosition($task_id, $position, $column_id, $swimlane_id) + { + $result = $this->db->table(Task::TABLE)->eq('id', $task_id)->update(array( + 'position' => $position, + 'column_id' => $column_id, + 'swimlane_id' => $swimlane_id, + )); + + if (! $result) { + $this->db->cancelTransaction(); + return false; + } - $position++; - } - } - }); + return true; } /** @@ -165,26 +232,4 @@ class TaskPosition extends Base $this->container['dispatcher']->dispatch(Task::EVENT_MOVE_POSITION, new TaskEvent($event_data)); } } - - /** - * Calculate the new position of all tasks - * - * @access private - * @param integer $project_id Project id - * @param integer $task_id Task id - * @param integer $column_id Column id - * @param integer $position Position (must be >= 1) - * @param integer $swimlane_id Swimlane id - * @return boolean - */ - private function calculateAndSave($project_id, $task_id, $column_id, $position, $swimlane_id) - { - $positions = $this->calculatePositions($project_id, $task_id, $column_id, $position, $swimlane_id); - - if ($positions === false || ! $this->savePositions($positions, $swimlane_id)) { - return false; - } - - return true; - } } diff --git a/app/Model/TaskValidator.php b/app/Model/TaskValidator.php index ec1383ad..95b8a26c 100644 --- a/app/Model/TaskValidator.php +++ b/app/Model/TaskValidator.php @@ -39,7 +39,7 @@ class TaskValidator extends Base new Validators\Integer('recurrence_status', t('This value must be an integer')), new Validators\MaxLength('title', t('The maximum length is %d characters', 200), 200), new Validators\Date('date_due', t('Invalid date'), $this->dateParser->getDateFormats()), - new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getDateFormats()), + new Validators\Date('date_started', t('Invalid date'), $this->dateParser->getAllFormats()), new Validators\Numeric('time_spent', t('This value must be numeric')), new Validators\Numeric('time_estimated', t('This value must be numeric')), ); diff --git a/app/Model/Transition.php b/app/Model/Transition.php index cb759e4a..ac3fba54 100644 --- a/app/Model/Transition.php +++ b/app/Model/Transition.php @@ -39,6 +39,22 @@ class Transition extends Base } /** + * Get time spent by task for each column + * + * @access public + * @param integer $task_id + * @return array + */ + public function getTimeSpentByTask($task_id) + { + return $this->db + ->hashtable(self::TABLE) + ->groupBy('src_column_id') + ->eq('task_id', $task_id) + ->getAll('src_column_id', 'SUM(time_spent) AS time_spent'); + } + + /** * Get all transitions by task * * @access public diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php index 6de4a182..44a9c2a2 100644 --- a/app/Model/UserSession.php +++ b/app/Model/UserSession.php @@ -118,4 +118,28 @@ class UserSession extends Base { $_SESSION['filters'][$project_id] = $filters; } + + /** + * Is board collapsed or expanded + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isBoardCollapsed($project_id) + { + return ! empty($_SESSION['board_collapsed'][$project_id]) ? $_SESSION['board_collapsed'][$project_id] : false; + } + + /** + * Set board display mode + * + * @access public + * @param integer $project_id + * @param boolean $collapsed + */ + public function setBoardDisplayMode($project_id, $collapsed) + { + $_SESSION['board_collapsed'][$project_id] = $collapsed; + } } diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 0c932104..769a7425 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,32 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 77; +const VERSION = 79; + +function version_79($pdo) +{ + $pdo->exec(" + CREATE TABLE project_daily_stats ( + id INT NOT NULL AUTO_INCREMENT, + day CHAR(10) NOT NULL, + project_id INT NOT NULL, + avg_lead_time INT NOT NULL DEFAULT 0, + avg_cycle_time INT NOT NULL DEFAULT 0, + PRIMARY KEY(id), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) ENGINE=InnoDB CHARSET=utf8 + "); + + $pdo->exec('CREATE UNIQUE INDEX project_daily_stats_idx ON project_daily_stats(day, project_id)'); + + $pdo->exec('RENAME TABLE project_daily_summaries TO project_daily_column_stats'); +} + +function version_78($pdo) +{ + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN slack_webhook_channel VARCHAR(255) DEFAULT ''"); + $pdo->exec("INSERT INTO settings VALUES ('integration_slack_webhook_channel', '')"); +} function version_77($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index a3309068..fca87766 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,31 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 57; +const VERSION = 59; + +function version_59($pdo) +{ + $pdo->exec(" + CREATE TABLE project_daily_stats ( + id SERIAL PRIMARY KEY, + day CHAR(10) NOT NULL, + project_id INTEGER NOT NULL, + avg_lead_time INTEGER NOT NULL DEFAULT 0, + avg_cycle_time INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); + + $pdo->exec('CREATE UNIQUE INDEX project_daily_stats_idx ON project_daily_stats(day, project_id)'); + + $pdo->exec('ALTER TABLE project_daily_summaries RENAME TO project_daily_column_stats'); +} + +function version_58($pdo) +{ + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN slack_webhook_channel VARCHAR(255) DEFAULT ''"); + $pdo->exec("INSERT INTO settings VALUES ('integration_slack_webhook_channel', '')"); +} function version_57($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index c3bbbac9..9981e72f 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,31 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 73; +const VERSION = 75; + +function version_75($pdo) +{ + $pdo->exec(" + CREATE TABLE project_daily_stats ( + id INTEGER PRIMARY KEY, + day TEXT NOT NULL, + project_id INTEGER NOT NULL, + avg_lead_time INTEGER NOT NULL DEFAULT 0, + avg_cycle_time INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); + + $pdo->exec('CREATE UNIQUE INDEX project_daily_stats_idx ON project_daily_stats(day, project_id)'); + + $pdo->exec('ALTER TABLE project_daily_summaries RENAME TO project_daily_column_stats'); +} + +function version_74($pdo) +{ + $pdo->exec("ALTER TABLE project_integrations ADD COLUMN slack_webhook_channel TEXT DEFAULT ''"); + $pdo->exec("INSERT INTO settings VALUES ('integration_slack_webhook_channel', '')"); +} function version_73($pdo) { diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 1fa0d0ef..8c55457d 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -33,7 +33,8 @@ class ClassProvider implements ServiceProviderInterface 'ProjectActivity', 'ProjectAnalytic', 'ProjectDuplication', - 'ProjectDailySummary', + 'ProjectDailyColumnStats', + 'ProjectDailyStats', 'ProjectIntegration', 'ProjectPermission', 'Subtask', @@ -42,6 +43,7 @@ class ClassProvider implements ServiceProviderInterface 'SubtaskTimeTracking', 'Swimlane', 'Task', + 'TaskAnalytic', 'TaskCreation', 'TaskDuplication', 'TaskExport', diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php index 9e4f15b0..db180dea 100644 --- a/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/app/Subscriber/ProjectDailySummarySubscriber.php @@ -22,7 +22,8 @@ class ProjectDailySummarySubscriber extends \Core\Base implements EventSubscribe public function execute(TaskEvent $event) { if (isset($event['project_id'])) { - $this->projectDailySummary->updateTotals($event['project_id'], date('Y-m-d')); + $this->projectDailyColumnStats->updateTotals($event['project_id'], date('Y-m-d')); + $this->projectDailyStats->updateTotals($event['project_id'], date('Y-m-d')); } } } diff --git a/app/Template/task/activity.php b/app/Template/activity/task.php index cc4aad03..cc4aad03 100644 --- a/app/Template/task/activity.php +++ b/app/Template/activity/task.php diff --git a/app/Template/analytic/avg_time_columns.php b/app/Template/analytic/avg_time_columns.php new file mode 100644 index 00000000..e74e7950 --- /dev/null +++ b/app/Template/analytic/avg_time_columns.php @@ -0,0 +1,29 @@ +<div class="page-header"> + <h2><?= t('Average time spent into each column') ?></h2> +</div> + +<?php if (empty($metrics)): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> + <section id="analytic-avg-time-column"> + + <div id="chart" data-metrics='<?= json_encode($metrics) ?>' data-label="<?= t('Average time spent') ?>"></div> + + <table class="table-stripped"> + <tr> + <th><?= t('Column') ?></th> + <th><?= t('Average time spent') ?></th> + </tr> + <?php foreach ($metrics as $column): ?> + <tr> + <td><?= $this->e($column['title']) ?></td> + <td><?= $this->dt->duration($column['average']) ?></td> + </tr> + <?php endforeach ?> + </table> + + <p class="alert alert-info"> + <?= t('This chart show the average time spent into each column for the last %d tasks.', 1000) ?> + </p> + </section> +<?php endif ?> diff --git a/app/Template/analytic/lead_cycle_time.php b/app/Template/analytic/lead_cycle_time.php new file mode 100644 index 00000000..d96bdcb8 --- /dev/null +++ b/app/Template/analytic/lead_cycle_time.php @@ -0,0 +1,42 @@ +<div class="page-header"> + <h2><?= t('Average Lead and Cycle time') ?></h2> +</div> + +<div class="listing"> + <ul> + <li><?= t('Average lead time: ').'<strong>'.$this->dt->duration($average['avg_lead_time']) ?></strong></li> + <li><?= t('Average cycle time: ').'<strong>'.$this->dt->duration($average['avg_cycle_time']) ?></strong></li> + </ul> +</div> + +<?php if (empty($metrics)): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> + <section id="analytic-lead-cycle-time"> + + <div id="chart" data-metrics='<?= json_encode($metrics) ?>' data-label-cycle="<?= t('Cycle Time') ?>" data-label-lead="<?= t('Lead Time') ?>"></div> + + <form method="post" class="form-inline" action="<?= $this->url->href('analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->form->csrf() ?> + + <div class="form-inline-group"> + <?= $this->form->label(t('Start Date'), 'from') ?> + <?= $this->form->text('from', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <?= $this->form->label(t('End Date'), 'to') ?> + <?= $this->form->text('to', $values, array(), array('required', 'placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> + </form> + + <p class="alert alert-info"> + <?= t('This chart show the average lead and cycle time for the last %d tasks over the time.', 1000) ?> + </p> + </section> +<?php endif ?> diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php index 2d1a7c96..de03bdf8 100644 --- a/app/Template/analytic/sidebar.php +++ b/app/Template/analytic/sidebar.php @@ -13,5 +13,11 @@ <li> <?= $this->url->link(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?> </li> + <li> + <?= $this->url->link(t('Average time into each column'), 'analytic', 'averageTimeByColumn', array('project_id' => $project['id'])) ?> + </li> + <li> + <?= $this->url->link(t('Lead and cycle time'), 'analytic', 'leadAndCycleTime', array('project_id' => $project['id'])) ?> + </li> </ul> </div>
\ No newline at end of file diff --git a/app/Template/board/task_private.php b/app/Template/board/task_private.php index 87121f2c..7eaff580 100644 --- a/app/Template/board/task_private.php +++ b/app/Template/board/task_private.php @@ -10,53 +10,55 @@ <?= $this->render('board/task_menu', array('task' => $task)) ?> - <div class="task-board-collapsed" style="display: none"> - <?php if (! empty($task['assignee_username'])): ?> - <span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>"> - <?= $this->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?> - </span> - - <?php endif ?> - <span class="tooltip" title="<?= $this->e($task['title']) ?>" - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title') ?> - </span> - </div> + <?php if ($this->board->isCollapsed($project['id'])): ?> + <div class="task-board-collapsed"> + <?php if (! empty($task['assignee_username'])): ?> + <span title="<?= $this->e($task['assignee_name'] ?: $task['assignee_username']) ?>"> + <?= $this->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?> + </span> - + <?php endif ?> + <span class="tooltip" title="<?= $this->e($task['title']) ?>" + <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-board-collapsed-title') ?> + </span> + </div> + <?php else: ?> + <div class="task-board-expanded"> - <div class="task-board-expanded"> + <?php if ($task['reference']): ?> + <span class="task-board-reference" title="<?= t('Reference') ?>"> + (<?= $task['reference'] ?>) + </span> + <?php endif ?> - <?php if ($task['reference']): ?> - <span class="task-board-reference" title="<?= t('Reference') ?>"> - (<?= $task['reference'] ?>) - </span> - <?php endif ?> + <span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>"> + <?= $this->url->link( + (! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')), + 'board', + 'changeAssignee', + array('task_id' => $task['id'], 'project_id' => $task['project_id']), + false, + 'task-board-popover', + t('Change assignee') + ) ?> + </span> - <span class="task-board-user <?= $this->user->isCurrentUser($task['owner_id']) ? 'task-board-current-user' : '' ?>"> - <?= $this->url->link( - (! empty($task['owner_id']) ? ($task['assignee_name'] ?: $task['assignee_username']) : t('Nobody assigned')), - 'board', - 'changeAssignee', - array('task_id' => $task['id'], 'project_id' => $task['project_id']), - false, - 'task-board-popover', - t('Change assignee') - ) ?> - </span> + <?php if ($task['is_active'] == 1): ?> + <div class="task-board-days"> + <span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->dt->age($task['date_creation']) ?></span> + <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->dt->age($task['date_moved']) ?></span> + </div> + <?php else: ?> + <div class="task-board-closed"><i class="fa fa-ban fa-fw"></i><?= t('Closed') ?></div> + <?php endif ?> - <?php if ($task['is_active'] == 1): ?> - <div class="task-board-days"> - <span title="<?= t('Task age in days')?>" class="task-days-age"><?= $this->datetime->age($task['date_creation']) ?></span> - <span title="<?= t('Days in this column')?>" class="task-days-incolumn"><?= $this->datetime->age($task['date_moved']) ?></span> - </div> - <?php else: ?> - <div class="task-board-closed"><i class="fa fa-ban fa-fw"></i><?= t('Closed') ?></div> - <?php endif ?> + <div class="task-board-title"> + <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + </div> - <div class="task-board-title"> - <?= $this->url->link($this->e($task['title']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, '', t('View this task')) ?> + <?= $this->render('board/task_footer', array( + 'task' => $task, + 'not_editable' => $not_editable, + )) ?> </div> - - <?= $this->render('board/task_footer', array( - 'task' => $task, - 'not_editable' => $not_editable, - )) ?> - </div> + <?php endif ?> </div> diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php index a1299806..9c80b499 100644 --- a/app/Template/config/integrations.php +++ b/app/Template/config/integrations.php @@ -77,6 +77,8 @@ <?= $this->form->label(t('Webhook URL'), 'integration_slack_webhook_url') ?> <?= $this->form->text('integration_slack_webhook_url', $values, $errors) ?> + <?= $this->form->label(t('Channel/Group/User (Optional)'), 'integration_slack_webhook_channel') ?> + <?= $this->form->text('integration_slack_webhook_channel', $values, $errors) ?> <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p> </div> diff --git a/app/Template/layout.php b/app/Template/layout.php index d804d3d5..a9f1cbc3 100644 --- a/app/Template/layout.php +++ b/app/Template/layout.php @@ -48,7 +48,7 @@ <ul> <?php if (isset($board_selector) && ! empty($board_selector)): ?> <li> - <select id="board-selector" tabindex=="-1" data-notfound="<?= t('No results match:') ?>" data-placeholder="<?= t('Display another project') ?>" data-board-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>"> + <select id="board-selector" tabindex="-1" data-notfound="<?= t('No results match:') ?>" data-placeholder="<?= t('Display another project') ?>" data-board-url="<?= $this->url->href('board', 'show', array('project_id' => 'PROJECT_ID')) ?>"> <option value=""></option> <?php foreach($board_selector as $board_id => $board_name): ?> <option value="<?= $board_id ?>"><?= $this->e($board_name) ?></option> diff --git a/app/Template/project/filters.php b/app/Template/project/filters.php index 396baadf..3beb2f44 100644 --- a/app/Template/project/filters.php +++ b/app/Template/project/filters.php @@ -5,12 +5,13 @@ <ul> <?php if (isset($is_board)): ?> <li> - <span class="filter-collapse"> - <i class="fa fa-compress fa-fw"></i> <a href="#" class="filter-collapse-link" title="<?= t('Keyboard shortcut: "%s"', 's') ?>"><?= t('Collapse tasks') ?></a> - </span> - <span class="filter-expand" style="display: none"> - <i class="fa fa-expand fa-fw"></i> <a href="#" class="filter-expand-link" title="<?= t('Keyboard shortcut: "%s"', 's') ?>"><?= t('Expand tasks') ?></a> - </span> + <?php if ($this->board->isCollapsed($project['id'])): ?> + <i class="fa fa-expand fa-fw"></i> + <?= $this->url->link(t('Expand tasks'), 'board', 'expand', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> + <?php else: ?> + <i class="fa fa-compress fa-fw"></i> + <?= $this->url->link(t('Collapse tasks'), 'board', 'collapse', array('project_id' => $project['id']), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?> + <?php endif ?> </li> <li> <span class="filter-compact"> diff --git a/app/Template/project/integrations.php b/app/Template/project/integrations.php index 698e438c..445e7bfb 100644 --- a/app/Template/project/integrations.php +++ b/app/Template/project/integrations.php @@ -85,6 +85,8 @@ <?= $this->form->label(t('Webhook URL'), 'slack_webhook_url') ?> <?= $this->form->text('slack_webhook_url', $values, $errors) ?> + <?= $this->form->label(t('Channel/Group/User (Optional)'), 'slack_webhook_channel') ?> + <?= $this->form->text('slack_webhook_channel', $values, $errors) ?> <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p> diff --git a/app/Template/subtask/show.php b/app/Template/subtask/show.php index b91e830f..cc82a74e 100644 --- a/app/Template/subtask/show.php +++ b/app/Template/subtask/show.php @@ -48,7 +48,7 @@ <?php if ($subtask['is_timer_started']): ?> <i class="fa fa-pause"></i> <?= $this->url->link(t('Stop timer'), 'timer', 'subtask', array('timer' => 'stop', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?> - (<?= $this->datetime->age($subtask['timer_start_date']) ?>) + (<?= $this->dt->age($subtask['timer_start_date']) ?>) <?php else: ?> <i class="fa fa-play-circle-o"></i> <?= $this->url->link(t('Start timer'), 'timer', 'subtask', array('timer' => 'start', 'project_id' => $task['project_id'], 'task_id' => $subtask['task_id'], 'subtask_id' => $subtask['id'])) ?> diff --git a/app/Template/task/analytics.php b/app/Template/task/analytics.php new file mode 100644 index 00000000..3b1d2855 --- /dev/null +++ b/app/Template/task/analytics.php @@ -0,0 +1,36 @@ +<div class="page-header"> + <h2><?= t('Analytics') ?></h2> +</div> + +<div class="listing"> + <ul> + <li><?= t('Lead time: ').'<strong>'.$this->dt->duration($lead_time) ?></strong></li> + <li><?= t('Cycle time: ').'<strong>'.$this->dt->duration($cycle_time) ?></strong></li> + </ul> +</div> + +<h3 id="analytic-task-time-column"><?= t('Time spent into each column') ?></h3> +<div id="chart" data-metrics='<?= json_encode($time_spent_columns) ?>' data-label="<?= t('Time spent') ?>"></div> +<table class="table-stripped"> + <tr> + <th><?= t('Column') ?></th> + <th><?= t('Time spent') ?></th> + </tr> + <?php foreach ($time_spent_columns as $column): ?> + <tr> + <td><?= $this->e($column['title']) ?></td> + <td><?= $this->dt->duration($column['time_spent']) ?></td> + </tr> + <?php endforeach ?> +</table> + +<div class="alert alert-info"> + <ul> + <li><?= t('The lead time is the duration between the task creation and the completion.') ?></li> + <li><?= t('The cycle time is the duration between the start date and the completion.') ?></li> + <li><?= t('If the task is not closed the current time is used instead of the completion date.') ?></li> + </ul> +</div> + +<?= $this->asset->js('assets/js/vendor/d3.v3.min.js') ?> +<?= $this->asset->js('assets/js/vendor/c3.min.js') ?>
\ No newline at end of file diff --git a/app/Template/task/layout.php b/app/Template/task/layout.php index ddce4bce..bbccf177 100644 --- a/app/Template/task/layout.php +++ b/app/Template/task/layout.php @@ -3,7 +3,7 @@ <ul> <li> <i class="fa fa-th fa-fw"></i> - <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, 'swimlane-'.$task['swimlane_id']) ?> + <?= $this->url->link(t('Back to the board'), 'board', 'show', array('project_id' => $task['project_id']), false, '', '', false, $task['swimlane_id'] != 0 ? 'swimlane-'.$task['swimlane_id'] : '') ?> </li> <li> <i class="fa fa-calendar fa-fw"></i> diff --git a/app/Template/task/sidebar.php b/app/Template/task/sidebar.php index bb137ac9..8b0f3c6e 100644 --- a/app/Template/task/sidebar.php +++ b/app/Template/task/sidebar.php @@ -5,11 +5,14 @@ <?= $this->url->link(t('Summary'), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> - <?= $this->url->link(t('Activity stream'), 'task', 'activites', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + <?= $this->url->link(t('Activity stream'), 'activity', 'task', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> <li> <?= $this->url->link(t('Transitions'), 'task', 'transitions', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> </li> + <li> + <?= $this->url->link(t('Analytics'), 'task', 'analytics', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> + </li> <?php if ($task['time_estimated'] > 0 || $task['time_spent'] > 0): ?> <li> <?= $this->url->link(t('Time tracking'), 'task', 'timesheet', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?> diff --git a/app/Template/task/time.php b/app/Template/task/time.php index 6682a08d..90407d7a 100644 --- a/app/Template/task/time.php +++ b/app/Template/task/time.php @@ -1,9 +1,14 @@ <form method="post" action="<?= $this->url->href('task', 'time', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" class="form-inline task-time-form" autocomplete="off"> + + <?php if (empty($values['date_started'])): ?> + <?= $this->url->link('<i class="fa fa-play"></i>', 'task', 'start', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'task-show-start-link', t('Set automatically the start date')) ?> + <?php endif ?> + <?= $this->form->csrf() ?> <?= $this->form->hidden('id', $values) ?> <?= $this->form->label(t('Start date'), 'date_started') ?> - <?= $this->form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-date') ?> + <?= $this->form->text('date_started', $values, array(), array('placeholder="'.$this->text->in($date_format, $date_formats).'"'), 'form-datetime') ?> <?= $this->form->label(t('Time estimated'), 'time_estimated') ?> <?= $this->form->numeric('time_estimated', $values, array(), array('placeholder="'.t('hours').'"')) ?> diff --git a/app/Template/task/transitions.php b/app/Template/task/transitions.php index 6455fd66..2ca2387f 100644 --- a/app/Template/task/transitions.php +++ b/app/Template/task/transitions.php @@ -19,7 +19,7 @@ <td><?= $this->e($transition['src_column']) ?></td> <td><?= $this->e($transition['dst_column']) ?></td> <td><?= $this->url->link($this->e($transition['name'] ?: $transition['username']), 'user', 'show', array('user_id' => $transition['user_id'])) ?></td> - <td><?= n(round($transition['time_spent'] / 3600, 2)).' '.t('hours') ?></td> + <td><?= $this->dt->duration($transition['time_spent']) ?></td> </tr> <?php endforeach ?> </table> diff --git a/app/Template/timetable_day/index.php b/app/Template/timetable_day/index.php index d2877816..386ceec2 100644 --- a/app/Template/timetable_day/index.php +++ b/app/Template/timetable_day/index.php @@ -30,10 +30,10 @@ <?= $this->form->csrf() ?> <?= $this->form->label(t('Start time'), 'start') ?> - <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?> <?= $this->form->label(t('End time'), 'end') ?> - <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?> <div class="form-actions"> <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> diff --git a/app/Template/timetable_extra/index.php b/app/Template/timetable_extra/index.php index d3224ae6..e9982335 100644 --- a/app/Template/timetable_extra/index.php +++ b/app/Template/timetable_extra/index.php @@ -42,10 +42,10 @@ <?= $this->form->checkbox('all_day', t('All day'), 1) ?> <?= $this->form->label(t('Start time'), 'start') ?> - <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?> <?= $this->form->label(t('End time'), 'end') ?> - <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?> <?= $this->form->label(t('Comment'), 'comment') ?> <?= $this->form->text('comment', $values, $errors) ?> diff --git a/app/Template/timetable_off/index.php b/app/Template/timetable_off/index.php index 75e02dbd..615c2b8d 100644 --- a/app/Template/timetable_off/index.php +++ b/app/Template/timetable_off/index.php @@ -42,10 +42,10 @@ <?= $this->form->checkbox('all_day', t('All day'), 1) ?> <?= $this->form->label(t('Start time'), 'start') ?> - <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?> <?= $this->form->label(t('End time'), 'end') ?> - <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?> <?= $this->form->label(t('Comment'), 'comment') ?> <?= $this->form->text('comment', $values, $errors) ?> diff --git a/app/Template/timetable_week/index.php b/app/Template/timetable_week/index.php index 552e9302..d58c6cfb 100644 --- a/app/Template/timetable_week/index.php +++ b/app/Template/timetable_week/index.php @@ -13,7 +13,7 @@ </tr> <?php foreach ($timetable as $slot): ?> <tr> - <td><?= $this->datetime->getWeekDay($slot['day']) ?></td> + <td><?= $this->dt->getWeekDay($slot['day']) ?></td> <td><?= $slot['start'] ?></td> <td><?= $slot['end'] ?></td> <td> @@ -32,13 +32,13 @@ <?= $this->form->csrf() ?> <?= $this->form->label(t('Day'), 'day') ?> - <?= $this->form->select('day', $this->datetime->getWeekDays(), $values, $errors) ?> + <?= $this->form->select('day', $this->dt->getWeekDays(), $values, $errors) ?> <?= $this->form->label(t('Start time'), 'start') ?> - <?= $this->form->select('start', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?> <?= $this->form->label(t('End time'), 'end') ?> - <?= $this->form->select('end', $this->datetime->getDayHours(), $values, $errors) ?> + <?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?> <div class="form-actions"> <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> diff --git a/app/common.php b/app/common.php index d1659018..b5871673 100644 --- a/app/common.php +++ b/app/common.php @@ -1,6 +1,6 @@ <?php -require 'vendor/autoload.php'; +require dirname(__DIR__) . '/vendor/autoload.php'; // Automatically parse environment configuration (Heroku) if (getenv('DATABASE_URL')) { |