diff --git a/src/schedule-2.0/.gitignore b/src/schedule-2.0/.gitignore
index ba27aaf..4a6cd2b 100644
--- a/src/schedule-2.0/.gitignore
+++ b/src/schedule-2.0/.gitignore
@@ -17,7 +17,6 @@
###> symfony/webpack-encore-bundle ###
/node_modules/
-/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
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 = $("