Time Tracking
This commit is contained in:
609
src/schedule-2.0/assets/js/timer.js
Normal file
609
src/schedule-2.0/assets/js/timer.js
Normal file
@@ -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("<div class='alert alert-danger' style='margin: 5px 0px'>"+response.error+"</div>");
|
||||||
|
}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 = $("<tr />");
|
||||||
|
|
||||||
|
var startStopLink = $(
|
||||||
|
"<a href='#' class='start-stop-link' title='Start/stop'>"
|
||||||
|
+ "<i class='fa " + computeStartStopLinkImageUrl(task.getState()) + "'></i>"
|
||||||
|
|
||||||
|
);
|
||||||
|
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 = $(
|
||||||
|
"<a href='#' title='Reset'>"
|
||||||
|
+ "<i class='fa fa-undo'></i>"
|
||||||
|
);
|
||||||
|
resetLink.click(function() {
|
||||||
|
task.reset();
|
||||||
|
saveTasks();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
var deleteLink = $(
|
||||||
|
"<a href='#' title='Delete' class='deletetask'>"
|
||||||
|
+ "<i class='fa fa-trash-alt' class='deletetask'></i>"
|
||||||
|
|
||||||
|
);
|
||||||
|
deleteLink.click(function() {
|
||||||
|
if (confirm("Do you really want to delete task \"" + task.getName() + "\"?")) {
|
||||||
|
tasks.remove(index);
|
||||||
|
saveTasks();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
var saveLink = $(
|
||||||
|
"<a href='#' title='Save' class='savetask'>"
|
||||||
|
+ "<i class='fa fa-download' class='savetask'></i>"
|
||||||
|
);
|
||||||
|
saveLink.click(function() {
|
||||||
|
tasks.remove(index);
|
||||||
|
saveTasks();
|
||||||
|
task.save(task);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
result
|
||||||
|
.addClass("state-" + task.getState())
|
||||||
|
.append($("<td class='task-name' />").text(task.getName()))
|
||||||
|
.append($("<td class='task-description' />").text(task.getDescription()))
|
||||||
|
.append($("<td class='task-time' />").text(formatTime(task.getTimeSpent())))
|
||||||
|
.append($("<td class='task-actions' />")
|
||||||
|
.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();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@@ -347,6 +347,26 @@ app_validationholiday_activeholiday:
|
|||||||
path: /validator/validateholiday/activeholiday
|
path: /validator/validateholiday/activeholiday
|
||||||
defaults: { _controller: App\Controller\ValidationController: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 ======================================================================================================
|
#== Customer ======================================================================================================
|
||||||
app_customer_report:
|
app_customer_report:
|
||||||
|
176
src/schedule-2.0/src/Controller/TimerController.php
Normal file
176
src/schedule-2.0/src/Controller/TimerController.php
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
use App\Entity\Timer as Entity;
|
||||||
|
use App\Form\TimerType as Form;
|
||||||
|
|
||||||
|
class TimerController extends AbstractController
|
||||||
|
{
|
||||||
|
private $data = "timer";
|
||||||
|
private $route = "app_timer";
|
||||||
|
private $render = "Timer/";
|
||||||
|
private $entity = "App:Timer";
|
||||||
|
|
||||||
|
public function list(Request $request)
|
||||||
|
{
|
||||||
|
$em = $this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -61,12 +61,17 @@ class Task
|
|||||||
*/
|
*/
|
||||||
private $events;
|
private $events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\OneToMany(targetEntity="Timer", mappedBy="task", cascade={"persist"}, orphanRemoval=false)
|
||||||
|
*/
|
||||||
|
private $timers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\OneToMany(targetEntity="Penalty", mappedBy="task", cascade={"persist"}, orphanRemoval=false)
|
* @ORM\OneToMany(targetEntity="Penalty", mappedBy="task", cascade={"persist"}, orphanRemoval=false)
|
||||||
*/
|
*/
|
||||||
private $penaltys;
|
private $penaltys;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate Displayname
|
* Calculate Displayname
|
||||||
*/
|
*/
|
||||||
public function getDisplayname(): ?string
|
public function getDisplayname(): ?string
|
||||||
@@ -74,7 +79,7 @@ class Task
|
|||||||
return $this->project->getCustomer()->getName()."-".$this->project->getName()."-".$this->name;
|
return $this->project->getCustomer()->getName()."-".$this->project->getName()."-".$this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate Duration
|
* Calculate Duration
|
||||||
*/
|
*/
|
||||||
public function getDuration($end): ?string
|
public function getDuration($end): ?string
|
||||||
@@ -90,6 +95,7 @@ class Task
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->events = new ArrayCollection();
|
$this->events = new ArrayCollection();
|
||||||
|
$this->timers = new ArrayCollection();
|
||||||
$this->penaltys = new ArrayCollection();
|
$this->penaltys = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +197,24 @@ class Task
|
|||||||
return $this;
|
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
|
public function removeEvent(Event $event): self
|
||||||
{
|
{
|
||||||
if ($this->events->contains($event)) {
|
if ($this->events->contains($event)) {
|
||||||
@@ -235,4 +259,17 @@ class Task
|
|||||||
return $this;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
142
src/schedule-2.0/src/Entity/Timer.php
Normal file
142
src/schedule-2.0/src/Entity/Timer.php
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timer
|
||||||
|
*
|
||||||
|
* @ORM\Table(name="timer")
|
||||||
|
* @ORM\Entity(repositoryClass="App\Repository\TimerRepository")
|
||||||
|
*/
|
||||||
|
class Timer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="id", type="integer")
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\GeneratedValue(strategy="AUTO")
|
||||||
|
*/
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="start", type="datetime")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private $start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="end", type="datetime")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private $end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(name="duration", type="datetime")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private $duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text", nullable=true)
|
||||||
|
*/
|
||||||
|
private $description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity="User")
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity="Task", inversedBy="timers"))
|
||||||
|
*/
|
||||||
|
private $task;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
99
src/schedule-2.0/src/Form/TimerType.php
Normal file
99
src/schedule-2.0/src/Form/TimerType.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TimeType;
|
||||||
|
use Symfony\Component\Form\ChoiceList\ChoiceList;
|
||||||
|
|
||||||
|
use FOS\CKEditorBundle\Form\Type\CKEditorType;
|
||||||
|
use Tetranz\Select2EntityBundle\Form\Type\Select2EntityType;
|
||||||
|
|
||||||
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
|
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\EntityManager;
|
||||||
|
|
||||||
|
class TimerType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->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',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
110
src/schedule-2.0/templates/Timer/edit.html.twig
Normal file
110
src/schedule-2.0/templates/Timer/edit.html.twig
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
|
{% block localstyle %}
|
||||||
|
td {
|
||||||
|
padding:5px !important;
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{{ form_start(form) }}
|
||||||
|
<h1 class="page-header">
|
||||||
|
{% if mode=="update" %}
|
||||||
|
Modification TIMER
|
||||||
|
{% elseif mode=="submit" %}
|
||||||
|
Création TIMER
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
|
||||||
|
{{ form_widget(form.submit) }}
|
||||||
|
|
||||||
|
<a class="btn btn-secondary" href={{ path('app_timer') }}>Annuler</a>
|
||||||
|
|
||||||
|
{% if mode=="update" %}
|
||||||
|
|
||||||
|
<a href="{{ path('app_timer_delete',{'id':timer.id}) }}"
|
||||||
|
class="btn btn-danger float-right"
|
||||||
|
data-method="delete"
|
||||||
|
data-confirm="Êtes-vous sûr de vouloir supprimer cet entregistrement ?">
|
||||||
|
Supprimer
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
{% if app.session.flashbag.has('error') %}
|
||||||
|
<div class='alert alert-danger' style='margin: 5px 0px'>
|
||||||
|
<strong>Erreur</strong><br>
|
||||||
|
{% for flashMessage in app.session.flashbag.get('error') %}
|
||||||
|
{{ flashMessage }}<br>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if app.session.flashbag.has('notice') %}
|
||||||
|
<div class='alert alert-info' style='margin: 5px 0px'>
|
||||||
|
<strong>Information</strong><br>
|
||||||
|
{% for flashMessage in app.session.flashbag.get('notice') %}
|
||||||
|
{{ flashMessage }}<br>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<i class="fa fa-pencil-alt fa-fw"></i> Informations
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
{{ form_row(form.task) }}
|
||||||
|
{{ form_row(form.description) }}
|
||||||
|
{{ form_row(form.start) }}
|
||||||
|
{{ form_row(form.end) }}
|
||||||
|
{{ form_row(form.duration) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ 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 %}
|
||||||
|
|
693
src/schedule-2.0/templates/Timer/list.html.twig
Normal file
693
src/schedule-2.0/templates/Timer/list.html.twig
Normal file
@@ -0,0 +1,693 @@
|
|||||||
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
|
{% block localstyle %}
|
||||||
|
#timer-task {
|
||||||
|
width:300px;
|
||||||
|
}
|
||||||
|
#timer-desc {
|
||||||
|
width:300px;
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1 class="page-header">
|
||||||
|
SUIVI HORAIRE
|
||||||
|
</h1>
|
||||||
|
<div id="timer" class="card">
|
||||||
|
|
||||||
|
<div class="card-header">
|
||||||
|
Lancer un Timer :
|
||||||
|
<select class="select2entity" id="timer-task" name="timer-task">
|
||||||
|
<option> Tâche </option>
|
||||||
|
{% for task in tasks %}
|
||||||
|
<option value="{{task.id}}">{{task.displayname}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<input id="timer-desc" name="timer-desc" placeholder="Description" />
|
||||||
|
<a href='#' title='Add' id='addtimer'>
|
||||||
|
<i class='fas fa-plus-circle'></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table id="task-table" class="table table-striped table-hover" >
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="btn btn-success" href={{ path('app_timer_submit') }}>Créer un Timer</a>
|
||||||
|
<p></p>
|
||||||
|
<div class="dataTable_wrapper">
|
||||||
|
<table class="table table-striped table-bordered table-hover small" id="dataTables" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="70px" class="no-sort">Tâche</th>
|
||||||
|
<th width="150px" class="no-sort">Description</th>
|
||||||
|
<th width="150px">Début</th>
|
||||||
|
<th width="100px">Fin</th>
|
||||||
|
<th width="100px">Durée</th>
|
||||||
|
<th width="100px" class="text-center no-string">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%for timer in timers %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ timer.task.displayname }}</td>
|
||||||
|
<td>{{ timer.description }}</td>
|
||||||
|
<td>{{ timer.start|date("d/m/Y H:i") }}</td>
|
||||||
|
<td>{{ timer.end|date("d/m/Y H:i") }}</td>
|
||||||
|
<td>{{ timer.duration|date("H:i") }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{path("app_timer_update",{id:timer.id})}}">
|
||||||
|
<i class="fa fa-file"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ path('app_timer_delete',{'id':timer.id}) }}"
|
||||||
|
data-method="delete"
|
||||||
|
data-confirm="Êtes-vous sûr de vouloir supprimer cet entregistrement ?">
|
||||||
|
<i class="fa fa-trash-alt"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% 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("<div class='alert alert-danger' style='margin: 5px 0px'>"+response.error+"</div>");
|
||||||
|
}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 = $("<tr />");
|
||||||
|
|
||||||
|
var startStopLink = $(
|
||||||
|
"<a href='#' class='start-stop-link' title='Start/stop'>"
|
||||||
|
+ "<i class='fa " + computeStartStopLinkImageUrl(task.getState()) + "'></i>"
|
||||||
|
|
||||||
|
);
|
||||||
|
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 = $(
|
||||||
|
"<a href='#' title='Reset'>"
|
||||||
|
+ "<i class='fa fa-undo'></i>"
|
||||||
|
);
|
||||||
|
resetLink.click(function() {
|
||||||
|
task.reset();
|
||||||
|
saveTasks();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
var deleteLink = $(
|
||||||
|
"<a href='#' title='Delete' class='deletetask'>"
|
||||||
|
+ "<i class='fa fa-trash-alt' class='deletetask'></i>"
|
||||||
|
|
||||||
|
);
|
||||||
|
deleteLink.click(function() {
|
||||||
|
if (confirm("Do you really want to delete task \"" + task.getName() + "\"?")) {
|
||||||
|
tasks.remove(index);
|
||||||
|
saveTasks();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
var saveLink = $(
|
||||||
|
"<a href='#' title='Save' class='savetask'>"
|
||||||
|
+ "<i class='fa fa-download' class='savetask'></i>"
|
||||||
|
);
|
||||||
|
saveLink.click(function() {
|
||||||
|
tasks.remove(index);
|
||||||
|
saveTasks();
|
||||||
|
task.save(task);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
result
|
||||||
|
.addClass("state-" + task.getState())
|
||||||
|
.append($("<td class='task-name' />").text(task.getName()))
|
||||||
|
.append($("<td class='task-description' />").text(task.getDescription()))
|
||||||
|
.append($("<td class='task-time' />").text(formatTime(task.getTimeSpent())))
|
||||||
|
.append($("<td class='task-actions' />")
|
||||||
|
.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 %}
|
||||||
|
|
||||||
|
|
@@ -359,6 +359,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="{{path("app_timer")}}">
|
||||||
|
<i class="fa fa-stopwatch"></i>Suivi Horaire
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="last">
|
<li class="last">
|
||||||
<a href="{{path("app_holiday")}}">
|
<a href="{{path("app_holiday")}}">
|
||||||
<i class="fa fa-cocktail"></i>Mes Congés
|
<i class="fa fa-cocktail"></i>Mes Congés
|
||||||
|
Reference in New Issue
Block a user