summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrédéric Guillot <fred@kanboard.net>2014-05-25 15:02:27 -0400
committerFrédéric Guillot <fred@kanboard.net>2014-05-25 15:02:27 -0400
commitf9753e91d288c4d87d6a83ffe994d312eae5a3fd (patch)
tree96877bed70873e55aa5139f0ac6245c817e5911d
parent9ddeb5d978db9b1f223c98bbe83ac40fc4474225 (diff)
Add subtasks
-rw-r--r--app/Controller/Base.php1
-rw-r--r--app/Controller/Subtask.php185
-rw-r--r--app/Controller/Task.php1
-rw-r--r--app/Locales/de_DE/translations.php25
-rw-r--r--app/Locales/es_ES/translations.php25
-rw-r--r--app/Locales/fr_FR/translations.php25
-rw-r--r--app/Locales/pl_PL/translations.php25
-rw-r--r--app/Locales/pt_BR/translations.php25
-rw-r--r--app/Model/Base.php1
-rw-r--r--app/Model/SubTask.php179
-rw-r--r--app/Schema/Mysql.php19
-rw-r--r--app/Schema/Sqlite.php18
-rw-r--r--app/Templates/comment_remove.php2
-rw-r--r--app/Templates/file_show.php17
-rw-r--r--app/Templates/subtask_create.php25
-rw-r--r--app/Templates/subtask_edit.php30
-rw-r--r--app/Templates/subtask_remove.php16
-rw-r--r--app/Templates/subtask_show.php60
-rw-r--r--app/Templates/task_show.php23
-rw-r--r--app/Templates/task_sidebar.php1
-rw-r--r--app/helpers.php5
-rw-r--r--assets/css/app.css19
22 files changed, 708 insertions, 19 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index b21d9b8f..5829fc36 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -23,6 +23,7 @@ use Model\LastLogin;
* @property \Model\Ldap $ldap
* @property \Model\Project $project
* @property \Model\RememberMe $rememberMe
+ * @property \Model\SubTask $subTask
* @property \Model\Task $task
* @property \Model\User $user
*/
diff --git a/app/Controller/Subtask.php b/app/Controller/Subtask.php
new file mode 100644
index 00000000..5ef193c8
--- /dev/null
+++ b/app/Controller/Subtask.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Controller;
+
+/**
+ * SubTask controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Subtask extends Base
+{
+ /**
+ * Get the current subtask
+ *
+ * @access private
+ * @return array
+ */
+ private function getSubtask()
+ {
+ $subtask = $this->subTask->getById($this->request->getIntegerParam('subtask_id'));
+
+ if (! $subtask) {
+ $this->notfound();
+ }
+
+ return $subtask;
+ }
+
+ /**
+ * Creation form
+ *
+ * @access public
+ */
+ public function create()
+ {
+ $task = $this->getTask();
+
+ $this->response->html($this->taskLayout('subtask_create', array(
+ 'values' => array(
+ 'task_id' => $task['id'],
+ ),
+ 'errors' => array(),
+ 'users_list' => $this->project->getUsersList($task['project_id']),
+ 'task' => $task,
+ 'menu' => 'tasks',
+ 'title' => t('Add a sub-task')
+ )));
+ }
+
+ /**
+ * Validation and creation
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+
+ list($valid, $errors) = $this->subTask->validate($values);
+
+ if ($valid) {
+
+ if ($this->subTask->create($values)) {
+ $this->session->flash(t('Sub-task added successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to create your sub-task.'));
+ }
+
+ if (isset($values['another_subtask']) && $values['another_subtask'] == 1) {
+ $this->response->redirect('?controller=subtask&action=create&task_id='.$task['id']);
+ }
+
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
+ }
+
+ $this->response->html($this->taskLayout('subtask_create', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'users_list' => $this->project->getUsersList($task['project_id']),
+ 'task' => $task,
+ 'menu' => 'tasks',
+ 'title' => t('Add a sub-task')
+ )));
+ }
+
+ /**
+ * Edit form
+ *
+ * @access public
+ */
+ public function edit()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubTask();
+
+ $this->response->html($this->taskLayout('subtask_edit', array(
+ 'values' => $subtask,
+ 'errors' => array(),
+ 'users_list' => $this->project->getUsersList($task['project_id']),
+ 'status_list' => $this->subTask->getStatusList(),
+ 'subtask' => $subtask,
+ 'task' => $task,
+ 'menu' => 'tasks',
+ 'title' => t('Edit a sub-task')
+ )));
+ }
+
+ /**
+ * Update and validate a subtask
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->subTask->validate($values);
+
+ if ($valid) {
+
+ if ($this->subTask->update($values)) {
+ $this->session->flash(t('Sub-task updated successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to update your sub-task.'));
+ }
+
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
+ }
+
+ $this->response->html($this->taskLayout('subtask_edit', array(
+ 'values' => $values,
+ 'errors' => $errors,
+ 'users_list' => $this->project->getUsersList($task['project_id']),
+ 'status_list' => $this->subTask->getStatusList(),
+ 'subtask' => $subtask,
+ 'task' => $task,
+ 'menu' => 'tasks',
+ 'title' => t('Edit a sub-task')
+ )));
+ }
+
+ /**
+ * Confirmation dialog before removing a subtask
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+
+ $this->response->html($this->taskLayout('subtask_remove', array(
+ 'subtask' => $subtask,
+ 'task' => $task,
+ 'menu' => 'tasks',
+ 'title' => t('Remove a sub-task')
+ )));
+ }
+
+ /**
+ * Remove a subtask
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $task = $this->getTask();
+ $subtask = $this->getSubtask();
+
+ if ($this->subTask->remove($subtask['id'])) {
+ $this->session->flash(t('Sub-task removed successfully.'));
+ }
+ else {
+ $this->session->flashError(t('Unable to remove this sub-task.'));
+ }
+
+ $this->response->redirect('?controller=task&action=show&task_id='.$task['id'].'#subtasks');
+ }
+}
diff --git a/app/Controller/Task.php b/app/Controller/Task.php
index 8230eef3..68e3728a 100644
--- a/app/Controller/Task.php
+++ b/app/Controller/Task.php
@@ -62,6 +62,7 @@ class Task extends Base
$this->response->html($this->taskLayout('task_show', array(
'files' => $this->file->getAll($task['id']),
'comments' => $this->comment->getAll($task['id']),
+ 'subtasks' => $this->subTask->getAll($task['id']),
'task' => $task,
'columns_list' => $this->board->getColumnsList($task['project_id']),
'colors_list' => $this->task->getColors(),
diff --git a/app/Locales/de_DE/translations.php b/app/Locales/de_DE/translations.php
index 90958853..b554b01d 100644
--- a/app/Locales/de_DE/translations.php
+++ b/app/Locales/de_DE/translations.php
@@ -346,4 +346,29 @@ return array(
// 'Add a comment' => '',
// 'Edit a comment' => '',
// 'Summary' => '',
+ // 'Time tracking' => '',
+ // 'Estimate:' => '',
+ // 'Spent:' => '',
+ // 'Do you really want to remove this sub-task?' => '',
+ // 'Remaining:' => '',
+ // 'hours' => '',
+ // 'spent' => '',
+ // 'estimated' => '',
+ // 'Sub-Tasks' => '',
+ // 'Add a sub-task' => '',
+ // 'Original Estimate' => '',
+ // 'Create another sub-task' => '',
+ // 'Time Spent' => '',
+ // 'Edit a sub-task' => '',
+ // 'Remove a sub-task' => '',
+ // 'The time must be a numeric value' => '',
+ // 'Todo' => '',
+ // 'In progress' => '',
+ // 'Done' => '',
+ // 'Sub-task removed successfully.' => '',
+ // 'Unable to remove this sub-task.' => '',
+ // 'Sub-task updated successfully.' => '',
+ // 'Unable to update your sub-task.' => '',
+ // 'Unable to create your sub-task.' => '',
+ // 'Sub-task added successfully.' => '',
);
diff --git a/app/Locales/es_ES/translations.php b/app/Locales/es_ES/translations.php
index d0d9efa8..0d8384dd 100644
--- a/app/Locales/es_ES/translations.php
+++ b/app/Locales/es_ES/translations.php
@@ -344,4 +344,29 @@ return array(
// 'Add a comment' => '',
// 'Edit a comment' => '',
// 'Summary' => '',
+ // 'Time tracking' => '',
+ // 'Estimate:' => '',
+ // 'Spent:' => '',
+ // 'Do you really want to remove this sub-task?' => '',
+ // 'Remaining:' => '',
+ // 'hours' => '',
+ // 'spent' => '',
+ // 'estimated' => '',
+ // 'Sub-Tasks' => '',
+ // 'Add a sub-task' => '',
+ // 'Original Estimate' => '',
+ // 'Create another sub-task' => '',
+ // 'Time Spent' => '',
+ // 'Edit a sub-task' => '',
+ // 'Remove a sub-task' => '',
+ // 'The time must be a numeric value' => '',
+ // 'Todo' => '',
+ // 'In progress' => '',
+ // 'Done' => '',
+ // 'Sub-task removed successfully.' => '',
+ // 'Unable to remove this sub-task.' => '',
+ // 'Sub-task updated successfully.' => '',
+ // 'Unable to update your sub-task.' => '',
+ // 'Unable to create your sub-task.' => '',
+ // 'Sub-task added successfully.' => '',
);
diff --git a/app/Locales/fr_FR/translations.php b/app/Locales/fr_FR/translations.php
index d1ed9f91..56acbed5 100644
--- a/app/Locales/fr_FR/translations.php
+++ b/app/Locales/fr_FR/translations.php
@@ -344,4 +344,29 @@ return array(
'Add a comment' => 'Ajouter un commentaire',
'Edit a comment' => 'Modifier un commentaire',
'Summary' => 'Résumé',
+ 'Time tracking' => 'Gestion du temps',
+ 'Estimate:' => 'Estimation :',
+ 'Spent:' => 'Passé :',
+ 'Do you really want to remove this sub-task?' => 'Voulez-vous vraiment supprimer cette sous-tâche ?',
+ 'Remaining:' => 'Restant :',
+ 'hours' => 'heures',
+ 'spent' => 'passé',
+ 'estimated' => 'estimé',
+ 'Sub-Tasks' => 'Sous-Tâches',
+ 'Add a sub-task' => 'Ajouter une sous-tâche',
+ 'Original Estimate' => 'Estimation originale',
+ 'Create another sub-task' => 'Créer une autre sous-tâche',
+ 'Time Spent' => 'Temps passé',
+ 'Edit a sub-task' => 'Modifier une sous-tâche',
+ 'Remove a sub-task' => 'Supprimer une sous-tâche',
+ 'The time must be a numeric value' => 'Le temps doit-être une valeur numérique',
+ 'Todo' => 'À faire',
+ 'In progress' => 'En cours',
+ 'Done' => 'Terminé',
+ 'Sub-task removed successfully.' => 'Sous-tâche supprimée avec succès.',
+ 'Unable to remove this sub-task.' => 'Impossible de supprimer cette sous-tâche.',
+ 'Sub-task updated successfully.' => 'Sous-tâche mise à jour avec succès.',
+ 'Unable to update your sub-task.' => 'Impossible de mettre à jour votre sous-tâche.',
+ 'Unable to create your sub-task.' => 'Impossible de créer votre sous-tâche.',
+ 'Sub-task added successfully.' => 'Sous-tâche ajouté avec succès.',
);
diff --git a/app/Locales/pl_PL/translations.php b/app/Locales/pl_PL/translations.php
index 3490810a..dca01a2c 100644
--- a/app/Locales/pl_PL/translations.php
+++ b/app/Locales/pl_PL/translations.php
@@ -349,4 +349,29 @@ return array(
// 'Add a comment' => '',
// 'Edit a comment' => '',
// 'Summary' => '',
+ // 'Time tracking' => '',
+ // 'Estimate:' => '',
+ // 'Spent:' => '',
+ // 'Do you really want to remove this sub-task?' => '',
+ // 'Remaining:' => '',
+ // 'hours' => '',
+ // 'spent' => '',
+ // 'estimated' => '',
+ // 'Sub-Tasks' => '',
+ // 'Add a sub-task' => '',
+ // 'Original Estimate' => '',
+ // 'Create another sub-task' => '',
+ // 'Time Spent' => '',
+ // 'Edit a sub-task' => '',
+ // 'Remove a sub-task' => '',
+ // 'The time must be a numeric value' => '',
+ // 'Todo' => '',
+ // 'In progress' => '',
+ // 'Done' => '',
+ // 'Sub-task removed successfully.' => '',
+ // 'Unable to remove this sub-task.' => '',
+ // 'Sub-task updated successfully.' => '',
+ // 'Unable to update your sub-task.' => '',
+ // 'Unable to create your sub-task.' => '',
+ // 'Sub-task added successfully.' => '',
);
diff --git a/app/Locales/pt_BR/translations.php b/app/Locales/pt_BR/translations.php
index 267006ce..00fcccef 100644
--- a/app/Locales/pt_BR/translations.php
+++ b/app/Locales/pt_BR/translations.php
@@ -345,4 +345,29 @@ return array(
// 'Add a comment' => '',
// 'Edit a comment' => '',
// 'Summary' => '',
+ // 'Time tracking' => '',
+ // 'Estimate:' => '',
+ // 'Spent:' => '',
+ // 'Do you really want to remove this sub-task?' => '',
+ // 'Remaining:' => '',
+ // 'hours' => '',
+ // 'spent' => '',
+ // 'estimated' => '',
+ // 'Sub-Tasks' => '',
+ // 'Add a sub-task' => '',
+ // 'Original Estimate' => '',
+ // 'Create another sub-task' => '',
+ // 'Time Spent' => '',
+ // 'Edit a sub-task' => '',
+ // 'Remove a sub-task' => '',
+ // 'The time must be a numeric value' => '',
+ // 'Todo' => '',
+ // 'In progress' => '',
+ // 'Done' => '',
+ // 'Sub-task removed successfully.' => '',
+ // 'Unable to remove this sub-task.' => '',
+ // 'Sub-task updated successfully.' => '',
+ // 'Unable to update your sub-task.' => '',
+ // 'Unable to create your sub-task.' => '',
+ // 'Sub-task added successfully.' => '',
);
diff --git a/app/Model/Base.php b/app/Model/Base.php
index e95296bb..ddc06c3d 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -14,6 +14,7 @@ require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php';
+require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php';
use Core\Event;
use PicoDb\Database;
diff --git a/app/Model/SubTask.php b/app/Model/SubTask.php
new file mode 100644
index 00000000..21ccdaac
--- /dev/null
+++ b/app/Model/SubTask.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Subtask model
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class SubTask extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'task_has_subtasks';
+
+ /**
+ * Task "done" status
+ *
+ * @var integer
+ */
+ const STATUS_DONE = 2;
+
+ /**
+ * Task "in progress" status
+ *
+ * @var integer
+ */
+ const STATUS_INPROGRESS = 1;
+
+ /**
+ * Task "todo" status
+ *
+ * @var integer
+ */
+ const STATUS_TODO = 0;
+
+ /**
+ * Get available status
+ *
+ * @access public
+ * @return array
+ */
+ public function getStatusList()
+ {
+ $status = array(
+ self::STATUS_TODO => t('Todo'),
+ self::STATUS_INPROGRESS => t('In progress'),
+ self::STATUS_DONE => t('Done'),
+ );
+
+ asort($status);
+
+ return $status;
+ }
+
+ /**
+ * Get all subtasks for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getAll($task_id)
+ {
+ $status = $this->getStatusList();
+ $subtasks = $this->db->table(self::TABLE)
+ ->eq('task_id', $task_id)
+ ->columns(self::TABLE.'.*', User::TABLE.'.username')
+ ->join(User::TABLE, 'id', 'user_id')
+ ->findAll();
+
+ foreach ($subtasks as &$subtask) {
+ $subtask['status_name'] = $status[$subtask['status']];
+ }
+
+ return $subtasks;
+ }
+
+ /**
+ * Get a subtask by the id
+ *
+ * @access public
+ * @param integer $subtask_id Subtask id
+ * @return array
+ */
+ public function getById($subtask_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $subtask_id)->findOne();
+ }
+
+ /**
+ * Create
+ *
+ * @access public
+ * @param array $values Form values
+ * @return bool
+ */
+ public function create(array $values)
+ {
+ if (isset($values['another_subtask'])) {
+ unset($values['another_subtask']);
+ }
+
+ if (isset($values['time_estimated']) && empty($values['time_estimated'])) {
+ $values['time_estimated'] = 0;
+ }
+
+ if (isset($values['time_spent']) && empty($values['time_spent'])) {
+ $values['time_spent'] = 0;
+ }
+
+ return $this->db->table(self::TABLE)->save($values);
+ }
+
+ /**
+ * Update
+ *
+ * @access public
+ * @param array $values Form values
+ * @return bool
+ */
+ public function update(array $values)
+ {
+ if (isset($values['time_estimated']) && empty($values['time_estimated'])) {
+ $values['time_estimated'] = 0;
+ }
+
+ if (isset($values['time_spent']) && empty($values['time_spent'])) {
+ $values['time_spent'] = 0;
+ }
+
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->save($values);
+ }
+
+ /**
+ * Remove
+ *
+ * @access public
+ * @param integer $subtask_id Subtask id
+ * @return bool
+ */
+ public function remove($subtask_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $subtask_id)->remove();
+ }
+
+ /**
+ * Validate creation/modification
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validate(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('task_id', t('The task id is required')),
+ new Validators\Integer('task_id', t('The task id must be an integer')),
+ new Validators\Required('title', t('The title is required')),
+ new Validators\MaxLength('title', t('The maximum length is %d characters', 100), 100),
+ new Validators\Integer('user_id', t('The user id must be an integer')),
+ new Validators\Integer('status', t('The status must be an integer')),
+ new Validators\Numeric('time_estimated', t('The time must be a numeric value')),
+ new Validators\Numeric('time_spent', t('The time must be a numeric value')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index d3b111f9..3df38dee 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -2,7 +2,24 @@
namespace Schema;
-const VERSION = 17;
+const VERSION = 18;
+
+function version_18($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_subtasks (
+ id INT NOT NULL AUTO_INCREMENT,
+ title VARCHAR(255),
+ status INT DEFAULT 0,
+ time_estimated INT DEFAULT 0,
+ time_spent INT DEFAULT 0,
+ task_id INT,
+ user_id INT,
+ PRIMARY KEY (id),
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB CHARSET=utf8"
+ );
+}
function version_17($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 94ef0316..663c7d34 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -2,7 +2,23 @@
namespace Schema;
-const VERSION = 17;
+const VERSION = 18;
+
+function version_18($pdo)
+{
+ $pdo->exec("
+ CREATE TABLE task_has_subtasks (
+ id INTEGER PRIMARY KEY,
+ title TEXT COLLATE NOCASE,
+ status INTEGER DEFAULT 0,
+ time_estimated INTEGER DEFAULT 0,
+ time_spent INTEGER DEFAULT 0,
+ task_id INTEGER,
+ user_id INTEGER,
+ FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
+ )"
+ );
+}
function version_17($pdo)
{
diff --git a/app/Templates/comment_remove.php b/app/Templates/comment_remove.php
index 02a23f93..6409d7c0 100644
--- a/app/Templates/comment_remove.php
+++ b/app/Templates/comment_remove.php
@@ -1,5 +1,5 @@
<div class="page-header">
- <h2><?= t('Add a comment') ?></h2>
+ <h2><?= t('Remove a comment') ?></h2>
</div>
<div class="confirm">
diff --git a/app/Templates/file_show.php b/app/Templates/file_show.php
new file mode 100644
index 00000000..674861dd
--- /dev/null
+++ b/app/Templates/file_show.php
@@ -0,0 +1,17 @@
+<div class="page-header">
+ <h2><?= t('Attachments') ?></h2>
+</div>
+
+<ul class="task-show-files">
+<?php foreach ($files as $file): ?>
+ <li>
+ <a href="?controller=file&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
+ <span class="task-show-file-actions">
+ <?php if ($file['is_image']): ?>
+ <a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="popover"><?= t('open') ?></a>,
+ <?php endif ?>
+ <a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('remove') ?></a>
+ </span>
+ </li>
+<?php endforeach ?>
+</ul> \ No newline at end of file
diff --git a/app/Templates/subtask_create.php b/app/Templates/subtask_create.php
new file mode 100644
index 00000000..a456aa37
--- /dev/null
+++ b/app/Templates/subtask_create.php
@@ -0,0 +1,25 @@
+<div class="page-header">
+ <h2><?= t('Add a sub-task') ?></h2>
+</div>
+
+<form method="post" action="?controller=subtask&amp;action=save&amp;task_id=<?= $task['id'] ?>" autocomplete="off">
+
+ <?= Helper\form_hidden('task_id', $values) ?>
+
+ <?= Helper\form_label(t('Title'), 'title') ?>
+ <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/>
+
+ <?= Helper\form_label(t('Assignee'), 'user_id') ?>
+ <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Original Estimate'), 'time_estimated') ?>
+ <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
+
+ <?= Helper\form_checkbox('another_subtask', t('Create another sub-task'), 1, isset($values['another_subtask']) && $values['another_subtask'] == 1) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
+ </div>
+</form>
diff --git a/app/Templates/subtask_edit.php b/app/Templates/subtask_edit.php
new file mode 100644
index 00000000..3080cdad
--- /dev/null
+++ b/app/Templates/subtask_edit.php
@@ -0,0 +1,30 @@
+<div class="page-header">
+ <h2><?= t('Edit a sub-task') ?></h2>
+</div>
+
+<form method="post" action="?controller=subtask&amp;action=update&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'] ?>" autocomplete="off">
+
+ <?= Helper\form_hidden('id', $values) ?>
+ <?= Helper\form_hidden('task_id', $values) ?>
+
+ <?= Helper\form_label(t('Title'), 'title') ?>
+ <?= Helper\form_text('title', $values, $errors, array('required autofocus')) ?><br/>
+
+ <?= Helper\form_label(t('Status'), 'status') ?>
+ <?= Helper\form_select('status', $status_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Assignee'), 'user_id') ?>
+ <?= Helper\form_select('user_id', $users_list, $values, $errors) ?><br/>
+
+ <?= Helper\form_label(t('Original Estimate'), 'time_estimated') ?>
+ <?= Helper\form_numeric('time_estimated', $values, $errors) ?> <?= t('hours') ?><br/>
+
+ <?= Helper\form_label(t('Time Spent'), 'time_spent') ?>
+ <?= Helper\form_numeric('time_spent', $values, $errors) ?> <?= t('hours') ?><br/>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ <?= t('or') ?>
+ <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('cancel') ?></a>
+ </div>
+</form>
diff --git a/app/Templates/subtask_remove.php b/app/Templates/subtask_remove.php
new file mode 100644
index 00000000..2862176c
--- /dev/null
+++ b/app/Templates/subtask_remove.php
@@ -0,0 +1,16 @@
+<div class="page-header">
+ <h2><?= t('Remove a sub-task') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this sub-task?') ?>
+ </p>
+
+ <p><strong><?= Helper\escape($subtask['title']) ?></strong></p>
+
+ <div class="form-actions">
+ <a href="?controller=subtask&amp;action=remove&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
+ <?= t('or') ?> <a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>#subtasks"><?= t('cancel') ?></a>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Templates/subtask_show.php b/app/Templates/subtask_show.php
new file mode 100644
index 00000000..b9385c7e
--- /dev/null
+++ b/app/Templates/subtask_show.php
@@ -0,0 +1,60 @@
+<div class="page-header">
+ <h2><?= t('Sub-Tasks') ?></h2>
+</div>
+
+<?php
+
+$total_spent = 0;
+$total_estimated = 0;
+$total_remaining = 0;
+
+?>
+
+<table class="subtasks-table">
+ <tr>
+ <th width="40%"><?= t('Title') ?></th>
+ <th><?= t('Status') ?></th>
+ <th><?= t('Assignee') ?></th>
+ <th><?= t('Time tracking') ?></th>
+ <th><?= t('Actions') ?></th>
+ </tr>
+ <?php foreach ($subtasks as $subtask): ?>
+ <tr>
+ <td><?= Helper\escape($subtask['title']) ?></td>
+ <td><?= Helper\escape($subtask['status_name']) ?></td>
+ <td>
+ <?php if (! empty($subtask['username'])): ?>
+ <?= Helper\escape($subtask['username']) ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <?php if (! empty($subtask['time_spent'])): ?>
+ <strong><?= Helper\escape($subtask['time_spent']).'h' ?></strong> <?= t('spent') ?>
+ <?php endif ?>
+
+ <?php if (! empty($subtask['time_estimated'])): ?>
+ <strong><?= Helper\escape($subtask['time_estimated']).'h' ?></strong> <?= t('estimated') ?>
+ <?php endif ?>
+ </td>
+ <td>
+ <a href="?controller=subtask&amp;action=edit&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'] ?>"><?= t('Edit') ?></a>
+ <?= t('or') ?>
+ <a href="?controller=subtask&amp;action=confirm&amp;task_id=<?= $task['id'] ?>&amp;subtask_id=<?= $subtask['id'] ?>"><?= t('Remove') ?></a>
+ </td>
+ </tr>
+ <?php
+ $total_estimated += $subtask['time_estimated'];
+ $total_spent += $subtask['time_spent'];
+ $total_remaining = $total_estimated - $total_spent;
+ ?>
+ <?php endforeach ?>
+</table>
+
+<div class="subtasks-time-tracking">
+ <h4><?= t('Time tracking') ?></h4>
+ <ul>
+ <li><?= t('Estimate:') ?> <strong><?= Helper\escape($total_estimated) ?></strong> <?= t('hours') ?></li>
+ <li><?= t('Spent:') ?> <strong><?= Helper\escape($total_spent) ?></strong> <?= t('hours') ?></li>
+ <li><?= t('Remaining:') ?> <strong><?= Helper\escape($total_remaining > 0 ? $total_remaining : 0) ?></strong> <?= t('hours') ?></li>
+ </ul>
+</div> \ No newline at end of file
diff --git a/app/Templates/task_show.php b/app/Templates/task_show.php
index 53cdbae8..4c3d4697 100644
--- a/app/Templates/task_show.php
+++ b/app/Templates/task_show.php
@@ -62,23 +62,14 @@
<?php if (! empty($files)): ?>
<div id="attachments" class="task-show-section">
- <div class="page-header">
- <h2><?= t('Attachments') ?></h2>
- </div>
+ <?= Helper\template('file_show', array('task' => $task, 'files' => $files)) ?>
+</div>
+<?php endif ?>
- <ul class="task-show-files">
- <?php foreach ($files as $file): ?>
- <li>
- <a href="?controller=file&amp;action=download&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= Helper\escape($file['name']) ?></a>
- <span class="task-show-file-actions">
- <?php if ($file['is_image']): ?>
- <a href="?controller=file&amp;action=open&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>" class="popover"><?= t('open') ?></a>,
- <?php endif ?>
- <a href="?controller=file&amp;action=confirm&amp;file_id=<?= $file['id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('remove') ?></a>
- </span>
- </li>
- <?php endforeach ?>
- </ul>
+
+<?php if (! empty($subtasks)): ?>
+<div id="subtasks" class="task-show-section">
+ <?= Helper\template('subtask_show', array('task' => $task, 'subtasks' => $subtasks)) ?>
</div>
<?php endif ?>
diff --git a/app/Templates/task_sidebar.php b/app/Templates/task_sidebar.php
index 8a3939b8..d97c44e2 100644
--- a/app/Templates/task_sidebar.php
+++ b/app/Templates/task_sidebar.php
@@ -5,6 +5,7 @@
<li><a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>"><?= t('Summary') ?></a></li>
<li><a href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>"><?= t('Edit the task') ?></a></li>
<li><a href="?controller=task&amp;action=editDescription&amp;task_id=<?= $task['id'] ?>"><?= t('Edit the description') ?></a></li>
+ <li><a href="?controller=subtask&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Add a sub-task') ?></a></li>
<li><a href="?controller=comment&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Add a comment') ?></a></li>
<li><a href="?controller=file&amp;action=create&amp;task_id=<?= $task['id'] ?>"><?= t('Attach a document') ?></a></li>
<li><a href="?controller=task&amp;action=duplicate&amp;project_id=<?= $task['project_id'] ?>&amp;task_id=<?= $task['id'] ?>"><?= t('Duplicate') ?></a></li>
diff --git a/app/helpers.php b/app/helpers.php
index 8351328a..2dcb38e8 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -260,3 +260,8 @@ function form_number($name, $values = array(), array $errors = array(), array $a
{
return form_input('number', $name, $values, $errors, $attributes, $class);
}
+
+function form_numeric($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+{
+ return form_input('text', $name, $values, $errors, $attributes, $class.' form-numeric');
+}
diff --git a/assets/css/app.css b/assets/css/app.css
index 73c45dd9..017a64fa 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -158,6 +158,7 @@ textarea:focus {
box-shadow: 0 0 8px rgba(82, 168, 236, 0.6);
}
+input.form-numeric,
input[type="number"] {
width: 70px;
}
@@ -797,6 +798,24 @@ a.task-board-nobody {
max-width: 800px;
}
+/* subtasks */
+.subtasks-table {
+ font-size: 0.85em;
+}
+
+.subtasks-table td {
+ vertical-align: middle;
+}
+
+.subtasks-time-tracking h4 {
+ margin-bottom: 5px;
+}
+
+.subtasks-time-tracking li {
+ list-style-type: square;
+ margin-left: 30px;
+}
+
/* markdown content */
.markdown {
line-height: 1.4em;