Move timetable to a plugin

Plugin repository: https://github.com/kanboard/plugin-timetable
This commit is contained in:
Frederic Guillot 2015-09-20 18:24:15 -04:00
parent 2021dccc5a
commit e6f547abcf
59 changed files with 137 additions and 2468 deletions

View File

@ -14,6 +14,7 @@ Core functionalities moved to plugins:
* Budget planning: https://github.com/kanboard/plugin-budget
* SubtaskForecast: https://github.com/kanboard/plugin-subtask-forecast
* Timetable: https://github.com/kanboard/plugin-timetable
Improvements:
@ -30,7 +31,7 @@ Improvements:
Bug fixes:
* Fix typo in template that prevent the Gitlab oauth link to be displayed
* Fix typo in template that prevent the Gitlab OAuth link to be displayed
* Fix Markdown preview links focus
* Avoid dropdown menu to be truncated inside a column with scrolling
* Deleting subtask doesn't update task time tracking

View File

@ -1,39 +0,0 @@
<?php
namespace Controller;
use DateTime;
/**
* Timetable controller
*
* @package controller
* @author Frederic Guillot
*/
class Timetable extends User
{
/**
* Display timetable for the user
*
* @access public
*/
public function index()
{
$user = $this->getUser();
$from = $this->request->getStringParam('from', date('Y-m-d'));
$to = $this->request->getStringParam('to', date('Y-m-d', strtotime('next week')));
$timetable = $this->timetable->calculate($user['id'], new DateTime($from), new DateTime($to));
$this->response->html($this->layout('timetable/index', array(
'user' => $user,
'timetable' => $timetable,
'values' => array(
'from' => $from,
'to' => $to,
'controller' => 'timetable',
'action' => 'index',
'user_id' => $user['id'],
),
)));
}
}

View File

@ -1,88 +0,0 @@
<?php
namespace Controller;
/**
* Day Timetable controller
*
* @package controller
* @author Frederic Guillot
*/
class Timetableday extends User
{
/**
* Display timetable for the user
*
* @access public
*/
public function index(array $values = array(), array $errors = array())
{
$user = $this->getUser();
$this->response->html($this->layout('timetable_day/index', array(
'timetable' => $this->timetableDay->getByUser($user['id']),
'values' => $values + array('user_id' => $user['id']),
'errors' => $errors,
'user' => $user,
)));
}
/**
* Validate and save
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->timetableDay->validateCreation($values);
if ($valid) {
if ($this->timetableDay->create($values['user_id'], $values['start'], $values['end'])) {
$this->session->flash(t('Time slot created successfully.'));
$this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $values['user_id'])));
}
else {
$this->session->flashError(t('Unable to save this time slot.'));
}
}
$this->index($values, $errors);
}
/**
* Confirmation dialag box to remove a row
*
* @access public
*/
public function confirm()
{
$user = $this->getUser();
$this->response->html($this->layout('timetable_day/remove', array(
'slot_id' => $this->request->getIntegerParam('slot_id'),
'user' => $user,
)));
}
/**
* Remove a row
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$user = $this->getUser();
if ($this->timetableDay->remove($this->request->getIntegerParam('slot_id'))) {
$this->session->flash(t('Time slot removed successfully.'));
}
else {
$this->session->flash(t('Unable to remove this time slot.'));
}
$this->response->redirect($this->helper->url->to('timetableday', 'index', array('user_id' => $user['id'])));
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace Controller;
/**
* Over-time Timetable controller
*
* @package controller
* @author Frederic Guillot
*/
class Timetableextra extends Timetableoff
{
protected $model = 'timetableExtra';
protected $controller_url = 'timetableextra';
protected $template_dir = 'timetable_extra';
}

View File

@ -1,107 +0,0 @@
<?php
namespace Controller;
/**
* Time-off Timetable controller
*
* @package controller
* @author Frederic Guillot
*/
class Timetableoff extends User
{
protected $model = 'timetableOff';
protected $controller_url = 'timetableoff';
protected $template_dir = 'timetable_off';
/**
* Display timetable for the user
*
* @access public
*/
public function index(array $values = array(), array $errors = array())
{
$user = $this->getUser();
$paginator = $this->paginator
->setUrl($this->controller_url, 'index', array('user_id' => $user['id']))
->setMax(10)
->setOrder('date')
->setDirection('desc')
->setQuery($this->{$this->model}->getUserQuery($user['id']))
->calculate();
$this->response->html($this->layout($this->template_dir.'/index', array(
'values' => $values + array('user_id' => $user['id']),
'errors' => $errors,
'paginator' => $paginator,
'user' => $user,
)));
}
/**
* Validate and save
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->{$this->model}->validateCreation($values);
if ($valid) {
if ($this->{$this->model}->create(
$values['user_id'],
$values['date'],
isset($values['all_day']) && $values['all_day'] == 1,
$values['start'],
$values['end'],
$values['comment'])) {
$this->session->flash(t('Time slot created successfully.'));
$this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $values['user_id'])));
}
else {
$this->session->flashError(t('Unable to save this time slot.'));
}
}
$this->index($values, $errors);
}
/**
* Confirmation dialag box to remove a row
*
* @access public
*/
public function confirm()
{
$user = $this->getUser();
$this->response->html($this->layout($this->template_dir.'/remove', array(
'slot_id' => $this->request->getIntegerParam('slot_id'),
'user' => $user,
)));
}
/**
* Remove a row
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$user = $this->getUser();
if ($this->{$this->model}->remove($this->request->getIntegerParam('slot_id'))) {
$this->session->flash(t('Time slot removed successfully.'));
}
else {
$this->session->flash(t('Unable to remove this time slot.'));
}
$this->response->redirect($this->helper->url->to($this->controller_url, 'index', array('user_id' => $user['id'])));
}
}

View File

@ -1,99 +0,0 @@
<?php
namespace Controller;
/**
* Week Timetable controller
*
* @package controller
* @author Frederic Guillot
*/
class Timetableweek extends User
{
/**
* Display timetable for the user
*
* @access public
*/
public function index(array $values = array(), array $errors = array())
{
$user = $this->getUser();
if (empty($values)) {
$day = $this->timetableDay->getByUser($user['id']);
$values = array(
'user_id' => $user['id'],
'start' => isset($day[0]['start']) ? $day[0]['start'] : null,
'end' => isset($day[0]['end']) ? $day[0]['end'] : null,
);
}
$this->response->html($this->layout('timetable_week/index', array(
'timetable' => $this->timetableWeek->getByUser($user['id']),
'values' => $values,
'errors' => $errors,
'user' => $user,
)));
}
/**
* Validate and save
*
* @access public
*/
public function save()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->timetableWeek->validateCreation($values);
if ($valid) {
if ($this->timetableWeek->create($values['user_id'], $values['day'], $values['start'], $values['end'])) {
$this->session->flash(t('Time slot created successfully.'));
$this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $values['user_id'])));
}
else {
$this->session->flashError(t('Unable to save this time slot.'));
}
}
$this->index($values, $errors);
}
/**
* Confirmation dialag box to remove a row
*
* @access public
*/
public function confirm()
{
$user = $this->getUser();
$this->response->html($this->layout('timetable_week/remove', array(
'slot_id' => $this->request->getIntegerParam('slot_id'),
'user' => $user,
)));
}
/**
* Remove a row
*
* @access public
*/
public function remove()
{
$this->checkCSRFParam();
$user = $this->getUser();
if ($this->timetableWeek->remove($this->request->getIntegerParam('slot_id'))) {
$this->session->flash(t('Time slot removed successfully.'));
}
else {
$this->session->flash(t('Unable to remove this time slot.'));
}
$this->response->redirect($this->helper->url->to('timetableweek', 'index', array('user_id' => $user['id'])));
}
}

View File

@ -70,11 +70,6 @@ use Pimple\Container;
* @property \Model\TaskPosition $taskPosition
* @property \Model\TaskStatus $taskStatus
* @property \Model\TaskValidator $taskValidator
* @property \Model\Timetable $timetable
* @property \Model\TimetableDay $timetableDay
* @property \Model\TimetableExtra $timetableExtra
* @property \Model\TimetableOff $timetableOff
* @property \Model\TimetableWeek $timetableWeek
* @property \Model\Transition $transition
* @property \Model\User $user
* @property \Model\UserSession $userSession

View File

@ -46,6 +46,18 @@ class Hook
return isset($this->hooks[$hook]) ? $this->hooks[$hook] : array();
}
/**
* Return true if the hook is used
*
* @access public
* @param string $hook
* @return boolean
*/
public function exists($hook)
{
return isset($this->hooks[$hook]);
}
/**
* Merge listener results with input array
*
@ -67,4 +79,21 @@ class Hook
return $values;
}
/**
* Execute only first listener
*
* @access public
* @param string $hook
* @param array $params
* @return mixed
*/
public function first($hook, array $params = array())
{
foreach ($this->getListeners($hook) as $listener) {
return call_user_func_array($listener, $params);
}
return null;
}
}

View File

@ -67,7 +67,7 @@ class Loader extends \Core\Base
$filename = __DIR__.'/../../../plugins/'.$plugin.'/Schema/'.ucfirst(DB_DRIVER).'.php';
if (file_exists($filename)) {
require($filename);
require_once($filename);
$this->migrateSchema($plugin);
}
}

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Kompaktní/plné zobrazení',
'No results match:' => 'Žádná shoda:',
'Currency' => 'Měna',
'Start time' => 'Počáteční datum',
'End time' => 'Konečné datum',
'Comment' => 'Komentář',
'All day' => 'Všechny dny',
'Day' => 'Den',
'Manage timetable' => 'Spravovat pracovní dobu',
'Overtime timetable' => 'Přesčasy',
'Time off timetable' => 'Pracovní volno',
'Timetable' => 'Pracovní doba',
'Work timetable' => 'Pracovní doba',
'Week timetable' => 'Týdenní pracovní doba',
'Day timetable' => 'Denní pracovní doba',
'From' => 'Od',
'To' => 'Do',
'Time slot created successfully.' => 'Časový úsek byl úspěšně vytvořen.',
'Unable to save this time slot.' => 'Nelze uložit tento časový úsek.',
'Time slot removed successfully.' => 'Časový úsek byl odstraněn.',
'Unable to remove this time slot.' => 'Nelze odstranit tento časový úsek',
'Do you really want to remove this time slot?' => 'Opravdu chcete odstranit tento časový úsek?',
'Remove time slot' => 'Odstranit časový úsek',
'Add new time slot' => 'Přidat nový časový úsek',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Tato pracovní doba se použije když je zaškrtnuto políčko "Celý den" pro plánovanou pracovní dobu i přesčas .',
'Files' => 'Soubory',
'Images' => 'Obrázky',
'Private project' => 'Soukromý projekt',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Přesunout úkol do jiného sloupce, když je úkol přiřazen uživateli.',
'Move the task to another column when assignee is cleared' => 'Přesunout úkol do jiného sloupce, když je pověření uživatele vymazáno.',
'Source column' => 'Zdrojový sloupec',
// 'Show subtask estimates (forecast of future work)' => '',
'Transitions' => 'Změny etap',
'Executer' => 'Vykonavatel',
'Time spent in the column' => 'Trvání jednotlivých etap',

View File

@ -666,28 +666,6 @@ return array(
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
// 'Start time' => '',
// 'End time' => '',
// 'Comment' => '',
// 'All day' => '',
// 'Day' => '',
// 'Manage timetable' => '',
// 'Overtime timetable' => '',
// 'Time off timetable' => '',
// 'Timetable' => '',
// 'Work timetable' => '',
// 'Week timetable' => '',
// 'Day timetable' => '',
// 'From' => '',
// 'To' => '',
// 'Time slot created successfully.' => '',
// 'Unable to save this time slot.' => '',
// 'Time slot removed successfully.' => '',
// 'Unable to remove this time slot.' => '',
// 'Do you really want to remove this time slot?' => '',
// 'Remove time slot' => '',
// 'Add new time slot' => '',
// 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
// 'Files' => '',
// 'Images' => '',
// 'Private project' => '',
@ -707,7 +685,6 @@ return array(
// 'Move the task to another column when assigned to a user' => '',
// 'Move the task to another column when assignee is cleared' => '',
// 'Source column' => '',
// 'Show subtask estimates (forecast of future work)' => '',
// 'Transitions' => '',
// 'Executer' => '',
// 'Time spent in the column' => '',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Kompakt/Breite-Ansicht',
'No results match:' => 'Keine Ergebnisse:',
'Currency' => 'Währung',
'Start time' => 'Startzeit',
'End time' => 'Endzeit',
'Comment' => 'Kommentar',
'All day' => 'ganztägig',
'Day' => 'Tag',
'Manage timetable' => 'Zeitplan verwalten',
'Overtime timetable' => 'Überstunden Zeitplan',
'Time off timetable' => 'Freizeit Zeitplan',
'Timetable' => 'Zeitplan',
'Work timetable' => 'Arbeitszeitplan',
'Week timetable' => 'Wochenzeitplan',
'Day timetable' => 'Tageszeitplan',
'From' => 'von',
'To' => 'bis',
'Time slot created successfully.' => 'Zeitfenster erfolgreich erstellt.',
'Unable to save this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu speichern.',
'Time slot removed successfully.' => 'Zeitfenster erfolgreich entfernt.',
'Unable to remove this time slot.' => 'Nicht in der Lage, dieses Zeitfenster zu entfernen',
'Do you really want to remove this time slot?' => 'Soll diese Zeitfenster wirklich gelöscht werden?',
'Remove time slot' => 'Zeitfenster entfernen',
'Add new time slot' => 'Neues Zeitfenster hinzufügen',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Dieses Zeitfenster wird verwendet, wenn die Checkbox "ganztägig" für Freizeit und Überstunden angeklickt ist.',
'Files' => 'Dateien',
'Images' => 'Bilder',
'Private project' => 'privates Projekt',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Aufgabe in eine andere Spalte verschieben, wenn ein User zugeordnet wurde.',
'Move the task to another column when assignee is cleared' => 'Aufgabe in eine andere Spalte verschieben, wenn die Zuordnung gelöscht wurde.',
'Source column' => 'Quellspalte',
'Show subtask estimates (forecast of future work)' => 'Teilaufgaben-Schätzungen anzeigen (Prognose)',
'Transitions' => 'Übergänge',
'Executer' => 'Ausführender',
'Time spent in the column' => 'Zeit in Spalte verbracht',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Vista compacta/amplia',
'No results match:' => 'No hay resultados coincidentes:',
'Currency' => 'Moneda',
'Start time' => 'Tiempo de inicio',
'End time' => 'Tiempo de fin',
'Comment' => 'Comentario',
'All day' => 'Todos los días',
'Day' => 'Día',
'Manage timetable' => 'Gestionar horario',
'Overtime timetable' => 'Horario de tiempo extra',
'Time off timetable' => 'Horario de tiempo libre',
'Timetable' => 'Horario',
'Work timetable' => 'Horario de trabajo',
'Week timetable' => 'Horario semanal',
'Day timetable' => 'Horario diario',
'From' => 'De',
'To' => 'Para',
'Time slot created successfully.' => 'Intervalo de tiempo creado correctamente.',
'Unable to save this time slot.' => 'No pude grabar este intervalo de tiempo.',
'Time slot removed successfully.' => 'Intervalo de tiempo quitado correctamente.',
'Unable to remove this time slot.' => 'No pude quitar este intervalo de tiempo.',
'Do you really want to remove this time slot?' => '¿Realmente quiere quitar este intervalo de tiempo?',
'Remove time slot' => 'Quitar intervalo de tiempo',
'Add new time slot' => 'Añadir nuevo intervalo de tiempo',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Este horario se usa cuando se marca la casilla "todos los días" para calendario de tiempo libre y horas extras.',
'Files' => 'Ficheros',
'Images' => 'Imágenes',
'Private project' => 'Proyecto privado',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Mover la tarea a otra columna al asignarse al usuario',
'Move the task to another column when assignee is cleared' => 'Mover la tarea a otra columna al quitar el concesionario',
'Source column' => 'Columna fuente',
'Show subtask estimates (forecast of future work)' => 'Mostrar estimaciones para la subtarea (pronóstico de trabajo futuro)',
'Transitions' => 'Transiciones',
'Executer' => 'Ejecutor',
'Time spent in the column' => 'Tiempo transcurrido en la columna',

View File

@ -666,28 +666,6 @@ return array(
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
// 'Start time' => '',
// 'End time' => '',
// 'Comment' => '',
// 'All day' => '',
// 'Day' => '',
// 'Manage timetable' => '',
// 'Overtime timetable' => '',
// 'Time off timetable' => '',
// 'Timetable' => '',
// 'Work timetable' => '',
// 'Week timetable' => '',
// 'Day timetable' => '',
// 'From' => '',
// 'To' => '',
// 'Time slot created successfully.' => '',
// 'Unable to save this time slot.' => '',
// 'Time slot removed successfully.' => '',
// 'Unable to remove this time slot.' => '',
// 'Do you really want to remove this time slot?' => '',
// 'Remove time slot' => '',
// 'Add new time slot' => '',
// 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
// 'Files' => '',
// 'Images' => '',
// 'Private project' => '',
@ -707,7 +685,6 @@ return array(
// 'Move the task to another column when assigned to a user' => '',
// 'Move the task to another column when assignee is cleared' => '',
// 'Source column' => '',
// 'Show subtask estimates (forecast of future work)' => '',
// 'Transitions' => '',
// 'Executer' => '',
// 'Time spent in the column' => '',

View File

@ -668,28 +668,6 @@ return array(
'Compact/wide view' => 'Basculer entre la vue compacte et étendue',
'No results match:' => 'Aucun résultat :',
'Currency' => 'Devise',
'Start time' => 'Date de début',
'End time' => 'Date de fin',
'Comment' => 'Commentaire',
'All day' => 'Toute la journée',
'Day' => 'Jour',
'Manage timetable' => 'Gérer les horaires',
'Overtime timetable' => 'Heures supplémentaires',
'Time off timetable' => 'Heures d\'absences',
'Timetable' => 'Horaires',
'Work timetable' => 'Horaires travaillés',
'Week timetable' => 'Horaires de la semaine',
'Day timetable' => 'Horaire d\'une journée',
'From' => 'Depuis',
'To' => 'À',
'Time slot created successfully.' => 'Créneau horaire créé avec succès.',
'Unable to save this time slot.' => 'Impossible de sauvegarder ce créneau horaire.',
'Time slot removed successfully.' => 'Créneau horaire supprimé avec succès.',
'Unable to remove this time slot.' => 'Impossible de supprimer ce créneau horaire.',
'Do you really want to remove this time slot?' => 'Voulez-vous vraiment supprimer ce créneau horaire ?',
'Remove time slot' => 'Supprimer un créneau horaire',
'Add new time slot' => 'Ajouter un créneau horaire',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ces horaires sont utilisés lorsque la case « Toute la journée » est cochée pour les heures d\'absences ou supplémentaires programmées.',
'Files' => 'Fichiers',
'Images' => 'Images',
'Private project' => 'Projet privé',
@ -709,7 +687,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci est assignée à quelqu\'un',
'Move the task to another column when assignee is cleared' => 'Déplacer la tâche dans une autre colonne lorsque celle-ci n\'est plus assignée',
'Source column' => 'Colonne d\'origine',
'Show subtask estimates (forecast of future work)' => 'Afficher l\'estimation des sous-tâches (prévision du travail à venir)',
'Transitions' => 'Transitions',
'Executer' => 'Exécutant',
'Time spent in the column' => 'Temps passé dans la colonne',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Kompakt/széles nézet',
'No results match:' => 'Nincs találat:',
'Currency' => 'Pénznem',
'Start time' => 'Kezdés ideje',
'End time' => 'Végzés ideje',
'Comment' => 'Megjegyzés',
'All day' => 'Egész nap',
'Day' => 'Nap',
'Manage timetable' => 'Időbeosztás kezelése',
'Overtime timetable' => 'Túlóra időbeosztás',
'Time off timetable' => 'Szabadság időbeosztás',
'Timetable' => 'Időbeosztás',
'Work timetable' => 'Munka időbeosztás',
'Week timetable' => 'Heti időbeosztás',
'Day timetable' => 'Napi időbeosztás',
'From' => 'Feladó:',
'To' => 'Címzett:',
'Time slot created successfully.' => 'Időszelet sikeresen létrehozva.',
'Unable to save this time slot.' => 'Időszelet mentése sikertelen.',
'Time slot removed successfully.' => 'Időszelet sikeresen törölve.',
'Unable to remove this time slot.' => 'Időszelet törlése sikertelen.',
'Do you really want to remove this time slot?' => 'Biztos törli ezt az időszeletet?',
'Remove time slot' => 'Időszelet törlése',
'Add new time slot' => 'Új Időszelet',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ez az időbeosztás van használatban ha az "egész nap" jelölőnégyzet be van jelölve a tervezett szabadságnál és túlóránál.',
'Files' => 'Fájlok',
'Images' => 'Képek',
'Private project' => 'Privát projekt',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés után',
'Move the task to another column when assignee is cleared' => 'Feladat másik oszlopba helyezése felhasználóhoz rendélés törlésekor',
'Source column' => 'Forrás oszlop',
// 'Show subtask estimates (forecast of future work)' => '',
// 'Transitions' => '',
// 'Executer' => '',
// 'Time spent in the column' => '',

View File

@ -25,7 +25,7 @@ return array(
'Dark Grey' => 'Abu-abu Gelap',
'Pink' => 'Merah Muda',
'Teal' => 'Teal',
'Cyan'=> 'Sian',
'Cyan' => 'Sian',
'Lime' => 'Lime',
'Light Green' => 'Hijau Muda',
'Amber' => 'Amber',
@ -338,7 +338,7 @@ return array(
'Display another project' => 'Lihat proyek lain',
'Login with my Github Account' => 'Masuk menggunakan akun Github saya',
'Link my Github Account' => 'Hubungkan akun Github saya ',
'Unlink my Github Account' => 'Putuskan akun Github saya',
'Unlink my Github Account' => 'Putuskan akun Github saya',
'Created by %s' => 'Dibuat oleh %s',
'Last modified on %B %e, %Y at %k:%M %p' => 'Modifikasi terakhir pada tanggal %d/%m/%Y à %H:%M',
'Tasks Export' => 'Ekspor Tugas',
@ -365,14 +365,12 @@ return array(
'Time tracking:' => 'Pelacakan waktu :',
'New sub-task' => 'Sub-tugas baru',
'New attachment added "%s"' => 'Lampiran baru ditambahkan « %s »',
'Comment updated' => 'Komentar ditambahkan',
'Comment updated' => 'Komentar diperbaharui',
'New comment posted by %s' => 'Komentar baru ditambahkan oleh « %s »',
'New attachment' => 'Lampirkan baru',
'New comment' => 'Komentar baru',
'Comment updated' => 'Komentar diperbaharui',
'New subtask' => 'Sub-tugas baru',
'Subtask updated' => 'Sub-tugas diperbaharui',
'New task' => 'Tugas baru',
'Task updated' => 'Tugas diperbaharui',
'Task closed' => 'Tugas ditutup',
'Task opened' => 'Tugas dibuka',
@ -397,8 +395,6 @@ return array(
'Remote' => 'Jauh',
'Enabled' => 'Aktif',
'Disabled' => 'Nonaktif',
'Google account linked' => 'Akun Google yang terhubung',
'Github account linked' => 'Akun Github yang terhubung',
'Username:' => 'Nama pengguna :',
'Name:' => 'Nama :',
'Email:' => 'Email :',
@ -669,75 +665,26 @@ return array(
'Horizontal scrolling' => 'Horisontal bergulir',
'Compact/wide view' => 'Beralih antara tampilan kompak dan diperluas',
'No results match:' => 'Tidak ada hasil :',
'Remove hourly rate' => 'Hapus tarif per jam',
'Do you really want to remove this hourly rate?' => 'Apakah anda yakin akan menghapus tarif per jam ini?',
'Hourly rates' => 'Tarif per jam',
'Hourly rate' => 'Tarif per jam',
'Currency' => 'Mata uang',
'Effective date' => 'Tanggal berlaku',
'Add new rate' => 'Tambah tarif per jam baru',
'Rate removed successfully.' => 'Tarif per jam berhasil dihapus.',
'Unable to remove this rate.' => 'Tidak dapat menghapus tarif per jam ini.',
'Unable to save the hourly rate.' => 'Tidak dapat menyimpan tarif per jam.',
'Hourly rate created successfully.' => 'Tarif per jam berhasil dibuat.',
'Start time' => 'Waktu mulai',
'End time' => 'Waktu selesai',
'Comment' => 'Komentar',
'All day' => 'Semua hari',
'Day' => 'Hari',
'Manage timetable' => 'Mengatur jadwal',
'Overtime timetable' => 'Jadwal lembur',
'Time off timetable' => 'Jam absensi',
'Timetable' => 'Jadwal',
'Work timetable' => 'Jadwal kerja',
'Week timetable' => 'Jadwal mingguan',
'Day timetable' => 'Jadwal harian',
'From' => 'Dari',
'To' => 'Untuk',
'Time slot created successfully.' => 'Slot waktu berhasil dibuat.',
'Unable to save this time slot.' => 'Tidak dapat menyimpan slot waktu ini.',
'Time slot removed successfully.' => 'Slot waktu berhasil dihapus.',
'Unable to remove this time slot.' => 'Tidak dapat menghapus slot waktu ini.',
'Do you really want to remove this time slot?' => 'Apakah anda yakin akan menghapus slot waktu ini?',
'Remove time slot' => 'Hapus slot waktu',
'Add new time slot' => 'Tambah slot waktu baru',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Jadwal ini digunakan ketika kotak centang "sepanjang hari" dicentang untuk dijadwalkan cuti dan lembur.',
'Files' => 'Arsip',
'Images' => 'Gambar',
'Private project' => 'Proyek pribadi',
'Amount' => 'Jumlah',
'AUD - Australian Dollar' => 'AUD - Dollar Australia',
'Budget' => 'Anggaran',
'Budget line' => 'Garis anggaran',
'Budget line removed successfully.' => 'Garis anggaran berhasil dihapus.',
'Budget lines' => 'Garis anggaran',
'CAD - Canadian Dollar' => 'CAD - Dollar Kanada',
'CHF - Swiss Francs' => 'CHF - Swiss Prancis',
'Cost' => 'Biaya',
'Cost breakdown' => 'Rincian biaya',
'Custom Stylesheet' => 'Kustomisasi Stylesheet',
'download' => 'unduh',
'Do you really want to remove this budget line?' => 'Apakah anda yakin akan menghapus garis anggaran ini?',
'EUR - Euro' => 'EUR - Euro',
'Expenses' => 'Beban',
'GBP - British Pound' => 'GBP - Poundsterling inggris',
'INR - Indian Rupee' => 'INR - Rupe India',
'JPY - Japanese Yen' => 'JPY - Yen Jepang',
'New budget line' => 'Garis anggaran baru',
'NZD - New Zealand Dollar' => 'NZD - Dollar Selandia baru',
'Remove a budget line' => 'Hapus garis anggaran',
'Remove budget line' => 'Hapus garis anggaran',
'RSD - Serbian dinar' => 'RSD - Dinar Serbia',
'The budget line have been created successfully.' => 'Garis anggaran berhasil dibuat.',
'Unable to create the budget line.' => 'Tidak dapat membuat garis anggaran.',
'Unable to remove this budget line.' => 'Tidak dapat menghapus garis anggaran.',
'USD - US Dollar' => 'USD - Dollar Amerika',
'Remaining' => 'Sisa',
'Destination column' => 'Kolom tujuan',
'Move the task to another column when assigned to a user' => 'Pindahkan tugas ke kolom lain ketika ditugaskan ke pengguna',
'Move the task to another column when assignee is cleared' => 'Pindahkan tugas ke kolom lain ketika orang yang ditugaskan dibersihkan',
'Source column' => 'Sumber kolom',
'Show subtask estimates (forecast of future work)' => 'Lihat perkiraan subtugas(perkiraan di masa depan)',
'Transitions' => 'Transisi',
'Executer' => 'Eksekusi',
'Time spent in the column' => 'Waktu yang dihabiskan dalam kolom',
@ -748,7 +695,6 @@ return array(
'Rate' => 'Tarif',
'Change reference currency' => 'Mengubah referensi mata uang',
'Add a new currency rate' => 'Tambahkan nilai tukar mata uang baru',
'Currency rates are used to calculate project budget.' => 'Nilai tukar mata uang digunakan untuk menghitung anggaran proyek.',
'Reference currency' => 'Referensi mata uang',
'The currency rate have been added successfully.' => 'Nilai tukar mata uang berhasil ditambahkan.',
'Unable to add this currency rate.' => 'Tidak dapat menambahkan nilai tukar mata uang',
@ -880,9 +826,6 @@ return array(
'%s moved the task #%d to the first swimlane' => '%s memindahkan tugas n°%d ke swimlane pertama',
'%s moved the task #%d to the swimlane "%s"' => '%s memindahkan tugas n°%d ke swimlane « %s »',
'Swimlane' => 'Swimlane',
'Budget overview' => 'Gambaran anggaran',
'Type' => 'Tipe',
'There is not enough data to show something.' => 'Tidak ada data yang cukup untuk menunjukkan sesuatu.',
'Gravatar' => 'Gravatar',
'Hipchat' => 'Hipchat',
'Slack' => 'Slack',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Vista compatta/estesa',
'No results match:' => 'Nessun risultato trovato:',
'Currency' => 'Valuta',
'Start time' => 'Data di inizio',
'End time' => 'Data di completamento',
'Comment' => 'Commento',
'All day' => 'Tutto il giorno',
'Day' => 'Giorno',
'Manage timetable' => 'Gestisci orario',
'Overtime timetable' => 'Straordinari',
'Time off timetable' => 'Fuori orario',
'Timetable' => 'Orario',
'Work timetable' => 'Orario di lavoro',
'Week timetable' => 'Orario settimanale',
'Day timetable' => 'Orario giornaliero',
'From' => 'Da',
'To' => 'A',
'Time slot created successfully.' => 'Fascia oraria creata con successo.',
'Unable to save this time slot.' => 'Impossibile creare questa fascia oraria.',
'Time slot removed successfully.' => 'Fascia oraria rimossa con successo.',
'Unable to remove this time slot.' => 'Impossibile rimuovere questa fascia oraria.',
'Do you really want to remove this time slot?' => 'Vuoi davvero rimuovere questa fascia oraria?',
'Remove time slot' => 'Rimuovi fascia oraria',
'Add new time slot' => 'Aggiungi nuova fascia oraria',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Questo orario è utilizzato quando la casella "tutto il giorno" è selezionata per i fuori orari e per gli straordinari',
// 'Files' => '',
'Images' => 'Immagini',
'Private project' => 'Progetto privato',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Sposta il compito in un\'altra colonna quando viene assegnato ad un utente',
'Move the task to another column when assignee is cleared' => 'Sposta il compito in un\'altra colonna quando l\'assegnatario cancellato',
'Source column' => 'Colonna sorgente',
// 'Show subtask estimates (forecast of future work)' => '',
'Transitions' => 'Transizioni',
'Executer' => 'Esecutore',
'Time spent in the column' => 'Tempo trascorso nella colonna',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'コンパクト/ワイドビュー',
'No results match:' => '結果が一致しませんでした',
'Currency' => '通貨',
'Start time' => '開始時間',
'End time' => '終了時間',
'Comment' => 'コメント',
'All day' => '終日',
'Day' => '日',
'Manage timetable' => 'タイムテーブルの管理',
'Overtime timetable' => '残業タイムテーブル',
'Time off timetable' => '休暇タイムテーブル',
'Timetable' => 'タイムテーブル',
'Work timetable' => 'ワークタイムテーブル',
'Week timetable' => '週次タイムテーブル',
'Day timetable' => '日時タイムテーブル',
'From' => 'ここから',
'To' => 'ここまで',
// 'Time slot created successfully.' => '',
// 'Unable to save this time slot.' => '',
// 'Time slot removed successfully.' => '',
// 'Unable to remove this time slot.' => '',
// 'Do you really want to remove this time slot?' => '',
'Remove time slot' => 'タイムスロットの削除',
'Add new time slot' => 'タイムラインの追加',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'このタイムテーブルは、残業や休暇で全日がチェックされた場合に用いられます。',
'Files' => 'ファイル',
'Images' => '画像',
'Private project' => 'プライベートプロジェクト',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'ユーザの割り当てをしたらタスクを他のカラムに移動',
'Move the task to another column when assignee is cleared' => 'ユーザの割り当てがなくなったらタスクを他のカラムに移動',
'Source column' => '移動元のカラム',
// 'Show subtask estimates (forecast of future work)' => '',
'Transitions' => '履歴',
'Executer' => '実行者',
'Time spent in the column' => 'カラムでの時間消費',

View File

@ -126,7 +126,6 @@ return array(
'The project name is required' => 'Prosjektnavn er påkrevet',
'This project must be unique' => 'Prosjektnavnet skal være unikt',
'The title is required' => 'Tittel er pårevet',
'There is no active project, the first step is to create a new project.' => 'Det er ingen aktive prosjekter. Førstesteg er åopprette et nytt prosjekt.',
'Settings saved successfully.' => 'Innstillinger lagret.',
'Unable to save your settings.' => 'Innstillinger kunne ikke lagres.',
'Database optimization done.' => 'Databaseoptimering er fullført.',
@ -165,8 +164,6 @@ return array(
'Date created' => 'Dato for opprettelse',
'Date completed' => 'Dato for fullført',
'Id' => 'ID',
'Completed tasks' => 'Fullførte oppgaver',
'Completed tasks for "%s"' => 'Fullførte oppgaver for "%s"',
'%d closed tasks' => '%d lukkede oppgaver',
'No task for this project' => 'Ingen oppgaver i dette prosjektet',
'Public link' => 'Offentligt lenke',
@ -255,25 +252,21 @@ return array(
'Expiration date' => 'Utløpsdato',
'Remember Me' => 'Husk meg',
'Creation date' => 'Opprettelsesdato',
'Filter by user' => 'Filtrer efter bruker',
'Filter by due date' => 'Filtrer etter forfallsdato',
'Everybody' => 'Alle',
'Open' => 'Åpen',
'Closed' => 'Lukket',
'Search' => 'Søk',
'Nothing found.' => 'Intet funnet.',
'Search in the project "%s"' => 'Søk i prosjektet "%s"',
'Due date' => 'Forfallsdato',
'Others formats accepted: %s and %s' => 'Andre formater: %s og %s',
'Description' => 'Beskrivelse',
'%d comments' => '%d kommentarer',
'%d comment' => '%d kommentar',
'Email address invalid' => 'Ugyldig epost',
'Your Google Account is not linked anymore to your profile.' => 'Din Google-konto er ikke lengre knyttet til din profil.',
'Unable to unlink your Google Account.' => 'Det var ikke mulig ø fjerne din Google-konto.',
'Google authentication failed' => 'Google godjenning mislyktes',
'Unable to link your Google Account.' => 'Det var ikke mulig åknytte opp til din Google-konto.',
'Your Google Account is linked to your profile successfully.' => 'Din Google-konto er knyttet til din profil.',
// 'Your external account is not linked anymore to your profile.' => '',
// 'Unable to unlink your external account.' => '',
// 'External authentication failed' => '',
// 'Your external account is linked to your profile successfully.' => '',
'Email' => 'Epost',
'Link my Google Account' => 'Knytt til min Google-konto',
'Unlink my Google Account' => 'Fjern knytningen til min Google-konto',
@ -301,7 +294,6 @@ return array(
'Category Name' => 'Kategorinavn',
'Add a new category' => 'Legg til ny kategori',
'Do you really want to remove this category: "%s"?' => 'Vil du fjerne kategorien: "%s"?',
'Filter by category' => 'Filter etter kategori',
'All categories' => 'Alle kategorier',
'No category' => 'Ingen kategori',
'The name is required' => 'Navnet er påkrevet',
@ -344,14 +336,9 @@ return array(
'Maximum size: ' => 'Maksimum størrelse: ',
'Unable to upload the file.' => 'Filen kunne ikke lastes opp.',
'Display another project' => 'Vis annet prosjekt...',
'Your GitHub account was successfully linked to your profile.' => 'Din GitHub-konto er knyttet til din profil.',
'Unable to link your GitHub Account.' => 'Det var ikke mulig å knytte din GitHub-konto.',
'GitHub authentication failed' => 'GitHub godkjenning mislyktes',
'Your GitHub account is no longer linked to your profile.' => 'Din GitHub-konto er ikke lengere knyttet til din profil.',
'Unable to unlink your GitHub Account.' => 'Det var ikke muligt at fjerne forbindelsen til din GitHub-konto.',
'Login with my GitHub Account' => 'Login med min GitHub-konto',
'Link my GitHub Account' => 'Knytt min GitHub-konto',
'Unlink my GitHub Account' => 'Fjern knytningen til min GitHub-konto',
// 'Login with my Github Account' => '',
// 'Link my Github Account' => '',
// 'Unlink my Github Account' => '',
'Created by %s' => 'Opprettet av %s',
'Last modified on %B %e, %Y at %k:%M %p' => 'Sist endret %d.%m.%Y - %H:%M',
'Tasks Export' => 'Oppgave eksport',
@ -408,8 +395,6 @@ return array(
'Remote' => 'Fjernstyrt',
'Enabled' => 'Aktiv',
'Disabled' => 'Deaktivert',
'Google account linked' => 'Google-konto knyttet',
'Github account linked' => 'GitHub-konto knyttet',
'Username:' => 'Brukernavn',
'Name:' => 'Navn:',
'Email:' => 'Epost:',
@ -445,7 +430,6 @@ return array(
'%s updated a comment on the task %s' => '%s oppdaterte en kommentar til oppgaven %s',
'%s commented the task %s' => '%s har kommentert oppgaven %s',
'%s\'s activity' => '%s\'s aktvitet',
'No activity.' => 'Ingen aktivitet',
'RSS feed' => 'RSS feed',
'%s updated a comment on the task #%d' => '%s oppdaterte en kommentar til oppgaven #%d',
'%s commented on the task #%d' => '%s kommenterte oppgaven #%d',
@ -605,14 +589,9 @@ return array(
'Language:' => 'Språk',
'Timezone:' => 'Tidssone',
'All columns' => 'Alle kolonner',
'Calendar for "%s"' => 'Kalender for "%s"',
'Filter by column' => 'Filtrer etter kolonne',
'Filter by status' => 'Filtrer etter status',
'Calendar' => 'Kalender',
'Next' => 'Neste',
// '#%d' => '',
'Filter by color' => 'Filtrer etter farge',
'Filter by swimlane' => 'Filtrer etter svømmebane',
'All swimlanes' => 'Alle svømmebaner',
'All colors' => 'Alle farger',
'All status' => 'Alle statuser',
@ -627,14 +606,7 @@ return array(
// 'Time Tracking' => '',
// 'You already have one subtask in progress' => '',
'Which parts of the project do you want to duplicate?' => 'Hvilke deler av dette prosjektet ønsker du å kopiere?',
'Change dashboard view' => 'Endre visning',
'Show/hide activities' => 'Vis/skjul aktiviteter',
'Show/hide projects' => 'Vis/skjul prosjekter',
'Show/hide subtasks' => 'Vis/skjul deloppgaver',
'Show/hide tasks' => 'Vis/skjul oppgaver',
'Disable login form' => 'Deaktiver innlogging',
'Show/hide calendar' => 'Vis/skjul kalender',
'User calendar' => 'Brukerens kalender',
// 'Disallow login form' => '',
// 'Bitbucket commit received' => '',
// 'Bitbucket webhooks' => '',
// 'Help on Bitbucket webhooks' => '',
@ -688,82 +660,31 @@ return array(
'Keyboard shortcuts' => 'Hurtigtaster',
// 'Open board switcher' => '',
// 'Application' => '',
'Filter recently updated' => 'Filter nylig oppdatert',
'since %B %e, %Y at %k:%M %p' => 'siden %B %e, %Y at %k:%M %p',
'More filters' => 'Flere filtre',
'Compact view' => 'Kompakt visning',
'Horizontal scrolling' => 'Bla horisontalt',
'Compact/wide view' => 'Kompakt/bred visning',
'No results match:' => 'Ingen resultater',
// 'Remove hourly rate' => '',
// 'Do you really want to remove this hourly rate?' => '',
'Hourly rates' => 'Timepriser',
'Hourly rate' => 'Timepris',
'Currency' => 'Valuta',
// 'Effective date' => '',
// 'Add new rate' => '',
// 'Rate removed successfully.' => '',
// 'Unable to remove this rate.' => '',
// 'Unable to save the hourly rate.' => '',
// 'Hourly rate created successfully.' => '',
// 'Start time' => '',
// 'End time' => '',
'Comment' => 'Kommentar',
'All day' => 'Alle dager',
'Day' => 'Dag',
'Manage timetable' => 'Tidstabell',
'Overtime timetable' => 'Overtidstabell',
'Time off timetable' => 'Fritidstabell',
'Timetable' => 'Tidstabell',
'Work timetable' => 'Arbeidstidstabell',
'Week timetable' => 'Uketidstabell',
'Day timetable' => 'Dagtidstabell',
'From' => 'Fra',
'To' => 'Til',
// 'Time slot created successfully.' => '',
// 'Unable to save this time slot.' => '',
// 'Time slot removed successfully.' => '',
// 'Unable to remove this time slot.' => '',
// 'Do you really want to remove this time slot?' => '',
// 'Remove time slot' => '',
// 'Add new time slot' => '',
// 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
'Files' => 'Filer',
'Images' => 'Bilder',
'Private project' => 'Privat prosjekt',
'Amount' => 'Beløp',
// 'AUD - Australian Dollar' => '',
'Budget' => 'Budsjett',
// 'Budget line' => '',
// 'Budget line removed successfully.' => '',
'Budget lines' => 'Budsjettlinjer',
// 'CAD - Canadian Dollar' => '',
// 'CHF - Swiss Francs' => '',
'Cost' => 'Kostnad',
'Cost breakdown' => 'Kostnadsnedbryting',
// 'Custom Stylesheet' => '',
'download' => 'last ned',
// 'Do you really want to remove this budget line?' => '',
// 'EUR - Euro' => '',
// 'Expenses' => '',
// 'GBP - British Pound' => '',
// 'INR - Indian Rupee' => '',
// 'JPY - Japanese Yen' => '',
// 'New budget line' => '',
// 'NZD - New Zealand Dollar' => '',
// 'Remove a budget line' => '',
// 'Remove budget line' => '',
// 'RSD - Serbian dinar' => '',
// 'The budget line have been created successfully.' => '',
// 'Unable to create the budget line.' => '',
// 'Unable to remove this budget line.' => '',
// 'USD - US Dollar' => '',
// 'Remaining' => '',
'Destination column' => 'Ny kolonne',
'Move the task to another column when assigned to a user' => 'Flytt oppgaven til en annen kolonne når den er tildelt en bruker',
'Move the task to another column when assignee is cleared' => 'Flytt oppgaven til en annen kolonne når ppgavetildeling fjernes ',
'Source column' => 'Opprinnelig kolonne',
// 'Show subtask estimates (forecast of future work)' => '',
'Transitions' => 'Statusendringer',
// 'Executer' => '',
// 'Time spent in the column' => '',
@ -774,7 +695,6 @@ return array(
// 'Rate' => '',
// 'Change reference currency' => '',
// 'Add a new currency rate' => '',
// 'Currency rates are used to calculate project budget.' => '',
// 'Reference currency' => '',
// 'The currency rate have been added successfully.' => '',
// 'Unable to add this currency rate.' => '',
@ -906,9 +826,6 @@ return array(
// '%s moved the task #%d to the first swimlane' => '',
// '%s moved the task #%d to the swimlane "%s"' => '',
'Swimlane' => 'Svømmebane',
'Budget overview' => 'Budsjettoversikt',
// 'Type' => '',
// 'There is not enough data to show something.' => '',
// 'Gravatar' => '',
// 'Hipchat' => '',
// 'Slack' => '',
@ -921,7 +838,6 @@ return array(
// 'The task have been moved to the first swimlane' => '',
// 'The task have been moved to another swimlane:' => '',
// 'Overdue tasks for the project "%s"' => '',
'There is no completed tasks at the moment.' => 'Ingen fullførte oppgaver funnet.',
// 'New title: %s' => '',
// 'The task is not assigned anymore' => '',
// 'New assignee: %s' => '',
@ -938,7 +854,6 @@ return array(
// 'The description have been modified' => '',
// 'Do you really want to close the task "%s" as well as all subtasks?' => '',
'Swimlane: %s' => 'Svømmebane: %s',
'Project calendar' => 'Prosjektkalender',
'I want to receive notifications for:' => 'Jeg vil motta varslinger om:',
'All tasks' => 'Alle oppgaver',
'Only for tasks assigned to me' => 'Kun oppgaver som er tildelt meg',
@ -951,7 +866,7 @@ return array(
// '%k:%M %p' => '',
// '%%Y-%%m-%%d' => '',
'Total for all columns' => 'Totalt for alle kolonner',
//'You need at least 2 days of data to show the chart.' => '',
// 'You need at least 2 days of data to show the chart.' => '',
// '<15m' => '',
// '<30m' => '',
'Stop timer' => 'Stopp timer',

View File

@ -666,28 +666,6 @@ return array(
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
// 'Start time' => '',
// 'End time' => '',
// 'Comment' => '',
// 'All day' => '',
// 'Day' => '',
// 'Manage timetable' => '',
// 'Overtime timetable' => '',
// 'Time off timetable' => '',
// 'Timetable' => '',
// 'Work timetable' => '',
// 'Week timetable' => '',
// 'Day timetable' => '',
// 'From' => '',
// 'To' => '',
// 'Time slot created successfully.' => '',
// 'Unable to save this time slot.' => '',
// 'Time slot removed successfully.' => '',
// 'Unable to remove this time slot.' => '',
// 'Do you really want to remove this time slot?' => '',
// 'Remove time slot' => '',
// 'Add new time slot' => '',
// 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
// 'Files' => '',
// 'Images' => '',
// 'Private project' => '',
@ -707,7 +685,6 @@ return array(
// 'Move the task to another column when assigned to a user' => '',
// 'Move the task to another column when assignee is cleared' => '',
// 'Source column' => '',
// 'Show subtask estimates (forecast of future work)' => '',
// 'Transitions' => '',
// 'Executer' => '',
// 'Time spent in the column' => '',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Pełny/Kompaktowy widok',
'No results match:' => 'Brak wyników:',
'Currency' => 'Waluta',
'Start time' => 'Rozpoczęto',
'End time' => 'Zakończono',
'Comment' => 'Komentarz',
'All day' => 'Cały dzień',
'Day' => 'Dzień',
'Manage timetable' => 'Zarządzaj rozkładami zajęć',
'Overtime timetable' => 'Rozkład zajęć - nadgodziny',
'Time off timetable' => 'Rozkład zajęć - czas wolny',
'Timetable' => 'Rozkład zajęć',
'Work timetable' => 'Rozkład zajęć - praca',
'Week timetable' => 'Tygodniowy rozkład zajęć',
'Day timetable' => 'Dzienny rozkład zajęć',
'From' => 'Od',
'To' => 'Do',
'Time slot created successfully.' => 'Przydział czasowy utworzony.',
'Unable to save this time slot.' => 'Nie można zapisać tego przydziału czasowego.',
'Time slot removed successfully.' => 'Przydział czasowy usunięty.',
'Unable to remove this time slot.' => 'Nie można usunąć tego przydziału czasowego.',
'Do you really want to remove this time slot?' => 'Czy na pewno chcesz usunąć ten przedział czasowy?',
'Remove time slot' => 'Usuń przedział czasowy',
'Add new time slot' => 'Dodaj przedział czasowy',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Ten rozkład zajęć jest używany przypadku zaznaczenia "cały dzień" dla zaplanowanego czasu wolnego i nadgodzin',
'Files' => 'Pliki',
'Images' => 'Obrazy',
'Private project' => 'Projekt prywatny',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Przenieś zadanie do innej kolumny gdy zostanie przypisane do osoby',
'Move the task to another column when assignee is cleared' => 'Przenieś zadanie do innej kolumny gdy osoba odpowiedzialna zostanie usunięta',
'Source column' => 'Kolumna źródłowa',
'Show subtask estimates (forecast of future work)' => 'Pokaż planowane czasy wykonania pod-zadań',
'Transitions' => 'Przeniesienia',
'Executer' => 'Wykonał',
'Time spent in the column' => 'Czas spędzony w tej kolumnie',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Alternar entre a vista compacta e ampliada',
'No results match:' => 'Nenhum resultado:',
'Currency' => 'Moeda',
'Start time' => 'Horário de início',
'End time' => 'Horário de término',
'Comment' => 'comentário',
'All day' => 'Dia inteiro',
'Day' => 'Dia',
'Manage timetable' => 'Gestão dos horários',
'Overtime timetable' => 'Horas extras',
'Time off timetable' => 'Horas de ausência',
'Timetable' => 'Horários',
'Work timetable' => 'Horas trabalhadas',
'Week timetable' => 'Horário da semana',
'Day timetable' => 'Horário de un dia',
'From' => 'Desde',
'To' => 'A',
'Time slot created successfully.' => 'Intervalo de tempo criado com sucesso.',
'Unable to save this time slot.' => 'Impossível de guardar este intervalo de tempo.',
'Time slot removed successfully.' => 'Intervalo de tempo removido com sucesso.',
'Unable to remove this time slot.' => 'Impossível de remover esse intervalo de tempo.',
'Do you really want to remove this time slot?' => 'Você deseja realmente remover este intervalo de tempo?',
'Remove time slot' => 'Remover um intervalo de tempo',
'Add new time slot' => 'Adicionar um intervalo de tempo',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Esses horários são usados quando a caixa de seleção "Dia inteiro" está marcada para Horas de ausência ou Extras',
'Files' => 'Arquivos',
'Images' => 'Imagens',
'Private project' => 'Projeto privado',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Mover a tarefa para uma outra coluna quando esta está atribuída a um usuário',
'Move the task to another column when assignee is cleared' => 'Mover a tarefa para uma outra coluna quando esta não está atribuída',
'Source column' => 'Coluna de origem',
'Show subtask estimates (forecast of future work)' => 'Mostrar a estimativa das subtarefas (previsão para o trabalho futuro)',
'Transitions' => 'Transições',
'Executer' => 'Executor(a)',
'Time spent in the column' => 'Tempo gasto na coluna',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Alternar entre a vista compacta e ampliada',
'No results match:' => 'Nenhum resultado:',
'Currency' => 'Moeda',
'Start time' => 'Horário de início',
'End time' => 'Horário de término',
'Comment' => 'comentário',
'All day' => 'Dia inteiro',
'Day' => 'Dia',
'Manage timetable' => 'Gestão dos horários',
'Overtime timetable' => 'Horas extras',
'Time off timetable' => 'Horas de ausência',
'Timetable' => 'Horários',
'Work timetable' => 'Horas trabalhadas',
'Week timetable' => 'Horário da semana',
'Day timetable' => 'Horário de um dia',
'From' => 'Desde',
'To' => 'A',
'Time slot created successfully.' => 'Intervalo de tempo criado com sucesso.',
'Unable to save this time slot.' => 'Impossível guardar este intervalo de tempo.',
'Time slot removed successfully.' => 'Intervalo de tempo removido com sucesso.',
'Unable to remove this time slot.' => 'Impossível remover esse intervalo de tempo.',
'Do you really want to remove this time slot?' => 'Tem a certeza que quer remover este intervalo de tempo?',
'Remove time slot' => 'Remover um intervalo de tempo',
'Add new time slot' => 'Adicionar um intervalo de tempo',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Esses horários são usados quando a caixa de seleção "Dia inteiro" está marcada para Horas de ausência ou Extras',
'Files' => 'Arquivos',
'Images' => 'Imagens',
'Private project' => 'Projecto privado',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Mover a tarefa para uma outra coluna quando esta está atribuída a um utilizador',
'Move the task to another column when assignee is cleared' => 'Mover a tarefa para uma outra coluna quando esta não está atribuída',
'Source column' => 'Coluna de origem',
'Show subtask estimates (forecast of future work)' => 'Mostrar a estimativa das subtarefas (previsão para o trabalho futuro)',
'Transitions' => 'Transições',
'Executer' => 'Executor(a)',
'Time spent in the column' => 'Tempo gasto na coluna',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Компактный/широкий вид',
'No results match:' => 'Отсутствуют результаты:',
'Currency' => 'Валюта',
'Start time' => 'Время начала',
'End time' => 'Время завершения',
'Comment' => 'Комментарий',
'All day' => 'Весь день',
'Day' => 'День',
'Manage timetable' => 'Управление графиками',
'Overtime timetable' => 'График сверхурочных',
'Time off timetable' => 'Время в графике',
'Timetable' => 'График',
'Work timetable' => 'Work timetable',
'Week timetable' => 'График на неделю',
'Day timetable' => 'График на день',
'From' => 'От кого',
'To' => 'Кому',
'Time slot created successfully.' => 'Временной интервал успешно создан.',
'Unable to save this time slot.' => 'Невозможно сохранить этот временной интервал.',
'Time slot removed successfully.' => 'Временной интервал успешно удален.',
'Unable to remove this time slot.' => 'Не удается удалить этот временной интервал.',
'Do you really want to remove this time slot?' => 'Вы действительно хотите удалить этот период времени?',
'Remove time slot' => 'Удалить новый интервал времени',
'Add new time slot' => 'Добавить новый интервал времени',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Это расписание используется, когда флажок "весь день" проверяется на установленное время выключения и сверхурочное время.',
'Files' => 'Файлы',
'Images' => 'Изображения',
'Private project' => 'Приватный проект',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'Переместить задачу в другую колонку, когда она назначена пользователю',
'Move the task to another column when assignee is cleared' => 'Переместить задачу в другую колонку, когда назначение снято ',
'Source column' => 'Исходная колонка',
'Show subtask estimates (forecast of future work)' => 'Показать оценку подзадач (прогноз будущей работы)',
'Transitions' => 'Перемещения',
'Executer' => 'Исполнитель',
'Time spent in the column' => 'Время проведенное в колонке',
@ -849,9 +826,6 @@ return array(
'%s moved the task #%d to the first swimlane' => '%s задач перемещено #%d в первой дорожке',
'%s moved the task #%d to the swimlane "%s"' => '%s задач перемещено #%d в дорожке "%s"',
'Swimlane' => 'Дорожки',
'Budget overview' => 'Обзор бюджета',
'Type' => 'Тип',
'There is not enough data to show something.' => 'Недостаточно существующих данных, что бы что-то показать.',
'Gravatar' => 'Граватар',
'Hipchat' => 'Hipchat',
'Slack' => 'Slack',

View File

@ -666,28 +666,6 @@ return array(
// 'Compact/wide view' => '',
// 'No results match:' => '',
// 'Currency' => '',
// 'Start time' => '',
// 'End time' => '',
// 'Comment' => '',
// 'All day' => '',
// 'Day' => '',
// 'Manage timetable' => '',
// 'Overtime timetable' => '',
// 'Time off timetable' => '',
// 'Timetable' => '',
// 'Work timetable' => '',
// 'Week timetable' => '',
// 'Day timetable' => '',
// 'From' => '',
// 'To' => '',
// 'Time slot created successfully.' => '',
// 'Unable to save this time slot.' => '',
// 'Time slot removed successfully.' => '',
// 'Unable to remove this time slot.' => '',
// 'Do you really want to remove this time slot?' => '',
// 'Remove time slot' => '',
// 'Add new time slot' => '',
// 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
// 'Files' => '',
// 'Images' => '',
// 'Private project' => '',
@ -707,7 +685,6 @@ return array(
// 'Move the task to another column when assigned to a user' => '',
// 'Move the task to another column when assignee is cleared' => '',
// 'Source column' => '',
// 'Show subtask estimates (forecast of future work)' => '',
// 'Transitions' => '',
// 'Executer' => '',
// 'Time spent in the column' => '',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Kompakt/bred vy',
'No results match:' => 'Inga matchande resultat',
'Currency' => 'Valuta',
'Start time' => 'Starttid',
'End time' => 'Sluttid',
'Comment' => 'Kommentar',
'All day' => 'Hela dagen',
'Day' => 'Dag',
'Manage timetable' => 'Hantera timplan',
'Overtime timetable' => 'Övertidstimplan',
'Time off timetable' => 'Ledighetstimplan',
'Timetable' => 'Timplan',
'Work timetable' => 'Arbetstimplan',
'Week timetable' => 'Veckotidplan',
'Day timetable' => 'Dagstimplan',
'From' => 'Från',
'To' => 'Till',
'Time slot created successfully.' => 'Tidslucka skapad.',
'Unable to save this time slot.' => 'Kunde inte spara tidsluckan.',
'Time slot removed successfully.' => 'Tidsluckan tog bort.',
'Unable to remove this time slot.' => 'Kunde inte ta bort tidsluckan.',
'Do you really want to remove this time slot?' => 'Vill du verkligen ta bort tidsluckan?',
'Remove time slot' => 'Ta bort tidslucka',
'Add new time slot' => 'Lägg till ny tidslucka',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => 'Denna tidslucka används när kryssrutan "hela dagen" är kryssad vid schemalagd ledighet eller övertid.',
'Files' => 'Filer',
'Images' => 'Bilder',
'Private project' => 'Privat projekt',
@ -707,7 +685,6 @@ 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)' => 'Visa uppskattningar för deluppgifter (prognos för framtida arbete)',
'Transitions' => 'Övergångar',
'Executer' => 'Verkställare',
'Time spent in the column' => 'Tid i kolumnen.',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'พอดี/กว้าง มุมมอง',
'No results match:' => 'ไม่มีผลลัพท์ที่ตรง',
'Currency' => 'สกุลเงิน',
'Start time' => 'เวลาเริ่มต้น',
'End time' => 'เวลาจบ',
'Comment' => 'ความคิดเห็น',
'All day' => 'ทั้งวัน',
'Day' => 'วัน',
'Manage timetable' => 'จัดการตารางเวลา',
'Overtime timetable' => 'ตารางเวลาโอที',
'Time off timetable' => 'ตารางเวลาวันหยุด',
'Timetable' => 'ตารางเวลา',
'Work timetable' => 'ตารางเวลางาน',
'Week timetable' => 'ตารางเวลาสัปดาห์',
'Day timetable' => 'ตารางเวลาวัน',
'From' => 'จาก',
'To' => 'ถึง',
'Time slot created successfully.' => 'สร้างช่วงเวลาเรียบร้อยแล้ว',
'Unable to save this time slot.' => 'ไม่สามารถบันทึกช่วงเวลานี้',
'Time slot removed successfully.' => 'ลบช่วงเวลาเรียบร้อยแล้ว',
'Unable to remove this time slot.' => 'ไม่สามารถลบช่วงเวลาได้',
'Do you really want to remove this time slot?' => 'คุณต้องการลบช่วงเวลานี้?',
'Remove time slot' => 'ลบช่วงเวลา',
'Add new time slot' => 'เพิ่มช่วงเวลาใหม่',
// 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
'Files' => 'ไฟล์',
'Images' => 'รูปภาพ',
'Private project' => 'โปรเจคส่วนตัว',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => 'ย้ายงานไปคอลัมน์อื่นเมื่อกำหนดบุคคลรับผิดชอบ',
'Move the task to another column when assignee is cleared' => 'ย้ายงานไปคอลัมน์อื่นเมื่อไม่กำหนดบุคคลรับผิดชอบ',
'Source column' => 'คอลัมน์ต้นทาง',
// 'Show subtask estimates (forecast of future work)' => '',
'Transitions' => 'การเปลี่ยนคอลัมน์',
'Executer' => 'ผู้ประมวลผล',
'Time spent in the column' => 'เวลาที่ใช้ในคอลัมน์',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => 'Ekrana sığdır / Geniş görünüm',
// 'No results match:' => '',
// 'Currency' => '',
// 'Start time' => '',
// 'End time' => '',
// 'Comment' => '',
// 'All day' => '',
// 'Day' => '',
// 'Manage timetable' => '',
// 'Overtime timetable' => '',
// 'Time off timetable' => '',
// 'Timetable' => '',
// 'Work timetable' => '',
// 'Week timetable' => '',
// 'Day timetable' => '',
// 'From' => '',
// 'To' => '',
// 'Time slot created successfully.' => '',
// 'Unable to save this time slot.' => '',
// 'Time slot removed successfully.' => '',
// 'Unable to remove this time slot.' => '',
// 'Do you really want to remove this time slot?' => '',
// 'Remove time slot' => '',
// 'Add new time slot' => '',
// 'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '',
// 'Files' => '',
// 'Images' => '',
// 'Private project' => '',
@ -707,7 +685,6 @@ return array(
// 'Move the task to another column when assigned to a user' => '',
// 'Move the task to another column when assignee is cleared' => '',
// 'Source column' => '',
// 'Show subtask estimates (forecast of future work)' => '',
// 'Transitions' => '',
// 'Executer' => '',
// 'Time spent in the column' => '',

View File

@ -666,28 +666,6 @@ return array(
'Compact/wide view' => '紧凑/宽视图',
'No results match:' => '无匹配结果:',
'Currency' => '货币',
'Start time' => '开始时间',
'End time' => '结束时1间',
'Comment' => '注释',
'All day' => '全天',
'Day' => '日期',
'Manage timetable' => '管理时间表',
// 'Overtime timetable' => '',
'Time off timetable' => '加班时间表',
'Timetable' => '时间表',
'Work timetable' => '工作时间表',
'Week timetable' => '周时间表',
'Day timetable' => '日时间表',
'From' => '从',
'To' => '到',
'Time slot created successfully.' => '成功创建时间段。',
'Unable to save this time slot.' => '无法保存此时间段。',
'Time slot removed successfully.' => '成功删除时间段。',
'Unable to remove this time slot.' => '无法删除此时间段。',
'Do you really want to remove this time slot?' => '确认要删除此时间段吗?',
'Remove time slot' => '删除时间段',
'Add new time slot' => '添加新时间段',
'This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.' => '如果在放假和加班计划中选择全天,则会使用这里配置的时间段。',
'Files' => '文件',
'Images' => '图片',
'Private project' => '私人项目',
@ -707,7 +685,6 @@ return array(
'Move the task to another column when assigned to a user' => '指定负责人时移动到其它栏目',
'Move the task to another column when assignee is cleared' => '移除负责人时移动到其它栏目',
'Source column' => '原栏目',
// 'Show subtask estimates (forecast of future work)' => '',
'Transitions' => '变更',
'Executer' => '执行者',
'Time spent in the column' => '栏目中的时间消耗',

View File

@ -150,13 +150,14 @@ class SubtaskTimeTracking extends Base
*
* @access public
* @param integer $user_id
* @param integer $start
* @param integer $end
* @param string $start ISO-8601 format
* @param string $end
* @return array
*/
public function getUserCalendarEvents($user_id, $start, $end)
{
$result = $this->getUserQuery($user_id)
$hook = 'model:subtask-time-tracking:calendar:events';
$events = $this->getUserQuery($user_id)
->addCondition($this->getCalendarCondition(
$this->dateParser->getTimestampFromIsoFormat($start),
$this->dateParser->getTimestampFromIsoFormat($end),
@ -165,9 +166,16 @@ class SubtaskTimeTracking extends Base
))
->findAll();
$result = $this->timetable->calculateEventsIntersect($user_id, $result, $start, $end);
if ($this->hook->exists($hook)) {
$events = $this->hook->first($hook, array(
'user_id' => $user_id,
'events' => $events,
'start' => $start,
'end' => $end,
));
}
return $this->toCalendarEvents($result);
return $this->toCalendarEvents($events);
}
/**
@ -293,6 +301,7 @@ class SubtaskTimeTracking extends Base
*/
public function getTimeSpent($subtask_id, $user_id)
{
$hook = 'model:subtask-time-tracking:calculate:time-spent';
$start_time = $this->db
->table(self::TABLE)
->eq('subtask_id', $subtask_id)
@ -300,14 +309,23 @@ class SubtaskTimeTracking extends Base
->eq('end', 0)
->findOneColumn('start');
if ($start_time) {
$start = new DateTime;
$start->setTimestamp($start_time);
return $this->timetable->calculateEffectiveDuration($user_id, $start, new DateTime);
if (empty($start_time)) {
return 0;
}
return 0;
$end = new DateTime;
$start = new DateTime;
$start->setTimestamp($start_time);
if ($this->hook->exists($hook)) {
return $this->hook->first($hook, array(
'user_id' => $user_id,
'start' => $start,
'end' => $end,
));
}
return $this->dateParser->getHours($start, $end);
}
/**

View File

@ -1,356 +0,0 @@
<?php
namespace Model;
use DateTime;
use DateInterval;
/**
* Timetable
*
* @package model
* @author Frederic Guillot
*/
class Timetable extends Base
{
/**
* User time slots
*
* @access private
* @var array
*/
private $day;
private $week;
private $overtime;
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 array $event
* @param array $timetable
* @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
* @param integer $user_id
* @param \DateTime $start
* @param \DateTime $end
* @return array
*/
public function calculate($user_id, DateTime $start, DateTime $end)
{
$timetable = array();
$this->day = $this->timetableDay->getByUser($user_id);
$this->week = $this->timetableWeek->getByUser($user_id);
$this->overtime = $this->timetableExtra->getByUserAndDate($user_id, $start->format('Y-m-d'), $end->format('Y-m-d'));
$this->timeoff = $this->timetableOff->getByUserAndDate($user_id, $start->format('Y-m-d'), $end->format('Y-m-d'));
for ($today = clone($start); $today <= $end; $today->add(new DateInterval('P1D'))) {
$week_day = $today->format('N');
$timetable = array_merge($timetable, $this->getWeekSlots($today, $week_day));
$timetable = array_merge($timetable, $this->getOvertimeSlots($today, $week_day));
}
return $timetable;
}
/**
* Return worked time slots for the given day
*
* @access public
* @param \DateTime $today
* @param string $week_day
* @return array
*/
public function getWeekSlots(DateTime $today, $week_day)
{
$slots = array();
$dayoff = $this->getDayOff($today);
if (! empty($dayoff) && $dayoff['all_day'] == 1) {
return array();
}
foreach ($this->week as $slot) {
if ($week_day == $slot['day']) {
$slots = array_merge($slots, $this->getDayWorkSlots($slot, $dayoff, $today));
}
}
return $slots;
}
/**
* Get the overtime time slots for the given day
*
* @access public
* @param \DateTime $today
* @param string $week_day
* @return array
*/
public function getOvertimeSlots(DateTime $today, $week_day)
{
$slots = array();
foreach ($this->overtime as $slot) {
$day = new DateTime($slot['date']);
if ($week_day == $day->format('N')) {
if ($slot['all_day'] == 1) {
$slots = array_merge($slots, $this->getDaySlots($today));
}
else {
$slots[] = $this->getTimeSlot($slot, $day);
}
}
}
return $slots;
}
/**
* Get worked time slots and remove time off
*
* @access public
* @param array $slot
* @param array $dayoff
* @param \DateTime $today
* @return array
*/
public function getDayWorkSlots(array $slot, array $dayoff, DateTime $today)
{
$slots = array();
if (! empty($dayoff) && $dayoff['start'] < $slot['end']) {
if ($dayoff['start'] > $slot['start']) {
$slots[] = $this->getTimeSlot(array('end' => $dayoff['start']) + $slot, $today);
}
if ($dayoff['end'] < $slot['end']) {
$slots[] = $this->getTimeSlot(array('start' => $dayoff['end']) + $slot, $today);
}
}
else {
$slots[] = $this->getTimeSlot($slot, $today);
}
return $slots;
}
/**
* Get regular day work time slots
*
* @access public
* @param \DateTime $today
* @return array
*/
public function getDaySlots(DateTime $today)
{
$slots = array();
foreach ($this->day as $day) {
$slots[] = $this->getTimeSlot($day, $today);
}
return $slots;
}
/**
* Get the start and end time slot for a given day
*
* @access public
* @param array $slot
* @param \DateTime $today
* @return array
*/
public function getTimeSlot(array $slot, DateTime $today)
{
$date = $today->format('Y-m-d');
return array(
new DateTime($date.' '.$slot['start']),
new DateTime($date.' '.$slot['end']),
);
}
/**
* Return day off time slot
*
* @access public
* @param \DateTime $today
* @return array
*/
public function getDayOff(DateTime $today)
{
foreach ($this->timeoff as $day) {
if ($day['date'] === $today->format('Y-m-d')) {
return $day;
}
}
return array();
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Timetable Workweek
*
* @package model
* @author Frederic Guillot
*/
class TimetableDay extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'timetable_day';
/**
* Get the timetable for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getByUser($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('start')->findAll();
}
/**
* Add a new time slot in the database
*
* @access public
* @param integer $user_id User id
* @param string $start Start hour (24h format)
* @param string $end End hour (24h format)
* @return boolean|integer
*/
public function create($user_id, $start, $end)
{
$values = array(
'user_id' => $user_id,
'start' => $start,
'end' => $end,
);
return $this->persist(self::TABLE, $values);
}
/**
* Remove a specific time slot
*
* @access public
* @param integer $slot_id
* @return boolean
*/
public function remove($slot_id)
{
return $this->db->table(self::TABLE)->eq('id', $slot_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('start', t('Field required')),
new Validators\Required('end', t('Field required')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Timetable over-time
*
* @package model
* @author Frederic Guillot
*/
class TimetableExtra extends TimetableOff
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'timetable_extra';
}

View File

@ -1,125 +0,0 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Timetable time off
*
* @package model
* @author Frederic Guillot
*/
class TimetableOff extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'timetable_off';
/**
* Get query to fetch everything (pagination)
*
* @access public
* @param integer $user_id User id
* @return \PicoDb\Table
*/
public function getUserQuery($user_id)
{
return $this->db->table(static::TABLE)->eq('user_id', $user_id);
}
/**
* Get the timetable for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getByUser($user_id)
{
return $this->db->table(static::TABLE)->eq('user_id', $user_id)->desc('date')->asc('start')->findAll();
}
/**
* Get the timetable for a given user
*
* @access public
* @param integer $user_id User id
* @param string $start_date
* @param string $end_date
* @return array
*/
public function getByUserAndDate($user_id, $start_date, $end_date)
{
return $this->db->table(static::TABLE)
->eq('user_id', $user_id)
->gte('date', $start_date)
->lte('date', $end_date)
->desc('date')
->asc('start')
->findAll();
}
/**
* Add a new time slot in the database
*
* @access public
* @param integer $user_id User id
* @param string $date Day (ISO8601 format)
* @param boolean $all_day All day flag
* @param float $start Start hour (24h format)
* @param float $end End hour (24h format)
* @param string $comment
* @return boolean|integer
*/
public function create($user_id, $date, $all_day, $start = '', $end = '', $comment = '')
{
$values = array(
'user_id' => $user_id,
'date' => $date,
'all_day' => (int) $all_day, // Postgres fix
'start' => $all_day ? '' : $start,
'end' => $all_day ? '' : $end,
'comment' => $comment,
);
return $this->persist(static::TABLE, $values);
}
/**
* Remove a specific time slot
*
* @access public
* @param integer $slot_id
* @return boolean
*/
public function remove($slot_id)
{
return $this->db->table(static::TABLE)->eq('id', $slot_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('date', t('Field required')),
new Validators\Numeric('all_day', t('This value must be numeric')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

View File

@ -1,91 +0,0 @@
<?php
namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Timetable Workweek
*
* @package model
* @author Frederic Guillot
*/
class TimetableWeek extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'timetable_week';
/**
* Get the timetable for a given user
*
* @access public
* @param integer $user_id User id
* @return array
*/
public function getByUser($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('day')->asc('start')->findAll();
}
/**
* Add a new time slot in the database
*
* @access public
* @param integer $user_id User id
* @param string $day Day of the week (ISO-8601)
* @param string $start Start hour (24h format)
* @param string $end End hour (24h format)
* @return boolean|integer
*/
public function create($user_id, $day, $start, $end)
{
$values = array(
'user_id' => $user_id,
'day' => $day,
'start' => $start,
'end' => $end,
);
return $this->persist(self::TABLE, $values);
}
/**
* Remove a specific time slot
*
* @access public
* @param integer $slot_id
* @return boolean
*/
public function remove($slot_id)
{
return $this->db->table(self::TABLE)->eq('id', $slot_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('day', t('Field required')),
new Validators\Numeric('day', t('This value must be numeric')),
new Validators\Required('start', t('Field required')),
new Validators\Required('end', t('Field required')),
));
return array(
$v->execute(),
$v->getErrors()
);
}
}

View File

@ -321,52 +321,6 @@ function version_53($pdo)
$pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent FLOAT DEFAULT 0");
}
function version_51($pdo)
{
$pdo->exec('CREATE TABLE timetable_day (
id INT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
start VARCHAR(5) NOT NULL,
end VARCHAR(5) NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY(id)
) ENGINE=InnoDB CHARSET=utf8');
$pdo->exec('CREATE TABLE timetable_week (
id INT NOT NULL AUTO_INCREMENT,
user_id INTEGER NOT NULL,
day INT NOT NULL,
start VARCHAR(5) NOT NULL,
end VARCHAR(5) NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY(id)
) ENGINE=InnoDB CHARSET=utf8');
$pdo->exec('CREATE TABLE timetable_off (
id INT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
date VARCHAR(10) NOT NULL,
all_day TINYINT(1) DEFAULT 0,
start VARCHAR(5) DEFAULT 0,
end VARCHAR(5) DEFAULT 0,
comment TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY(id)
) ENGINE=InnoDB CHARSET=utf8');
$pdo->exec('CREATE TABLE timetable_extra (
id INT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
date VARCHAR(10) NOT NULL,
all_day TINYINT(1) DEFAULT 0,
start VARCHAR(5) DEFAULT 0,
end VARCHAR(5) DEFAULT 0,
comment TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY(id)
) ENGINE=InnoDB CHARSET=utf8');
}
function version_49($pdo)
{
$pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1');

View File

@ -314,48 +314,6 @@ function version_34($pdo)
$pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0");
}
function version_32($pdo)
{
$pdo->exec('CREATE TABLE timetable_day (
"id" SERIAL PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"start" VARCHAR(5) NOT NULL,
"end" VARCHAR(5) NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE timetable_week (
"id" SERIAL PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"day" INTEGER NOT NULL,
"start" VARCHAR(5) NOT NULL,
"end" VARCHAR(5) NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE timetable_off (
"id" SERIAL PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"date" VARCHAR(10) NOT NULL,
"all_day" BOOLEAN DEFAULT \'0\',
"start" VARCHAR(5) DEFAULT 0,
"end" VARCHAR(5) DEFAULT 0,
"comment" TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE timetable_extra (
"id" SERIAL PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"date" VARCHAR(10) NOT NULL,
"all_day" BOOLEAN DEFAULT \'0\',
"start" VARCHAR(5) DEFAULT 0,
"end" VARCHAR(5) DEFAULT 0,
"comment" TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
}
function version_30($pdo)
{
$pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1');

View File

@ -291,48 +291,6 @@ function version_52($pdo)
$pdo->exec("ALTER TABLE subtask_time_tracking ADD COLUMN time_spent REAL DEFAULT 0");
}
function version_50($pdo)
{
$pdo->exec('CREATE TABLE timetable_day (
"id" INTEGER PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"start" TEXT NOT NULL,
"end" TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE timetable_week (
"id" INTEGER PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"day" INTEGER NOT NULL,
"start" TEXT NOT NULL,
"end" TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE timetable_off (
"id" INTEGER PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"date" TEXT NOT NULL,
"all_day" INTEGER DEFAULT 0,
"start" TEXT DEFAULT 0,
"end" TEXT DEFAULT 0,
"comment" TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
$pdo->exec('CREATE TABLE timetable_extra (
"id" INTEGER PRIMARY KEY,
"user_id" INTEGER NOT NULL,
"date" TEXT NOT NULL,
"all_day" INTEGER DEFAULT 0,
"start" TEXT DEFAULT 0,
"end" TEXT DEFAULT 0,
"comment" TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)');
}
function version_48($pdo)
{
$pdo->exec('ALTER TABLE subtasks ADD COLUMN position INTEGER DEFAULT 1');

View File

@ -56,11 +56,6 @@ class ClassProvider implements ServiceProviderInterface
'TaskPosition',
'TaskStatus',
'TaskValidator',
'Timetable',
'TimetableDay',
'TimetableExtra',
'TimetableWeek',
'TimetableOff',
'Transition',
'User',
'UserSession',

View File

@ -6,22 +6,24 @@
<?= $this->form->csrf() ?>
<h3><?= t('Project calendar view') ?></h3>
<div class="listing">
<h3><?= t('Project calendar view') ?></h3>
<?= $this->form->radios('calendar_project_tasks', array(
'date_creation' => t('Show tasks based on the creation date'),
'date_started' => t('Show tasks based on the start date'),
), $values) ?>
</div>
<h3><?= t('User calendar view') ?></h3>
<div class="listing">
<h3><?= t('User calendar view') ?></h3>
<?= $this->form->radios('calendar_user_tasks', array(
'date_creation' => t('Show tasks based on the creation date'),
'date_started' => t('Show tasks based on the start date'),
), $values) ?>
</div>
<h4><?= t('Subtasks time tracking') ?></h4>
<div class="listing">
<h3><?= t('Subtasks time tracking') ?></h3>
<?= $this->form->checkbox('calendar_user_subtasks_time_tracking', t('Show subtasks based on the time tracking'), 1, $values['calendar_user_subtasks_time_tracking'] == 1) ?>
</div>

View File

@ -1,44 +0,0 @@
<div class="page-header">
<h2><?= t('Timetable') ?></h2>
<ul>
<li><?= $this->url->link(t('Day timetable'), 'timetableday', 'index', array('user_id' => $user['id'])) ?></li>
<li><?= $this->url->link(t('Week timetable'), 'timetableweek', 'index', array('user_id' => $user['id'])) ?></li>
<li><?= $this->url->link(t('Time off timetable'), 'timetableoff', 'index', array('user_id' => $user['id'])) ?></li>
<li><?= $this->url->link(t('Overtime timetable'), 'timetableextra', 'index', array('user_id' => $user['id'])) ?></li>
</ul>
</div>
<form method="get" action="?" autocomplete="off" class="form-inline">
<?= $this->form->hidden('controller', $values) ?>
<?= $this->form->hidden('action', $values) ?>
<?= $this->form->hidden('user_id', $values) ?>
<?= $this->form->label(t('From'), 'from') ?>
<?= $this->form->text('from', $values, array(), array(), 'form-date') ?>
<?= $this->form->label(t('To'), 'to') ?>
<?= $this->form->text('to', $values, array(), array(), 'form-date') ?>
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
</form>
<?php if (! empty($timetable)): ?>
<hr/>
<h3><?= t('Work timetable') ?></h3>
<table class="table-fixed table-stripped">
<tr>
<th><?= t('Day') ?></th>
<th><?= t('Start') ?></th>
<th><?= t('End') ?></th>
</tr>
<?php foreach ($timetable as $slot): ?>
<tr>
<td><?= dt('%B %e, %Y', $slot[0]->getTimestamp()) ?></td>
<td><?= dt('%k:%M %p', $slot[0]->getTimestamp()) ?></td>
<td><?= dt('%k:%M %p', $slot[1]->getTimestamp()) ?></td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>

View File

@ -1,45 +0,0 @@
<div class="page-header">
<h2><?= t('Day timetable') ?></h2>
</div>
<?php if (! empty($timetable)): ?>
<table class="table-fixed table-stripped">
<tr>
<th><?= t('Start time') ?></th>
<th><?= t('End time') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($timetable as $slot): ?>
<tr>
<td><?= $slot['start'] ?></td>
<td><?= $slot['end'] ?></td>
<td>
<?= $this->url->link(t('Remove'), 'timetableday', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
</td>
</tr>
<?php endforeach ?>
</table>
<h3><?= t('Add new time slot') ?></h3>
<?php endif ?>
<form method="post" action="<?= $this->url->href('timetableday', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
<?= $this->form->hidden('user_id', $values) ?>
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Start time'), 'start') ?>
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>
<p class="alert alert-info">
<?= t('This timetable is used when the checkbox "all day" is checked for scheduled time off and overtime.') ?>
</p>

View File

@ -1,13 +0,0 @@
<div class="page-header">
<h2><?= t('Remove time slot') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'timetableday', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'timetableday', 'index', array('user_id' => $user['id'])) ?>
</div>
</div>

View File

@ -1,56 +0,0 @@
<div class="page-header">
<h2><?= t('Overtime timetable') ?></h2>
</div>
<?php if (! $paginator->isEmpty()): ?>
<table class="table-fixed table-stripped">
<tr>
<th><?= $paginator->order(t('Day'), 'Day') ?></th>
<th><?= $paginator->order(t('All day'), 'all_day') ?></th>
<th><?= $paginator->order(t('Start time'), 'start') ?></th>
<th><?= $paginator->order(t('End time'), 'end') ?></th>
<th class="column-40"><?= t('Comment') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($paginator->getCollection() as $slot): ?>
<tr>
<td><?= $slot['date'] ?></td>
<td><?= $slot['all_day'] == 1 ? t('Yes') : t('No') ?></td>
<td><?= $slot['start'] ?></td>
<td><?= $slot['end'] ?></td>
<td><?= $this->e($slot['comment']) ?></td>
<td>
<?= $this->url->link(t('Remove'), 'timetableextra', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
</td>
</tr>
<?php endforeach ?>
</table>
<?= $paginator ?>
<?php endif ?>
<form method="post" action="<?= $this->url->href('timetableextra', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
<?= $this->form->hidden('user_id', $values) ?>
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Day'), 'date') ?>
<?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?>
<?= $this->form->checkbox('all_day', t('All day'), 1) ?>
<?= $this->form->label(t('Start time'), 'start') ?>
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('Comment'), 'comment') ?>
<?= $this->form->text('comment', $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>

View File

@ -1,13 +0,0 @@
<div class="page-header">
<h2><?= t('Remove time slot') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'timetableextra', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'timetableextra', 'index', array('user_id' => $user['id'])) ?>
</div>
</div>

View File

@ -1,56 +0,0 @@
<div class="page-header">
<h2><?= t('Time off timetable') ?></h2>
</div>
<?php if (! $paginator->isEmpty()): ?>
<table class="table-fixed table-stripped">
<tr>
<th><?= $paginator->order(t('Day'), 'Day') ?></th>
<th><?= $paginator->order(t('All day'), 'all_day') ?></th>
<th><?= $paginator->order(t('Start time'), 'start') ?></th>
<th><?= $paginator->order(t('End time'), 'end') ?></th>
<th class="column-40"><?= t('Comment') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($paginator->getCollection() as $slot): ?>
<tr>
<td><?= $slot['date'] ?></td>
<td><?= $slot['all_day'] == 1 ? t('Yes') : t('No') ?></td>
<td><?= $slot['start'] ?></td>
<td><?= $slot['end'] ?></td>
<td><?= $this->e($slot['comment']) ?></td>
<td>
<?= $this->url->link(t('Remove'), 'timetableoff', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
</td>
</tr>
<?php endforeach ?>
</table>
<?= $paginator ?>
<?php endif ?>
<form method="post" action="<?= $this->url->href('timetableoff', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
<?= $this->form->hidden('user_id', $values) ?>
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Day'), 'date') ?>
<?= $this->form->text('date', $values, $errors, array('required'), 'form-date') ?>
<?= $this->form->checkbox('all_day', t('All day'), 1) ?>
<?= $this->form->label(t('Start time'), 'start') ?>
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('Comment'), 'comment') ?>
<?= $this->form->text('comment', $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>

View File

@ -1,13 +0,0 @@
<div class="page-header">
<h2><?= t('Remove time slot') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'timetableoff', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'timetableoff', 'index', array('user_id' => $user['id'])) ?>
</div>
</div>

View File

@ -1,46 +0,0 @@
<div class="page-header">
<h2><?= t('Week timetable') ?></h2>
</div>
<?php if (! empty($timetable)): ?>
<table class="table-fixed table-stripped">
<tr>
<th><?= t('Day') ?></th>
<th><?= t('Start time') ?></th>
<th><?= t('End time') ?></th>
<th><?= t('Action') ?></th>
</tr>
<?php foreach ($timetable as $slot): ?>
<tr>
<td><?= $this->dt->getWeekDay($slot['day']) ?></td>
<td><?= $slot['start'] ?></td>
<td><?= $slot['end'] ?></td>
<td>
<?= $this->url->link(t('Remove'), 'timetableweek', 'confirm', array('user_id' => $user['id'], 'slot_id' => $slot['id'])) ?>
</td>
</tr>
<?php endforeach ?>
</table>
<h3><?= t('Add new time slot') ?></h3>
<?php endif ?>
<form method="post" action="<?= $this->url->href('timetableweek', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
<?= $this->form->hidden('user_id', $values) ?>
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Day'), 'day') ?>
<?= $this->form->select('day', $this->dt->getWeekDays(), $values, $errors) ?>
<?= $this->form->label(t('Start time'), 'start') ?>
<?= $this->form->select('start', $this->dt->getDayHours(), $values, $errors) ?>
<?= $this->form->label(t('End time'), 'end') ?>
<?= $this->form->select('end', $this->dt->getDayHours(), $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>
</form>

View File

@ -1,13 +0,0 @@
<div class="page-header">
<h2><?= t('Remove time slot') ?></h2>
</div>
<div class="confirm">
<p class="alert alert-info"><?= t('Do you really want to remove this time slot?') ?></p>
<div class="form-actions">
<?= $this->url->link(t('Yes'), 'timetableweek', 'remove', array('user_id' => $user['id'], 'slot_id' => $slot_id), true, 'btn btn-red') ?>
<?= t('or') ?>
<?= $this->url->link(t('cancel'), 'timetableweek', 'index', array('user_id' => $user['id'])) ?>
</div>
</div>

View File

@ -62,9 +62,6 @@
<li <?= $this->app->getRouterController() === 'user' && $this->app->getRouterAction() === 'authentication' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Edit Authentication'), 'user', 'authentication', array('user_id' => $user['id'])) ?>
</li>
<li <?= $this->app->getRouterController() === 'timetable' ? 'class="active"' : '' ?>>
<?= $this->url->link(t('Manage timetable'), 'timetable', 'index', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
<?= $this->hook->render('template:user:sidebar:actions', array('user' => $user)) ?>

View File

@ -1,34 +0,0 @@
Budget management
=================
Budget management is based on the subtask time tracking, the user timetable and the user hourly rate.
This section is available from project settings page: **Project > Budget**. There is also a shortcut from the dropdown menu on the board.
Budget lines
------------
![Cost Lines](http://kanboard.net/screenshots/documentation/budget-lines.png)
Budget lines are used to define a budget for the project.
This budget can be adjusted by adding a new entry with an effective date.
Cost breakdown
--------------
![Cost Breakdown](http://kanboard.net/screenshots/documentation/budget-cost-breakdown.png)
Based on the subtask time tracking table and user information you can see the cost of each subtask.
The time spent is rounded to nearest quarter.
Budget graph
------------
![Budget Graph](http://kanboard.net/screenshots/documentation/budget-graph.png)
Finally, by combining all information we can generate a graph:
- Expenses represents user cost
- Budget lines are the provisioned budget
- Remaining is the budget left at the given time

View File

@ -1,34 +0,0 @@
Gestion du budget
=================
La gestion du budget repose sur le suivi du temps d'une sous-tâche, l'emploi du temps de l'utilisateur et le taux horaire de l'utilisateur.
Cette section est accessible depuis la page de paramètres du projet : **Project > Budget**. Il existe également un raccourci depuis le menu déroulant sur le tableau.
Lignes budgétaires
------------
![Ligne des coûts](http://kanboard.net/screenshots/documentation/budget-lines.png)
Les lignes budgétaires sont utilisées pour définir le budget du projet.
Celui-ci peut être ajusté en ajoutant une nouvelle entrée avec une date effective.
Détail des coûts
--------------
![Détail des coûts](http://kanboard.net/screenshots/documentation/budget-cost-breakdown.png)
Selon le tableau qui donne le suivi temporel de la sous-tâche et les informations sur l'utilisateur vous pouvez voir le coût de chaque sous-tâche.
Le temps passé est arrondi au quart d'heure le plus proche.
Graphique du budget
------------
![Graphique du budget](http://kanboard.net/screenshots/documentation/budget-graph.png)
Finalement, en combinant toutes les informations nous pouvons générer un graphique :
- Les dépenses représentent le coût utilisateur
- Les lignes budgétaires sont le budget prévisionnel
- Le restant est le budget qui reste après un délai donné

View File

@ -1,11 +0,0 @@
Hourly Rate
===========
Each user can have a predefined hourly rate.
This feature is used for budget calculation.
To define a new price, go to **User profile > Hourly rates**.
![Hourly Rate](http://kanboard.net/screenshots/documentation/hourly-rate.png)
Each hourly rate can have an effective date and and different currency.

View File

@ -26,7 +26,6 @@ Using Kanboard
- [Project permissions](project-permissions.markdown)
- [Swimlanes](swimlanes.markdown)
- [Calendar](calendar.markdown)
- [Budget](budget.markdown)
- [Analytics](analytics.markdown)
- [Gantt chart for tasks](gantt-chart-tasks.markdown)
- [Gantt chart for projects](gantt-chart-projects.markdown)
@ -49,8 +48,6 @@ Using Kanboard
- [User management](user-management.markdown)
- [Notifications](notifications.markdown)
- [Hourly rate](hourly-rate.markdown)
- [Timetable](timetable.markdown)
- [Two factor authentication](2fa.markdown)
### Settings

View File

@ -94,6 +94,27 @@ $this->hook->on('hook_name', $callable);
The first argument is the name of the hook and the second is a PHP callable.
### Hooks executed only one time
Some hooks can have only one listener:
#### model:subtask-time-tracking:calculate:time-spent
- Override time spent calculation when subtask timer is stopped
- Arguments:
- `$user_id` (integer)
- `$start` (DateTime)
- `$end` (DateTime)
#### model:subtask-time-tracking:calendar:events
- Override subtask time tracking events to display the calendar
- Arguments:
- `$user_id` (integer)
- `$events` (array)
- `$start` (string, ISO-8601 format)
- `$end` (string, ISO-8601 format)
### Merge hooks
"Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one.
@ -313,5 +334,7 @@ Kanboard will compare the version defined in your schema and the version stored
Examples of plugins
-------------------
- Budget planning: https://github.com/kanboard/plugin-budget
- Theme plugin sample: https://github.com/kanboard/plugin-example-theme
- [Budget planning](https://github.com/kanboard/plugin-budget)
- [User timetable](https://github.com/kanboard/plugin-timetable)
- [Subtask Forecast](https://github.com/kanboard/plugin-subtask-forecast)
- [Theme plugin sample](https://github.com/kanboard/plugin-example-theme)

View File

@ -1,46 +0,0 @@
User Timetable
==============
Each user can have a predefined timetable.
This feature mainly is used for time tracking, project budget calculation and to display subtasks in the calendar.
Each user have his own timetable. At the moment, that need to be specified manually for each person.
You can also schedule time-off or overtime.
The timetable section is available from the user profile: **User profile > Timetable**.
Work timetable
--------------
This timetable is dynamically calculated according to the regular week timetable, time-off and overtime.
![Timetable](http://kanboard.net/screenshots/documentation/timetable.png)
Week timetable
--------------
![Week Timetable](http://kanboard.net/screenshots/documentation/week-timetable.png)
The week timetable is used to define regular work hours for the selected user.
To add a new time slot, just select the day of the week and the time range.
Time off timetable
------------------
The time-off timetable is used to schedule not worked time slot.
This time is deducted from the regular work hours.
When you check the box "All day", the regular day timetable is used to define the regular work hours.
Overtime timetable
------------------
![Overtime Timetable](http://kanboard.net/screenshots/documentation/overtime-timetable.png)
The overtime timetable is used to define worked hours outside of regular hours.
Day timetable
-------------
This timetable is used when the checkbox "All day" is checked for overtime and time-off entries.

View File

@ -17,6 +17,16 @@ class HookTest extends Base
$this->assertEquals(array('A', 'B'), $h->getListeners('myhook'));
}
public function testExists()
{
$h = new Hook;
$this->assertFalse($h->exists('myhook'));
$h->on('myhook', 'A');
$this->assertTrue($h->exists('myhook'));
}
public function testMergeWithNoBinding()
{
$h = new Hook;
@ -59,4 +69,28 @@ class HookTest extends Base
$this->assertEquals($expected, $result);
$this->assertEquals($expected, $values);
}
public function testFirstWithNoBinding()
{
$h = new Hook;
$result = $h->first('myhook', array('p' => 2));
$this->assertEquals(null, $result);
}
public function testFirstWithMultipleBindings()
{
$h = new Hook;
$h->on('myhook', function($p) {
return $p + 1;
});
$h->on('myhook', function($p) {
return $p;
});
$result = $h->first('myhook', array('p' => 3));
$this->assertEquals(4, $result);
}
}

View File

@ -1,256 +0,0 @@
<?php
require_once __DIR__.'/../Base.php';
use Model\User;
use Model\Timetable;
use Model\TimetableDay;
use Model\TimetableWeek;
use Model\TimetableOff;
use Model\TimetableExtra;
class TimetableTest extends Base
{
public function testCalculateWorkDays()
{
$w = new TimetableWeek($this->container);
$t = new Timetable($this->container);
$this->assertNotFalse($w->create(1, 1, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 1, '13:00', '17:00'));
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
$monday = new DateTime('next Monday');
$timetable = $t->calculate(1, $monday, new DateTime('next Monday + 6 days'));
$this->assertNotEmpty($timetable);
$this->assertCount(4, $timetable);
$this->assertEquals($monday->format('Y-m-d').' 09:30', $timetable[0][0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 12:00', $timetable[0][1]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 13:00', $timetable[1][0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 17:00', $timetable[1][1]->format('Y-m-d H:i'));
$this->assertEquals($monday->add(new DateInterval('P1D'))->format('Y-m-d').' 09:30', $timetable[2][0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 12:00', $timetable[2][1]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 13:00', $timetable[3][0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 17:00', $timetable[3][1]->format('Y-m-d H:i'));
}
public function testCalculateOverTime()
{
$d = new TimetableDay($this->container);
$w = new TimetableWeek($this->container);
$e = new TimetableExtra($this->container);
$t = new Timetable($this->container);
$monday = new DateTime('next Monday');
$tuesday = new DateTime('next Monday + 1 day');
$friday = new DateTime('next Monday + 4 days');
$this->assertNotFalse($d->create(1, '08:00', '12:00'));
$this->assertNotFalse($d->create(1, '14:00', '18:00'));
$this->assertNotFalse($w->create(1, 1, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 1, '13:00', '17:00'));
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
$this->assertNotFalse($e->create(1, $tuesday->format('Y-m-d'), 0, '17:00', '22:00'));
$this->assertNotFalse($e->create(1, $friday->format('Y-m-d'), 1));
$timetable = $t->calculate(1, $monday, new DateTime('next Monday + 6 days'));
$this->assertNotEmpty($timetable);
$this->assertCount(7, $timetable);
$this->assertEquals($monday->format('Y-m-d').' 09:30', $timetable[0][0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 12:00', $timetable[0][1]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 13:00', $timetable[1][0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 17:00', $timetable[1][1]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 09:30', $timetable[2][0]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 12:00', $timetable[2][1]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 13:00', $timetable[3][0]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 17:00', $timetable[3][1]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 17:00', $timetable[4][0]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 22:00', $timetable[4][1]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 08:00', $timetable[5][0]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 12:00', $timetable[5][1]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 14:00', $timetable[6][0]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 18:00', $timetable[6][1]->format('Y-m-d H:i'));
}
public function testCalculateTimeOff()
{
$d = new TimetableDay($this->container);
$w = new TimetableWeek($this->container);
$o = new TimetableOff($this->container);
$t = new Timetable($this->container);
$monday = new DateTime('next Monday');
$tuesday = new DateTime('next Monday + 1 day');
$friday = new DateTime('next Monday + 4 days');
$this->assertNotFalse($d->create(1, '08:00', '12:00'));
$this->assertNotFalse($d->create(1, '14:00', '18:00'));
$this->assertNotFalse($w->create(1, 1, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 1, '13:00', '17:00'));
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
$this->assertNotFalse($w->create(1, 5, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 5, '13:00', '17:00'));
$this->assertNotFalse($o->create(1, $tuesday->format('Y-m-d'), 0, '14:00', '15:00'));
$this->assertNotFalse($o->create(1, $monday->format('Y-m-d'), 1));
$timetable = $t->calculate(1, $monday, new DateTime('next Monday + 6 days'));
$this->assertNotEmpty($timetable);
$this->assertCount(5, $timetable);
$this->assertEquals($tuesday->format('Y-m-d').' 09:30', $timetable[0][0]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 12:00', $timetable[0][1]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 13:00', $timetable[1][0]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 14:00', $timetable[1][1]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 15:00', $timetable[2][0]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 17:00', $timetable[2][1]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 09:30', $timetable[3][0]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 12:00', $timetable[3][1]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 13:00', $timetable[4][0]->format('Y-m-d H:i'));
$this->assertEquals($friday->format('Y-m-d').' 17:00', $timetable[4][1]->format('Y-m-d H:i'));
}
public function testClosestTimeSlot()
{
$w = new TimetableWeek($this->container);
$t = new Timetable($this->container);
$this->assertNotFalse($w->create(1, 1, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 1, '13:00', '17:00'));
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
$monday = new DateTime('next Monday');
$tuesday = new DateTime('next Monday + 1 day');
$timetable = $t->calculate(1, new DateTime('next Monday'), new DateTime('next Monday + 6 days'));
$this->assertNotEmpty($timetable);
$this->assertCount(4, $timetable);
// Start to work before timetable
$date = clone($monday);
$date->setTime(5, 02);
$slot = $t->findClosestTimeSlot($date, $timetable);
$this->assertNotEmpty($slot);
$this->assertEquals($monday->format('Y-m-d').' 09:30', $slot[0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i'));
// Start to work at the end of the timeslot
$date = clone($monday);
$date->setTime(12, 02);
$slot = $t->findClosestTimeSlot($date, $timetable);
$this->assertNotEmpty($slot);
$this->assertEquals($monday->format('Y-m-d').' 09:30', $slot[0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i'));
// Start to work at lunch time
$date = clone($monday);
$date->setTime(12, 32);
$slot = $t->findClosestTimeSlot($date, $timetable);
$this->assertNotEmpty($slot);
$this->assertEquals($monday->format('Y-m-d').' 13:00', $slot[0]->format('Y-m-d H:i'));
$this->assertEquals($monday->format('Y-m-d').' 17:00', $slot[1]->format('Y-m-d H:i'));
// Start to work early in the morning
$date = clone($tuesday);
$date->setTime(8, 02);
$slot = $t->findClosestTimeSlot($date, $timetable);
$this->assertNotEmpty($slot);
$this->assertEquals($tuesday->format('Y-m-d').' 09:30', $slot[0]->format('Y-m-d H:i'));
$this->assertEquals($tuesday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i'));
}
public function testCalculateDuration()
{
$w = new TimetableWeek($this->container);
$t = new Timetable($this->container);
$this->assertNotFalse($w->create(1, 1, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 1, '13:00', '17:00'));
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
$monday = new DateTime('next Monday');
$tuesday = new DateTime('next Monday + 1 day');
// Different day
$start = clone($monday);
$start->setTime(16, 02);
$end = clone($tuesday);
$end->setTime(10, 03);
$this->assertEquals(1.5, $t->calculateEffectiveDuration(1, $start, $end));
// Same time slot
$start = clone($monday);
$start->setTime(16, 02);
$end = clone($monday);
$end->setTime(17, 03);
$this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end));
// Intermediate time slot
$start = clone($monday);
$start->setTime(10, 02);
$end = clone($tuesday);
$end->setTime(16, 03);
$this->assertEquals(11.5, $t->calculateEffectiveDuration(1, $start, $end));
// Different day
$start = clone($monday);
$start->setTime(9, 02);
$end = clone($tuesday);
$end->setTime(10, 03);
$this->assertEquals(7, $t->calculateEffectiveDuration(1, $start, $end));
// Start before first time slot
$start = clone($monday);
$start->setTime(5, 32);
$end = clone($tuesday);
$end->setTime(11, 17);
$this->assertEquals(8.25, $t->calculateEffectiveDuration(1, $start, $end));
}
public function testCalculateDurationWithEmptyTimetable()
{
$t = new Timetable($this->container);
$start = new DateTime('next Monday');
$start->setTime(16, 02);
$end = new DateTime('next Monday');
$end->setTime(17, 03);
$this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end));
}
}