diff options
-rw-r--r-- | app/Controller/Base.php | 1 | ||||
-rw-r--r-- | app/Controller/HourlyRate.php | 89 | ||||
-rw-r--r-- | app/Controller/User.php | 8 | ||||
-rw-r--r-- | app/Locale/fr_FR/translations.php | 13 | ||||
-rw-r--r-- | app/Model/Acl.php | 1 | ||||
-rw-r--r-- | app/Model/Config.php | 20 | ||||
-rw-r--r-- | app/Model/HourlyRate.php | 103 | ||||
-rw-r--r-- | app/Schema/Mysql.php | 15 | ||||
-rw-r--r-- | app/Schema/Postgres.php | 14 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 14 | ||||
-rw-r--r-- | app/ServiceProvider/ClassProvider.php | 1 | ||||
-rw-r--r-- | app/Template/hourlyrate/index.php | 46 | ||||
-rw-r--r-- | app/Template/hourlyrate/remove.php | 13 | ||||
-rw-r--r-- | app/Template/user/sidebar.php | 5 | ||||
-rw-r--r-- | tests/units/HourlyRate.php | 43 |
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); + } +} |