summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Controller/Base.php1
-rw-r--r--app/Controller/HourlyRate.php89
-rw-r--r--app/Controller/User.php8
-rw-r--r--app/Locale/fr_FR/translations.php13
-rw-r--r--app/Model/Acl.php1
-rw-r--r--app/Model/Config.php20
-rw-r--r--app/Model/HourlyRate.php103
-rw-r--r--app/Schema/Mysql.php15
-rw-r--r--app/Schema/Postgres.php14
-rw-r--r--app/Schema/Sqlite.php14
-rw-r--r--app/ServiceProvider/ClassProvider.php1
-rw-r--r--app/Template/hourlyrate/index.php46
-rw-r--r--app/Template/hourlyrate/remove.php13
-rw-r--r--app/Template/user/sidebar.php5
-rw-r--r--tests/units/HourlyRate.php43
15 files changed, 378 insertions, 8 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index d949048d..e175bc40 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -34,6 +34,7 @@ use Symfony\Component\EventDispatcher\Event;
* @property \Model\Config $config
* @property \Model\DateParser $dateParser
* @property \Model\File $file
+ * @property \Model\HourlyRate $hourlyRate
* @property \Model\LastLogin $lastLogin
* @property \Model\Notification $notification
* @property \Model\Project $project
diff --git a/app/Controller/HourlyRate.php b/app/Controller/HourlyRate.php
new file mode 100644
index 00000000..f8f88d17
--- /dev/null
+++ b/app/Controller/HourlyRate.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Controller;
+
+/**
+ * Hourly Rate controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class HourlyRate extends User
+{
+ /**
+ * Display rate and form
+ *
+ * @access public
+ */
+ public function index(array $values = array(), array $errors = array())
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('hourlyrate/index', array(
+ 'rates' => $this->hourlyRate->getAllByUser($user['id']),
+ 'currencies_list' => $this->config->getCurrencies(),
+ 'values' => $values + array('user_id' => $user['id']),
+ 'errors' => $errors,
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Validate and save a new rate
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ list($valid, $errors) = $this->hourlyRate->validateCreation($values);
+
+ if ($valid) {
+
+ if ($this->hourlyRate->create($values['user_id'], $values['rate'], $values['currency'], $values['date_effective'])) {
+ $this->session->flash(t('Hourly rate created successfully.'));
+ $this->response->redirect($this->helper->url('hourlyrate', 'index', array('user_id' => $values['user_id'])));
+ }
+ else {
+ $this->session->flashError(t('Unable to save the hourly rate.'));
+ }
+ }
+
+ $this->index($values, $errors);
+ }
+
+ /**
+ * Confirmation dialag box to remove a row
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->layout('hourlyrate/remove', array(
+ 'rate_id' => $this->request->getIntegerParam('rate_id'),
+ 'user' => $user,
+ )));
+ }
+
+ /**
+ * Remove a row
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $user = $this->getUser();
+
+ if ($this->hourlyRate->remove($this->request->getIntegerParam('rate_id'))) {
+ $this->session->flash(t('Rate removed successfully.'));
+ }
+ else {
+ $this->session->flash(t('Unable to remove this rate.'));
+ }
+
+ $this->response->redirect($this->helper->url('hourlyrate', 'index', array('user_id' => $user['id'])));
+ }
+}
diff --git a/app/Controller/User.php b/app/Controller/User.php
index decdb646..46d0214d 100644
--- a/app/Controller/User.php
+++ b/app/Controller/User.php
@@ -69,12 +69,12 @@ class User extends Base
/**
* Common layout for user views
*
- * @access private
+ * @access protected
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
- private function layout($template, array $params)
+ protected function layout($template, array $params)
{
$content = $this->template->render($template, $params);
$params['user_content_for_layout'] = $content;
@@ -90,10 +90,10 @@ class User extends Base
/**
* Common method to get the user
*
- * @access private
+ * @access protected
* @return array
*/
- private function getUser()
+ protected function getUser()
{
$user = $this->user->getById($this->request->getIntegerParam('user_id'));
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index 30531d78..50e96a47 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -1,6 +1,8 @@
<?php
return array(
+ 'number.decimals_separator' => ',',
+ 'number.thousands_separator' => ' ',
'None' => 'Aucun',
'edit' => 'modifier',
'Edit' => 'Modifier',
@@ -740,4 +742,15 @@ return array(
'Horizontal scrolling' => 'Défilement horizontal',
'Compact/wide view' => 'Basculer entre la vue compacte et étendue',
'No results match:' => 'Aucun résultat :',
+ 'Remove hourly rate' => 'Supprimer un taux horaire',
+ 'Do you really want to remove this hourly rate?' => 'Voulez-vous vraiment supprimer ce taux horaire ?',
+ 'Hourly rates' => 'Taux horaires',
+ 'Hourly rate' => 'Taux horaire',
+ 'Currency' => 'Devise',
+ 'Effective date' => 'Date d\'effet',
+ 'Add new rate' => 'Ajouter un nouveau taux horaire',
+ 'Rate removed successfully.' => 'Taux horaire supprimé avec succès.',
+ 'Unable to remove this rate.' => 'Impossible de supprimer ce taux horaire.',
+ 'Unable to save the hourly rate.' => 'Impossible de sauvegarder ce taux horaire.',
+ 'Hourly rate created successfully.' => 'Taux horaire créé avec succès.',
);
diff --git a/app/Model/Acl.php b/app/Model/Acl.php
index 9fc81747..56938f9d 100644
--- a/app/Model/Acl.php
+++ b/app/Model/Acl.php
@@ -70,6 +70,7 @@ class Acl extends Base
'config' => '*',
'link' => '*',
'project' => array('remove'),
+ 'hourlyrate' => '*',
);
/**
diff --git a/app/Model/Config.php b/app/Model/Config.php
index 3ee6bfdf..8afc59fd 100644
--- a/app/Model/Config.php
+++ b/app/Model/Config.php
@@ -22,6 +22,26 @@ class Config extends Base
const TABLE = 'settings';
/**
+ * Get available currencies
+ *
+ * @access public
+ * @return array
+ */
+ public function getCurrencies()
+ {
+ return array(
+ 'USD' => t('USD - US Dollar'),
+ 'EUR' => t('EUR - Euro'),
+ 'GBP' => t('GBP - British Pound'),
+ 'CAD' => t('CAD - Canadian Dollar'),
+ 'AUD' => t('AUD - Australian Dollar'),
+ 'NZD' => t('NZD - New Zealand Dollar'),
+ 'INR' => t('INR - Indian Rupee'),
+ 'JPY' => t('JPY - Japanese Yen'),
+ );
+ }
+
+ /**
* Get available timezones
*
* @access public
diff --git a/app/Model/HourlyRate.php b/app/Model/HourlyRate.php
new file mode 100644
index 00000000..c2ce3eaf
--- /dev/null
+++ b/app/Model/HourlyRate.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Model;
+
+use SimpleValidator\Validator;
+use SimpleValidator\Validators;
+
+/**
+ * Hourly Rate
+ *
+ * @package model
+ * @author Frederic Guillot
+ */
+class HourlyRate extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'hourly_rates';
+
+ /**
+ * Get all rates for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return array
+ */
+ public function getAllByUser($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_effective')->findAll();
+ }
+
+ /**
+ * Get current rate for a given user
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return float
+ */
+ public function getCurrentRate($user_id)
+ {
+ return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_effective')->findOneColumn('rate') ?: 0;
+ }
+
+ /**
+ * Add a new rate in the database
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @param float $rate Hourly rate
+ * @param string $currency Currency code
+ * @param string $date ISO8601 date format
+ * @return boolean|integer
+ */
+ public function create($user_id, $rate, $currency, $date)
+ {
+ $values = array(
+ 'user_id' => $user_id,
+ 'rate' => $rate,
+ 'currency' => $currency,
+ 'date_effective' => $this->dateParser->removeTimeFromTimestamp($this->dateParser->getTimestamp($date)),
+ );
+
+ return $this->persist(self::TABLE, $values);
+ }
+
+ /**
+ * Remove a specific rate
+ *
+ * @access public
+ * @param integer $rate_id
+ * @return boolean
+ */
+ public function remove($rate_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $rate_id)->remove();
+ }
+
+ /**
+ * Validate creation
+ *
+ * @access public
+ * @param array $values Form values
+ * @return array $valid, $errors [0] = Success or not, [1] = List of errors
+ */
+ public function validateCreation(array $values)
+ {
+ $v = new Validator($values, array(
+ new Validators\Required('user_id', t('Field required')),
+ new Validators\Required('rate', t('Field required')),
+ new Validators\Numeric('rate', t('This value must be numeric')),
+ new Validators\Required('date_effective', t('Field required')),
+ new Validators\Required('currency', t('Field required')),
+ ));
+
+ return array(
+ $v->execute(),
+ $v->getErrors()
+ );
+ }
+}
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 21c304c1..a4b8c25a 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,20 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 49;
+const VERSION = 50;
+
+function version_50($pdo)
+{
+ $pdo->exec("CREATE TABLE hourly_rates (
+ id INT NOT NULL AUTO_INCREMENT,
+ user_id INT NOT NULL,
+ rate FLOAT DEFAULT 0,
+ date_effective INTEGER NOT NULL,
+ currency TEXT NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8");
+}
function version_49($pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index df003267..7054bca2 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,19 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 30;
+const VERSION = 31;
+
+function version_31($pdo)
+{
+ $pdo->exec("CREATE TABLE hourly_rates (
+ id SERIAL,
+ user_id INTEGER NOT NULL,
+ rate REAL DEFAULT 0,
+ date_effective INTEGER NOT NULL,
+ currency TEXT NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )");
+}
function version_30($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 9134760f..a389fc17 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,19 @@ use Core\Security;
use PDO;
use Model\Link;
-const VERSION = 48;
+const VERSION = 49;
+
+function version_49($pdo)
+{
+ $pdo->exec("CREATE TABLE hourly_rates (
+ id INTEGER PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ rate REAL DEFAULT 0,
+ date_effective INTEGER NOT NULL,
+ currency TEXT NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )");
+}
function version_48($pdo)
{
diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php
index 213972ed..67f59dd8 100644
--- a/app/ServiceProvider/ClassProvider.php
+++ b/app/ServiceProvider/ClassProvider.php
@@ -23,6 +23,7 @@ class ClassProvider implements ServiceProviderInterface
'Config',
'DateParser',
'File',
+ 'HourlyRate',
'LastLogin',
'Link',
'Notification',
diff --git a/app/Template/hourlyrate/index.php b/app/Template/hourlyrate/index.php
new file mode 100644
index 00000000..9d0b77c8
--- /dev/null
+++ b/app/Template/hourlyrate/index.php
@@ -0,0 +1,46 @@
+<div class="page-header">
+ <h2><?= t('Hourly rates') ?></h2>
+</div>
+
+<?php if (! empty($rates)): ?>
+
+<table>
+ <tr>
+ <th><?= t('Hourly rate') ?></th>
+ <th><?= t('Currency') ?></th>
+ <th><?= t('Effective date') ?></th>
+ <th><?= t('Action') ?></th>
+ </tr>
+ <?php foreach ($rates as $rate): ?>
+ <tr>
+ <td><?= n($rate['rate']) ?></td>
+ <td><?= $rate['currency'] ?></td>
+ <td><?= dt('%b %e, %Y', $rate['date_effective']) ?></td>
+ <td>
+ <?= $this->a(t('Remove'), 'hourlyrate', 'confirm', array('user_id' => $user['id'], 'rate_id' => $rate['id'])) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+</table>
+
+<h3><?= t('Add new rate') ?></h3>
+<?php endif ?>
+
+<form method="post" action="<?= $this->u('hourlyrate', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->formHidden('user_id', $values) ?>
+ <?= $this->formCsrf() ?>
+
+ <?= $this->formLabel(t('Hourly rate'), 'rate') ?>
+ <?= $this->formText('rate', $values, $errors, array('required'), 'form-numeric') ?>
+
+ <?= $this->formLabel(t('Currency'), 'currency') ?>
+ <?= $this->formSelect('currency', $currencies_list, $values, $errors, array('required')) ?>
+
+ <?= $this->formLabel(t('Effective date'), 'date_effective') ?>
+ <?= $this->formText('date_effective', $values, $errors, array('required'), 'form-date') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
diff --git a/app/Template/hourlyrate/remove.php b/app/Template/hourlyrate/remove.php
new file mode 100644
index 00000000..7f22728e
--- /dev/null
+++ b/app/Template/hourlyrate/remove.php
@@ -0,0 +1,13 @@
+<div class="page-header">
+ <h2><?= t('Remove hourly rate') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info"><?= t('Do you really want to remove this hourly rate?') ?></p>
+
+ <div class="form-actions">
+ <?= $this->a(t('Yes'), 'hourlyrate', 'remove', array('user_id' => $user['id'], 'rate_id' => $rate_id), true, 'btn btn-red') ?>
+ <?= t('or') ?>
+ <?= $this->a(t('cancel'), 'hourlyrate', 'index', array('user_id' => $user['id'])) ?>
+ </div>
+</div> \ No newline at end of file
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index e41851a9..88977a9e 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -32,7 +32,7 @@
<?= $this->a(t('Time tracking'), 'user', 'timesheet', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
-
+
<?php if ($this->userSession->isAdmin()): ?>
<li>
<?= $this->a(t('User dashboard'), 'app', 'dashboard', array('user_id' => $user['id'])) ?>
@@ -40,6 +40,9 @@
<li>
<?= $this->a(t('User calendar'), 'user', 'calendar', array('user_id' => $user['id'])) ?>
</li>
+ <li>
+ <?= $this->a(t('Hourly rates'), 'hourlyrate', 'index', array('user_id' => $user['id'])) ?>
+ </li>
<?php endif ?>
<?php if ($this->userSession->isAdmin() && ! $this->userSession->isCurrentUser($user['id'])): ?>
diff --git a/tests/units/HourlyRate.php b/tests/units/HourlyRate.php
new file mode 100644
index 00000000..5daf0446
--- /dev/null
+++ b/tests/units/HourlyRate.php
@@ -0,0 +1,43 @@
+<?php
+
+require_once __DIR__.'/Base.php';
+
+use Model\User;
+use Model\HourlyRate;
+
+class HourlyRateTest extends Base
+{
+ public function testCreation()
+ {
+ $hr = new HourlyRate($this->container);
+ $this->assertEquals(1, $hr->create(1, 32.4, 'EUR', '2015-01-01'));
+ $this->assertEquals(2, $hr->create(1, 42, 'CAD', '2015-02-01'));
+
+ $rates = $hr->getAllByUser(0);
+ $this->assertEmpty($rates);
+
+ $rates = $hr->getAllByUser(1);
+ $this->assertNotEmpty($rates);
+ $this->assertCount(2, $rates);
+
+ $this->assertEquals(42, $rates[0]['rate']);
+ $this->assertEquals('CAD', $rates[0]['currency']);
+ $this->assertEquals('2015-02-01', date('Y-m-d', $rates[0]['date_effective']));
+
+ $this->assertEquals(32.4, $rates[1]['rate']);
+ $this->assertEquals('EUR', $rates[1]['currency']);
+ $this->assertEquals('2015-01-01', date('Y-m-d', $rates[1]['date_effective']));
+
+ $this->assertEquals(0, $hr->getCurrentRate(0));
+ $this->assertEquals(42, $hr->getCurrentRate(1));
+
+ $this->assertTrue($hr->remove(2));
+ $this->assertEquals(32.4, $hr->getCurrentRate(1));
+
+ $this->assertTrue($hr->remove(1));
+ $this->assertEquals(0, $hr->getCurrentRate(1));
+
+ $rates = $hr->getAllByUser(1);
+ $this->assertEmpty($rates);
+ }
+}