A LOT of improvements
This commit is contained in:
parent
5c30876c80
commit
4c64f99ced
26 changed files with 1307 additions and 861 deletions
|
@ -1,4 +1,8 @@
|
|||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
|
||||
$config = [
|
||||
"host" => "localhost",
|
||||
"dbname" => "juggl",
|
|
@ -14,7 +14,6 @@ class EndRecordBranch extends ApiBranch
|
|||
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");
|
29
juggl-server/api/getProjects.php
Normal file
29
juggl-server/api/getProjects.php
Normal 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();
|
|
@ -15,7 +15,6 @@ class GetRecordBranch extends ApiBranch
|
|||
function post(ParamCleaner $params)
|
||||
{
|
||||
$user_id = $params->get("user_id");
|
||||
$params->select("request");
|
||||
|
||||
if ($params->exists(["record_id"]) == false) {
|
||||
respondStatus(400, "Missing parameter");
|
|
@ -28,13 +28,31 @@ class JsonBuilder
|
|||
"start_device_id" => ""
|
||||
);
|
||||
|
||||
foreach ($records as $record) {
|
||||
$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();
|
|
@ -1,5 +1,5 @@
|
|||
<?php
|
||||
require_once(__DIR__ . "/services/dbOperations.inc.php");
|
||||
require_once(__DIR__ . "/dbOperations.inc.php");
|
||||
|
||||
function addStartRecord($user_id, $params, $project_id = null, $start_device_id = null)
|
||||
{
|
||||
|
@ -52,6 +52,45 @@ function getTimeRecord($user_id, $record_id)
|
|||
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();
|
|
@ -15,7 +15,6 @@ class StartRecordBranch extends ApiBranch
|
|||
function post(ParamCleaner $params)
|
||||
{
|
||||
$user_id = $params->get("user_id");
|
||||
$params->select("request");
|
||||
|
||||
if ($params->exists(["start_time"]) == false) {
|
||||
respondStatus(400, "Missing parameter");
|
BIN
juggl-server/assets/logo.ico
Normal file
BIN
juggl-server/assets/logo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 103 KiB |
BIN
juggl-server/assets/logo_title.png
Normal file
BIN
juggl-server/assets/logo_title.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
91
juggl-server/css/style.css
Normal file
91
juggl-server/css/style.css
Normal 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
61
juggl-server/index.html
Normal 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
32
juggl-server/js/api.js
Normal 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
58
juggl-server/js/auth.js
Normal 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
59
juggl-server/js/helper.js
Normal 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
3
juggl-server/js/umbrella.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
55
juggl-server/js/visibility.js
Normal file
55
juggl-server/js/visibility.js
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue