diff --git a/src/schedule-2.0/assets/js/timer.js b/src/schedule-2.0/assets/js/timer.js
new file mode 100644
index 0000000..95638e4
--- /dev/null
+++ b/src/schedule-2.0/assets/js/timer.js
@@ -0,0 +1,609 @@
+
+
+/* Creates a new Task object. */
+function Task(id ,name, description) {
+ this._name = name;
+ this._taskid = id;
+ this._description = description;
+ this._state = Task.State.STOPPED;
+ this._timeSpentInPreviousIterations = 0;
+ this._startDate = 0;
+ this._endDate = 0;
+ this._currentIterationStartTime = 0;
+ this._customTimeSpent = 0;
+ this._onChange = null;
+}
+
+/* Possible task states. */
+Task.State = {
+ STOPPED: "stopped",
+ RUNNING: "running"
+}
+
+Task.prototype = {
+ /* Returns the task name. */
+ getName: function() {
+ return this._name;
+ },
+
+ /* Returns the task description. */
+ getDescription: function() {
+ if (this._description == "NaN") {
+ this._description = ""
+ }
+ return this._description;
+ },
+
+ /* Returns the task state. */
+ getState: function() {
+ return this._state;
+ },
+
+ /* Is the task stopped? */
+ isStopped: function() {
+ return this._state == Task.State.STOPPED;
+ },
+
+ /* Is the task running? */
+ isRunning: function() {
+ return this._state == Task.State.RUNNING;
+ },
+
+ /*
+ * Sets the "onChange" event handler. The "onChange" event is fired when the
+ * task is started, stopped, or reset.
+ */
+ setOnChange: function(onChange) {
+ this._onChange = onChange;
+ },
+
+ /*
+ * Returns the time spent on the task in the current work iteration. Works
+ * correctly only when the task is running.
+ */
+ _getCurrentIterationTime: function() {
+ return (new Date).getTime() - this._currentIterationStartTime;
+ },
+
+ /*
+ * Returns the total time spent on the task. This includes time spent in
+ * the current work iteration if the task is running.
+ */
+ getTimeSpent: function() {
+ var result = this._timeSpentInPreviousIterations;
+ if (this._state == Task.State.RUNNING) {
+ result += this._getCurrentIterationTime();
+ }
+ return result;
+ },
+
+ /* Calls the "onChange" event handler if set. */
+ _callOnChange: function() {
+ if (typeof this._onChange == "function") {
+ this._onChange();
+ }
+ },
+
+ /* Starts a new task work iteration. */
+ start: function() {
+ if (this._state == Task.State.RUNNING) { return };
+ if (this._startDate == 0) {this._startDate = new Date()}
+ this._state = Task.State.RUNNING;
+ this._currentIterationStartTime = (new Date).getTime();
+ this._callOnChange();
+ },
+
+ /* Stops the current task work iteration. */
+ stop: function() {
+ if (this._state == Task.State.STOPPED) { return };
+
+ this._state = Task.State.STOPPED;
+ this._timeSpentInPreviousIterations += this._getCurrentIterationTime();
+ this._currentIterationStartTime = 0;
+ this._endDate = new Date();
+ this._callOnChange();
+ },
+
+ /* Stops the current task work iteration and resets the time data. */
+ reset: function() {
+ this.stop();
+ this._timeSpentInPreviousIterations = 0;
+ this._callOnChange();
+ },
+ save: function(task){
+ console.log(task)
+
+ $.ajax({
+ type: "POST",
+ data: {
+ taskname: task._name,
+ taskid: task._taskid,
+ description: task._description,
+ start: task._startDate,
+ end: task._endDate,
+ duration: task._timeSpentInPreviousIterations,
+ },
+ url: "{{ path('app_timer_create') }}",
+ success: function (response) {
+ response=JSON.parse(response);
+ if(response.return=="KO") {
+ $("#dataTable_wrapper").append("
"+response.error+"
");
+ }else{
+ location.reload();
+ }
+ }
+ });
+ },
+ /* Serializes the task into a string. */
+ serialize: function() {
+ /*
+ * Originally, I wanted to use "toSource" and "eval" for serialization and
+ * deserialization, but "toSource" is not supported by WebKit, so I resorted
+ * to ugly hackery...
+ */
+ return [
+ encodeURIComponent(this._name),
+ this._state,
+ this._timeSpentInPreviousIterations,
+ this._currentIterationStartTime,
+ this._startDate,
+ this._endDate,
+ this._taskid,
+ this._description,
+ ].join("&");
+ },
+
+ /* Deserializes the task from a string. */
+ deserialize: function(serialized) {
+ var parts = serialized.split("&");
+ this._name = decodeURIComponent(parts[0]);
+ this._state = parts[1];
+ this._timeSpentInPreviousIterations = parseInt(parts[2]);
+ this._currentIterationStartTime = parseInt(parts[3]);
+ this._startDate = parseInt(parts[4]);
+ this._endDate = parseInt(parts[5]);
+ this._taskid = parseInt(parts[6]);
+ this._description = parseInt(parts[7]);
+
+ }
+}
+
+/* ===== Tasks ===== */
+
+/* The Tasks class represents a list of tasks. */
+
+/* Creates a new Tasks object. */
+function Tasks() {
+ this._tasks = [];
+
+ this._onAdd = null;
+ this._onRemove = null;
+}
+
+Tasks.prototype = {
+ /*
+ * Sets the "onAdd" event handler. The "onAdd" event is fired when a task
+ * is added to the list.
+ */
+ setOnAdd: function(onAdd) {
+ this._onAdd = onAdd;
+ },
+
+ /*
+ * Sets the "onRemove" event handler. The "onRemove" event is fired when a
+ * task is removed from the list.
+ */
+ setOnRemove: function(onRemove) {
+ this._onRemove = onRemove;
+ },
+
+ /* Returns the length of the task list. */
+ length: function() {
+ return this._tasks.length
+ },
+
+ /*
+ * Returns index-th task in the list, or "undefined" if the index is out of
+ * bounds.
+ */
+ get: function(index) {
+ return this._tasks[index];
+ },
+
+ /*
+ * Calls the callback function for each task in the list. The function is
+ * called with three parameters - the task, its index and the task list
+ * object. This is modeled after "Array.forEach" in JavaScript 1.6.
+ */
+ forEach: function(callback) {
+ for (var i = 0; i < this._tasks.length; i++) {
+ callback(this._tasks[i], i, this);
+ }
+ },
+
+ /* Calls the "onAdd" event handler if set. */
+ _callOnAdd: function(task) {
+ if (typeof this._onAdd == "function") {
+ this._onAdd(task);
+ }
+ },
+
+ /* Adds a new task to the end of the list. */
+ add: function(task) {
+ this._tasks.push(task);
+ this._callOnAdd(task);
+ },
+
+ /* Calls the "onRemove" event handler if set. */
+ _callOnRemove: function(index) {
+ if (typeof this._onRemove == "function") {
+ this._onRemove(index);
+ }
+ },
+
+ /*
+ * Removes index-th task from the list. Does not do anything if the index
+ * is out of bounds.
+ */
+ remove: function(index) {
+ this._callOnRemove(index);
+ this._tasks.splice(index, 1);
+ },
+
+
+
+ /* Serializes the list of tasks into a string. */
+ serialize: function() {
+ var serializedTasks = [];
+ this.forEach(function(task) {
+ serializedTasks.push(task.serialize());
+ });
+ return serializedTasks.join("|");
+ },
+
+ /* Deserializes the list of tasks from a string. */
+ deserialize: function(serialized) {
+ /*
+ * Repeatedly use "remove" so the "onRemove" event is triggered. Do the same
+ * with the "add" method below.
+ */
+ while (this._tasks.length > 0) {
+ this.remove(0);
+ }
+
+ var serializedTasks = serialized.split("|");
+ for (var i = 0; i < serializedTasks.length; i++) {
+ var task = new Task(0 ,"", "");
+ task.deserialize(serializedTasks[i]);
+ this.add(task);
+ }
+ }
+}
+
+/* ===== Extensions ===== */
+
+/*
+ * Pads this string with another string on the left until the resulting string
+ * has specified length. If the padding string has more than one character, the
+ * resulting string may be longer than desired (the padding string is not
+ * truncated and it is only prepended as a whole). Bad API, I know, but it's
+ * good enough for me.
+ */
+String.prototype.pad = function(length, padding) {
+ var result = this;
+ while (result.length < length) {
+ result = padding + result;
+ }
+ return result;
+}
+
+/* ===== Task List + DOM Storage ===== */
+
+/* The list of tasks. */
+var tasks = new Tasks();
+
+/* The last value of the serialized task list string. */
+var lastSerializedTasksString;
+
+/*
+ * The key under which the serialized task list string is stored in the DOM
+ * Storage.
+ */
+var TASKS_DOM_STORAGE_KEY = "timerTasks";
+
+/*
+ * Returns DOM Storage used by the application, or "null" if the browser does
+ * not support DOM Storage.
+ */
+function getStorage() {
+ /*
+ * HTML 5 says that the persistent storage is available in the
+ * "window.localStorage" attribute, however Firefox implements older version
+ * of the proposal, which uses "window.globalStorage" indexed by the domain
+ * name. We deal with this situation here as gracefully as possible (i.e.
+ * without concrete browser detection and with forward compatibility).
+ *
+ * For more information, see:
+ *
+ * http://www.whatwg.org/specs/web-apps/current-work/#storage
+ * https://developer.mozilla.org/En/DOM/Storage
+ */
+ if (window.localStorage !== undefined) {
+ return window.localStorage;
+ } else if (window.globalStorage !== undefined) {
+ return window.globalStorage[location.hostname];
+ } else {
+ return null;
+ }
+}
+
+/*
+ * Saves the task list into a DOM Storage. Also updates the value of the
+ * "lastSerializedTasksString" variable.
+ */
+function saveTasks() {
+ var serializedTasksString = tasks.serialize();
+ getStorage()[TASKS_DOM_STORAGE_KEY] = serializedTasksString;
+ lastSerializedTasksString = serializedTasksString;
+}
+
+/*
+ * Loads the serialized task list string from the DOM Storage. Returns
+ * "undefined" if the storage does not contain the string (this happens when
+ * running the application for the first time).
+ */
+function loadSerializedTasksString() {
+ var storedValue = getStorage()[TASKS_DOM_STORAGE_KEY];
+ /*
+ * The spec says "null" should be returned when the key is not found, but some
+ * browsers return "undefined" instead. Maybe it was in some earlier version
+ * of the spec (I didn't bother to check).
+ */
+ if (storedValue !== null && storedValue !== undefined && storedValue.length > 0) {
+ /*
+ * The values retrieved from "globalStorage" use one more level of
+ * indirection.
+ */
+ return (window.localStorage === undefined) ? storedValue.value : storedValue;
+ } else {
+ return undefined;
+ }
+}
+
+/*
+ * Loads the task list from the DOM Storage. Also updates the value of the
+ * "lastSerializedTasksString" variable.
+ */
+function loadTasks() {
+ var serializedTasksString = loadSerializedTasksString();
+ if (serializedTasksString !== undefined) {
+ tasks.deserialize(serializedTasksString);
+ lastSerializedTasksString = serializedTasksString;
+ }
+}
+
+/*
+ * Was the task list changed outside of the application? Detects the change
+ * by comparing the current serialized task list string in the DOM Storage
+ * with a kept old value.
+ */
+function tasksHaveChangedOutsideApplication() {
+ var serializedTasksString = loadSerializedTasksString();
+ if (serializedTasksString != lastSerializedTasksString) {
+ return true
+ }
+ return false;
+}
+
+/* ===== View ===== */
+
+/* Some time constants. */
+var MILISECONDS_IN_SECOND = 1000;
+var MILISECONDS_IN_MINUTE = 60 * MILISECONDS_IN_SECOND;
+var MINUTES_IN_HOUR = 60;
+
+/* Formats the time in the H:MM format. */
+function formatTime(time) {
+ var timeInMinutes = time / MILISECONDS_IN_MINUTE;
+ var hours = Math.floor(timeInMinutes / MINUTES_IN_HOUR);
+ var minutes = Math.floor(timeInMinutes - hours * MINUTES_IN_HOUR);
+ return hours + ":" + String(minutes).pad(2, "0");
+}
+
+/*
+ * Computes the URL of the image in the start/stop link according to the task
+ * state.
+ */
+function computeStartStopLinkImageUrl(state) {
+ switch (state) {
+ case Task.State.STOPPED:
+ return "fa-play";
+ case Task.State.RUNNING:
+ return "fa-stop";
+ default:
+ throw "Invalid task state."
+ }
+}
+
+/*
+ * Builds the HTML element of the row in the task table corresponding to the
+ * specified task and index.
+ */
+function buildTaskRow(task, index) {
+ var result = $("
");
+
+ var startStopLink = $(
+ ""
+ + ""
+
+ );
+ startStopLink.click(function() {
+ switch (task.getState()) {
+ case Task.State.STOPPED:
+ task.start();
+ break;
+ case Task.State.RUNNING:
+ task.stop();
+ break;
+ default:
+ throw "Invalid task state."
+ }
+ saveTasks();
+ return false;
+ });
+
+ var resetLink = $(
+ ""
+ + ""
+ );
+ resetLink.click(function() {
+ task.reset();
+ saveTasks();
+ return false;
+ });
+
+ var deleteLink = $(
+ ""
+ + ""
+
+ );
+ deleteLink.click(function() {
+ if (confirm("Do you really want to delete task \"" + task.getName() + "\"?")) {
+ tasks.remove(index);
+ saveTasks();
+ }
+ return false;
+ });
+
+ var saveLink = $(
+ ""
+ + ""
+ );
+ saveLink.click(function() {
+ tasks.remove(index);
+ saveTasks();
+ task.save(task);
+ return false;
+ });
+ result
+ .addClass("state-" + task.getState())
+ .append($(" | ").text(task.getName()))
+ .append($(" | ").text(task.getDescription()))
+ .append($(" | ").text(formatTime(task.getTimeSpent())))
+ .append($(" | ")
+ .append(startStopLink)
+ .append(" ")
+ .append(resetLink)
+ .append(" ")
+ .append(saveLink)
+ .append(" ")
+ .append(deleteLink)
+ );
+
+ return result;
+}
+
+/* Finds row with the specified index in the task table. */
+function findRowWithIndex(index) {
+ return $("#task-table").find("tr").slice(1).eq(index);
+}
+
+/*
+ * Updates the row in the task table corresponding to a task according to
+ * its state.
+ */
+function updateTaskRow(row, task) {
+ if (task.isStopped()) {
+ row.removeClass("state-running");
+ row.addClass("state-stopped");
+ } else if (task.isRunning()) {
+ row.removeClass("state-stopped");
+ row.addClass("state-running");
+ }
+
+ row.find(".task-time").text(formatTime(task.getTimeSpent()))
+
+ row.find(".start-stop-link i").removeClass();
+ row.find(".start-stop-link i").addClass("fa "+computeStartStopLinkImageUrl(task.getState()));
+}
+
+/* ===== Initialization ===== */
+
+/* Initializes event handlers on the task list. */
+function initializeTasksEventHandlers() {
+ tasks.setOnAdd(function(task) {
+ var row = buildTaskRow(task, tasks.length() - 1);
+ $("#task-table").append(row);
+ task.setOnChange(function() {
+ updateTaskRow(row, task);
+ });
+ });
+
+ tasks.setOnRemove(function(index) {
+ findRowWithIndex(index).remove();
+ if (tasks.length() == 1 ) {
+ $( "#task-table" ).css({"display":"none"});
+ }
+ });
+}
+
+/* Initializes GUI event handlers. */
+function initializeGuiEventHandlers() {
+ $( "#addtimer" ).click(function() {
+ displayTaskAdd();
+ });
+
+ $( "#timer-desc" ).keypress(function( event ) {
+ if ( event.which == 13 ) {
+ event.preventDefault();
+ displayTaskAdd();
+ }
+ });
+
+}
+function displayTaskAdd() {
+ $( "#task-table" ).css({"display":"inline"});
+ var taskName = $("#timer-task option:selected").text();
+ var taskId = $("#timer-task option:selected").val();
+ var taskDesc = $("#timer-desc").val();
+
+ var task = new Task(taskId, taskName, taskDesc);
+ tasks.add(task);
+ saveTasks();
+ $("#timer-desc").val("");
+}
+/*
+ * Initializes a timer used to update task times and detect changes in the
+ * data stored in the DOM Storage.
+ */
+function initializeTimer() {
+ setInterval(function() {
+ tasks.forEach(function(task, index) {
+ updateTaskRow(findRowWithIndex(index), task);
+ });
+
+ if (tasksHaveChangedOutsideApplication()) {
+ loadTasks();
+ }
+ }, 10 * MILISECONDS_IN_SECOND);
+}
+
+/* Initializes the application. */
+$(document).ready(function(){
+ try {
+ if (!getStorage()) {
+ alert("Timer requires a browser with DOM Storage support, such as Firefox 3+ or Safari 4+.");
+ return;
+ }
+ } catch (e) {
+ alert("Timer does not work with file: URLs in Firefox.");
+ return;
+ }
+
+ initializeTasksEventHandlers();
+ loadTasks();
+ initializeGuiEventHandlers();
+ initializeTimer();
+});
+
+
diff --git a/src/schedule-2.0/config/routes.yaml b/src/schedule-2.0/config/routes.yaml
index 079aadb..d088699 100644
--- a/src/schedule-2.0/config/routes.yaml
+++ b/src/schedule-2.0/config/routes.yaml
@@ -347,6 +347,26 @@ app_validationholiday_activeholiday:
path: /validator/validateholiday/activeholiday
defaults: { _controller: App\Controller\ValidationController:activeholiday }
+#== Timer ====================================================================================================
+app_timer:
+ path: /user/timer
+ defaults: { _controller: App\Controller\TimerController:list }
+
+app_timer_submit:
+ path: /user/timer/submit
+ defaults: { _controller: App\Controller\TimerController:submit }
+
+app_timer_create:
+ path: /user/timer/create
+ defaults: { _controller: App\Controller\TimerController:create }
+
+app_timer_update:
+ path: /user/timer/update/{id}
+ defaults: { _controller: App\Controller\TimerController:update }
+
+app_timer_delete:
+ path: /user/timer/delete/{id}
+ defaults: { _controller: App\Controller\TimerController:delete }
#== Customer ======================================================================================================
app_customer_report:
diff --git a/src/schedule-2.0/src/Controller/TimerController.php b/src/schedule-2.0/src/Controller/TimerController.php
new file mode 100644
index 0000000..effe14c
--- /dev/null
+++ b/src/schedule-2.0/src/Controller/TimerController.php
@@ -0,0 +1,176 @@
+getDoctrine()->getManager();
+
+ $iduser = $this->get("session")->get("iduser");
+ $user = $em->getRepository("App:User")->find($iduser);
+ $tasks = $em->getRepository("App:Task")->findAll();
+ $timers = $em->getRepository("App:Timer")->findBy(["user"=>$iduser]);
+
+ return $this->render($this->render.'list.html.twig',[
+ "useheader" => true,
+ "usesidebar" => true,
+ "user" => $user,
+ "tasks" => $tasks,
+ "timers" => $timers,
+ ]);
+ }
+
+ public function create(Request $request)
+ {
+ // Initialisation de l'enregistrement
+ $em = $this->getDoctrine()->getManager();
+ $iduser = $this->get("session")->get("iduser");
+ $user = $em->getRepository("App:User")->find($iduser);
+ $taskid = $request->request->get('taskid');
+ $description = $request->request->get('description');
+ $task = $em->getRepository("App:Task")->find($taskid);
+ $start = \DateTime::createFromFormat('D M d Y H:i:s e+',$request->request->get('start'));
+ $end = \DateTime::createFromFormat('D M d Y H:i:s e+',$request->request->get('end'));
+ $duration = new \DateTime(date("H:i:s", ($request->request->get('duration')/1000)));
+ $duration->sub(new \DateInterval('PT1H'));
+
+ $timer = new Entity();
+ $timer->setUser($user);
+ $timer->setTask($task);
+ $timer->setStart($start);
+ $timer->setEnd($end);
+ $timer->setDuration($duration);
+ $timer->setDescription($description);
+ $em->persist($timer);
+ $em->flush();
+
+ $output=["return"=>"OK"];
+ return new Response(json_encode($output));
+ }
+ public function submit(Request $request)
+ {
+ // Initialisation de l'enregistrement
+ $em = $this->getDoctrine()->getManager();
+ $data = new Entity();
+
+ // Création du formulaire
+ $form = $this->createForm(Form::class,$data,array("mode"=>"submit"));
+
+ // Récupération des data du formulaire
+ $form->handleRequest($request);
+
+ // Sur erreur
+ $this->getErrorForm(null,$form,$request,$data,"submit");
+
+ // Sur validation
+ if ($form->get('submit')->isClicked() && $form->isValid()) {
+ $data = $form->getData();
+ $em->persist($data);
+ $em->flush();
+
+ // Retour à la liste
+ return $this->redirectToRoute($this->route);
+ }
+
+ // Affichage du formulaire
+ return $this->render($this->render.'edit.html.twig', [
+ 'useheader' => true,
+ 'usesidebar' => true,
+ $this->data => $data,
+ 'mode' => 'submit',
+ 'form' => $form->createView()
+ ]);
+ }
+
+ public function update($id,Request $request)
+ {
+ // Initialisation de l'enregistrement
+ $em = $this->getDoctrine()->getManager();
+ $data=$em->getRepository($this->entity)->find($id);
+
+ // Création du formulaire
+ $form = $this->createForm(Form::class,$data,array("mode"=>"update"));
+
+ // Récupération des data du formulaire
+ $form->handleRequest($request);
+
+ // Sur erreur
+ $this->getErrorForm(null,$form,$request,$data,"update");
+
+ // Sur validation
+ if ($form->get('submit')->isClicked() && $form->isValid()) {
+ $data = $form->getData();
+ $em->persist($data);
+ $em->flush();
+
+ // Retour à la liste
+ return $this->redirectToRoute($this->route);
+ }
+
+
+ return $this->render($this->render.'edit.html.twig', [
+ 'useheader' => true,
+ 'usesidebar' => true,
+ $this->data => $data,
+ 'mode' => 'update',
+ 'form' => $form->createView()
+ ]);
+
+ }
+ public function delete($id, Request $request)
+ {
+ // Initialisation de l'enregistrement
+ $em = $this->getDoctrine()->getManager();
+ $data=$em->getRepository($this->entity)->find($id);
+
+ // Controle avant suppression
+ $error=false;
+ if($error)
+ return $this->redirectToRoute($this->route."_update",["id"=>$id]);
+ else {
+ try {
+ $em->remove($data);
+ $em->flush();
+ }
+ catch(\Doctrine\DBAL\DBALException $e) {
+ // Création du formulaire
+ $this->get('session')->getFlashBag()->add('error', 'Impossible de supprimer cet enregistrement');
+ return $this->redirectToRoute($this->route."_update",["id"=>$id]);
+ }
+
+ // Retour à la liste
+ return $this->redirectToRoute($this->route);
+ }
+ }
+ protected function getErrorForm($id,$form,$request,$data,$mode) {
+ if ($form->get('submit')->isClicked()&&$mode=="delete") {
+ }
+
+ if ($form->get('submit')->isClicked() && $mode=="submit") {
+ }
+
+ if ($form->get('submit')->isClicked() && !$form->isValid()) {
+ $this->get('session')->getFlashBag()->clear();
+
+ $errors = $form->getErrors();
+ foreach( $errors as $error ) {
+ $request->getSession()->getFlashBag()->add("error", $error->getMessage());
+ }
+ }
+ }
+}
diff --git a/src/schedule-2.0/src/Entity/Task.php b/src/schedule-2.0/src/Entity/Task.php
index ff59675..3a9f470 100644
--- a/src/schedule-2.0/src/Entity/Task.php
+++ b/src/schedule-2.0/src/Entity/Task.php
@@ -61,12 +61,17 @@ class Task
*/
private $events;
+ /**
+ * @ORM\OneToMany(targetEntity="Timer", mappedBy="task", cascade={"persist"}, orphanRemoval=false)
+ */
+ private $timers;
+
/**
* @ORM\OneToMany(targetEntity="Penalty", mappedBy="task", cascade={"persist"}, orphanRemoval=false)
*/
private $penaltys;
- /**
+ /**
* Calculate Displayname
*/
public function getDisplayname(): ?string
@@ -74,7 +79,7 @@ class Task
return $this->project->getCustomer()->getName()."-".$this->project->getName()."-".$this->name;
}
- /**
+ /**
* Calculate Duration
*/
public function getDuration($end): ?string
@@ -90,6 +95,7 @@ class Task
public function __construct()
{
$this->events = new ArrayCollection();
+ $this->timers = new ArrayCollection();
$this->penaltys = new ArrayCollection();
}
@@ -191,6 +197,24 @@ class Task
return $this;
}
+ /**
+ * @return Collection|Timer[]
+ */
+ public function getTimers(): Collection
+ {
+ return $this->tasks;
+ }
+
+ public function addTimer(Timer $timer): self
+ {
+ if (!$this->timers->contains($timer)) {
+ $this->timers[] = $timer;
+ $timer->setTask($this);
+ }
+
+ return $this;
+ }
+
public function removeEvent(Event $event): self
{
if ($this->events->contains($event)) {
@@ -235,4 +259,17 @@ class Task
return $this;
}
+ public function removeTimer(Timer $timer): self
+ {
+ if ($this->timers->contains($timer)) {
+ $this->timers->removeElement($timer);
+ // set the owning side to null (unless already changed)
+ if ($timer->getTask() === $this) {
+ $timer->setTask(null);
+ }
+ }
+
+ return $this;
+ }
+
}
diff --git a/src/schedule-2.0/src/Entity/Timer.php b/src/schedule-2.0/src/Entity/Timer.php
new file mode 100644
index 0000000..2d5ace0
--- /dev/null
+++ b/src/schedule-2.0/src/Entity/Timer.php
@@ -0,0 +1,142 @@
+start = new \DateTime();
+ $this->end = new \DateTime();
+ $this->duration = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getStart(): ?\DateTimeInterface
+ {
+ return $this->start;
+ }
+
+ public function setStart(\DateTimeInterface $start): self
+ {
+ $this->start = $start;
+
+ return $this;
+ }
+
+ public function getEnd(): ?\DateTimeInterface
+ {
+ return $this->end;
+ }
+
+ public function setEnd(\DateTimeInterface $end): self
+ {
+ $this->end = $end;
+
+ return $this;
+ }
+
+ public function getDuration(): ?\DateTimeInterface
+ {
+ return $this->duration;
+ }
+
+ public function setDuration(\DateTimeInterface $duration): self
+ {
+ $this->duration = $duration;
+
+ return $this;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function setDescription(?string $description): self
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function setUser(?User $user): self
+ {
+ $this->user = $user;
+
+ return $this;
+ }
+
+ public function getTask(): ?Task
+ {
+ return $this->task;
+ }
+
+ public function setTask(?Task $task): self
+ {
+ $this->task = $task;
+
+ return $this;
+ }
+
+}
diff --git a/src/schedule-2.0/src/Form/TimerType.php b/src/schedule-2.0/src/Form/TimerType.php
new file mode 100644
index 0000000..f53771f
--- /dev/null
+++ b/src/schedule-2.0/src/Form/TimerType.php
@@ -0,0 +1,99 @@
+add('submit',
+ SubmitType::class, [
+ "label" => "Valider",
+ "attr" => ["class" => "btn btn-success no-print"],
+ ]
+ );
+ $builder->add('task',
+ EntityType::class, [
+ "label" => "Tâche",
+ "class" => "App:Task",
+ "choice_label" => function ($task) {
+ return $task->getDisplayname();},
+
+ "disabled" => false,
+ "required" => true,
+ "multiple" => false,
+ "placeholder" => "Selectionner une Tâche",
+ ]
+ );
+
+ $builder->add('description',
+ TextType::class, [
+ "label" => "Description"
+ ]
+ );
+
+ $builder->add('start',
+ DateTimeType::class, [
+ "label" =>"Début",
+ "date_widget" => "single_text",
+ "time_widget" => "single_text",
+ "format" => "yyyy-MM-dd HH:mm",
+ ]
+ );
+
+ $builder->add('end',
+ DateTimeType::class, [
+ "label" =>"Fin",
+ "date_widget" => "single_text",
+ "time_widget" => "single_text",
+ "format" => "yyyy-MM-dd HH:mm",
+ ]
+ );
+
+ $builder->addEventListener(
+
+ );
+ $builder->add('duration',
+ TimeType::class, [
+ "label" =>"Durée",
+ "widget" => "single_text",
+ ]
+ );
+
+
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'data_class' => 'App\Entity\Timer',
+ 'mode' => 'string',
+ ));
+ }
+}
diff --git a/src/schedule-2.0/templates/Timer/edit.html.twig b/src/schedule-2.0/templates/Timer/edit.html.twig
new file mode 100644
index 0000000..a0a8403
--- /dev/null
+++ b/src/schedule-2.0/templates/Timer/edit.html.twig
@@ -0,0 +1,110 @@
+{% extends "base.html.twig" %}
+
+{% block localstyle %}
+ td {
+ padding:5px !important;
+ }
+{% endblock %}
+
+{% block body %}
+{{ form_start(form) }}
+
+
+
+ {{ form_widget(form.submit) }}
+
+ Annuler
+
+ {% if mode=="update" %}
+
+
+ Supprimer
+
+ {% endif %}
+
+
+
+ {% if app.session.flashbag.has('error') %}
+
+ Erreur
+ {% for flashMessage in app.session.flashbag.get('error') %}
+ {{ flashMessage }}
+ {% endfor %}
+
+ {% endif %}
+
+ {% if app.session.flashbag.has('notice') %}
+
+ Information
+ {% for flashMessage in app.session.flashbag.get('notice') %}
+ {{ flashMessage }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+ {{ form_row(form.task) }}
+ {{ form_row(form.description) }}
+ {{ form_row(form.start) }}
+ {{ form_row(form.end) }}
+ {{ form_row(form.duration) }}
+
+
+{{ form_end(form) }}
+
+
+{% endblock %}
+
+{% block localjavascript %}
+$("#timer_task").addClass("select2entity");
+/*
+ * Pads this string with another string on the left until the resulting string
+ * has specified length. If the padding string has more than one character, the
+ * resulting string may be longer than desired (the padding string is not
+ * truncated and it is only prepended as a whole). Bad API, I know, but it's
+ * good enough for me.
+ */
+String.prototype.pad = function(length, padding) {
+ var result = this;
+ while (result.length < length) {
+ result = padding + result;
+ }
+ return result;
+}
+/* Some time constants. */
+var MILISECONDS_IN_SECOND = 1000;
+var MILISECONDS_IN_MINUTE = 60 * MILISECONDS_IN_SECOND;
+var MINUTES_IN_HOUR = 60;
+
+/* Formats the time in the H:MM format. */
+function formatTime(time) {
+ var timeInMinutes = time / MILISECONDS_IN_MINUTE;
+ var hours = Math.floor(timeInMinutes / MINUTES_IN_HOUR);
+ var minutes = Math.floor(timeInMinutes - hours * MINUTES_IN_HOUR);
+ return String(hours).pad(2, "0") + ":" + String(minutes).pad(2, "0");
+}
+
+$("#timer_start_time,#timer_end_time").change(function(){
+ console.log($("#timer_start_date").val() +"T"+$("#timer_start_time").val()+":00Z")
+ console.log($("#timer_end_date").val()+"T"+$("#timer_end_time").val()+":00Z")
+ var start = Date.parse($("#timer_start_date").val() +"T"+$("#timer_start_time").val()+":00Z");
+ var end = Date.parse($("#timer_end_date").val()+"T"+$("#timer_end_time").val()+":00Z");
+ var diff = end - start;
+ $("#timer_duration").val(formatTime(diff))
+})
+
+{% endblock %}
+
diff --git a/src/schedule-2.0/templates/Timer/list.html.twig b/src/schedule-2.0/templates/Timer/list.html.twig
new file mode 100644
index 0000000..8c82319
--- /dev/null
+++ b/src/schedule-2.0/templates/Timer/list.html.twig
@@ -0,0 +1,693 @@
+{% extends "base.html.twig" %}
+
+{% block localstyle %}
+#timer-task {
+ width:300px;
+ }
+#timer-desc {
+ width:300px;
+}
+{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+
+
+Créer un Timer
+
+
+
+
+
+ Tâche |
+ Description |
+ Début |
+ Fin |
+ Durée |
+ Actions |
+
+
+
+ {%for timer in timers %}
+
+ {{ timer.task.displayname }} |
+ {{ timer.description }} |
+ {{ timer.start|date("d/m/Y H:i") }} |
+ {{ timer.end|date("d/m/Y H:i") }} |
+ {{ timer.duration|date("H:i") }} |
+
+
+
+
+
+
+
+
+ |
+
+ {% endfor %}
+
+
+
+{% endblock %}
+
+{% block localjavascript %}
+
+
+/* Creates a new Task object. */
+function Task(id ,name, description) {
+ this._name = name;
+ this._taskid = id;
+ this._description = description;
+ this._state = Task.State.STOPPED;
+ this._timeSpentInPreviousIterations = 0;
+ this._startDate = 0;
+ this._endDate = 0;
+ this._currentIterationStartTime = 0;
+ this._customTimeSpent = 0;
+ this._onChange = null;
+}
+
+/* Possible task states. */
+Task.State = {
+ STOPPED: "stopped",
+ RUNNING: "running"
+}
+
+Task.prototype = {
+ /* Returns the task name. */
+ getName: function() {
+ return this._name;
+ },
+
+ /* Returns the task description. */
+ getDescription: function() {
+ if (this._description == "NaN") {
+ this._description = ""
+ }
+ return this._description;
+ },
+
+ /* Returns the task state. */
+ getState: function() {
+ return this._state;
+ },
+
+ /* Is the task stopped? */
+ isStopped: function() {
+ return this._state == Task.State.STOPPED;
+ },
+
+ /* Is the task running? */
+ isRunning: function() {
+ return this._state == Task.State.RUNNING;
+ },
+
+ /*
+ * Sets the "onChange" event handler. The "onChange" event is fired when the
+ * task is started, stopped, or reset.
+ */
+ setOnChange: function(onChange) {
+ this._onChange = onChange;
+ },
+
+ /*
+ * Returns the time spent on the task in the current work iteration. Works
+ * correctly only when the task is running.
+ */
+ _getCurrentIterationTime: function() {
+ return (new Date).getTime() - this._currentIterationStartTime;
+ },
+
+ /*
+ * Returns the total time spent on the task. This includes time spent in
+ * the current work iteration if the task is running.
+ */
+ getTimeSpent: function() {
+ var result = this._timeSpentInPreviousIterations;
+ if (this._state == Task.State.RUNNING) {
+ result += this._getCurrentIterationTime();
+ }
+ return result;
+ },
+
+ /* Calls the "onChange" event handler if set. */
+ _callOnChange: function() {
+ if (typeof this._onChange == "function") {
+ this._onChange();
+ }
+ },
+
+ /* Starts a new task work iteration. */
+ start: function() {
+ if (this._state == Task.State.RUNNING) { return };
+ if (this._startDate == 0) {this._startDate = new Date()}
+ this._state = Task.State.RUNNING;
+ this._currentIterationStartTime = (new Date).getTime();
+ this._callOnChange();
+ },
+
+ /* Stops the current task work iteration. */
+ stop: function() {
+ if (this._state == Task.State.STOPPED) { return };
+
+ this._state = Task.State.STOPPED;
+ this._timeSpentInPreviousIterations += this._getCurrentIterationTime();
+ this._currentIterationStartTime = 0;
+ this._endDate = new Date();
+ this._callOnChange();
+ },
+
+ /* Stops the current task work iteration and resets the time data. */
+ reset: function() {
+ this.stop();
+ this._timeSpentInPreviousIterations = 0;
+ this._callOnChange();
+ },
+ save: function(task){
+ console.log(task)
+
+ $.ajax({
+ type: "POST",
+ data: {
+ taskname: task._name,
+ taskid: task._taskid,
+ description: task._description,
+ start: task._startDate,
+ end: task._endDate,
+ duration: task._timeSpentInPreviousIterations,
+ },
+ url: "{{ path('app_timer_create') }}",
+ success: function (response) {
+ response=JSON.parse(response);
+ if(response.return=="KO") {
+ $("#dataTable_wrapper").append(""+response.error+"
");
+ }else{
+ location.reload();
+ }
+ }
+ });
+ },
+ /* Serializes the task into a string. */
+ serialize: function() {
+ /*
+ * Originally, I wanted to use "toSource" and "eval" for serialization and
+ * deserialization, but "toSource" is not supported by WebKit, so I resorted
+ * to ugly hackery...
+ */
+ return [
+ encodeURIComponent(this._name),
+ this._state,
+ this._timeSpentInPreviousIterations,
+ this._currentIterationStartTime,
+ this._startDate,
+ this._endDate,
+ this._taskid,
+ this._description,
+ ].join("&");
+ },
+
+ /* Deserializes the task from a string. */
+ deserialize: function(serialized) {
+ var parts = serialized.split("&");
+ this._name = decodeURIComponent(parts[0]);
+ this._state = parts[1];
+ this._timeSpentInPreviousIterations = parseInt(parts[2]);
+ this._currentIterationStartTime = parseInt(parts[3]);
+ this._startDate = parseInt(parts[4]);
+ this._endDate = parseInt(parts[5]);
+ this._taskid = parseInt(parts[6]);
+ this._description = parseInt(parts[7]);
+
+ }
+}
+
+/* ===== Tasks ===== */
+
+/* The Tasks class represents a list of tasks. */
+
+/* Creates a new Tasks object. */
+function Tasks() {
+ this._tasks = [];
+
+ this._onAdd = null;
+ this._onRemove = null;
+}
+
+Tasks.prototype = {
+ /*
+ * Sets the "onAdd" event handler. The "onAdd" event is fired when a task
+ * is added to the list.
+ */
+ setOnAdd: function(onAdd) {
+ this._onAdd = onAdd;
+ },
+
+ /*
+ * Sets the "onRemove" event handler. The "onRemove" event is fired when a
+ * task is removed from the list.
+ */
+ setOnRemove: function(onRemove) {
+ this._onRemove = onRemove;
+ },
+
+ /* Returns the length of the task list. */
+ length: function() {
+ return this._tasks.length
+ },
+
+ /*
+ * Returns index-th task in the list, or "undefined" if the index is out of
+ * bounds.
+ */
+ get: function(index) {
+ return this._tasks[index];
+ },
+
+ /*
+ * Calls the callback function for each task in the list. The function is
+ * called with three parameters - the task, its index and the task list
+ * object. This is modeled after "Array.forEach" in JavaScript 1.6.
+ */
+ forEach: function(callback) {
+ for (var i = 0; i < this._tasks.length; i++) {
+ callback(this._tasks[i], i, this);
+ }
+ },
+
+ /* Calls the "onAdd" event handler if set. */
+ _callOnAdd: function(task) {
+ if (typeof this._onAdd == "function") {
+ this._onAdd(task);
+ }
+ },
+
+ /* Adds a new task to the end of the list. */
+ add: function(task) {
+ this._tasks.push(task);
+ this._callOnAdd(task);
+ },
+
+ /* Calls the "onRemove" event handler if set. */
+ _callOnRemove: function(index) {
+ if (typeof this._onRemove == "function") {
+ this._onRemove(index);
+ }
+ },
+
+ /*
+ * Removes index-th task from the list. Does not do anything if the index
+ * is out of bounds.
+ */
+ remove: function(index) {
+ this._callOnRemove(index);
+ this._tasks.splice(index, 1);
+ },
+
+
+
+ /* Serializes the list of tasks into a string. */
+ serialize: function() {
+ var serializedTasks = [];
+ this.forEach(function(task) {
+ serializedTasks.push(task.serialize());
+ });
+ return serializedTasks.join("|");
+ },
+
+ /* Deserializes the list of tasks from a string. */
+ deserialize: function(serialized) {
+ /*
+ * Repeatedly use "remove" so the "onRemove" event is triggered. Do the same
+ * with the "add" method below.
+ */
+ while (this._tasks.length > 0) {
+ this.remove(0);
+ }
+
+ var serializedTasks = serialized.split("|");
+ for (var i = 0; i < serializedTasks.length; i++) {
+ var task = new Task(0 ,"", "");
+ task.deserialize(serializedTasks[i]);
+ this.add(task);
+ }
+ }
+}
+
+/* ===== Extensions ===== */
+
+/*
+ * Pads this string with another string on the left until the resulting string
+ * has specified length. If the padding string has more than one character, the
+ * resulting string may be longer than desired (the padding string is not
+ * truncated and it is only prepended as a whole). Bad API, I know, but it's
+ * good enough for me.
+ */
+String.prototype.pad = function(length, padding) {
+ var result = this;
+ while (result.length < length) {
+ result = padding + result;
+ }
+ return result;
+}
+
+/* ===== Task List + DOM Storage ===== */
+
+/* The list of tasks. */
+var tasks = new Tasks();
+
+/* The last value of the serialized task list string. */
+var lastSerializedTasksString;
+
+/*
+ * The key under which the serialized task list string is stored in the DOM
+ * Storage.
+ */
+var TASKS_DOM_STORAGE_KEY = "timerTasks";
+
+/*
+ * Returns DOM Storage used by the application, or "null" if the browser does
+ * not support DOM Storage.
+ */
+function getStorage() {
+ /*
+ * HTML 5 says that the persistent storage is available in the
+ * "window.localStorage" attribute, however Firefox implements older version
+ * of the proposal, which uses "window.globalStorage" indexed by the domain
+ * name. We deal with this situation here as gracefully as possible (i.e.
+ * without concrete browser detection and with forward compatibility).
+ *
+ * For more information, see:
+ *
+ * http://www.whatwg.org/specs/web-apps/current-work/#storage
+ * https://developer.mozilla.org/En/DOM/Storage
+ */
+ if (window.localStorage !== undefined) {
+ return window.localStorage;
+ } else if (window.globalStorage !== undefined) {
+ return window.globalStorage[location.hostname];
+ } else {
+ return null;
+ }
+}
+
+/*
+ * Saves the task list into a DOM Storage. Also updates the value of the
+ * "lastSerializedTasksString" variable.
+ */
+function saveTasks() {
+ var serializedTasksString = tasks.serialize();
+ getStorage()[TASKS_DOM_STORAGE_KEY] = serializedTasksString;
+ lastSerializedTasksString = serializedTasksString;
+}
+
+/*
+ * Loads the serialized task list string from the DOM Storage. Returns
+ * "undefined" if the storage does not contain the string (this happens when
+ * running the application for the first time).
+ */
+function loadSerializedTasksString() {
+ var storedValue = getStorage()[TASKS_DOM_STORAGE_KEY];
+ /*
+ * The spec says "null" should be returned when the key is not found, but some
+ * browsers return "undefined" instead. Maybe it was in some earlier version
+ * of the spec (I didn't bother to check).
+ */
+ if (storedValue !== null && storedValue !== undefined && storedValue.length > 0) {
+ /*
+ * The values retrieved from "globalStorage" use one more level of
+ * indirection.
+ */
+ return (window.localStorage === undefined) ? storedValue.value : storedValue;
+ } else {
+ return undefined;
+ }
+}
+
+/*
+ * Loads the task list from the DOM Storage. Also updates the value of the
+ * "lastSerializedTasksString" variable.
+ */
+function loadTasks() {
+ var serializedTasksString = loadSerializedTasksString();
+ if (serializedTasksString !== undefined) {
+ tasks.deserialize(serializedTasksString);
+ lastSerializedTasksString = serializedTasksString;
+ }
+}
+
+/*
+ * Was the task list changed outside of the application? Detects the change
+ * by comparing the current serialized task list string in the DOM Storage
+ * with a kept old value.
+ */
+function tasksHaveChangedOutsideApplication() {
+ var serializedTasksString = loadSerializedTasksString();
+ if (serializedTasksString != lastSerializedTasksString) {
+ return true
+ }
+ return false;
+}
+
+/* ===== View ===== */
+
+/* Some time constants. */
+var MILISECONDS_IN_SECOND = 1000;
+var MILISECONDS_IN_MINUTE = 60 * MILISECONDS_IN_SECOND;
+var MINUTES_IN_HOUR = 60;
+
+/* Formats the time in the H:MM format. */
+function formatTime(time) {
+ var timeInMinutes = time / MILISECONDS_IN_MINUTE;
+ var hours = Math.floor(timeInMinutes / MINUTES_IN_HOUR);
+ var minutes = Math.floor(timeInMinutes - hours * MINUTES_IN_HOUR);
+ return hours + ":" + String(minutes).pad(2, "0");
+}
+
+/*
+ * Computes the URL of the image in the start/stop link according to the task
+ * state.
+ */
+function computeStartStopLinkImageUrl(state) {
+ switch (state) {
+ case Task.State.STOPPED:
+ return "fa-play";
+ case Task.State.RUNNING:
+ return "fa-stop";
+ default:
+ throw "Invalid task state."
+ }
+}
+
+/*
+ * Builds the HTML element of the row in the task table corresponding to the
+ * specified task and index.
+ */
+function buildTaskRow(task, index) {
+ var result = $("
");
+
+ var startStopLink = $(
+ ""
+ + ""
+
+ );
+ startStopLink.click(function() {
+ switch (task.getState()) {
+ case Task.State.STOPPED:
+ task.start();
+ break;
+ case Task.State.RUNNING:
+ task.stop();
+ break;
+ default:
+ throw "Invalid task state."
+ }
+ saveTasks();
+ return false;
+ });
+
+ var resetLink = $(
+ ""
+ + ""
+ );
+ resetLink.click(function() {
+ task.reset();
+ saveTasks();
+ return false;
+ });
+
+ var deleteLink = $(
+ ""
+ + ""
+
+ );
+ deleteLink.click(function() {
+ if (confirm("Do you really want to delete task \"" + task.getName() + "\"?")) {
+ tasks.remove(index);
+ saveTasks();
+ }
+ return false;
+ });
+
+ var saveLink = $(
+ ""
+ + ""
+ );
+ saveLink.click(function() {
+ tasks.remove(index);
+ saveTasks();
+ task.save(task);
+ return false;
+ });
+ result
+ .addClass("state-" + task.getState())
+ .append($(" | ").text(task.getName()))
+ .append($(" | ").text(task.getDescription()))
+ .append($(" | ").text(formatTime(task.getTimeSpent())))
+ .append($(" | ")
+ .append(startStopLink)
+ .append(" ")
+ .append(resetLink)
+ .append(" ")
+ .append(saveLink)
+ .append(" ")
+ .append(deleteLink)
+ );
+
+ return result;
+}
+
+/* Finds row with the specified index in the task table. */
+function findRowWithIndex(index) {
+ return $("#task-table").find("tr").slice(1).eq(index);
+}
+
+/*
+ * Updates the row in the task table corresponding to a task according to
+ * its state.
+ */
+function updateTaskRow(row, task) {
+ if (task.isStopped()) {
+ row.removeClass("state-running");
+ row.addClass("state-stopped");
+ } else if (task.isRunning()) {
+ row.removeClass("state-stopped");
+ row.addClass("state-running");
+ }
+
+ row.find(".task-time").text(formatTime(task.getTimeSpent()))
+
+ row.find(".start-stop-link i").removeClass();
+ row.find(".start-stop-link i").addClass("fa "+computeStartStopLinkImageUrl(task.getState()));
+}
+
+/* ===== Initialization ===== */
+
+/* Initializes event handlers on the task list. */
+function initializeTasksEventHandlers() {
+ tasks.setOnAdd(function(task) {
+ var row = buildTaskRow(task, tasks.length() - 1);
+ $("#task-table").append(row);
+ task.setOnChange(function() {
+ updateTaskRow(row, task);
+ });
+ });
+
+ tasks.setOnRemove(function(index) {
+ findRowWithIndex(index).remove();
+ if (tasks.length() == 1 ) {
+ $( "#task-table" ).css({"display":"none"});
+ }
+ });
+}
+
+/* Initializes GUI event handlers. */
+function initializeGuiEventHandlers() {
+ $( "#addtimer" ).click(function() {
+ displayTaskAdd();
+ });
+
+ $( "#timer-desc" ).keypress(function( event ) {
+ if ( event.which == 13 ) {
+ event.preventDefault();
+ displayTaskAdd();
+ }
+ });
+
+}
+function displayTaskAdd() {
+ $( "#task-table" ).css({"display":"inline"});
+ var taskName = $("#timer-task option:selected").text();
+ var taskId = $("#timer-task option:selected").val();
+ var taskDesc = $("#timer-desc").val();
+
+ var task = new Task(taskId, taskName, taskDesc);
+ tasks.add(task);
+ saveTasks();
+ $("#timer-desc").val("");
+}
+/*
+ * Initializes a timer used to update task times and detect changes in the
+ * data stored in the DOM Storage.
+ */
+function initializeTimer() {
+ setInterval(function() {
+ tasks.forEach(function(task, index) {
+ updateTaskRow(findRowWithIndex(index), task);
+ });
+
+ if (tasksHaveChangedOutsideApplication()) {
+ loadTasks();
+ }
+ }, 10 * MILISECONDS_IN_SECOND);
+}
+
+/* Initializes the application. */
+$(document).ready(function(){
+ try {
+ if (!getStorage()) {
+ alert("Timer requires a browser with DOM Storage support, such as Firefox 3+ or Safari 4+.");
+ return;
+ }
+ } catch (e) {
+ alert("Timer does not work with file: URLs in Firefox.");
+ return;
+ }
+
+ initializeTasksEventHandlers();
+ loadTasks();
+ initializeGuiEventHandlers();
+ initializeTimer();
+});
+
+
+
+
+
+{% endblock %}
+
+
diff --git a/src/schedule-2.0/templates/base.html.twig b/src/schedule-2.0/templates/base.html.twig
index cbaf97b..baa390b 100644
--- a/src/schedule-2.0/templates/base.html.twig
+++ b/src/schedule-2.0/templates/base.html.twig
@@ -359,6 +359,12 @@
+
+
+ Suivi Horaire
+
+
+
Mes Congés