summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/Controller/Base.php1
-rw-r--r--app/Controller/Budget.php24
-rw-r--r--app/Controller/Calendar.php6
-rw-r--r--app/Controller/Config.php6
-rw-r--r--app/Controller/Timetableextra.php2
-rw-r--r--app/Model/Base.php2
-rw-r--r--app/Model/Budget.php60
-rw-r--r--app/Model/DateParser.php41
-rw-r--r--app/Model/HourlyRate.php18
-rw-r--r--app/Model/SubtaskTimeTracking.php91
-rw-r--r--app/Model/Timetable.php164
-rw-r--r--app/Schema/Mysql.php7
-rw-r--r--app/Schema/Postgres.php7
-rw-r--r--app/Schema/Sqlite.php7
-rw-r--r--app/ServiceProvider/ClassProvider.php1
-rw-r--r--app/Template/budget/breakdown.php34
-rw-r--r--app/Template/budget/create.php2
-rw-r--r--app/Template/budget/index.php2
-rw-r--r--app/Template/task/time_tracking.php10
-rw-r--r--app/Template/user/timesheet.php10
20 files changed, 444 insertions, 51 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index ec202c06..a4e94343 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -67,7 +67,6 @@ use Symfony\Component\EventDispatcher\Event;
* @property \Model\CommentHistory $commentHistory
* @property \Model\SubtaskHistory $subtaskHistory
* @property \Model\SubtaskTimeTracking $subtaskTimeTracking
- * @property \Model\TimeTracking $timeTracking
* @property \Model\User $user
* @property \Model\UserSession $userSession
* @property \Model\Webhook $webhook
diff --git a/app/Controller/Budget.php b/app/Controller/Budget.php
index 01090550..b8279f19 100644
--- a/app/Controller/Budget.php
+++ b/app/Controller/Budget.php
@@ -27,6 +27,30 @@ class Budget extends Base
}
/**
+ * Cost breakdown by users/subtasks/tasks
+ *
+ * @access public
+ */
+ public function breakdown()
+ {
+ $project = $this->getProject();
+
+ $paginator = $this->paginator
+ ->setUrl('budget', 'breakdown', array('project_id' => $project['id']))
+ ->setMax(30)
+ ->setOrder('start')
+ ->setDirection('DESC')
+ ->setQuery($this->budget->getBreakdown($project['id']))
+ ->calculate();
+
+ $this->response->html($this->projectLayout('budget/breakdown', array(
+ 'paginator' => $paginator,
+ 'project' => $project,
+ 'title' => t('Budget')
+ )));
+ }
+
+ /**
* Create budget lines
*
* @access public
diff --git a/app/Controller/Calendar.php b/app/Controller/Calendar.php
index 1c7ac7c0..2a11edab 100644
--- a/app/Controller/Calendar.php
+++ b/app/Controller/Calendar.php
@@ -14,7 +14,7 @@ use Model\Task as TaskModel;
class Calendar extends Base
{
/**
- * Show calendar view
+ * Show calendar view for projects
*
* @access public
*/
@@ -59,9 +59,7 @@ class Calendar extends Base
->filterByDueDateRange($start, $end)
->toCalendarEvents();
- $subtask_timeslots = $this->subtaskTimeTracking->getProjectCalendarEvents($project_id, $start, $end);
-
- $this->response->json(array_merge($due_tasks, $subtask_timeslots));
+ $this->response->json($due_tasks);
}
/**
diff --git a/app/Controller/Config.php b/app/Controller/Config.php
index 01c7ad53..bee897be 100644
--- a/app/Controller/Config.php
+++ b/app/Controller/Config.php
@@ -38,7 +38,11 @@ class Config extends Base
{
if ($this->request->isPost()) {
- $values = $this->request->getValues() + array('subtask_restriction' => 0, 'subtask_time_tracking' => 0);
+ $values = $this->request->getValues();
+
+ if ($redirect === 'board') {
+ $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0);
+ }
if ($this->config->save($values)) {
$this->config->reload();
diff --git a/app/Controller/Timetableextra.php b/app/Controller/Timetableextra.php
index c362bd1a..7c6fe265 100644
--- a/app/Controller/Timetableextra.php
+++ b/app/Controller/Timetableextra.php
@@ -8,7 +8,7 @@ namespace Controller;
* @package controller
* @author Frederic Guillot
*/
-class Timetableextra extends TimetableOff
+class Timetableextra extends Timetableoff
{
protected $model = 'timetableExtra';
protected $controller_url = 'timetableextra';
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 8a90e286..0217aae3 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -43,7 +43,7 @@ use Pimple\Container;
* @property \Model\TaskLink $taskLink
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskValidator $taskValidator
- * @property \Model\TimeTracking $timeTracking
+ * @property \Model\Timetable $timetable
* @property \Model\SubtaskTimeTracking $subtaskTimeTracking
* @property \Model\User $user
* @property \Model\UserSession $userSession
diff --git a/app/Model/Budget.php b/app/Model/Budget.php
index 03a90f7f..827182a3 100644
--- a/app/Model/Budget.php
+++ b/app/Model/Budget.php
@@ -46,6 +46,66 @@ class Budget extends Base
}
/**
+ * Get breakdown by tasks/subtasks/users
+ *
+ * @access public
+ * @param integer $project_id
+ * @return \PicoDb\Table
+ */
+ public function getBreakdown($project_id)
+ {
+ return $this->db
+ ->table(SubtaskTimeTracking::TABLE)
+ ->columns(
+ SubtaskTimeTracking::TABLE.'.id',
+ SubtaskTimeTracking::TABLE.'.user_id',
+ SubtaskTimeTracking::TABLE.'.subtask_id',
+ SubtaskTimeTracking::TABLE.'.start',
+ SubtaskTimeTracking::TABLE.'.time_spent',
+ Subtask::TABLE.'.task_id',
+ Subtask::TABLE.'.title AS subtask_title',
+ Task::TABLE.'.title AS task_title',
+ Task::TABLE.'.project_id',
+ User::TABLE.'.username',
+ User::TABLE.'.name'
+ )
+ ->join(Subtask::TABLE, 'id', 'subtask_id')
+ ->join(Task::TABLE, 'id', 'task_id', Subtask::TABLE)
+ ->join(User::TABLE, 'id', 'user_id')
+ ->eq(Task::TABLE.'.project_id', $project_id)
+ ->filter(array($this, 'applyUserRate'));
+ }
+
+ /**
+ * Filter callback to apply the rate according to the effective date
+ *
+ * @access public
+ * @param array $records
+ * @return array
+ */
+ public function applyUserRate(array $records)
+ {
+ $rates = $this->hourlyRate->getAllByProject($records[0]['project_id']);
+
+ foreach ($records as &$record) {
+
+ $hourly_price = 0;
+
+ foreach ($rates as $rate) {
+
+ if ($rate['user_id'] == $record['user_id'] && date('Y-m-d', $rate['date_effective']) <= date('Y-m-d', $record['start'])) {
+ $hourly_price = $rate['rate'];
+ break;
+ }
+ }
+
+ $record['cost'] = $hourly_price * $record['time_spent'];
+ }
+
+ return $records;
+ }
+
+ /**
* Add a new budget line in the database
*
* @access public
diff --git a/app/Model/DateParser.php b/app/Model/DateParser.php
index 8a4d3edd..a0d10a36 100644
--- a/app/Model/DateParser.php
+++ b/app/Model/DateParser.php
@@ -13,6 +13,47 @@ use DateTime;
class DateParser extends Base
{
/**
+ * Return true if the date is within the date range
+ *
+ * @access public
+ * @param DateTime $date
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return boolean
+ */
+ public function withinDateRange(DateTime $date, DateTime $start, DateTime $end)
+ {
+ return $date >= $start && $date <= $end;
+ }
+
+ /**
+ * Get the total number of hours between 2 datetime objects
+ * Minutes are rounded to the nearest quarter
+ *
+ * @access public
+ * @param DateTime $d1
+ * @param DateTime $d2
+ * @return float
+ */
+ public function getHours(DateTime $d1, DateTime $d2)
+ {
+ $seconds = $this->getRoundedSeconds(abs($d1->getTimestamp() - $d2->getTimestamp()));
+ return round($seconds / 3600, 2);
+ }
+
+ /**
+ * Round the timestamp to the nearest quarter
+ *
+ * @access public
+ * @param integer $seconds Timestamp
+ * @return integer
+ */
+ public function getRoundedSeconds($seconds)
+ {
+ return (int) round($seconds / (15 * 60)) * (15 * 60);
+ }
+
+ /**
* Return a timestamp if the given date format is correct otherwise return 0
*
* @access public
diff --git a/app/Model/HourlyRate.php b/app/Model/HourlyRate.php
index c2ce3eaf..1550bdae 100644
--- a/app/Model/HourlyRate.php
+++ b/app/Model/HourlyRate.php
@@ -21,6 +21,24 @@ class HourlyRate extends Base
const TABLE = 'hourly_rates';
/**
+ * Get all user rates for a given project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getAllByProject($project_id)
+ {
+ $members = $this->projectPermission->getMembers($project_id);
+
+ if (empty($members)) {
+ return array();
+ }
+
+ return $this->db->table(self::TABLE)->in('user_id', array_keys($members))->desc('date_effective')->findAll();
+ }
+
+ /**
* Get all rates for a given user
*
* @access public
diff --git a/app/Model/SubtaskTimeTracking.php b/app/Model/SubtaskTimeTracking.php
index 8b197c46..a984533f 100644
--- a/app/Model/SubtaskTimeTracking.php
+++ b/app/Model/SubtaskTimeTracking.php
@@ -2,6 +2,8 @@
namespace Model;
+use DateTime;
+
/**
* Subtask timesheet
*
@@ -33,6 +35,7 @@ class SubtaskTimeTracking extends Base
self::TABLE.'.subtask_id',
self::TABLE.'.end',
self::TABLE.'.start',
+ self::TABLE.'.time_spent',
Subtask::TABLE.'.task_id',
Subtask::TABLE.'.title AS subtask_title',
Task::TABLE.'.title AS task_title',
@@ -60,6 +63,7 @@ class SubtaskTimeTracking extends Base
self::TABLE.'.subtask_id',
self::TABLE.'.end',
self::TABLE.'.start',
+ self::TABLE.'.time_spent',
self::TABLE.'.user_id',
Subtask::TABLE.'.task_id',
Subtask::TABLE.'.title AS subtask_title',
@@ -89,6 +93,7 @@ class SubtaskTimeTracking extends Base
self::TABLE.'.subtask_id',
self::TABLE.'.end',
self::TABLE.'.start',
+ self::TABLE.'.time_spent',
self::TABLE.'.user_id',
Subtask::TABLE.'.task_id',
Subtask::TABLE.'.title AS subtask_title',
@@ -133,6 +138,8 @@ class SubtaskTimeTracking extends Base
->addCondition($this->getCalendarCondition($start, $end))
->findAll();
+ $result = $this->timetable->calculateEventsIntersect($user_id, $result, $start, $end);
+
return $this->toCalendarEvents($result);
}
@@ -235,7 +242,11 @@ class SubtaskTimeTracking extends Base
*/
public function logEndTime($subtask_id, $user_id)
{
- $this->updateSubtaskTimeSpent($subtask_id, $user_id);
+ $time_spent = $this->getTimeSpent($subtask_id, $user_id);
+
+ if ($time_spent > 0) {
+ $this->updateSubtaskTimeSpent($subtask_id, $time_spent);
+ }
return $this->db
->table(self::TABLE)
@@ -243,11 +254,60 @@ class SubtaskTimeTracking extends Base
->eq('user_id', $user_id)
->eq('end', 0)
->update(array(
- 'end' => time()
+ 'end' => time(),
+ 'time_spent' => $time_spent,
));
}
/**
+ * Calculate the time spent when the clock is stopped
+ *
+ * @access public
+ * @param integer $subtask_id
+ * @param integer $user_id
+ * @return float
+ */
+ public function getTimeSpent($subtask_id, $user_id)
+ {
+ $start_time = $this->db
+ ->table(self::TABLE)
+ ->eq('subtask_id', $subtask_id)
+ ->eq('user_id', $user_id)
+ ->eq('end', 0)
+ ->findOneColumn('start');
+
+ if ($start_time) {
+
+ $start = new DateTime;
+ $start->setTimestamp($start_time);
+
+ return $this->timetable->calculateEffectiveDuration($user_id, $start, new DateTime);
+ }
+
+ return 0;
+ }
+
+ /**
+ * Update subtask time spent
+ *
+ * @access public
+ * @param integer $subtask_id
+ * @param float $time_spent
+ * @return bool
+ */
+ public function updateSubtaskTimeSpent($subtask_id, $time_spent)
+ {
+ $subtask = $this->subtask->getById($subtask_id);
+
+ // Fire the event subtask.update
+ return $this->subtask->update(array(
+ 'id' => $subtask['id'],
+ 'time_spent' => $subtask['time_spent'] + $time_spent,
+ 'task_id' => $subtask['task_id'],
+ ));
+ }
+
+ /**
* Update task time tracking based on subtasks time tracking
*
* @access public
@@ -289,31 +349,4 @@ class SubtaskTimeTracking extends Base
)
->findOne();
}
-
- /**
- * Update subtask time spent based on the punch clock table
- *
- * @access public
- * @param integer $subtask_id
- * @param integer $user_id
- * @return bool
- */
- public function updateSubtaskTimeSpent($subtask_id, $user_id)
- {
- $start_time = $this->db
- ->table(self::TABLE)
- ->eq('subtask_id', $subtask_id)
- ->eq('user_id', $user_id)
- ->eq('end', 0)
- ->findOneColumn('start');
-
- $subtask = $this->subtask->getById($subtask_id);
-
- return $start_time &&
- $this->subtask->update(array( // Fire the event subtask.update
- 'id' => $subtask['id'],
- 'time_spent' => $subtask['time_spent'] + round((time() - $start_time) / 3600, 1),
- 'task_id' => $subtask['task_id'],
- ));
- }
}
diff --git a/app/Model/Timetable.php b/app/Model/Timetable.php
index eb37cefd..da2ec10c 100644
--- a/app/Model/Timetable.php
+++ b/app/Model/Timetable.php
@@ -25,6 +25,170 @@ class Timetable extends Base
private $timeoff;
/**
+ * Get a set of events by using the intersection between the timetable and the time tracking data
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $events Time tracking data
+ * @param string $start ISO8601 date
+ * @param string $end ISO8601 date
+ * @return array
+ */
+ public function calculateEventsIntersect($user_id, array $events, $start, $end)
+ {
+ $start_dt = new DateTime($start);
+ $start_dt->setTime(0, 0);
+
+ $end_dt = new DateTime($end);
+ $end_dt->setTime(23, 59);
+
+ $timetable = $this->calculate($user_id, $start_dt, $end_dt);
+
+ // The user has no timetable
+ if (empty($this->week)) {
+ return $events;
+ }
+
+ $results = array();
+
+ foreach ($events as $event) {
+ $results = array_merge($results, $this->calculateEventIntersect($event, $timetable));
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get a serie of events based on the timetable and the provided event
+ *
+ * @access public
+ * @param integer $user_id
+ * @param array $events Time tracking data
+ * @param string $start ISO8601 date
+ * @param string $end ISO8601 date
+ * @return array
+ */
+ public function calculateEventIntersect(array $event, array $timetable)
+ {
+ $events = array();
+
+ foreach ($timetable as $slot) {
+
+ $start_ts = $slot[0]->getTimestamp();
+ $end_ts = $slot[1]->getTimestamp();
+
+ if ($start_ts > $event['end']) {
+ break;
+ }
+
+ if ($event['start'] <= $start_ts) {
+ $event['start'] = $start_ts;
+ }
+
+ if ($event['start'] >= $start_ts && $event['start'] <= $end_ts) {
+
+ if ($event['end'] >= $end_ts) {
+ $events[] = array_merge($event, array('end' => $end_ts));
+ }
+ else {
+ $events[] = $event;
+ break;
+ }
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Calculate effective worked hours by taking into consideration the timetable
+ *
+ * @access public
+ * @param integer $user_id
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return float
+ */
+ public function calculateEffectiveDuration($user_id, DateTime $start, DateTime $end)
+ {
+ $end_timetable = clone($end);
+ $end_timetable->setTime(23, 59);
+
+ $timetable = $this->calculate($user_id, $start, $end_timetable);
+ $found_start = false;
+ $hours = 0;
+
+ // The user has no timetable
+ if (empty($this->week)) {
+ return $this->dateParser->getHours($start, $end);
+ }
+
+ foreach ($timetable as $slot) {
+
+ $isStartSlot = $this->dateParser->withinDateRange($start, $slot[0], $slot[1]);
+ $isEndSlot = $this->dateParser->withinDateRange($end, $slot[0], $slot[1]);
+
+ // Start and end are within the same time slot
+ if ($isStartSlot && $isEndSlot) {
+ return $this->dateParser->getHours($start, $end);
+ }
+
+ // We found the start slot
+ if (! $found_start && $isStartSlot) {
+ $found_start = true;
+ $hours = $this->dateParser->getHours($start, $slot[1]);
+ }
+ else if ($found_start) {
+
+ // We found the end slot
+ if ($isEndSlot) {
+ $hours += $this->dateParser->getHours($slot[0], $end);
+ break;
+ }
+ else {
+
+ // Sum hours of the intermediate time slots
+ $hours += $this->dateParser->getHours($slot[0], $slot[1]);
+ }
+ }
+ }
+
+ // The start date was not found in regular hours so we get the nearest time slot
+ if (! empty($timetable) && ! $found_start) {
+ $slot = $this->findClosestTimeSlot($start, $timetable);
+
+ if ($start < $slot[0]) {
+ return $this->calculateEffectiveDuration($user_id, $slot[0], $end);
+ }
+ }
+
+ return $hours;
+ }
+
+ /**
+ * Find the nearest time slot
+ *
+ * @access public
+ * @param DateTime $date
+ * @param array $timetable
+ * @return array
+ */
+ public function findClosestTimeSlot(DateTime $date, array $timetable)
+ {
+ $values = array();
+
+ foreach ($timetable as $slot) {
+ $t1 = abs($slot[0]->getTimestamp() - $date->getTimestamp());
+ $t2 = abs($slot[1]->getTimestamp() - $date->getTimestamp());
+
+ $values[] = min($t1, $t2);
+ }
+
+ asort($values);
+ return $timetable[key($values)];
+ }
+
+ /**
* Get the timetable for a user for a given date range
*
* @access public
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 03868748..b4db10a4 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,12 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 52;
+const VERSION = 53;
+
+function version_53($pdo)
+{
+ $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent FLOAT DEFAULT 0");
+}
function version_52($pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 124aec76..331205fa 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,12 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 33;
+const VERSION = 34;
+
+function version_34($pdo)
+{
+ $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0");
+}
function version_33($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 818ed78d..551e186e 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,12 @@ use Core\Security;
use PDO;
use Model\Link;
-const VERSION = 51;
+const VERSION = 52;
+
+function version_52($pdo)
+{
+ $pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0");
+}
function version_51($pdo)
{
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 6f597a5c..fc71ebf9 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -55,7 +55,6 @@ class ClassProvider implements ServiceProviderInterface
'TimetableWeek',
'TimetableOff',
'TimetableExtra',
- 'TimeTracking',
'User',
'UserSession',
'Webhook',
diff --git a/app/Template/budget/breakdown.php b/app/Template/budget/breakdown.php
new file mode 100644
index 00000000..d4168406
--- /dev/null
+++ b/app/Template/budget/breakdown.php
@@ -0,0 +1,34 @@
+<div class="page-header">
+ <h2><?= t('Budget') ?></h2>
+ <ul>
+ <li><?= $this->a(t('Budget lines'), 'budget', 'create', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->a(t('Cost breakdown'), 'budget', 'breakdown', array('project_id' => $project['id'])) ?></li>
+ </ul>
+</div>
+
+<?php if ($paginator->isEmpty()): ?>
+ <p class="alert"><?= t('There is nothing to show.') ?></p>
+<?php else: ?>
+ <table class="table-fixed">
+ <tr>
+ <th class="column-20"><?= $paginator->order(t('Task'), 'task_title') ?></th>
+ <th class="column-25"><?= $paginator->order(t('Subtask'), 'subtask_title') ?></th>
+ <th class="column-20"><?= $paginator->order(t('User'), 'username') ?></th>
+ <th class="column-10"><?= t('Cost') ?></th>
+ <th class="column-10"><?= $paginator->order(t('Time spent'), 'time_spent') ?></th>
+ <th class="column-15"><?= $paginator->order(t('Date'), 'start') ?></th>
+ </tr>
+ <?php foreach ($paginator->getCollection() as $record): ?>
+ <tr>
+ <td><?= $this->a($this->e($record['task_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td>
+ <td><?= $this->a($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $project['id'], 'task_id' => $record['task_id'])) ?></td>
+ <td><?= $this->e($record['name'] ?: $record['username']) ?></td>
+ <td><?= n($record['cost']) ?></td>
+ <td><?= n($record['time_spent']).' '.t('hours') ?></td>
+ <td><?= dt('%B %e, %Y', $record['start']) ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <?= $paginator ?>
+<?php endif ?> \ No newline at end of file
diff --git a/app/Template/budget/create.php b/app/Template/budget/create.php
index 0ff395c9..5a919ce6 100644
--- a/app/Template/budget/create.php
+++ b/app/Template/budget/create.php
@@ -2,7 +2,7 @@
<h2><?= t('Budget') ?></h2>
<ul>
<li><?= $this->a(t('Budget lines'), 'budget', 'create', array('project_id' => $project['id'])) ?></li>
- <li><?= $this->a(t('Burn rate'), 'budget', 'index', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->a(t('Cost breakdown'), 'budget', 'breakdown', array('project_id' => $project['id'])) ?></li>
</ul>
</div>
diff --git a/app/Template/budget/index.php b/app/Template/budget/index.php
index 8bdf1a57..bdeda781 100644
--- a/app/Template/budget/index.php
+++ b/app/Template/budget/index.php
@@ -2,7 +2,7 @@
<h2><?= t('Budget') ?></h2>
<ul>
<li><?= $this->a(t('Budget lines'), 'budget', 'create', array('project_id' => $project['id'])) ?></li>
- <li><?= $this->a(t('Burn rate'), 'budget', 'index', array('project_id' => $project['id'])) ?></li>
+ <li><?= $this->a(t('Cost breakdown'), 'budget', 'breakdown', array('project_id' => $project['id'])) ?></li>
</ul>
</div>
diff --git a/app/Template/task/time_tracking.php b/app/Template/task/time_tracking.php
index 1dea0f0b..55d33e5e 100644
--- a/app/Template/task/time_tracking.php
+++ b/app/Template/task/time_tracking.php
@@ -6,10 +6,11 @@
<?php else: ?>
<table class="table-fixed">
<tr>
- <th class="column-20"><?= $subtask_paginator->order(t('User'), 'username') ?></th>
- <th class="column-30"><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th>
- <th><?= $subtask_paginator->order(t('Start'), 'start') ?></th>
- <th><?= $subtask_paginator->order(t('End'), 'end') ?></th>
+ <th class="column-15"><?= $subtask_paginator->order(t('User'), 'username') ?></th>
+ <th><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th>
+ <th class="column-20"><?= $subtask_paginator->order(t('Start'), 'start') ?></th>
+ <th class="column-20"><?= $subtask_paginator->order(t('End'), 'end') ?></th>
+ <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), 'time_spent') ?></th>
</tr>
<?php foreach ($subtask_paginator->getCollection() as $record): ?>
<tr>
@@ -17,6 +18,7 @@
<td><?= t($record['subtask_title']) ?></td>
<td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td>
<td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td>
+ <td><?= n($record['time_spent']).' '.t('hours') ?></td>
</tr>
<?php endforeach ?>
</table>
diff --git a/app/Template/user/timesheet.php b/app/Template/user/timesheet.php
index 4f052006..3ae84df0 100644
--- a/app/Template/user/timesheet.php
+++ b/app/Template/user/timesheet.php
@@ -8,10 +8,11 @@
<?php else: ?>
<table class="table-fixed">
<tr>
- <th class="column-20"><?= $subtask_paginator->order(t('Task'), 'task_title') ?></th>
- <th class="column-20"><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th>
- <th><?= $subtask_paginator->order(t('Start'), 'start') ?></th>
- <th><?= $subtask_paginator->order(t('End'), 'end') ?></th>
+ <th class="column-25"><?= $subtask_paginator->order(t('Task'), 'task_title') ?></th>
+ <th class="column-25"><?= $subtask_paginator->order(t('Subtask'), 'subtask_title') ?></th>
+ <th class="column-20"><?= $subtask_paginator->order(t('Start'), 'start') ?></th>
+ <th class="column-20"><?= $subtask_paginator->order(t('End'), 'end') ?></th>
+ <th class="column-10"><?= $subtask_paginator->order(t('Time spent'), 'time_spent') ?></th>
</tr>
<?php foreach ($subtask_paginator->getCollection() as $record): ?>
<tr>
@@ -19,6 +20,7 @@
<td><?= $this->a($this->e($record['subtask_title']), 'task', 'show', array('project_id' => $record['project_id'], 'task_id' => $record['task_id'])) ?></td>
<td><?= dt('%B %e, %Y at %k:%M %p', $record['start']) ?></td>
<td><?= dt('%B %e, %Y at %k:%M %p', $record['end']) ?></td>
+ <td><?= n($record['time_spent']).' '.t('hours') ?></td>
</tr>
<?php endforeach ?>
</table>