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
|
||||
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:
|
||||
|
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;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
|
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>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{path("app_timer")}}">
|
||||
<i class="fa fa-stopwatch"></i>Suivi Horaire
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="last">
|
||||
<a href="{{path("app_holiday")}}">
|
||||
<i class="fa fa-cocktail"></i>Mes Congés
|
||||
|
Reference in New Issue
Block a user