A LOT of improvements

This commit is contained in:
Maximilian Giller 2020-11-08 15:21:51 +01:00
parent 5c30876c80
commit 4c64f99ced
26 changed files with 1307 additions and 861 deletions

8
.gitignore vendored
View file

@ -1,5 +1,5 @@
juggl/config/config.txt
juggl/config/config.path
juggl/config/config.php
graphics
juggl/config/config.txt
juggl/config/config.path
juggl/config/config.php
graphics
.vscode

View file

@ -1,8 +1,12 @@
<?php
$config = [
"host" => "localhost",
"dbname" => "juggl",
"username" => "juggl",
"password" => "?=5,}f_F&){;@xthx-[i",
"table_prefix" => "ju_"
];
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL | E_STRICT);
$config = [
"host" => "localhost",
"dbname" => "juggl",
"username" => "juggl",
"password" => "?=5,}f_F&){;@xthx-[i",
"table_prefix" => "ju_"
];

View file

@ -1,8 +1,8 @@
<?php
$config = [
"host" => "",
"dbname" => "",
"username" => "",
"password" => "",
"table_prefix" => "ju_"
<?php
$config = [
"host" => "",
"dbname" => "",
"username" => "",
"password" => "",
"table_prefix" => "ju_"
];

View file

@ -1,28 +1,27 @@
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class EndRecordBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
$params->select("request");
if ($params->exists(["end_time", "record_id"]) == false) {
respondStatus(400, "Missing parameter");
}
updateEndRecord($user_id, $params);
}
}
$branch = new EndRecordBranch();
$branch->execute();
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class EndRecordBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
if ($params->exists(["end_time", "record_id"]) == false) {
respondStatus(400, "Missing parameter");
}
updateEndRecord($user_id, $params);
}
}
$branch = new EndRecordBranch();
$branch->execute();

View file

@ -0,0 +1,29 @@
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class GetProjectsBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
$projects = getProjects($user_id);
$json = new JsonBuilder();
$json->addProjects($projects);
respondJson($json);
}
}
$branch = new GetProjectsBranch();
$branch->execute();

View file

@ -1,34 +1,33 @@
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class GetRecordBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
$params->select("request");
if ($params->exists(["record_id"]) == false) {
respondStatus(400, "Missing parameter");
}
$record = getTimeRecord($user_id, $params->get("record_id"));
$json = new JsonBuilder();
$json->addRecords([$record]);
respondJson($json);
}
}
$branch = new GetRecordBranch();
$branch->execute();
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class GetRecordBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
if ($params->exists(["record_id"]) == false) {
respondStatus(400, "Missing parameter");
}
$record = getTimeRecord($user_id, $params->get("record_id"));
$json = new JsonBuilder();
$json->addRecords([$record]);
respondJson($json);
}
}
$branch = new GetRecordBranch();
$branch->execute();

View file

@ -1,38 +1,38 @@
<?php
require_once(__DIR__."/authenticator.inc.php");
require_once(__DIR__."/responses.inc.php");
require_once(__DIR__."/requestTypes.inc.php");
require_once(__DIR__."/paramCleaner.inc.php");
abstract class ApiBranch {
function get (ParamCleaner $params) {}
function post (ParamCleaner $params) {}
function authenticationMissing (ParamCleaner $params) {
respondStatus(403);
}
function execute ($authenticationRequired = true) {
$params = $this->getParams();
if ($authenticationRequired) {
$auth = new Authenticator();
if (!$auth->isAuthenticated($params)) {
$this->authenticationMissing($params);
return;
}
}
$currentType = currentRequestType();
if($currentType === RequestType::GET) {
$this->get($params);
} else if ($currentType === RequestType::POST) {
$this->post($params);
}
}
private function getParams() {
$content = json_decode(file_get_contents('php://input'), true);
return new ParamCleaner(array_merge($content, $_REQUEST, $_SESSION, $_FILES));
}
}
<?php
require_once(__DIR__."/authenticator.inc.php");
require_once(__DIR__."/responses.inc.php");
require_once(__DIR__."/requestTypes.inc.php");
require_once(__DIR__."/paramCleaner.inc.php");
abstract class ApiBranch {
function get (ParamCleaner $params) {}
function post (ParamCleaner $params) {}
function authenticationMissing (ParamCleaner $params) {
respondStatus(403);
}
function execute ($authenticationRequired = true) {
$params = $this->getParams();
if ($authenticationRequired) {
$auth = new Authenticator();
if (!$auth->isAuthenticated($params)) {
$this->authenticationMissing($params);
return;
}
}
$currentType = currentRequestType();
if($currentType === RequestType::GET) {
$this->get($params);
} else if ($currentType === RequestType::POST) {
$this->post($params);
}
}
private function getParams() {
$content = json_decode(file_get_contents('php://input'), true);
return new ParamCleaner(array_merge($content, $_REQUEST, $_SESSION, $_FILES));
}
}
?>

View file

@ -1,20 +1,20 @@
<?php
require_once(__DIR__."/dbOperations.inc.php");
class Authenticator {
function isApiKeyAuthenticated($api_key, $user_id) {
$db = new DbOperations();
$db->select("api_keys", ["enabled"]);
$db->where("api_key", Comparison::EQUAL, $api_key);
$db->where("user_id", Comparison::EQUAL, $user_id);
$result = $db->execute();
return count($result) == 1 && $result[0]['enabled'];
}
function isAuthenticated($params) {
return $this->isApiKeyAuthenticated($params->get('api_key'), $params->get('user_id'));
}
}
<?php
require_once(__DIR__."/dbOperations.inc.php");
class Authenticator {
function isApiKeyAuthenticated($api_key, $user_id) {
$db = new DbOperations();
$db->select("api_keys", ["enabled"]);
$db->where("api_key", Comparison::EQUAL, $api_key);
$db->where("user_id", Comparison::EQUAL, $user_id);
$result = $db->execute();
return count($result) == 1 && $result[0]['enabled'];
}
function isAuthenticated($params) {
return $this->isApiKeyAuthenticated($params->get('api_key'), $params->get('user_id'));
}
}
?>

View file

@ -1,33 +1,33 @@
<?php
abstract class BasicEnum {
private static $constCacheArray = NULL;
private static function getConstants() {
if (self::$constCacheArray == NULL) {
self::$constCacheArray = [];
}
$calledClass = get_called_class();
if (!array_key_exists($calledClass, self::$constCacheArray)) {
$reflect = new ReflectionClass($calledClass);
self::$constCacheArray[$calledClass] = $reflect->getConstants();
}
return self::$constCacheArray[$calledClass];
}
public static function isValidName($name, $strict = false) {
$constants = self::getConstants();
if ($strict) {
return array_key_exists($name, $constants);
}
$keys = array_map('strtolower', array_keys($constants));
return in_array(strtolower($name), $keys);
}
public static function isValidValue($value, $strict = true) {
$values = array_values(self::getConstants());
return in_array($value, $values, $strict);
}
}
<?php
abstract class BasicEnum {
private static $constCacheArray = NULL;
private static function getConstants() {
if (self::$constCacheArray == NULL) {
self::$constCacheArray = [];
}
$calledClass = get_called_class();
if (!array_key_exists($calledClass, self::$constCacheArray)) {
$reflect = new ReflectionClass($calledClass);
self::$constCacheArray[$calledClass] = $reflect->getConstants();
}
return self::$constCacheArray[$calledClass];
}
public static function isValidName($name, $strict = false) {
$constants = self::getConstants();
if ($strict) {
return array_key_exists($name, $constants);
}
$keys = array_map('strtolower', array_keys($constants));
return in_array(strtolower($name), $keys);
}
public static function isValidValue($value, $strict = true) {
$values = array_values(self::getConstants());
return in_array($value, $values, $strict);
}
}
?>

View file

@ -1,291 +1,291 @@
<?php
require_once(__DIR__ . "/basicEnum.inc.php");
class DbOperations
{
function __construct($tablePrefix = null)
{
$this->resetQuery();
$this->tablePrefix = $tablePrefix;
require(__DIR__ . "/../config/config.php");
$this->config = $config;
if ($this->tablePrefix == null) {
$this->tablePrefix = $this->config["table_prefix"];
}
}
function resetQuery()
{
$this->query = "";
$this->data = array();
$this->table = "";
}
private function openConnection()
{
$host = $this->config['host'];
$dbname = $this->config['dbname'];
$dsn = "mysql:host=$host;dbname=$dbname";
$options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC);
$this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], $options);
}
function select(string $table, array $attributes = array())
{
$this->table = $this->tablePrefix . $table;
if (count($attributes) == 0)
$formattedAttributes = "*";
else {
for ($i = 0; $i < count($attributes); $i++) {
$a = $attributes[$i];
if (strpos($a, ".") === false) {
$attributes[$i] = "$this->table.$a";
}
}
$formattedAttributes = implode(', ', $attributes);
}
$this->addToQuery("SELECT $formattedAttributes FROM $this->tablePrefix$table");
return $this;
}
function orderBy(string $attribute, string $order = Order::ASC)
{
$this->addToQuery("ORDER BY $attribute $order");
return $this;
}
static function getLatestIdInTable(string $table, string $attribute = "id")
{
$db = new DbOperations();
$db->select($table, array($attribute));
$db->orderBy($attribute, Order::DESC);
return $db->execute()[0][$attribute];
}
function insert(string $table, array $data)
{
$this->table = $this->tablePrefix . $table;
$attributes = implode(", ", array_keys($data));
$valuesIds = array();
foreach ($data as $attribute => $value) {
$valuesIds[] = $this->addData($value, $attribute);
}
$values = implode(" , ", $valuesIds);
$this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributes ) VALUES ( $values )");
return $this;
}
function insertMultiple(string $table, array $attributes, array $data)
{
$this->table = $this->tablePrefix . $table;
$attributesString = implode(", ", $attributes);
$valueGroups = array();
$groupIndex = 0; // To avoid same value ids
foreach ($data as $dataGroup) {
if (sizeof($attributes) != sizeof($dataGroup)) {
continue;
}
$valueIds = array();
// Indexed for used, so that attributes can easily be assigned to the according values
for ($i = 0; $i < sizeof($dataGroup); $i++) {
$valueIds[] = $this->addData($dataGroup[$i], $attributes[$i] . "_" . (string) $groupIndex);
}
$valueGroups[] = "(" . implode(", ", $valueIds) . ")";
$groupIndex++;
}
$values = implode(", ", $valueGroups);
$this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributesString ) VALUES $values");
return $this;
}
function update(string $table, array $data)
{
$this->table = $this->tablePrefix . $table;
$sets = array();
foreach ($data as $attribute => $value) {
$valueId = $this->addData($value, $attribute);
$sets[] = "$attribute = $valueId";
}
$setString = implode(", ", $sets);
$this->addToQuery("UPDATE $this->tablePrefix$table SET $setString");
return $this;
}
function delete(string $table)
{
$this->table = $this->tablePrefix . $table;
$this->addToQuery("DELETE FROM $this->tablePrefix$table");
return $this;
}
function limit(int $limit, int $offset = null)
{
$this->addToQuery("LIMIT $limit");
if ($offset != null) {
$this->addToQuery("OFFSET $offset");
}
return $this;
}
private function addToQuery(string $phrase)
{
$delimeter = " ";
$this->query = implode($delimeter, array($this->query, $phrase));
}
function where(string $attribute, string $comparison, $value, string $connector = Combination::AND)
{
if (Comparison::isValidValue($comparison) == false)
return;
$keyWord = "WHERE";
if (!(strpos($this->query, $keyWord) === false))
$keyWord = $connector;
$valueId = $this->addData($value, $attribute);
$this->addToQuery("$keyWord $attribute $comparison $valueId");
return $this;
}
function whereOneOf(string $attribute, string $comparison, $values, string $connector = Combination::AND)
{
if (Comparison::isValidValue($comparison) == false)
return;
$keyWord = "WHERE";
if (!(strpos($this->query, $keyWord) === false))
$keyWord = $connector;
$whereClause = "$keyWord ( ";
for ($i = 0; $i < sizeof($values); $i++) {
if ($i > 0) {
$whereClause .= " OR ";
}
$valueId = $this->addData($values[$i], $attribute . '_' . $i);
$whereClause .= "$attribute $comparison $valueId";
}
$whereClause .= " )";
$this->addToQuery($whereClause);
return $this;
}
function innerJoin(string $table, string $externAttribute, string $internAttribute = "", string $internTable = "")
{
if ($internTable === "") {
$internTable = substr($this->table, sizeof($this->tablePrefix));
}
if ($internAttribute === "") {
$internAttribute = $externAttribute;
}
$innerJoin = "INNER JOIN $this->tablePrefix$table ON $this->tablePrefix$table.$externAttribute = $this->tablePrefix$internTable.$internAttribute";
$this->addToQuery($innerJoin);
return $this;
}
private function addData($data, $attribute)
{
$name = str_replace(".", "", $attribute);
$this->data[$name] = $data;
return ":" . $name;
}
function addSql($sql)
{
$this->addToQuery($sql);
}
function addValue($value)
{
$identifier = "customIdentifier" . $this->customValueId;
$this->customValueId += 1;
$this->addToQuery($this->addData($value, $identifier));
}
function execute()
{
try {
$this->openConnection();
$pdoQuery = $this->pdo->prepare($this->query);
$pdoQuery->execute($this->data);
$results = array();
while ($row = $pdoQuery->fetch()) {
$results[] = $row;
}
$this->resetQuery();
return $results;
} catch (PDOException $e) {
// TODO: Hide errors from user and log them
print($e);
return array();
}
}
function sql(string $sqlStatement, array $data)
{
$this->query = $sqlStatement;
foreach ($data as $attribute => $value) {
$this->addData($value, $attribute);
}
}
}
abstract class Comparison extends BasicEnum
{
const EQUAL = "=";
const GREATER_THAN = ">";
const GREATER_THAN_OR_EQUAL = ">=";
const LESS_THAN = "<";
const LESS_THAN_OR_EQUAL = "<=";
const UNEQUAL = "!=";
const LIKE = "LIKE";
}
abstract class Combination extends BasicEnum
{
const AND = "AND";
const OR = "OR";
}
abstract class Order extends BasicEnum
{
const ASC = "ASC";
const DESC = "DESC";
}
<?php
require_once(__DIR__ . "/basicEnum.inc.php");
class DbOperations
{
function __construct($tablePrefix = null)
{
$this->resetQuery();
$this->tablePrefix = $tablePrefix;
require(__DIR__ . "/../config/config.php");
$this->config = $config;
if ($this->tablePrefix == null) {
$this->tablePrefix = $this->config["table_prefix"];
}
}
function resetQuery()
{
$this->query = "";
$this->data = array();
$this->table = "";
}
private function openConnection()
{
$host = $this->config['host'];
$dbname = $this->config['dbname'];
$dsn = "mysql:host=$host;dbname=$dbname";
$options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC);
$this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], $options);
}
function select(string $table, array $attributes = array())
{
$this->table = $this->tablePrefix . $table;
if (count($attributes) == 0)
$formattedAttributes = "*";
else {
for ($i = 0; $i < count($attributes); $i++) {
$a = $attributes[$i];
if (strpos($a, ".") === false) {
$attributes[$i] = "$this->table.$a";
}
}
$formattedAttributes = implode(', ', $attributes);
}
$this->addToQuery("SELECT $formattedAttributes FROM $this->tablePrefix$table");
return $this;
}
function orderBy(string $attribute, string $order = Order::ASC)
{
$this->addToQuery("ORDER BY $attribute $order");
return $this;
}
static function getLatestIdInTable(string $table, string $attribute = "id")
{
$db = new DbOperations();
$db->select($table, array($attribute));
$db->orderBy($attribute, Order::DESC);
return $db->execute()[0][$attribute];
}
function insert(string $table, array $data)
{
$this->table = $this->tablePrefix . $table;
$attributes = implode(", ", array_keys($data));
$valuesIds = array();
foreach ($data as $attribute => $value) {
$valuesIds[] = $this->addData($value, $attribute);
}
$values = implode(" , ", $valuesIds);
$this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributes ) VALUES ( $values )");
return $this;
}
function insertMultiple(string $table, array $attributes, array $data)
{
$this->table = $this->tablePrefix . $table;
$attributesString = implode(", ", $attributes);
$valueGroups = array();
$groupIndex = 0; // To avoid same value ids
foreach ($data as $dataGroup) {
if (sizeof($attributes) != sizeof($dataGroup)) {
continue;
}
$valueIds = array();
// Indexed for used, so that attributes can easily be assigned to the according values
for ($i = 0; $i < sizeof($dataGroup); $i++) {
$valueIds[] = $this->addData($dataGroup[$i], $attributes[$i] . "_" . (string) $groupIndex);
}
$valueGroups[] = "(" . implode(", ", $valueIds) . ")";
$groupIndex++;
}
$values = implode(", ", $valueGroups);
$this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributesString ) VALUES $values");
return $this;
}
function update(string $table, array $data)
{
$this->table = $this->tablePrefix . $table;
$sets = array();
foreach ($data as $attribute => $value) {
$valueId = $this->addData($value, $attribute);
$sets[] = "$attribute = $valueId";
}
$setString = implode(", ", $sets);
$this->addToQuery("UPDATE $this->tablePrefix$table SET $setString");
return $this;
}
function delete(string $table)
{
$this->table = $this->tablePrefix . $table;
$this->addToQuery("DELETE FROM $this->tablePrefix$table");
return $this;
}
function limit(int $limit, int $offset = null)
{
$this->addToQuery("LIMIT $limit");
if ($offset != null) {
$this->addToQuery("OFFSET $offset");
}
return $this;
}
private function addToQuery(string $phrase)
{
$delimeter = " ";
$this->query = implode($delimeter, array($this->query, $phrase));
}
function where(string $attribute, string $comparison, $value, string $connector = Combination::AND)
{
if (Comparison::isValidValue($comparison) == false)
return;
$keyWord = "WHERE";
if (!(strpos($this->query, $keyWord) === false))
$keyWord = $connector;
$valueId = $this->addData($value, $attribute);
$this->addToQuery("$keyWord $attribute $comparison $valueId");
return $this;
}
function whereOneOf(string $attribute, string $comparison, $values, string $connector = Combination::AND)
{
if (Comparison::isValidValue($comparison) == false)
return;
$keyWord = "WHERE";
if (!(strpos($this->query, $keyWord) === false))
$keyWord = $connector;
$whereClause = "$keyWord ( ";
for ($i = 0; $i < sizeof($values); $i++) {
if ($i > 0) {
$whereClause .= " OR ";
}
$valueId = $this->addData($values[$i], $attribute . '_' . $i);
$whereClause .= "$attribute $comparison $valueId";
}
$whereClause .= " )";
$this->addToQuery($whereClause);
return $this;
}
function innerJoin(string $table, string $externAttribute, string $internAttribute = "", string $internTable = "")
{
if ($internTable === "") {
$internTable = substr($this->table, sizeof($this->tablePrefix));
}
if ($internAttribute === "") {
$internAttribute = $externAttribute;
}
$innerJoin = "INNER JOIN $this->tablePrefix$table ON $this->tablePrefix$table.$externAttribute = $this->tablePrefix$internTable.$internAttribute";
$this->addToQuery($innerJoin);
return $this;
}
private function addData($data, $attribute)
{
$name = str_replace(".", "", $attribute);
$this->data[$name] = $data;
return ":" . $name;
}
function addSql($sql)
{
$this->addToQuery($sql);
}
function addValue($value)
{
$identifier = "customIdentifier" . $this->customValueId;
$this->customValueId += 1;
$this->addToQuery($this->addData($value, $identifier));
}
function execute()
{
try {
$this->openConnection();
$pdoQuery = $this->pdo->prepare($this->query);
$pdoQuery->execute($this->data);
$results = array();
while ($row = $pdoQuery->fetch()) {
$results[] = $row;
}
$this->resetQuery();
return $results;
} catch (PDOException $e) {
// TODO: Hide errors from user and log them
print($e);
return array();
}
}
function sql(string $sqlStatement, array $data)
{
$this->query = $sqlStatement;
foreach ($data as $attribute => $value) {
$this->addData($value, $attribute);
}
}
}
abstract class Comparison extends BasicEnum
{
const EQUAL = "=";
const GREATER_THAN = ">";
const GREATER_THAN_OR_EQUAL = ">=";
const LESS_THAN = "<";
const LESS_THAN_OR_EQUAL = "<=";
const UNEQUAL = "!=";
const LIKE = "LIKE";
}
abstract class Combination extends BasicEnum
{
const AND = "AND";
const OR = "OR";
}
abstract class Order extends BasicEnum
{
const ASC = "ASC";
const DESC = "DESC";
}

View file

@ -1,49 +1,67 @@
<?php
class JsonBuilder
{
function __construct()
{
$this->jsonData = array();
}
function getJson()
{
return json_encode($this->jsonData, JSON_FORCE_OBJECT);
}
function getArray()
{
return $this->jsonData;
}
function addRecords(array $records)
{
$columns = array(
"record_id" => "",
"start_time" => "",
"end_time" => "",
"duration" => "",
"user_id" => "",
"project_id" => "",
"start_device_id" => ""
);
foreach ($records as $record) {
$this->jsonData['records'] = array();
$this->jsonData['records'][] = $this->createJsonArray($record, $columns);
}
return $this;
}
private function createJsonArray(array $data, array $columns)
{
$jsonArray = array();
foreach ($columns as $key => $column) {
if ($column === "") {
$column = $key;
}
$jsonArray[$key] = $data[$column];
}
return $jsonArray;
}
}
<?php
class JsonBuilder
{
function __construct()
{
$this->jsonData = array();
}
function getJson()
{
return json_encode($this->jsonData, JSON_FORCE_OBJECT);
}
function getArray()
{
return $this->jsonData;
}
function addRecords(array $records)
{
$columns = array(
"record_id" => "",
"start_time" => "",
"end_time" => "",
"duration" => "",
"user_id" => "",
"project_id" => "",
"start_device_id" => ""
);
$this->jsonData['records'] = array();
foreach ($records as $record) {
$this->jsonData['records'][] = $this->createJsonArray($record, $columns);
}
return $this;
}
function addProjects(array $projects)
{
$columns = array(
"project_id" => "",
"name" => "",
"user_id" => "",
"start_date" => "",
"duration" => "",
"record_count" => ""
);
$this->jsonData['projects'] = array();
foreach ($projects as $project) {
$this->jsonData['projects'][] = $this->createJsonArray($project, $columns);
}
return $this;
}
private function createJsonArray(array $data, array $columns)
{
$jsonArray = array();
foreach ($columns as $key => $column) {
if ($column === "") {
$column = $key;
}
$jsonArray[$key] = $data[$column];
}
return $jsonArray;
}
}

View file

@ -1,167 +1,206 @@
<?php
require_once(__DIR__ . "/services/dbOperations.inc.php");
function addStartRecord($user_id, $params, $project_id = null, $start_device_id = null)
{
$data = [
"user_id" => $user_id,
"start_time" => $params->get("start_time"),
"project_id" => $project_id,
"start_device_id" => $start_device_id
];
$db = new DbOperations();
$db->insert("time_records", $data);
$db->execute();
}
function addTimeRecord($user_id, $params, $project_id = null, $start_device_id = null)
{
$data = [
"user_id" => $user_id,
"start_time" => $params->get("start_time"),
"end_time" => $params->get("end_time"),
"duration" => $params->get("duration"),
"project_id" => $project_id,
"start_device_id" => $start_device_id
];
$db = new DbOperations();
$db->insert("time_records", $data);
$db->execute();
}
function getTimeRecord($user_id, $record_id)
{
$db = new DbOperations();
$db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $record_id);
$result = $db->execute();
if (count($result) <= 0) {
return null;
}
$result = $result[0];
// Is still running?
if ($result["end_time"] == null) {
$result["duration"] = calcDuration($result["start_time"]);
}
return $result;
}
function getProjectRecord($user_id, $project_id, $finished = null)
{
$db = new DbOperations();
$db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("project_id", Comparison::EQUAL, $project_id);
if ($finished != null) {
$comp = Comparison::UNEQUAL;
if ($finished == false) {
$comp = Comparison::EQUAL;
}
$db->where("end_time", $comp, null);
}
$db->orderBy("start_time", Order::DESC);
$result = $db->execute();
if (count($result) <= 0) {
return null;
}
$result = $result[0];
// Is still running?
if ($result["end_time"] == null) {
$result["duration"] = calcDuration($result["start_time"]);
}
return $result;
}
function updateEndRecord($user_id, $params)
{
$record_id = $params->get("record_id");
// Get start instance to calculate duration
$start_time = getTimeRecord($user_id, $record_id)[0]["start_time"];
$data = [
"end_time" => $params->get("end_time"),
"duration" => calcDuration($start_time, $params->get("end_time"))
];
$db = new DbOperations();
$db->update("time_records", $data);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $record_id);
$db->execute();
}
function updateTimeRecord($user_id, $params)
{
$data = [];
$anythingUpdated = false;
if ($params->exists(["start_time"])) {
$data["start_time"] = $params->get("start_time");
$anythingUpdated = true;
}
if ($params->exists(["end_time"])) {
$data["end_time"] = $params->get("end_time");
$anythingUpdated = true;
}
if ($params->exists(["duration"])) {
$data["duration"] = $params->get("duration");
$anythingUpdated = true;
}
if ($params->exists(["project_id"])) {
$data["project_id"] = $params->get("project_id");
$anythingUpdated = true;
}
if ($params->exists(["start_device_id"])) {
$data["start_device_id"] = $params->get("start_device_id");
$anythingUpdated = true;
}
if ($anythingUpdated == false) {
return;
}
$db = new DbOperations();
$db->update("time_records", $data);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $params->get("record_id"));
$db->execute();
}
function isProjectValid($project_id, $user_id)
{
$db = new DbOperations();
$db->select("projects");
$db->where("project_id", Comparison::EQUAL, $project_id);
$db->where("user_id", Comparison::EQUAL, $user_id);
return count($db->execute()) == 1;
}
function isDeviceValid($start_device_id, $user_id)
{
$db = new DbOperations();
$db->select("devices");
$db->where("start_device_id", Comparison::EQUAL, $start_device_id);
$db->where("user_id", Comparison::EQUAL, $user_id);
return count($db->execute()) == 1;
}
function calcDuration($start_time, $end_time = "NOW")
{
return (int)((new DateTime($start_time))->diff(new DateTime($end_time))->format("%s"));
}
<?php
require_once(__DIR__ . "/dbOperations.inc.php");
function addStartRecord($user_id, $params, $project_id = null, $start_device_id = null)
{
$data = [
"user_id" => $user_id,
"start_time" => $params->get("start_time"),
"project_id" => $project_id,
"start_device_id" => $start_device_id
];
$db = new DbOperations();
$db->insert("time_records", $data);
$db->execute();
}
function addTimeRecord($user_id, $params, $project_id = null, $start_device_id = null)
{
$data = [
"user_id" => $user_id,
"start_time" => $params->get("start_time"),
"end_time" => $params->get("end_time"),
"duration" => $params->get("duration"),
"project_id" => $project_id,
"start_device_id" => $start_device_id
];
$db = new DbOperations();
$db->insert("time_records", $data);
$db->execute();
}
function getTimeRecord($user_id, $record_id)
{
$db = new DbOperations();
$db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $record_id);
$result = $db->execute();
if (count($result) <= 0) {
return null;
}
$result = $result[0];
// Is still running?
if ($result["end_time"] == null) {
$result["duration"] = calcDuration($result["start_time"]);
}
return $result;
}
function getProjects($user_id)
{
$db = new DbOperations();
$db->select("projects");
$db->where("user_id", Comparison::EQUAL, $user_id);
$results = $db->execute();
foreach ($results as $key => $project) {
$meta = getProjectRecordDerivedData($user_id, $project["project_id"]);
foreach ($meta as $metaKey => $value) {
$results[$key][$metaKey] = $value;
}
}
return $results;
}
function getProjectRecordDerivedData($user_id, $project_id)
{
$durationAttribute = "SUM(duration) AS total_duration";
$recordCountAttribute = "COUNT(*) AS record_count";
$db = new DbOperations();
$db->select("time_records", ["*", $durationAttribute, $recordCountAttribute]);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("project_id", Comparison::EQUAL, $project_id);
$results = $db->execute();
if (count($results) <= 0) {
return ["duration" => 0, "record_count" => 0];
} else {
return [
"duration" => (int)$results[0]["total_duration"],
"record_count" => (int)$results[0]["record_count"]
];
}
}
function getProjectRecord($user_id, $project_id, $finished = null)
{
$db = new DbOperations();
$db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("project_id", Comparison::EQUAL, $project_id);
if ($finished != null) {
$comp = Comparison::UNEQUAL;
if ($finished == false) {
$comp = Comparison::EQUAL;
}
$db->where("end_time", $comp, null);
}
$db->orderBy("start_time", Order::DESC);
$result = $db->execute();
if (count($result) <= 0) {
return null;
}
$result = $result[0];
// Is still running?
if ($result["end_time"] == null) {
$result["duration"] = calcDuration($result["start_time"]);
}
return $result;
}
function updateEndRecord($user_id, $params)
{
$record_id = $params->get("record_id");
// Get start instance to calculate duration
$start_time = getTimeRecord($user_id, $record_id)[0]["start_time"];
$data = [
"end_time" => $params->get("end_time"),
"duration" => calcDuration($start_time, $params->get("end_time"))
];
$db = new DbOperations();
$db->update("time_records", $data);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $record_id);
$db->execute();
}
function updateTimeRecord($user_id, $params)
{
$data = [];
$anythingUpdated = false;
if ($params->exists(["start_time"])) {
$data["start_time"] = $params->get("start_time");
$anythingUpdated = true;
}
if ($params->exists(["end_time"])) {
$data["end_time"] = $params->get("end_time");
$anythingUpdated = true;
}
if ($params->exists(["duration"])) {
$data["duration"] = $params->get("duration");
$anythingUpdated = true;
}
if ($params->exists(["project_id"])) {
$data["project_id"] = $params->get("project_id");
$anythingUpdated = true;
}
if ($params->exists(["start_device_id"])) {
$data["start_device_id"] = $params->get("start_device_id");
$anythingUpdated = true;
}
if ($anythingUpdated == false) {
return;
}
$db = new DbOperations();
$db->update("time_records", $data);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $params->get("record_id"));
$db->execute();
}
function isProjectValid($project_id, $user_id)
{
$db = new DbOperations();
$db->select("projects");
$db->where("project_id", Comparison::EQUAL, $project_id);
$db->where("user_id", Comparison::EQUAL, $user_id);
return count($db->execute()) == 1;
}
function isDeviceValid($start_device_id, $user_id)
{
$db = new DbOperations();
$db->select("devices");
$db->where("start_device_id", Comparison::EQUAL, $start_device_id);
$db->where("user_id", Comparison::EQUAL, $user_id);
return count($db->execute()) == 1;
}
function calcDuration($start_time, $end_time = "NOW")
{
return (int)((new DateTime($start_time))->diff(new DateTime($end_time))->format("%s"));
}

View file

@ -1,50 +1,50 @@
<?php
class ParamCleaner {
function __construct (array $params) {
$this->sourceParams = $params;
$this->selectedParams = $params;
$this->errorCount = 0;
$this->errorMessage = "";
}
function select (string $prop = "") {
if ($prop == "") {
$this->selectedParams = $this->sourceParams;
} else {
$this->selectedParams = $this->selectedParams[$prop];
}
}
function get (string $prop) {
if(isset($this->selectedParams[$prop])) {
return $this->selectedParams[$prop];
} else {
$this->errorCount += 1;
$this->errorMessage .= "Property \"{$prop}\" missing. ";
return null;
}
}
function exists (array $props) {
foreach ($props as $prop) {
if(isset($this->selectedParams[$prop]) == false) {
return false;
}
}
return true;
}
function hasErrorOccurred () {
return $this->errorCount > 0;
}
function getErrorMessage () {
return $this->errorMessage;
}
function resetErrors () {
$this->errorMessage = "";
$this->errorCount = 0;
}
}
<?php
class ParamCleaner {
function __construct (array $params) {
$this->sourceParams = $params;
$this->selectedParams = $params;
$this->errorCount = 0;
$this->errorMessage = "";
}
function select (string $prop = "") {
if ($prop == "") {
$this->selectedParams = $this->sourceParams;
} else {
$this->selectedParams = $this->selectedParams[$prop];
}
}
function get (string $prop) {
if(isset($this->selectedParams[$prop])) {
return $this->selectedParams[$prop];
} else {
$this->errorCount += 1;
$this->errorMessage .= "Property \"{$prop}\" missing. ";
return null;
}
}
function exists (array $props) {
foreach ($props as $prop) {
if(isset($this->selectedParams[$prop]) == false) {
return false;
}
}
return true;
}
function hasErrorOccurred () {
return $this->errorCount > 0;
}
function getErrorMessage () {
return $this->errorMessage;
}
function resetErrors () {
$this->errorMessage = "";
$this->errorCount = 0;
}
}
?>

View file

@ -1,19 +1,19 @@
<?php
require_once(__DIR__.'/basicEnum.inc.php');
abstract class RequestType extends BasicEnum {
const GET = "GET";
const POST = "POST";
const PUT = "PUT";
const DELETE = "DELETE";
}
function currentRequestType () {
$requestType = $_SERVER['REQUEST_METHOD'];
if (RequestType::isValidValue($requestType)) {
return $requestType;
} else {
return null;
}
}
<?php
require_once(__DIR__.'/basicEnum.inc.php');
abstract class RequestType extends BasicEnum {
const GET = "GET";
const POST = "POST";
const PUT = "PUT";
const DELETE = "DELETE";
}
function currentRequestType () {
$requestType = $_SERVER['REQUEST_METHOD'];
if (RequestType::isValidValue($requestType)) {
return $requestType;
} else {
return null;
}
}
?>

View file

@ -1,25 +1,25 @@
<?php
require_once(__DIR__."/jsonBuilder.inc.php");
function respondJson(JsonBuilder $builder) {
header('Content-type: application/json');
echo($builder->getJson());
}
function respondHtml(string $html) {
print($html);
}
function respondStatus(int $statusCode, string $message = "") {
http_response_code($statusCode);
die($message);
}
function redirectBack() {
header("Location: {$_SERVER['HTTP_REFERER']}");
}
function redirectTo($url) {
header("Location: {$url}");
}
<?php
require_once(__DIR__."/jsonBuilder.inc.php");
function respondJson(JsonBuilder $builder) {
header('Content-type: application/json');
echo($builder->getJson());
}
function respondHtml(string $html) {
print($html);
}
function respondStatus(int $statusCode, string $message = "") {
http_response_code($statusCode);
die($message);
}
function redirectBack() {
header("Location: {$_SERVER['HTTP_REFERER']}");
}
function redirectTo($url) {
header("Location: {$url}");
}
?>

View file

@ -1,50 +1,49 @@
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class StartRecordBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
$params->select("request");
if ($params->exists(["start_time"]) == false) {
respondStatus(400, "Missing parameter");
}
$project_id = $params->get("project_id");
if (isProjectValid($project_id, $user_id) == false) {
$project_id = null;
}
$device_id = $params->get("start_device_id");
if (isDeviceValid($device_id, $user_id) == false) {
$device_id = null;
}
// Does a running record for that project already exist?
if (getProjectRecord($user_id, $project_id, false) != null) {
respondStatus(409, "Project record already started");
}
addStartRecord($user_id, $params, $project_id, $device_id);
$record = getProjectRecord($user_id, $project_id, false);
$json = new JsonBuilder();
$json->addRecords([$record]);
respondJson($json);
}
}
$branch = new StartRecordBranch();
$branch->execute();
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class StartRecordBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
if ($params->exists(["start_time"]) == false) {
respondStatus(400, "Missing parameter");
}
$project_id = $params->get("project_id");
if (isProjectValid($project_id, $user_id) == false) {
$project_id = null;
}
$device_id = $params->get("start_device_id");
if (isDeviceValid($device_id, $user_id) == false) {
$device_id = null;
}
// Does a running record for that project already exist?
if (getProjectRecord($user_id, $project_id, false) != null) {
respondStatus(409, "Project record already started");
}
addStartRecord($user_id, $params, $project_id, $device_id);
$record = getProjectRecord($user_id, $project_id, false);
$json = new JsonBuilder();
$json->addRecords([$record]);
respondJson($json);
}
}
$branch = new StartRecordBranch();
$branch->execute();

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -0,0 +1,91 @@
body {
margin: 0;
background-color: #222;
}
* {
color: white;
font-family: 'Karla', sans-serif;
transition-duration: 0.1s;
}
.hidden {
display: none;
}
header {
width: 100%;
height: 4rem;
position: fixed;
top: 0px;
background-color: #222;
/* background: rgb(10, 133, 168);
background: linear-gradient(137deg, rgba(10, 133, 168, 1) 0%, rgba(136, 0, 0, 1) 100%); */
/* background: rgb(10, 133, 168);
background: linear-gradient(137deg, rgba(10, 133, 168, 1) 0%, rgba(255, 255, 255, 1) 48%, rgba(136, 0, 0, 1) 100%); */
/* offset-x | offset-y | blur-radius | spread-radius | color */
box-shadow: 0px 0px 2rem 1rem #000D;
}
main {
max-width: 800px;
padding: 0px 20px;
margin: auto;
margin-top: 6rem;
background-color: #0000;
}
#header-container {
max-width: 1000px;
padding: 0px 20px;
margin: auto;
list-style: none;
}
#header-container img {
margin-top: 0.5rem;
height: 3rem;
width: auto;
}
#header-container>* {
line-height: 4rem;
vertical-align: baseline;
display: inline-block;
}
textarea:focus,
input:focus,
button:focus {
outline: none;
}
input,
button {
border-radius: 2px;
border: solid red 1px;
background-color: #0000;
color: #BBB;
padding: 5px;
font-size: 12pt;
}
button:hover {
color: white;
background-color: #F007;
}
button:active {
transform: translate(0px, 2px);
}
table {
border-collapse: collapse;
width: 100%;
}
thead * {
color: #AAA;
text-align: left;
}

61
juggl-server/index.html Normal file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<title>Juggl</title>
<meta charset="UTF-8" />
<link rel="shortcut icon" type="image/ico" href="/assets/logo.ico" />
<link href="https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<script src="js/umbrella.min.js"></script>
<script src="js/visibility.js"></script>
<script src="js/auth.js"></script>
<script src="js/api.js"></script>
<script src="js/helper.js"></script>
<script>
window.onload = () => {
// Create Callbacks
callbacks.leaving[States.PUBLIC] = () => {
loadProjectList();
};
initState();
updateVisibility();
updateAuthBtnText();
}
</script>
</head>
<body>
<header>
<ul id="header-container">
<li>
<img src="assets/logo_title.png">
</li>
<li style="float: right">
<input id="user-id" class="public" type="text" placeholder="User ID" />
<input id="api-key" class="public" type="password" placeholder="API Key" />
<button id="auth-btn" onclick="handleAuthBtn()">Log In</button>
</li>
</ul>
</header>
<main>
<table class="not-public hidden">
<thead>
<th>Project</th>
<th>Start date</th>
<th>Duration</th>
<th>Records</th>
<th>Average record</th>
</thead>
<tbody id="project-list">
</tbody>
</table>
</main>
</body>
</html>

32
juggl-server/js/api.js Normal file
View file

@ -0,0 +1,32 @@
API_URL = "/api";
const api = {
getProjects() {
return request("/getProjects.php")
.then((r) => {
return r.json();
})
.then((j) => {
return j.projects;
})
.catch((e) => {
console.log(e);
return [];
});
},
};
function request(path, json = {}, options = {}) {
json.api_key = getApiKey();
json.user_id = getUserId();
options.method = "POST";
options.body = JSON.stringify(json);
options.headers = {
"Content-Type": "application/json",
};
var url = API_URL + path;
return fetch(url, options);
}

58
juggl-server/js/auth.js Normal file
View file

@ -0,0 +1,58 @@
function logIn(apiKey, userId) {
localStorage.apiKey = apiKey;
localStorage.userId = userId;
setState(States.IDLE);
}
function logOut() {
delete localStorage.apiKey;
delete localStorage.userId;
setState(States.PUBLIC);
}
function isLoggedIn() {
return !!localStorage.apiKey && !!localStorage.userId;
}
function getApiKey() {
return localStorage.apiKey;
}
function getUserId() {
return localStorage.userId;
}
function handleAuthBtn() {
if (isLoggedIn()) {
logOut();
} else {
var apiKey = u("#api-key").first().value;
if (apiKey === undefined || apiKey === "") {
return;
}
var userId = u("#user-id").first().value;
if (userId === undefined || userId === "") {
return;
}
logIn(apiKey, userId);
onLogIn();
}
u("#api-key").first().value = "";
updateAuthBtnText();
}
function updateAuthBtnText() {
var btn = u("#auth-btn");
if (isLoggedIn()) {
btn.text("Log Out");
} else {
btn.text("Log In");
}
}
function onLogIn() {
loadProjectList();
}

59
juggl-server/js/helper.js Normal file
View file

@ -0,0 +1,59 @@
const TABLE_ROW = "tr";
const TABLE_DATA = "td";
function loadProjectList() {
api.getProjects().then((projects) => {
var table = u(u("#project-list").first());
Object.values(projects).forEach((project) => {
var row = createNode(TABLE_ROW);
var data = undefined;
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(project["name"]);
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(new Date(project["start_date"]).toDateString());
var duration = parseFloat(project["duration"]) / 60 / 60;
var unit = "hours";
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(duration + " " + unit);
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(project["record_count"]);
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(
duration / parseInt(project["record_count"]) + " " + unit + "/record"
);
row = u(row);
row.data("project-id", project["project_id"]);
row.on("click", projectClicked);
table.append(row);
});
});
}
// Created new DOM object
// element: Type of DOM object (div, p, ...)
function createNode(element) {
return document.createElement(element);
}
// Appends child to parent
// parent: DOM to append child to
// el: DOM child to append to parent
function append(parent, el) {
return parent.appendChild(el);
}
function projectClicked(event) {
console.log(event);
}

3
juggl-server/js/umbrella.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,55 @@
/**
* Values represent classes.
*
* classname: Only visible in class-state.
* not-classname: Not visible in class-state.
*/
const States = {
PUBLIC: "public",
IDLE: "idle",
RECORDING: "recording",
SETUP: "setup",
};
Object.freeze(States);
let previousState = States.PUBLIC;
let currentState = States.PUBLIC;
let callbacks = {
leaving: {},
entering: {},
};
function updateVisibility() {
u("." + previousState).addClass("hidden");
u("." + currentState).removeClass("hidden");
u(".not-" + previousState).removeClass("hidden");
u(".not-" + currentState).addClass("hidden");
processCallbacks();
}
function processCallbacks() {
var cb = callbacks.leaving[previousState];
if (cb !== undefined) cb();
cb = callbacks.entering[currentState];
if (cb !== undefined) cb();
}
function setState(state) {
if (Object.values(States).includes(state) === false) {
return;
}
previousState = currentState;
currentState = state;
localStorage.state = state;
updateVisibility();
}
function initState() {
var savedState = localStorage.state;
if (savedState !== undefined) currentState = localStorage.state;
}

View file

@ -1,45 +1,45 @@
Juggl Documentation
Goal
Hold information about projects
Timetracking
Invoice generation
Shared files
ToDo
Progress
Shared Workspace for client
Deadlines
API interface
Ticket system
ToDo
High Priority
Tracking Windows Client
Refactor TimeLogger
Offline BackUps
Nur tracking wenn aktiviert
Time tracking
API interface
Web interface
General
Hold some kind of current state
e.g. "Currently working on"
Low Priority
Web interface
Datbase
Table prefix
ju_
API
timetracking
POST
api_key
user_id
request
start_time (required for insert)
end_time (required for insert)
duration (required for insert)
project_id (optional)
device_id (optional)
Juggl Documentation
Goal
Hold information about projects
Timetracking
Invoice generation
Shared files
ToDo
Progress
Shared Workspace for client
Deadlines
API interface
Ticket system
ToDo
High Priority
Tracking Windows Client
Refactor TimeLogger
Offline BackUps
Nur tracking wenn aktiviert
Time tracking
API interface
Web interface
General
Hold some kind of current state
e.g. "Currently working on"
Low Priority
Web interface
Datbase
Table prefix
ju_
API
timetracking
POST
api_key
user_id
request
start_time (required for insert)
end_time (required for insert)
duration (required for insert)
project_id (optional)
device_id (optional)
record_id (optional, update when given)