Refactored to be focues on vue.js
This commit is contained in:
parent
9695f7d9e3
commit
444acaf046
67 changed files with 16034 additions and 772 deletions
36
.gitignore
vendored
36
.gitignore
vendored
|
@ -1,6 +1,30 @@
|
||||||
juggl/config/config.txt
|
.DS_Store
|
||||||
juggl/config/config.path
|
node_modules
|
||||||
juggl/config/config.php
|
/dist
|
||||||
graphics
|
|
||||||
.vscode
|
|
||||||
juggl-vue/package-lock.json
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
juggl/config/config.txt
|
||||||
|
juggl/config/config.path
|
||||||
|
juggl/config/config.php
|
||||||
|
graphics
|
||||||
|
.vscode
|
||||||
|
juggl-vue/package-lock.json
|
||||||
|
|
23
juggl-vue/.gitignore
vendored
23
juggl-vue/.gitignore
vendored
|
@ -1,23 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/dist
|
|
||||||
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
|
@ -1,22 +0,0 @@
|
||||||
<template>
|
|
||||||
<b-dropdown id="dropdown" :text=this.username variant="outline-danger" right>
|
|
||||||
<b-dropdown-item to="/logout">Log out</b-dropdown-item>
|
|
||||||
</b-dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import store from "@/store";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "BaseUserDropdown",
|
|
||||||
computed: {
|
|
||||||
username: () => store.getters.user.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
|
||||||
#dropdown
|
|
||||||
*
|
|
||||||
line-height: 1.5
|
|
||||||
</style>
|
|
|
@ -1,74 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="project-list">
|
|
||||||
<div v-for="project in projects" :key="project.project_id" @click="() => startProject(project.project_id)">
|
|
||||||
<h1>{{project.name}}</h1>
|
|
||||||
<p>{{getDurationTimestamp(project)}}</p>
|
|
||||||
<p>{{project.record_count}} records</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import store from "@/store"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "JugglProjectPanel",
|
|
||||||
props: {
|
|
||||||
projects: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getDurationTimestamp: (project) => {
|
|
||||||
var totalSeconds = project.duration;
|
|
||||||
// var days = Math.floor(totalSeconds / 86400);
|
|
||||||
var hours = Math.floor(totalSeconds / 3600); //Math.floor((totalSeconds - (days * 86400)) / 3600);
|
|
||||||
var minutes = Math.floor((totalSeconds - (hours * 3600)) / 60);
|
|
||||||
var seconds = totalSeconds - (hours * 3600) - (minutes * 60);
|
|
||||||
|
|
||||||
// if (days < 10) {days = "0"+days;}
|
|
||||||
if (hours < 10) {hours = "0"+hours;}
|
|
||||||
if (minutes < 10) {minutes = "0"+minutes;}
|
|
||||||
if (seconds < 10) {seconds = "0"+seconds;}
|
|
||||||
return hours+':'+minutes+':'+seconds;
|
|
||||||
},
|
|
||||||
startProject: function (id) {
|
|
||||||
store.dispatch("startRecord", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
|
||||||
@import '@/style/theme.sass'
|
|
||||||
|
|
||||||
#project-list
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
flex-wrap: wrap
|
|
||||||
justify-content: center
|
|
||||||
align-content: flex-start
|
|
||||||
padding: 5px
|
|
||||||
|
|
||||||
> *
|
|
||||||
margin: 5px
|
|
||||||
border: 1px dashed $grey
|
|
||||||
border-radius: 5px
|
|
||||||
padding: 10px
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color: $primary !important
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
h1
|
|
||||||
font-weight: bold
|
|
||||||
font-size: 24pt
|
|
||||||
margin: 0px
|
|
||||||
padding: 0px
|
|
||||||
|
|
||||||
p
|
|
||||||
font-size: 10pt
|
|
||||||
color: $grey
|
|
||||||
margin: 0px
|
|
||||||
padding: 0px
|
|
||||||
</style>
|
|
|
@ -1,97 +0,0 @@
|
||||||
<template>
|
|
||||||
<b-table :items="records" hover :busy="isLoading" :fields="fields">
|
|
||||||
<template #table-busy>
|
|
||||||
<div class="text-center">
|
|
||||||
<b-spinner></b-spinner>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Custom data -->
|
|
||||||
<template #cell(project)="data">
|
|
||||||
{{ getProject(data.item.project_id).name }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell(start)="data">
|
|
||||||
{{ data.item.start_time }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell(duration)="data">
|
|
||||||
{{ data.item.duration }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell(stop)="data">
|
|
||||||
<b-button size="sm" @click="stopRecord(data.item.record_id)" variant="outline-success">
|
|
||||||
<b-icon class="icon-btn" icon="check" />
|
|
||||||
</b-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell(abort)="data">
|
|
||||||
<b-button size="sm" @click="() => abortRecord(data.item.record_id)" variant="outline-danger">
|
|
||||||
<b-icon class="icon-btn" icon="x" />
|
|
||||||
</b-button>
|
|
||||||
</template>
|
|
||||||
</b-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import store from "@/store";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "JugglRecordsList",
|
|
||||||
props: {
|
|
||||||
records: {
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
iconScale: 1.6,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
key: "project",
|
|
||||||
label: "Project",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "start",
|
|
||||||
label: "Start",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "duration",
|
|
||||||
label: "Duration",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "stop",
|
|
||||||
label: "Stop",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "abort",
|
|
||||||
label: "Abort",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isLoading: function () {
|
|
||||||
return this.records === undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getProject: function (id) {
|
|
||||||
return store.getters.getProjectById(id);
|
|
||||||
},
|
|
||||||
stopRecord: function (id) {
|
|
||||||
store.dispatch("endRecord", id);
|
|
||||||
},
|
|
||||||
abortRecord: function (id) {
|
|
||||||
console.log(id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
.icon-btn:hover
|
|
||||||
color: green
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
</styl>
|
|
|
@ -1,55 +0,0 @@
|
||||||
import Vue from "vue";
|
|
||||||
import VueRouter from "vue-router";
|
|
||||||
import store from "../store";
|
|
||||||
import Login from "../views/Login.vue";
|
|
||||||
import Home from "../views/Home.vue";
|
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
redirect: "/home"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/login",
|
|
||||||
name: "Login",
|
|
||||||
component: Login,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/home",
|
|
||||||
name: "Home",
|
|
||||||
component: Home,
|
|
||||||
beforeEnter: requireAuth,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/logout",
|
|
||||||
name: "Logout",
|
|
||||||
beforeEnter: (to, from, next) => {
|
|
||||||
store.dispatch("logout");
|
|
||||||
next("/");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const router = new VueRouter({
|
|
||||||
mode: "history",
|
|
||||||
base: process.env.BASE_URL,
|
|
||||||
routes,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks authentication before proceeding
|
|
||||||
*/
|
|
||||||
function requireAuth(to, from, next) {
|
|
||||||
if (!store.getters.isLoggedIn) {
|
|
||||||
next({
|
|
||||||
path: "/login",
|
|
||||||
query: { redirect: to.fullPath },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default router;
|
|
|
@ -1,49 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
import store from "@/store";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper for the used fetch API, currently axios.
|
|
||||||
* Uses some default values from the config (e.g. ApiUrl).
|
|
||||||
*
|
|
||||||
* Authentication already integrated.
|
|
||||||
*
|
|
||||||
* Returns promises.
|
|
||||||
*/
|
|
||||||
export const apiService = {
|
|
||||||
get(resource, options) {
|
|
||||||
return this.getApi().get(resource, options);
|
|
||||||
},
|
|
||||||
|
|
||||||
post(resource, json, options) {
|
|
||||||
return this.getApi().post(
|
|
||||||
resource,
|
|
||||||
{ ...this.getDefaultJson(), ...json },
|
|
||||||
options
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
put(resource, json, options) {
|
|
||||||
return this.getApi().put(
|
|
||||||
resource,
|
|
||||||
{ ...this.getDefaultJson(), ...json },
|
|
||||||
options
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance of the used api and sets necessary headers
|
|
||||||
*/
|
|
||||||
getApi() {
|
|
||||||
var options = {
|
|
||||||
baseURL: store.getters.apiUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
return axios.create(options);
|
|
||||||
},
|
|
||||||
getDefaultJson() {
|
|
||||||
return {
|
|
||||||
user_id: store.getters.auth.userId,
|
|
||||||
api_key: store.getters.auth.apiKey,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,111 +0,0 @@
|
||||||
/**
|
|
||||||
* Contains a bunch of helper functions.
|
|
||||||
*/
|
|
||||||
export const helperService = {
|
|
||||||
/**
|
|
||||||
* Converts number into a human readable percent integer.
|
|
||||||
*
|
|
||||||
* @param {*} r Floating point percent number
|
|
||||||
*/
|
|
||||||
asPercent(r) {
|
|
||||||
return Math.floor(r * 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts number into a human readable floating point number with a digit behind the delimiter.
|
|
||||||
*
|
|
||||||
* @param {*} r Floating point number
|
|
||||||
*/
|
|
||||||
asFloat(r) {
|
|
||||||
return Math.floor(r * 10) / 10;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts timestamp into a human readable date format.
|
|
||||||
*
|
|
||||||
* @param {*} d ISO Timestamp
|
|
||||||
*/
|
|
||||||
asDateFromISO(d) {
|
|
||||||
return helperService.asDateFromObj(new Date(d));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts timestamp into a human readable date format.
|
|
||||||
*
|
|
||||||
* @param {*} d Date object
|
|
||||||
*/
|
|
||||||
asDateFromObj(d) {
|
|
||||||
return new Intl.DateTimeFormat("de").format(d);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes sure, that the given text is not too long.
|
|
||||||
*
|
|
||||||
* @param {*} text Some text that might be shortened
|
|
||||||
* @param {*} maxLength Max number of characters
|
|
||||||
*
|
|
||||||
* @returns The shortened or same text
|
|
||||||
*/
|
|
||||||
keepItShort(text, maxLength = 20, ellipsis = "...") {
|
|
||||||
if (text.length > maxLength) {
|
|
||||||
// Adding ellipsis
|
|
||||||
var short = text.substring(0, maxLength - ellipsis.length).trim();
|
|
||||||
return short + ellipsis;
|
|
||||||
} else return text;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a date object into a date-time-timezone string, and sets times and timezone to zero.
|
|
||||||
*
|
|
||||||
* @source https://stackoverflow.com/a/43528844/7376120
|
|
||||||
*
|
|
||||||
* @param {*} date A date object
|
|
||||||
*
|
|
||||||
* @returns Date as string in the used format
|
|
||||||
*/
|
|
||||||
toISODate(date) {
|
|
||||||
var timezoneOffset = date.getMinutes() + date.getTimezoneOffset();
|
|
||||||
var timestamp = date.getTime() + timezoneOffset * 1000;
|
|
||||||
var correctDate = new Date(timestamp);
|
|
||||||
|
|
||||||
correctDate.setUTCHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
return correctDate.toISOString();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds current duration to a record.
|
|
||||||
* Copied from original juggl code.
|
|
||||||
* @param {*} record The record instance to update.
|
|
||||||
*/
|
|
||||||
addDuration(record) {
|
|
||||||
if (record.end_time != null) return record;
|
|
||||||
|
|
||||||
record.duration =
|
|
||||||
(new Date().getTime() - new Date(record.start_time).getTime()) /
|
|
||||||
1000;
|
|
||||||
|
|
||||||
return record;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a datetime object into the necessary string format for server requests.
|
|
||||||
* Copied from original juggl code.
|
|
||||||
* @param {*} date
|
|
||||||
*/
|
|
||||||
dateToString(date) {
|
|
||||||
return (
|
|
||||||
date.getFullYear() +
|
|
||||||
"-" +
|
|
||||||
(date.getMonth() + 1) +
|
|
||||||
"-" +
|
|
||||||
date.getDate() +
|
|
||||||
" " +
|
|
||||||
date.getHours() +
|
|
||||||
":" +
|
|
||||||
date.getMinutes() +
|
|
||||||
":" +
|
|
||||||
date.getSeconds()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,98 +0,0 @@
|
||||||
import { apiService } from "@/services/api.service";
|
|
||||||
import { helperService } from "@/services/helper.service";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A collection of functions to retreive and send all user-specific data.
|
|
||||||
*/
|
|
||||||
export const jugglService = {
|
|
||||||
/**
|
|
||||||
* Fetches the user from the API.
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
getUser() {
|
|
||||||
return apiService.post("/getUser.php").then((r) => {
|
|
||||||
return {
|
|
||||||
data: r.data,
|
|
||||||
msg: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getProjects() {
|
|
||||||
return apiService.post("/getProjects.php").then((r) => {
|
|
||||||
return {
|
|
||||||
data: r.data,
|
|
||||||
msg: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getRecord(recordId) {
|
|
||||||
return apiService
|
|
||||||
.post("/getRecord.php", {
|
|
||||||
record_id: recordId,
|
|
||||||
})
|
|
||||||
.then((r) => {
|
|
||||||
return {
|
|
||||||
data: processRecords(r.data),
|
|
||||||
msg: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getRecords() {
|
|
||||||
return apiService.post("/getRecords.php").then((r) => {
|
|
||||||
return {
|
|
||||||
data: processRecords(r.data),
|
|
||||||
msg: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getRunningRecords() {
|
|
||||||
return apiService.post("/getRunningRecords.php").then((r) => {
|
|
||||||
return {
|
|
||||||
data: processRecords(r.data),
|
|
||||||
msg: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
startRecord(projectId, startTime = null) {
|
|
||||||
if (startTime == null) startTime = new Date();
|
|
||||||
return apiService
|
|
||||||
.post("/startRecord.php", {
|
|
||||||
project_id: projectId,
|
|
||||||
start_time: helperService.dateToString(startTime),
|
|
||||||
})
|
|
||||||
.then((r) => {
|
|
||||||
return {
|
|
||||||
data: r.data,
|
|
||||||
msg: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
endRecord(recordId, endTime = null) {
|
|
||||||
if (endTime == null) endTime = new Date();
|
|
||||||
return apiService
|
|
||||||
.post("/endRecord.php", {
|
|
||||||
record_id: recordId,
|
|
||||||
end_time: helperService.dateToString(endTime),
|
|
||||||
})
|
|
||||||
.then((r) => {
|
|
||||||
return {
|
|
||||||
data: r.data,
|
|
||||||
msg: "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function processRecords(data) {
|
|
||||||
Object.values(data.records).forEach(rec => {
|
|
||||||
rec.running = rec.end_time === null;
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
import router from "@/router";
|
|
||||||
import { userService } from "@/services/user.service";
|
|
||||||
|
|
||||||
// TODO: Do I need this file?
|
|
||||||
/**
|
|
||||||
* Contains a bunch of path helper functions.
|
|
||||||
*/
|
|
||||||
export const pathService = {
|
|
||||||
/**
|
|
||||||
* Redirects to the not found page.
|
|
||||||
*
|
|
||||||
* @param {*} reason Optional: A reason to explain to the user, why they got redirected
|
|
||||||
* @param {*} sourceRoute Optional: Adds additional information from the source route to the query
|
|
||||||
*/
|
|
||||||
notFound(reason, sourceRoute) {
|
|
||||||
var payload = {};
|
|
||||||
|
|
||||||
if (reason !== undefined) {
|
|
||||||
payload.reason = reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceRoute !== undefined) {
|
|
||||||
payload.source = sourceRoute.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
path: "/404",
|
|
||||||
query: payload,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates and gets all data to a path of the form /dashboard/:sem:-sem
|
|
||||||
*
|
|
||||||
* @param {*} route Object that describes the current path and its parameters
|
|
||||||
*
|
|
||||||
* @returns A promise of the form ({semester: semester})
|
|
||||||
*/
|
|
||||||
validateSemPath(route) {
|
|
||||||
var semesterNr = this.getSemesterNrFromParam(route.params.semesterName);
|
|
||||||
|
|
||||||
// Invalid path?
|
|
||||||
if (semesterNr === undefined) {
|
|
||||||
this.notFound("Invalid URL", route);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather more data
|
|
||||||
return userService
|
|
||||||
.getSemester(semesterNr)
|
|
||||||
.then((semester) => {
|
|
||||||
return { semester: semester.data };
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Semester could not be found
|
|
||||||
this.notFound(semesterNr + ". semester not found", route);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates and gets all data to a path of the form /dashboard/:sem:-sem/:module:
|
|
||||||
*
|
|
||||||
* @param {*} route Object that describes the current path and its parameters
|
|
||||||
*
|
|
||||||
* @returns A promise of the form ({semester: semester, module: module})
|
|
||||||
*/
|
|
||||||
validateSemModulePath(route) {
|
|
||||||
// First evaluate semester and than the module
|
|
||||||
return this.validateSemPath(route).then((obj) => {
|
|
||||||
var moduleName = route.params.moduleName;
|
|
||||||
|
|
||||||
// Gather more data
|
|
||||||
return userService
|
|
||||||
.getModule(obj.semester.nr, moduleName)
|
|
||||||
.then((module) => {
|
|
||||||
return { semester: obj.semester, module: module.data };
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Module could not be found
|
|
||||||
this.notFound(
|
|
||||||
'"' + moduleName + '" module not found',
|
|
||||||
route
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the semester nr based on the url parameter in the format ":semesterNr:-sem".
|
|
||||||
*
|
|
||||||
* @param {*} semesterParam In the format ":semesterNr:-sem"
|
|
||||||
*
|
|
||||||
* @returns Semester Nr. if valid, otherwise undefined
|
|
||||||
*/
|
|
||||||
getSemesterNrFromParam(semesterParam) {
|
|
||||||
let re = new RegExp("^([1-9][0-9]*)-sem$");
|
|
||||||
var res = semesterParam.match(re);
|
|
||||||
|
|
||||||
// Found a match?
|
|
||||||
if (res === null) {
|
|
||||||
return undefined; // No
|
|
||||||
} else {
|
|
||||||
// First group is semester nr
|
|
||||||
return res[1];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getBreadcrumbs(route) {
|
|
||||||
let sites = route.path.split("/");
|
|
||||||
let siteCount = sites.length;
|
|
||||||
|
|
||||||
return sites.reduce((crumbs, current, index) => {
|
|
||||||
// Ignore empty parts
|
|
||||||
if (current.length <= 0) {
|
|
||||||
return crumbs;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = current;
|
|
||||||
// Handle semester name
|
|
||||||
var semNr = this.getSemesterNrFromParam(current);
|
|
||||||
if (semNr !== undefined) {
|
|
||||||
name = semNr + ". Semester";
|
|
||||||
}
|
|
||||||
// Handle decoding and capitalization
|
|
||||||
name = decodeURIComponent(name);
|
|
||||||
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
||||||
|
|
||||||
let part = {
|
|
||||||
text: name,
|
|
||||||
};
|
|
||||||
|
|
||||||
let isActive = index + 1 == siteCount;
|
|
||||||
if (isActive) {
|
|
||||||
part.active = true;
|
|
||||||
} else {
|
|
||||||
part.to = crumbs[crumbs.length - 1]
|
|
||||||
? crumbs[crumbs.length - 1].to + "/" + current
|
|
||||||
: "/" + current;
|
|
||||||
}
|
|
||||||
|
|
||||||
crumbs.push(part);
|
|
||||||
return crumbs;
|
|
||||||
}, []);
|
|
||||||
},
|
|
||||||
};
|
|
13332
package-lock.json
generated
Normal file
13332
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint",
|
||||||
|
"lint:fix": "eslint --fix --ext .js,.vue ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
|
@ -14,7 +15,8 @@
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-router": "^3.2.0",
|
"vue-router": "^3.2.0",
|
||||||
"vuex": "^3.4.0"
|
"vuex": "^3.4.0",
|
||||||
|
"vuex-persistedstate": "^4.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
30
public/api/addProject.php
Normal file
30
public/api/addProject.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?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 AddProjectBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$user_id = $params->get("user_id");
|
||||||
|
|
||||||
|
if ($params->exists(["name", "start_date"]) == false) {
|
||||||
|
respondStatus(400, "Missing parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
addProject($user_id, $params);
|
||||||
|
|
||||||
|
respondStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new AddProjectBranch();
|
||||||
|
$branch->execute();
|
26
public/api/addRecordTag.php
Normal file
26
public/api/addRecordTag.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?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 AddRecordTagBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$user_id = $params->get("user_id");
|
||||||
|
$tag_name = $params->get("tag_name");
|
||||||
|
|
||||||
|
addRecordTag($user_id, $tag_name);
|
||||||
|
|
||||||
|
respondStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new AddRecordTagBranch();
|
||||||
|
$branch->execute();
|
26
public/api/addTagToRecord.php
Normal file
26
public/api/addTagToRecord.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?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 AddTagToRecordBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$record_id = $params->get("record_id");
|
||||||
|
$tag_id = $params->get("tag_id");
|
||||||
|
|
||||||
|
addTagToRecord($tag_id, $record_id);
|
||||||
|
|
||||||
|
respondStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new AddTagToRecordBranch();
|
||||||
|
$branch->execute();
|
12
public/api/config/config.php
Normal file
12
public/api/config/config.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?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_"
|
||||||
|
];
|
8
public/api/config/config.php.sample
Normal file
8
public/api/config/config.php.sample
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
$config = [
|
||||||
|
"host" => "",
|
||||||
|
"dbname" => "",
|
||||||
|
"username" => "",
|
||||||
|
"password" => "",
|
||||||
|
"table_prefix" => "ju_"
|
||||||
|
];
|
29
public/api/endRecord.php
Normal file
29
public/api/endRecord.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?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);
|
||||||
|
|
||||||
|
respondStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new EndRecordBranch();
|
||||||
|
$branch->execute();
|
29
public/api/getProjects.php
Normal file
29
public/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();
|
33
public/api/getRecord.php
Normal file
33
public/api/getRecord.php
Normal file
|
@ -0,0 +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");
|
||||||
|
|
||||||
|
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();
|
29
public/api/getRecordTags.php
Normal file
29
public/api/getRecordTags.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 GetRecordTagsBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$user_id = $params->get("user_id");
|
||||||
|
|
||||||
|
$tags = getRecordTags($user_id);
|
||||||
|
|
||||||
|
$json = new JsonBuilder();
|
||||||
|
$json->addRecordTags($tags);
|
||||||
|
|
||||||
|
respondJson($json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new GetRecordTagsBranch();
|
||||||
|
$branch->execute();
|
38
public/api/getRecords.php
Normal file
38
public/api/getRecords.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?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 GetRecordsBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$user_id = $params->get("user_id");
|
||||||
|
|
||||||
|
$limit = NULL;
|
||||||
|
if ($params->exists(["limit"])) {
|
||||||
|
$limit = $params->get("limit");
|
||||||
|
}
|
||||||
|
$finished = NULL;
|
||||||
|
if ($params->exists(["finished"])) {
|
||||||
|
$finished = $params->get("finished");
|
||||||
|
}
|
||||||
|
|
||||||
|
$records = getRecords($user_id, $limit, $finished);
|
||||||
|
|
||||||
|
$json = new JsonBuilder();
|
||||||
|
$json->addRecords($records);
|
||||||
|
|
||||||
|
respondJson($json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new GetRecordsBranch();
|
||||||
|
$branch->execute();
|
29
public/api/getRunningRecords.php
Normal file
29
public/api/getRunningRecords.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 GetRunningRecordsBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$user_id = $params->get("user_id");
|
||||||
|
|
||||||
|
$records = getRunningRecords($user_id);
|
||||||
|
|
||||||
|
$json = new JsonBuilder();
|
||||||
|
$json->addRecords($records);
|
||||||
|
|
||||||
|
respondJson($json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new GetRunningRecordsBranch();
|
||||||
|
$branch->execute();
|
29
public/api/getUser.php
Normal file
29
public/api/getUser.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 GetUserBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$user_id = $params->get("user_id");
|
||||||
|
|
||||||
|
$user = getUser($user_id);
|
||||||
|
|
||||||
|
$json = new JsonBuilder();
|
||||||
|
$json->addUsers([$user]);
|
||||||
|
|
||||||
|
respondJson($json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new GetUserBranch();
|
||||||
|
$branch->execute();
|
29
public/api/removeRecord.php
Normal file
29
public/api/removeRecord.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?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 RemoveRecordBranch 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRecord($user_id, $params);
|
||||||
|
|
||||||
|
respondStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new RemoveRecordBranch();
|
||||||
|
$branch->execute();
|
26
public/api/removeTagFromRecord.php
Normal file
26
public/api/removeTagFromRecord.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?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 RemoveTagFromRecordBranch extends ApiBranch
|
||||||
|
{
|
||||||
|
function get(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(ParamCleaner $params)
|
||||||
|
{
|
||||||
|
$record_id = $params->get("record_id");
|
||||||
|
$tag_id = $params->get("tag_id");
|
||||||
|
|
||||||
|
removeTagFromRecord($tag_id, $record_id);
|
||||||
|
|
||||||
|
respondStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = new RemoveTagFromRecordBranch();
|
||||||
|
$branch->execute();
|
45
public/api/services/apiBranch.inc.php
Normal file
45
public/api/services/apiBranch.inc.php
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?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();
|
||||||
|
|
||||||
|
$currentType = currentRequestType();
|
||||||
|
if ($currentType === RequestType::OPTIONS) {
|
||||||
|
respondStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authenticationRequired) {
|
||||||
|
$auth = new Authenticator();
|
||||||
|
if (!$auth->isAuthenticated($params)) {
|
||||||
|
$this->authenticationMissing($params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentType === RequestType::GET) {
|
||||||
|
$this->get($params);
|
||||||
|
} else if ($currentType === RequestType::POST) {
|
||||||
|
$this->post($params);
|
||||||
|
} else {
|
||||||
|
respondStatus(405);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getParams() {
|
||||||
|
$content = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if ($content == NULL) $content = array();
|
||||||
|
return new ParamCleaner(array_merge($content, $_REQUEST, $_SESSION, $_FILES));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
20
public/api/services/authenticator.inc.php
Normal file
20
public/api/services/authenticator.inc.php
Normal file
|
@ -0,0 +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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
33
public/api/services/basicEnum.inc.php
Normal file
33
public/api/services/basicEnum.inc.php
Normal file
|
@ -0,0 +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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
292
public/api/services/dbOperations.inc.php
Normal file
292
public/api/services/dbOperations.inc.php
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
<?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(), bool $addTableName = true)
|
||||||
|
{
|
||||||
|
$this->table = $this->tablePrefix . $table;
|
||||||
|
if (count($attributes) == 0)
|
||||||
|
$formattedAttributes = "*";
|
||||||
|
else {
|
||||||
|
for ($i = 0; $i < count($attributes); $i++) {
|
||||||
|
$a = $attributes[$i];
|
||||||
|
// Add table name prefix if missing
|
||||||
|
if ($addTableName && 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, strlen($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";
|
||||||
|
}
|
105
public/api/services/jsonBuilder.inc.php
Normal file
105
public/api/services/jsonBuilder.inc.php
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<?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)
|
||||||
|
{
|
||||||
|
if ($records === null) return;
|
||||||
|
|
||||||
|
$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)
|
||||||
|
{
|
||||||
|
if ($projects === null) return;
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUsers(array $users)
|
||||||
|
{
|
||||||
|
if ($users === null) return;
|
||||||
|
|
||||||
|
$columns = array(
|
||||||
|
"user_id" => "",
|
||||||
|
"name" => "",
|
||||||
|
"mail_address" => ""
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->jsonData['users'] = array();
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$this->jsonData['users'][] = $this->createJsonArray($user, $columns);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRecordTags(array $record_tags)
|
||||||
|
{
|
||||||
|
if ($record_tags === null) return;
|
||||||
|
|
||||||
|
$columns = array(
|
||||||
|
"record_tag_id" => "",
|
||||||
|
"name" => "",
|
||||||
|
"user_id" => ""
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->jsonData['record_tags'] = array();
|
||||||
|
foreach ($record_tags as $tag) {
|
||||||
|
$this->jsonData['record_tags'][] = $this->createJsonArray($tag, $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;
|
||||||
|
}
|
||||||
|
}
|
358
public/api/services/jugglDbApi.inc.php
Normal file
358
public/api/services/jugglDbApi.inc.php
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
<?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 addProject($user_id, $params)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
"user_id" => $user_id,
|
||||||
|
"name" => $params->get("name"),
|
||||||
|
"start_date" => $params->get("start_date")
|
||||||
|
];
|
||||||
|
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->insert("projects", $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];
|
||||||
|
|
||||||
|
$result = getRecordExternalData($result);
|
||||||
|
|
||||||
|
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 getUser($user_id)
|
||||||
|
{
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->select("users", ["user_id", "name", "mail_address"]);
|
||||||
|
$db->where("user_id", Comparison::EQUAL, $user_id);
|
||||||
|
$result = $db->execute();
|
||||||
|
|
||||||
|
if (count($result) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$result = $result[0];
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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], false);
|
||||||
|
$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) {
|
||||||
|
if ($finished) {
|
||||||
|
$db->addSql(" AND end_time IS NOT NULL");
|
||||||
|
} else {
|
||||||
|
$db->addSql(" AND end_time IS NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->orderBy("start_time", Order::DESC);
|
||||||
|
$result = $db->execute();
|
||||||
|
|
||||||
|
if (count($result) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$result = $result[0];
|
||||||
|
|
||||||
|
$result = getRecordExternalData($result);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRunningRecords($user_id)
|
||||||
|
{
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->select("time_records");
|
||||||
|
$db->where("user_id", Comparison::EQUAL, $user_id);
|
||||||
|
$db->addSql(" AND end_time IS NULL");
|
||||||
|
$results = $db->execute();
|
||||||
|
|
||||||
|
// Is still running?
|
||||||
|
foreach ($results as $key => $record) {
|
||||||
|
$results[$key] = getRecordExternalData($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecords($user_id, $limit = NULL, $finished = NULL)
|
||||||
|
{
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->select("time_records");
|
||||||
|
$db->where("user_id", Comparison::EQUAL, $user_id);
|
||||||
|
if ($finished != NULL) {
|
||||||
|
if ($finished) {
|
||||||
|
$db->addSql(" AND end_time IS NOT NULL");
|
||||||
|
} else {
|
||||||
|
$db->addSql(" AND end_time IS NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($limit != NULL) {
|
||||||
|
$db->orderBy("record_id", Order::DESC);
|
||||||
|
$db->limit($limit);
|
||||||
|
}
|
||||||
|
$results = $db->execute();
|
||||||
|
|
||||||
|
// Is still running?
|
||||||
|
foreach ($results as $key => $record) {
|
||||||
|
$results[$key] = getRecordExternalData($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEndRecord($user_id, $params)
|
||||||
|
{
|
||||||
|
$record_id = $params->get("record_id");
|
||||||
|
|
||||||
|
// Get start instance to calculate duration
|
||||||
|
$start_time = getTimeRecord($user_id, $record_id)["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 removeRecord($user_id, $params)
|
||||||
|
{
|
||||||
|
$record_id = $params->get("record_id");
|
||||||
|
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->delete("time_records");
|
||||||
|
$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 getRecordExternalData($record)
|
||||||
|
{
|
||||||
|
if ($record == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration and running
|
||||||
|
if ($record["end_time"] == NULL) {
|
||||||
|
$record["duration"] = calcDuration($record["start_time"]);
|
||||||
|
$record["running"] = true;
|
||||||
|
} else {
|
||||||
|
$record["running"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
$tags = array();
|
||||||
|
foreach (getTagsOnRecord($record["record_id"]) as $key => $tag) {
|
||||||
|
$data = [
|
||||||
|
"record_tag_id" => $tag["record_tag_id"],
|
||||||
|
"name" => $tag["name"],
|
||||||
|
"user_id" => $tag["user_id"]
|
||||||
|
];
|
||||||
|
$tags[] = $data;
|
||||||
|
}
|
||||||
|
$record["tags"] = $tags;
|
||||||
|
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)(strtotime($end_time) - strtotime($start_time));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordTags($user_id)
|
||||||
|
{
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->select("record_tags");
|
||||||
|
$db->where("user_id", Comparison::EQUAL, $user_id);
|
||||||
|
return $db->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagsOnRecord($record_id)
|
||||||
|
{
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->select("tags_on_records");
|
||||||
|
$db->innerJoin("record_tags", "record_tag_id");
|
||||||
|
$db->where("record_id", Comparison::EQUAL, $record_id);
|
||||||
|
return $db->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRecordTag($user_id, $tag_name)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
"user_id" => $user_id,
|
||||||
|
"name" => $tag_name
|
||||||
|
];
|
||||||
|
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->insert("record_tags", $data);
|
||||||
|
$db->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTagToRecord($tag_id, $record_id)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
"record_tag_id" => $tag_id,
|
||||||
|
"record_id" => $record_id
|
||||||
|
];
|
||||||
|
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->insert("tags_on_records", $data);
|
||||||
|
$db->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTagFromRecord($tag_id, $record_id)
|
||||||
|
{
|
||||||
|
$db = new DbOperations();
|
||||||
|
$db->delete("tags_on_records");
|
||||||
|
$db->where("record_tag_id", Comparison::EQUAL, $tag_id);
|
||||||
|
$db->where("record_id", Comparison::EQUAL, $record_id);
|
||||||
|
$db->execute();
|
||||||
|
}
|
50
public/api/services/paramCleaner.inc.php
Normal file
50
public/api/services/paramCleaner.inc.php
Normal file
|
@ -0,0 +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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
20
public/api/services/requestTypes.inc.php
Normal file
20
public/api/services/requestTypes.inc.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
require_once(__DIR__.'/basicEnum.inc.php');
|
||||||
|
|
||||||
|
abstract class RequestType extends BasicEnum {
|
||||||
|
const GET = "GET";
|
||||||
|
const POST = "POST";
|
||||||
|
const PUT = "PUT";
|
||||||
|
const DELETE = "DELETE";
|
||||||
|
const OPTIONS = "OPTIONS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentRequestType () {
|
||||||
|
$requestType = $_SERVER['REQUEST_METHOD'];
|
||||||
|
if (RequestType::isValidValue($requestType)) {
|
||||||
|
return $requestType;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
38
public/api/services/responses.inc.php
Normal file
38
public/api/services/responses.inc.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
require_once(__DIR__."/jsonBuilder.inc.php");
|
||||||
|
|
||||||
|
function setDefaultHeader()
|
||||||
|
{
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
|
||||||
|
header("Access-Control-Allow-Headers: X-PINGOTHER, Content-Type");
|
||||||
|
header("Access-Control-Max-Age: 86400");
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondJson(JsonBuilder $builder) {
|
||||||
|
setDefaultHeader();
|
||||||
|
header('Content-type: application/json');
|
||||||
|
echo($builder->getJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondHtml(string $html)
|
||||||
|
{
|
||||||
|
setDefaultHeader();
|
||||||
|
print($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondStatus(int $statusCode, string $message = "")
|
||||||
|
{
|
||||||
|
setDefaultHeader();
|
||||||
|
http_response_code($statusCode);
|
||||||
|
die($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectBack() {
|
||||||
|
header("Location: {$_SERVER['HTTP_REFERER']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectTo($url) {
|
||||||
|
header("Location: {$url}");
|
||||||
|
}
|
||||||
|
?>
|
49
public/api/startRecord.php
Normal file
49
public/api/startRecord.php
Normal file
|
@ -0,0 +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");
|
||||||
|
|
||||||
|
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();
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
@ -12,8 +12,4 @@ export default {
|
||||||
// Import custom SASS variable overrides, or alternatively
|
// Import custom SASS variable overrides, or alternatively
|
||||||
// define your variable overrides here instead
|
// define your variable overrides here instead
|
||||||
@import '@/style/theme.sass'
|
@import '@/style/theme.sass'
|
||||||
|
</style>
|
||||||
// Import Bootstrap and BootstrapVue source SCSS files
|
|
||||||
@import '~bootstrap/scss/bootstrap.scss'
|
|
||||||
@import '~bootstrap-vue/src/index.scss'
|
|
||||||
</style>
|
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<b-container id="grid" fluid>
|
<b-container id="grid" fluid>
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col id="column" :class="[width, {'mx-auto':center}]">
|
<b-col id="column" :class="[width, { 'mx-auto': center }]">
|
||||||
<slot />
|
<slot />
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
31
src/components/base/BaseUserDropdown.vue
Normal file
31
src/components/base/BaseUserDropdown.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<b-dropdown
|
||||||
|
id="dropdown"
|
||||||
|
:text="this.username"
|
||||||
|
variant="outline-primary"
|
||||||
|
right
|
||||||
|
>
|
||||||
|
<b-dropdown-item to="/logout">Log out</b-dropdown-item>
|
||||||
|
</b-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "BaseUserDropdown",
|
||||||
|
computed: {
|
||||||
|
username: () => {
|
||||||
|
var user = store.getters.user;
|
||||||
|
if (user === undefined) return "Loading";
|
||||||
|
else return user.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
#dropdown
|
||||||
|
*
|
||||||
|
line-height: 1.5
|
||||||
|
</style>
|
|
@ -24,7 +24,7 @@
|
||||||
Password or email invalid.
|
Password or email invalid.
|
||||||
</b-form-invalid-feedback>
|
</b-form-invalid-feedback>
|
||||||
<b-button variant="primary" type="submit" block :disabled="working">
|
<b-button variant="primary" type="submit" block :disabled="working">
|
||||||
<b-spinner v-if="working" small/>
|
<b-spinner v-if="working" small />
|
||||||
Log in
|
Log in
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-form>
|
</b-form>
|
||||||
|
@ -55,7 +55,11 @@ export default {
|
||||||
this.working = true;
|
this.working = true;
|
||||||
|
|
||||||
// Try to login
|
// Try to login
|
||||||
store.dispatch("login", { userId: this.form.user_id, apiKey: this.form.api_key})
|
store
|
||||||
|
.dispatch("login", {
|
||||||
|
userId: this.form.user_id,
|
||||||
|
apiKey: this.form.api_key
|
||||||
|
})
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r !== true) {
|
if (r !== true) {
|
||||||
this.failed = true;
|
this.failed = true;
|
||||||
|
@ -80,5 +84,4 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style></style>
|
||||||
</style>
|
|
73
src/components/forms/FormProjectAdd.vue
Normal file
73
src/components/forms/FormProjectAdd.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<form @submit="submitForm">
|
||||||
|
|
||||||
|
<b-input-group id="form">
|
||||||
|
<b-form-input
|
||||||
|
id="name"
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="Project name"
|
||||||
|
trim
|
||||||
|
>
|
||||||
|
</b-form-input>
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="outline-secondary" type="submit" :disabled="working">
|
||||||
|
<b-spinner v-if="working" small />
|
||||||
|
Add project
|
||||||
|
</b-button>
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FormProjectAdd",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
name: ""
|
||||||
|
},
|
||||||
|
working: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Submits the form. Assupmtion: Form is valid, based on required flags.
|
||||||
|
*/
|
||||||
|
submitForm: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.form.name == "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.failed = false;
|
||||||
|
this.working = true;
|
||||||
|
|
||||||
|
// Try to login
|
||||||
|
store
|
||||||
|
.dispatch("addProject", {
|
||||||
|
name: this.form.name,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.form.name = "";
|
||||||
|
this.working = false;
|
||||||
|
}).catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
|
||||||
|
this.working = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
#form
|
||||||
|
max-width: 20rem
|
||||||
|
margin: auto
|
||||||
|
</style>
|
130
src/components/forms/FormRecordDetails.vue
Normal file
130
src/components/forms/FormRecordDetails.vue
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<template>
|
||||||
|
<b-form @submit="submitForm">
|
||||||
|
<b-form-group id="id-group" label-for="id" label="Record ID">
|
||||||
|
<b-form-input id="id" v-model="record.record_id" required trim disabled>
|
||||||
|
</b-form-input>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group id="project-group" label-for="project" label="Project">
|
||||||
|
<b-form-select
|
||||||
|
id="project"
|
||||||
|
v-model="record.project_id"
|
||||||
|
:options="selectableProjects"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</b-form-select>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group id="startdate-group" label="Start date" label-for="startdate">
|
||||||
|
<b-form-datepicker
|
||||||
|
id="startdate"
|
||||||
|
v-model="times.start.date"
|
||||||
|
required
|
||||||
|
placeholder="Choose a start date"
|
||||||
|
:max="times.end.date"
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
</b-form-datepicker>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group id="starttime-group" label="Start time" label-for="starttime">
|
||||||
|
<b-form-timepicker
|
||||||
|
id="starttime"
|
||||||
|
v-model="times.start.time"
|
||||||
|
required
|
||||||
|
placeholder="Choose a start time"
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
</b-form-timepicker>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group id="enddate-group" label="End date" label-for="enddate">
|
||||||
|
<b-form-datepicker
|
||||||
|
id="enddate"
|
||||||
|
v-model="times.end.date"
|
||||||
|
required
|
||||||
|
placeholder="Choose an end date"
|
||||||
|
:min="times.start.date"
|
||||||
|
dark
|
||||||
|
>
|
||||||
|
</b-form-datepicker>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-invalid-feedback :state="!failed">
|
||||||
|
Something went wrong.
|
||||||
|
</b-form-invalid-feedback>
|
||||||
|
<b-button variant="primary" type="submit" class="right" :disabled="working">
|
||||||
|
<b-spinner v-if="working" small />
|
||||||
|
Save
|
||||||
|
</b-button>
|
||||||
|
<b-card class="mt-3" header="Form Data Result">
|
||||||
|
<pre class="m-0">{{ record }}</pre>
|
||||||
|
</b-card>
|
||||||
|
<b-card class="mt-3" header="Form Data Result">
|
||||||
|
<pre class="m-0">{{ times }}</pre>
|
||||||
|
</b-card>
|
||||||
|
</b-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FormRecordDetails",
|
||||||
|
props: {
|
||||||
|
record: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
times: {
|
||||||
|
start: {},
|
||||||
|
end: {}
|
||||||
|
},
|
||||||
|
selectableProjects: [],
|
||||||
|
failed: false,
|
||||||
|
working: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Submits the form. Assupmtion: Form is valid, based on required flags.
|
||||||
|
*/
|
||||||
|
submitForm: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.failed = false;
|
||||||
|
this.working = true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function() {
|
||||||
|
// Load selectable projects
|
||||||
|
var projects = [];
|
||||||
|
Object.values(store.getters.projects).forEach(project => {
|
||||||
|
projects.push({ value: project.project_id, text: project.name });
|
||||||
|
});
|
||||||
|
this.selectableProjects = projects;
|
||||||
|
|
||||||
|
// Load record times
|
||||||
|
let dateAndTime = /([0-9-]*)\s([0-9:]*)/;
|
||||||
|
var startMatch = String(this.record.start_time).match(dateAndTime);
|
||||||
|
this.times.start.date = startMatch[1];
|
||||||
|
this.times.start.time = startMatch[2];
|
||||||
|
|
||||||
|
var endMatch = String(this.record.end_time).match(dateAndTime);
|
||||||
|
if (endMatch !== null) {
|
||||||
|
this.times.end.date = endMatch[1];
|
||||||
|
this.times.end.time = endMatch[2];
|
||||||
|
} else {
|
||||||
|
this.times.end.date = new Date().toISOString();
|
||||||
|
this.times.end.time = new Date().toTimeString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
.left
|
||||||
|
float: left !important
|
||||||
|
|
||||||
|
.right
|
||||||
|
float: right !important
|
||||||
|
</style>
|
68
src/components/juggl/JugglProjectsPanel.vue
Normal file
68
src/components/juggl/JugglProjectsPanel.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div id="project-list">
|
||||||
|
<div
|
||||||
|
v-for="project in projects"
|
||||||
|
:key="project.project_id"
|
||||||
|
@click="() => startProject(project.project_id)"
|
||||||
|
>
|
||||||
|
<h1>{{ project.name }}</h1>
|
||||||
|
<p>{{ getDurationTimestamp(project.duration) }}</p>
|
||||||
|
<p>{{ project.record_count }} records</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from "@/store";
|
||||||
|
import { helperService } from "@/services/helper.service.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "JugglProjectPanel",
|
||||||
|
props: {
|
||||||
|
projects: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getDurationTimestamp: helperService.getDurationTimestamp,
|
||||||
|
startProject: function(id) {
|
||||||
|
store.dispatch("startRecord", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
@import '@/style/theme.sass'
|
||||||
|
|
||||||
|
#project-list
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: center
|
||||||
|
align-content: flex-start
|
||||||
|
padding: 5px
|
||||||
|
|
||||||
|
> *
|
||||||
|
margin: 5px
|
||||||
|
border: 1px dashed $grey
|
||||||
|
border-radius: 5px
|
||||||
|
padding: 10px
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color: $primary !important
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 24pt
|
||||||
|
margin: 0px
|
||||||
|
padding: 0px
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 10pt
|
||||||
|
color: $grey
|
||||||
|
margin: 0px
|
||||||
|
padding: 0px
|
||||||
|
</style>
|
159
src/components/juggl/JugglRecordsList.vue
Normal file
159
src/components/juggl/JugglRecordsList.vue
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<template>
|
||||||
|
<b-table
|
||||||
|
:items="records"
|
||||||
|
hover
|
||||||
|
:busy="isLoading"
|
||||||
|
:fields="fields"
|
||||||
|
sort-by="start"
|
||||||
|
:sort-desc="sortDesc"
|
||||||
|
>
|
||||||
|
<template #table-busy>
|
||||||
|
<div class="text-center">
|
||||||
|
<b-spinner></b-spinner>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Custom data -->
|
||||||
|
<template #cell(project)="data">
|
||||||
|
{{ getProject(data.item.project_id).name }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(start)="data">
|
||||||
|
{{ data.item.start_time }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(duration)="data">
|
||||||
|
{{ getDurationTimestamp(data.item.duration) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(details)="data">
|
||||||
|
<b-button
|
||||||
|
size="sm"
|
||||||
|
@click="() => detailsRecord(data.item.record_id)"
|
||||||
|
variant="outline-dark"
|
||||||
|
>
|
||||||
|
<b-icon class="icon-btn" icon="gear" />
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(abort)="data">
|
||||||
|
<b-button
|
||||||
|
size="sm"
|
||||||
|
@click="() => abortRecord(data.item.record_id)"
|
||||||
|
variant="outline-primary"
|
||||||
|
>
|
||||||
|
<b-icon class="icon-btn" icon="x" />
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(stop)="data">
|
||||||
|
<b-button
|
||||||
|
size="sm"
|
||||||
|
@click="stopRecord(data.item.record_id)"
|
||||||
|
variant="outline-success"
|
||||||
|
>
|
||||||
|
<b-icon class="icon-btn" icon="check" />
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from "@/store";
|
||||||
|
import { helperService } from "@/services/helper.service.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "JugglRecordsList",
|
||||||
|
props: {
|
||||||
|
records: {
|
||||||
|
required: true,
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
sortDesc: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
running: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
iconScale: 1.6,
|
||||||
|
requiredFields: [
|
||||||
|
{
|
||||||
|
key: "project",
|
||||||
|
label: "Project",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "start",
|
||||||
|
label: "Start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "duration",
|
||||||
|
label: "Duration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "details",
|
||||||
|
label: "Details",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
runningFields: [
|
||||||
|
{
|
||||||
|
key: "abort",
|
||||||
|
label: "Abort",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "stop",
|
||||||
|
label: "Stop",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoading: function() {
|
||||||
|
return this.records === undefined;
|
||||||
|
},
|
||||||
|
fields: function() {
|
||||||
|
var fields = this.requiredFields;
|
||||||
|
|
||||||
|
if (this.running) {
|
||||||
|
fields = [...fields, ...this.runningFields];
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getDurationTimestamp: helperService.getDurationTimestamp,
|
||||||
|
getProject: function(id) {
|
||||||
|
var project = store.getters.getProjectById(id);
|
||||||
|
|
||||||
|
if (project === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return project;
|
||||||
|
},
|
||||||
|
stopRecord: function(id) {
|
||||||
|
store.dispatch("endRecord", id);
|
||||||
|
},
|
||||||
|
abortRecord: function(id) {
|
||||||
|
store.dispatch("removeRecord", id);
|
||||||
|
},
|
||||||
|
detailsRecord: function(id) {
|
||||||
|
return id;
|
||||||
|
// this.$router.push("/record/" + id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
.icon-btn:hover
|
||||||
|
color: green
|
||||||
|
cursor: pointer
|
||||||
|
</style>
|
|
@ -2,18 +2,25 @@
|
||||||
<div>
|
<div>
|
||||||
<header>
|
<header>
|
||||||
<BaseContainer width="wide" center>
|
<BaseContainer width="wide" center>
|
||||||
<ul id="header-container">
|
<ul id="header-container">
|
||||||
<li>
|
<li>
|
||||||
<BaseLogo id="logo" size="normal"/>
|
<BaseLogo id="logo" size="normal" />
|
||||||
</li>
|
</li>
|
||||||
<li class="right">
|
<li class="right">
|
||||||
<BaseUserDropdown/>
|
<BaseUserDropdown />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</BaseContainer>
|
</BaseContainer>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<BaseContainer width="medium" center>
|
<BaseContainer
|
||||||
|
width="medium"
|
||||||
|
center
|
||||||
|
:class="{ 'center-content': center }"
|
||||||
|
>
|
||||||
|
<BaseTitle v-if="title" center size="huge" class="centered">
|
||||||
|
{{ title }}
|
||||||
|
</BaseTitle>
|
||||||
<slot />
|
<slot />
|
||||||
</BaseContainer>
|
</BaseContainer>
|
||||||
</main>
|
</main>
|
||||||
|
@ -24,13 +31,26 @@
|
||||||
import BaseContainer from "@/components/base/BaseContainer.vue";
|
import BaseContainer from "@/components/base/BaseContainer.vue";
|
||||||
import BaseLogo from "@/components/base/BaseLogo.vue";
|
import BaseLogo from "@/components/base/BaseLogo.vue";
|
||||||
import BaseUserDropdown from "@/components/base/BaseUserDropdown.vue";
|
import BaseUserDropdown from "@/components/base/BaseUserDropdown.vue";
|
||||||
|
import BaseTitle from "@/components/base/BaseTitle";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LayoutNavbarPrivate",
|
name: "LayoutNavbarPrivate",
|
||||||
components: {
|
components: {
|
||||||
BaseContainer,
|
BaseContainer,
|
||||||
BaseLogo,
|
BaseLogo,
|
||||||
BaseUserDropdown
|
BaseUserDropdown,
|
||||||
|
BaseTitle
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
default: "",
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
default: false,
|
||||||
|
required: false,
|
||||||
|
type: Boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -45,6 +65,7 @@ header
|
||||||
box-shadow: 0px 0px 2rem 1rem #000D
|
box-shadow: 0px 0px 2rem 1rem #000D
|
||||||
height: $navbar-height
|
height: $navbar-height
|
||||||
background-color: $background-primary
|
background-color: $background-primary
|
||||||
|
z-index: 100
|
||||||
|
|
||||||
#header-container
|
#header-container
|
||||||
padding: 0px 20px
|
padding: 0px 20px
|
||||||
|
@ -63,5 +84,8 @@ header
|
||||||
main
|
main
|
||||||
padding: 0px 20px
|
padding: 0px 20px
|
||||||
margin-top: 6rem
|
margin-top: 6rem
|
||||||
|
margin-bottom: 6rem
|
||||||
|
|
||||||
|
.center-content
|
||||||
|
text-align: center
|
||||||
</style>
|
</style>
|
|
@ -11,7 +11,7 @@ Vue.use(BootstrapVue);
|
||||||
Vue.use(BootstrapVueIcons);
|
Vue.use(BootstrapVueIcons);
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
render: (h) => h(App),
|
render: h => h(App)
|
||||||
}).$mount("#app");
|
}).$mount("#app");
|
62
src/router/index.js
Normal file
62
src/router/index.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import Vue from "vue";
|
||||||
|
import VueRouter from "vue-router";
|
||||||
|
import store from "../store";
|
||||||
|
import Login from "../views/Login.vue";
|
||||||
|
import Home from "../views/Home.vue";
|
||||||
|
import RecordDetails from "../views/RecordDetails.vue";
|
||||||
|
|
||||||
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
redirect: "/home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "Login",
|
||||||
|
component: Login
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/home",
|
||||||
|
name: "Home",
|
||||||
|
component: Home,
|
||||||
|
beforeEnter: requireAuth
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/record/:id",
|
||||||
|
name: "Record Details",
|
||||||
|
component: RecordDetails,
|
||||||
|
beforeEnter: requireAuth
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/logout",
|
||||||
|
name: "Logout",
|
||||||
|
beforeEnter: (to, from, next) => {
|
||||||
|
store.dispatch("logout");
|
||||||
|
next("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
mode: "history",
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks authentication before proceeding
|
||||||
|
*/
|
||||||
|
function requireAuth(to, from, next) {
|
||||||
|
if (!store.getters.isLoggedIn) {
|
||||||
|
next({
|
||||||
|
path: "/login",
|
||||||
|
query: { redirect: to.fullPath }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
49
src/services/api.service.js
Normal file
49
src/services/api.service.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the used fetch API, currently axios.
|
||||||
|
* Uses some default values from the config (e.g. ApiUrl).
|
||||||
|
*
|
||||||
|
* Authentication already integrated.
|
||||||
|
*
|
||||||
|
* Returns promises.
|
||||||
|
*/
|
||||||
|
export const apiService = {
|
||||||
|
get(resource, options) {
|
||||||
|
return this.getApi().get(resource, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
post(resource, json, options) {
|
||||||
|
return this.getApi().post(
|
||||||
|
resource,
|
||||||
|
{ ...this.getDefaultJson(), ...json },
|
||||||
|
options
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
put(resource, json, options) {
|
||||||
|
return this.getApi().put(
|
||||||
|
resource,
|
||||||
|
{ ...this.getDefaultJson(), ...json },
|
||||||
|
options
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of the used api and sets necessary headers
|
||||||
|
*/
|
||||||
|
getApi() {
|
||||||
|
var options = {
|
||||||
|
baseURL: store.getters.apiUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
return axios.create(options);
|
||||||
|
},
|
||||||
|
getDefaultJson() {
|
||||||
|
return {
|
||||||
|
user_id: store.getters.auth.userId,
|
||||||
|
api_key: store.getters.auth.apiKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
140
src/services/helper.service.js
Normal file
140
src/services/helper.service.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/**
|
||||||
|
* Contains a bunch of helper functions.
|
||||||
|
*/
|
||||||
|
export const helperService = {
|
||||||
|
/**
|
||||||
|
* Converts number into a human readable percent integer.
|
||||||
|
*
|
||||||
|
* @param {*} r Floating point percent number
|
||||||
|
*/
|
||||||
|
asPercent(r) {
|
||||||
|
return Math.floor(r * 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts number into a human readable floating point number with a digit behind the delimiter.
|
||||||
|
*
|
||||||
|
* @param {*} r Floating point number
|
||||||
|
*/
|
||||||
|
asFloat(r) {
|
||||||
|
return Math.floor(r * 10) / 10;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts timestamp into a human readable date format.
|
||||||
|
*
|
||||||
|
* @param {*} d ISO Timestamp
|
||||||
|
*/
|
||||||
|
asDateFromISO(d) {
|
||||||
|
return helperService.asDateFromObj(new Date(d));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts timestamp into a human readable date format.
|
||||||
|
*
|
||||||
|
* @param {*} d Date object
|
||||||
|
*/
|
||||||
|
asDateFromObj(d) {
|
||||||
|
return new Intl.DateTimeFormat("de").format(d);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure, that the given text is not too long.
|
||||||
|
*
|
||||||
|
* @param {*} text Some text that might be shortened
|
||||||
|
* @param {*} maxLength Max number of characters
|
||||||
|
*
|
||||||
|
* @returns The shortened or same text
|
||||||
|
*/
|
||||||
|
keepItShort(text, maxLength = 20, ellipsis = "...") {
|
||||||
|
if (text.length > maxLength) {
|
||||||
|
// Adding ellipsis
|
||||||
|
var short = text.substring(0, maxLength - ellipsis.length).trim();
|
||||||
|
return short + ellipsis;
|
||||||
|
} else return text;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a date object into a date-time-timezone string, and sets times and timezone to zero.
|
||||||
|
*
|
||||||
|
* @source https://stackoverflow.com/a/43528844/7376120
|
||||||
|
*
|
||||||
|
* @param {*} date A date object
|
||||||
|
*
|
||||||
|
* @returns Date as string in the used format
|
||||||
|
*/
|
||||||
|
toISODate(date) {
|
||||||
|
var timezoneOffset = date.getMinutes() + date.getTimezoneOffset();
|
||||||
|
var timestamp = date.getTime() + timezoneOffset * 1000;
|
||||||
|
var correctDate = new Date(timestamp);
|
||||||
|
|
||||||
|
correctDate.setUTCHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return correctDate.toISOString();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds current duration to a record.
|
||||||
|
* Copied from original juggl code.
|
||||||
|
* @param {*} record The record instance to update.
|
||||||
|
*/
|
||||||
|
addDuration(record) {
|
||||||
|
if (record.end_time != null) return record;
|
||||||
|
|
||||||
|
record.duration =
|
||||||
|
(new Date().getTime() - new Date(record.start_time).getTime()) / 1000;
|
||||||
|
|
||||||
|
return record;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a datetime object into the necessary string format for server requests.
|
||||||
|
* Copied from original juggl code.
|
||||||
|
* @param {*} date
|
||||||
|
*/
|
||||||
|
dateToString(date) {
|
||||||
|
return (
|
||||||
|
date.getFullYear() +
|
||||||
|
"-" +
|
||||||
|
(date.getMonth() + 1) +
|
||||||
|
"-" +
|
||||||
|
date.getDate() +
|
||||||
|
" " +
|
||||||
|
date.getHours() +
|
||||||
|
":" +
|
||||||
|
date.getMinutes() +
|
||||||
|
":" +
|
||||||
|
date.getSeconds()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getDurationTimestamp(totalSeconds) {
|
||||||
|
totalSeconds = Math.ceil(totalSeconds);
|
||||||
|
var days = Math.floor(totalSeconds / 86400);
|
||||||
|
var hours = Math.floor((totalSeconds - days * 86400) / 3600);
|
||||||
|
var minutes = Math.floor((totalSeconds - days * 86400 - hours * 3600) / 60);
|
||||||
|
var seconds = totalSeconds - days * 86400 - hours * 3600 - minutes * 60;
|
||||||
|
|
||||||
|
if (days < 10) {
|
||||||
|
days = "0" + days;
|
||||||
|
}
|
||||||
|
if (hours < 10) {
|
||||||
|
hours = "0" + hours;
|
||||||
|
}
|
||||||
|
if (minutes < 10) {
|
||||||
|
minutes = "0" + minutes;
|
||||||
|
}
|
||||||
|
if (seconds < 10) {
|
||||||
|
seconds = "0" + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestamp = minutes + ":" + seconds;
|
||||||
|
if (totalSeconds >= 3600) {
|
||||||
|
timestamp = hours + ":" + timestamp;
|
||||||
|
}
|
||||||
|
if (totalSeconds >= 86400) {
|
||||||
|
timestamp = days + ":" + timestamp;
|
||||||
|
}
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
};
|
140
src/services/juggl.service.js
Normal file
140
src/services/juggl.service.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import { apiService } from "@/services/api.service";
|
||||||
|
import { helperService } from "@/services/helper.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of functions to retreive and send all user-specific data.
|
||||||
|
*/
|
||||||
|
export const jugglService = {
|
||||||
|
/**
|
||||||
|
* Fetches the user from the API.
|
||||||
|
*
|
||||||
|
* @returns A promise
|
||||||
|
*/
|
||||||
|
getUser() {
|
||||||
|
return apiService.post("/getUser.php").then(r => {
|
||||||
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getProjects() {
|
||||||
|
return apiService.post("/getProjects.php").then(r => {
|
||||||
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecord(recordId) {
|
||||||
|
return apiService
|
||||||
|
.post("/getRecord.php", {
|
||||||
|
record_id: recordId
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
return {
|
||||||
|
data: processRecords(r.data),
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeRecord(recordId) {
|
||||||
|
return apiService
|
||||||
|
.post("/removeRecord.php", {
|
||||||
|
record_id: recordId
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRecords({ limit = undefined, finished = undefined}) {
|
||||||
|
var payload = {};
|
||||||
|
|
||||||
|
if (limit !== undefined && limit > 0) {
|
||||||
|
payload.limit = limit;
|
||||||
|
}
|
||||||
|
if (finished !== undefined) {
|
||||||
|
payload.finished = finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiService.post("/getRecords.php", payload).then(r => {
|
||||||
|
return {
|
||||||
|
data: processRecords(r.data),
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRunningRecords() {
|
||||||
|
return apiService.post("/getRunningRecords.php").then(r => {
|
||||||
|
return {
|
||||||
|
data: processRecords(r.data),
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addProject(name, startDate = null) {
|
||||||
|
if (startDate == null) startDate = new Date().toISOString();
|
||||||
|
return apiService
|
||||||
|
.post("/addProject.php", {
|
||||||
|
name: name,
|
||||||
|
start_date: startDate
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
startRecord(projectId, startTime = null) {
|
||||||
|
if (startTime == null) startTime = new Date();
|
||||||
|
return apiService
|
||||||
|
.post("/startRecord.php", {
|
||||||
|
project_id: projectId,
|
||||||
|
start_time: helperService.dateToString(startTime)
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
endRecord(recordId, endTime = null) {
|
||||||
|
if (endTime == null) endTime = new Date();
|
||||||
|
return apiService
|
||||||
|
.post("/endRecord.php", {
|
||||||
|
record_id: recordId,
|
||||||
|
end_time: helperService.dateToString(endTime)
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function processRecords(data) {
|
||||||
|
Object.values(data.records).forEach(rec => {
|
||||||
|
rec.running = rec.end_time === null;
|
||||||
|
|
||||||
|
if (rec.running) {
|
||||||
|
rec.duration =
|
||||||
|
(new Date().getTime() - new Date(rec.start_time).getTime()) / 1000;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
141
src/services/path.service.js
Normal file
141
src/services/path.service.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import router from "@/router";
|
||||||
|
import { userService } from "@/services/user.service";
|
||||||
|
|
||||||
|
// TODO: Do I need this file?
|
||||||
|
/**
|
||||||
|
* Contains a bunch of path helper functions.
|
||||||
|
*/
|
||||||
|
export const pathService = {
|
||||||
|
/**
|
||||||
|
* Redirects to the not found page.
|
||||||
|
*
|
||||||
|
* @param {*} reason Optional: A reason to explain to the user, why they got redirected
|
||||||
|
* @param {*} sourceRoute Optional: Adds additional information from the source route to the query
|
||||||
|
*/
|
||||||
|
notFound(reason, sourceRoute) {
|
||||||
|
var payload = {};
|
||||||
|
|
||||||
|
if (reason !== undefined) {
|
||||||
|
payload.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceRoute !== undefined) {
|
||||||
|
payload.source = sourceRoute.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
path: "/404",
|
||||||
|
query: payload
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and gets all data to a path of the form /dashboard/:sem:-sem
|
||||||
|
*
|
||||||
|
* @param {*} route Object that describes the current path and its parameters
|
||||||
|
*
|
||||||
|
* @returns A promise of the form ({semester: semester})
|
||||||
|
*/
|
||||||
|
validateSemPath(route) {
|
||||||
|
var semesterNr = this.getSemesterNrFromParam(route.params.semesterName);
|
||||||
|
|
||||||
|
// Invalid path?
|
||||||
|
if (semesterNr === undefined) {
|
||||||
|
this.notFound("Invalid URL", route);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather more data
|
||||||
|
return userService
|
||||||
|
.getSemester(semesterNr)
|
||||||
|
.then(semester => {
|
||||||
|
return { semester: semester.data };
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Semester could not be found
|
||||||
|
this.notFound(semesterNr + ". semester not found", route);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and gets all data to a path of the form /dashboard/:sem:-sem/:module:
|
||||||
|
*
|
||||||
|
* @param {*} route Object that describes the current path and its parameters
|
||||||
|
*
|
||||||
|
* @returns A promise of the form ({semester: semester, module: module})
|
||||||
|
*/
|
||||||
|
validateSemModulePath(route) {
|
||||||
|
// First evaluate semester and than the module
|
||||||
|
return this.validateSemPath(route).then(obj => {
|
||||||
|
var moduleName = route.params.moduleName;
|
||||||
|
|
||||||
|
// Gather more data
|
||||||
|
return userService
|
||||||
|
.getModule(obj.semester.nr, moduleName)
|
||||||
|
.then(module => {
|
||||||
|
return { semester: obj.semester, module: module.data };
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Module could not be found
|
||||||
|
this.notFound('"' + moduleName + '" module not found', route);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the semester nr based on the url parameter in the format ":semesterNr:-sem".
|
||||||
|
*
|
||||||
|
* @param {*} semesterParam In the format ":semesterNr:-sem"
|
||||||
|
*
|
||||||
|
* @returns Semester Nr. if valid, otherwise undefined
|
||||||
|
*/
|
||||||
|
getSemesterNrFromParam(semesterParam) {
|
||||||
|
let re = new RegExp("^([1-9][0-9]*)-sem$");
|
||||||
|
var res = semesterParam.match(re);
|
||||||
|
|
||||||
|
// Found a match?
|
||||||
|
if (res === null) {
|
||||||
|
return undefined; // No
|
||||||
|
} else {
|
||||||
|
// First group is semester nr
|
||||||
|
return res[1];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getBreadcrumbs(route) {
|
||||||
|
let sites = route.path.split("/");
|
||||||
|
let siteCount = sites.length;
|
||||||
|
|
||||||
|
return sites.reduce((crumbs, current, index) => {
|
||||||
|
// Ignore empty parts
|
||||||
|
if (current.length <= 0) {
|
||||||
|
return crumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = current;
|
||||||
|
// Handle semester name
|
||||||
|
var semNr = this.getSemesterNrFromParam(current);
|
||||||
|
if (semNr !== undefined) {
|
||||||
|
name = semNr + ". Semester";
|
||||||
|
}
|
||||||
|
// Handle decoding and capitalization
|
||||||
|
name = decodeURIComponent(name);
|
||||||
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
|
|
||||||
|
let part = {
|
||||||
|
text: name
|
||||||
|
};
|
||||||
|
|
||||||
|
let isActive = index + 1 == siteCount;
|
||||||
|
if (isActive) {
|
||||||
|
part.active = true;
|
||||||
|
} else {
|
||||||
|
part.to = crumbs[crumbs.length - 1]
|
||||||
|
? crumbs[crumbs.length - 1].to + "/" + current
|
||||||
|
: "/" + current;
|
||||||
|
}
|
||||||
|
|
||||||
|
crumbs.push(part);
|
||||||
|
return crumbs;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
};
|
17
src/store/index.js
Normal file
17
src/store/index.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import Vue from "vue";
|
||||||
|
import Vuex from "vuex";
|
||||||
|
import { juggl } from "./modules/juggl";
|
||||||
|
import createPersistedState from "vuex-persistedstate";
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
juggl
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
createPersistedState({
|
||||||
|
storage: window.sessionStorage
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
|
@ -1,16 +1,16 @@
|
||||||
import Vue from "vue";
|
|
||||||
import Vuex from "vuex";
|
|
||||||
import { jugglService } from "@/services/juggl.service.js";
|
import { jugglService } from "@/services/juggl.service.js";
|
||||||
|
|
||||||
Vue.use(Vuex);
|
export const juggl = {
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
state: {
|
state: {
|
||||||
apiUrl: "https://juggl.giller.dev/api",
|
apiUrl: "https://juggl.giller.dev/api",
|
||||||
projects: {},
|
projects: {},
|
||||||
records: {},
|
records: {},
|
||||||
user: undefined,
|
user: undefined,
|
||||||
auth: undefined,
|
auth: undefined,
|
||||||
|
usingFinishedRecords: false,
|
||||||
|
usingRunningRecords: false,
|
||||||
|
usingProjects: false,
|
||||||
|
recordsLimit: 0,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setKey(state, key) {
|
setKey(state, key) {
|
||||||
|
@ -22,12 +22,23 @@ export default new Vuex.Store({
|
||||||
setRecords(state, records) {
|
setRecords(state, records) {
|
||||||
state.records = records;
|
state.records = records;
|
||||||
},
|
},
|
||||||
|
usingFinishedRecords(state, using) {
|
||||||
|
state.usingFinishedRecords = using;
|
||||||
|
},
|
||||||
|
usingRunningRecords(state, using) {
|
||||||
|
state.usingRunningRecords = using;
|
||||||
|
},
|
||||||
|
usingProjects(state, using) {
|
||||||
|
state.usingProjects = using;
|
||||||
|
},
|
||||||
|
setRecordsLimit(state, limit) {
|
||||||
|
state.recordsLimit = limit;
|
||||||
|
},
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
state.user = user;
|
state.user = user;
|
||||||
},
|
},
|
||||||
logout(state) {
|
logout(state) {
|
||||||
state.auth = undefined;
|
state.auth = undefined;
|
||||||
// TODO: Doesn't work apparently
|
|
||||||
localStorage.removeItem("apiKey");
|
localStorage.removeItem("apiKey");
|
||||||
localStorage.removeItem("userId");
|
localStorage.removeItem("userId");
|
||||||
},
|
},
|
||||||
|
@ -39,7 +50,9 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
runningRecords: (state) => {
|
runningRecords: (state) => {
|
||||||
return Object.values(state.records).filter((record) => record.running);
|
return Object.values(state.records).filter(
|
||||||
|
(record) => record.running
|
||||||
|
);
|
||||||
},
|
},
|
||||||
finishedRecords: (state) => {
|
finishedRecords: (state) => {
|
||||||
return Object.values(state.records).filter(
|
return Object.values(state.records).filter(
|
||||||
|
@ -93,27 +106,45 @@ export default new Vuex.Store({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getRecordById: (state, getters) => (id) => {
|
getRecordById: (state, getters) => (id) => {
|
||||||
return Object.values(getters.records).find((record) => record.record_id === id);
|
return Object.values(getters.records).find(
|
||||||
|
(record) => record.record_id === id
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
loadProjects({ commit }) {
|
loadProjects({ commit }) {
|
||||||
jugglService.getProjects().then((r) => {
|
return jugglService.getProjects().then((r) => {
|
||||||
commit("setProjects", r.data.projects);
|
commit("setProjects", r.data.projects);
|
||||||
|
commit("usingProjects", true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadAllRecords({ commit }) {
|
loadUser({ commit }) {
|
||||||
jugglService.getRecords().then((r) => {
|
return jugglService
|
||||||
|
.getUser()
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
commit("setUser", r.data.users[0]);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadRecords({ commit, state }, { limit, finished }) {
|
||||||
|
commit("setRecordsLimit", limit);
|
||||||
|
return jugglService.getRecords({ limit: state.recordsLimit, finished: finished }).then((r) => {
|
||||||
commit("setRecords", r.data.records);
|
commit("setRecords", r.data.records);
|
||||||
|
commit("usingFinishedRecords", true);
|
||||||
|
commit("usingRunningRecords", true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadRunningRecords({ commit, getters }) {
|
loadRunningRecords({ commit, getters }) {
|
||||||
jugglService.getRunningRecords().then((r) => {
|
return jugglService.getRunningRecords().then((r) => {
|
||||||
var allRecords = {
|
var allRecords = {
|
||||||
...getters.finishedRecords,
|
...getters.finishedRecords,
|
||||||
...r.data.records,
|
...r.data.records,
|
||||||
};
|
};
|
||||||
commit("setRecords", allRecords);
|
commit("setRecords", allRecords);
|
||||||
|
commit("usingRunningRecords", true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
login({ commit, getters }, { userId, apiKey }) {
|
login({ commit, getters }, { userId, apiKey }) {
|
||||||
|
@ -124,15 +155,18 @@ export default new Vuex.Store({
|
||||||
|
|
||||||
commit("login", { apiKey: apiKey, userId: userId });
|
commit("login", { apiKey: apiKey, userId: userId });
|
||||||
|
|
||||||
return jugglService
|
return this.dispatch("loadUser")
|
||||||
.getUser()
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.dispatch("logout");
|
this.dispatch("logout");
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
commit("setUser", r.data.users[0]);
|
if (r === false) {
|
||||||
return true;
|
this.dispatch("logout");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logout({ commit }) {
|
logout({ commit }) {
|
||||||
|
@ -146,8 +180,18 @@ export default new Vuex.Store({
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.dispatch("loadRunningRecords");
|
this.dispatch("updateState");
|
||||||
this.dispatch("loadProjects");
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addProject(context, { name }) {
|
||||||
|
return jugglService
|
||||||
|
.addProject(name)
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.dispatch("updateState");
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -158,9 +202,43 @@ export default new Vuex.Store({
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.dispatch("loadRunningRecords");
|
this.dispatch("updateState");
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
removeRecord(context, recordId) {
|
||||||
|
return jugglService
|
||||||
|
.removeRecord(recordId)
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.dispatch("updateState");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateState({ state }) {
|
||||||
|
if (state.usingProjects) {
|
||||||
|
this.dispatch("loadProjects");
|
||||||
|
}
|
||||||
|
if (state.usingRunningRecords && state.usingFinishedRecords) {
|
||||||
|
this.dispatch("loadRecords");
|
||||||
|
} else if (state.usingRunningRecords) {
|
||||||
|
this.dispatch("loadRunningRecords");
|
||||||
|
}
|
||||||
|
if (state.user === undefined) {
|
||||||
|
this.dispatch("loadUser");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadSavedLogin({ commit }) {
|
||||||
|
var userId = localStorage.getItem("userId");
|
||||||
|
var apiKey = localStorage.getItem("apiKey");
|
||||||
|
|
||||||
|
if (userId === undefined || apiKey === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commit("login", { apiKey: apiKey, userId: userId });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
|
@ -32,7 +32,7 @@ $navbar-height: 4rem
|
||||||
|
|
||||||
body
|
body
|
||||||
background: $background-primary !important
|
background: $background-primary !important
|
||||||
margin: 0;
|
margin: 0
|
||||||
// background: $background-primary !important
|
// background: $background-primary !important
|
||||||
// background: -moz-linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) !important
|
// background: -moz-linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) !important
|
||||||
// background: -webkit-linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) !important
|
// background: -webkit-linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) !important
|
||||||
|
@ -45,8 +45,8 @@ body
|
||||||
.form-group
|
.form-group
|
||||||
label
|
label
|
||||||
color: $font-secondary
|
color: $font-secondary
|
||||||
margin-bottom: -0.3em
|
margin-bottom: -0.1em
|
||||||
margin-left: 0.3em
|
margin-left: 0.1em
|
||||||
input
|
input
|
||||||
background-color: $background-primary !important
|
background-color: $background-primary !important
|
||||||
color: $font-primary !important
|
color: $font-primary !important
|
||||||
|
@ -58,54 +58,72 @@ a
|
||||||
border-radius: 0
|
border-radius: 0
|
||||||
background: #0000
|
background: #0000
|
||||||
|
|
||||||
ul.dropdown-menu
|
.dropdown-menu
|
||||||
background-color: $background-primary
|
background-color: $background-primary !important
|
||||||
border: 1px solid $primary
|
border: 1px solid $primary !important
|
||||||
color: $primary
|
color: $primary
|
||||||
|
|
||||||
|
.b-time *,
|
||||||
|
background-color: $background-primary !important
|
||||||
|
|
||||||
|
#dropdown .dropdown-menu
|
||||||
*:hover
|
*:hover
|
||||||
color: $white !important
|
color: $white !important
|
||||||
background: darken($primary, 20)
|
background: darken($primary, 20) !important
|
||||||
|
|
||||||
*:active
|
*:active
|
||||||
color: $white !important
|
color: $white !important
|
||||||
background: $primary
|
background: $primary !important
|
||||||
|
|
||||||
// .custom-checkbox
|
.custom-checkbox
|
||||||
// label
|
label
|
||||||
// color: $font-primary !important
|
color: $font-primary !important
|
||||||
|
|
||||||
// a.btn-secondary, a.btn-primary
|
a.btn-secondary, a.btn-primary
|
||||||
// color: $font-primary !important
|
color: $font-primary !important
|
||||||
|
|
||||||
// #title
|
#title
|
||||||
// font-weight: bold
|
font-weight: bold
|
||||||
|
|
||||||
// .breadcrumb
|
.breadcrumb
|
||||||
// background-color: #FFF0 !important
|
background-color: #FFF0 !important
|
||||||
|
|
||||||
// .breadcrumb-item + .breadcrumb-item::before
|
.breadcrumb-item + .breadcrumb-item::before
|
||||||
// content: ">" !important
|
content: ">" !important
|
||||||
|
|
||||||
// .breadcrumb-item.active
|
.breadcrumb-item.active
|
||||||
// span
|
span
|
||||||
// color: $font-secondary !important
|
color: $font-secondary !important
|
||||||
|
|
||||||
// .modal-content
|
// .modal-content
|
||||||
// background-color: $background-primary
|
// background-color: $background-primary
|
||||||
|
|
||||||
// header.b-calendar-grid-caption, .b-calendar-grid-weekdays *, header.b-calendar-header *
|
|
||||||
// color: $font-inverted !important
|
|
||||||
|
|
||||||
// .b-form-datepicker
|
|
||||||
// background-color: $background-primary !important
|
|
||||||
|
|
||||||
// .b-form-datepicker label:not(.text-muted)
|
|
||||||
// color: $font-primary !important
|
|
||||||
|
|
||||||
// .modal-header button.close
|
// .modal-header button.close
|
||||||
// color: $font-primary !important
|
// color: $font-primary !important
|
||||||
|
|
||||||
// input:disabled
|
header.b-calendar-grid-caption, .b-calendar-grid-weekdays *, header.b-calendar-header *
|
||||||
// color: $font-secondary !important
|
color: $font-inverted !important
|
||||||
// border-color: $grey !important
|
|
||||||
|
.b-form-datepicker, .b-form-timepicker
|
||||||
|
background-color: $background-primary !important
|
||||||
|
|
||||||
|
.b-form-datepicker label:not(.text-muted), .b-form-timepicker label:not(.text-muted)
|
||||||
|
color: $font-primary !important
|
||||||
|
|
||||||
|
input:disabled
|
||||||
|
color: $font-secondary !important
|
||||||
|
border-color: $grey !important
|
||||||
|
|
||||||
|
input
|
||||||
|
color: $font-primary !important
|
||||||
|
border-color: $grey !important
|
||||||
|
background-color: $background-primary !important
|
||||||
|
|
||||||
|
select
|
||||||
|
background-color: $background-primary !important
|
||||||
|
color: $font-primary !important
|
||||||
|
|
||||||
|
|
||||||
|
// Import Bootstrap and BootstrapVue source SCSS files
|
||||||
|
@import '~bootstrap/scss/bootstrap.scss'
|
||||||
|
@import '~bootstrap-vue/src/index.scss'
|
|
@ -1,13 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutNavbarPrivate>
|
<LayoutNavbarPrivate>
|
||||||
<div v-if="runningRecords.length > 0">
|
<section v-if="runningRecords.length > 0">
|
||||||
<h2 class="center">Tracking</h2>
|
<h2 class="center">Tracking</h2>
|
||||||
<JugglRecordsList :records="runningRecords"/>
|
<JugglRecordsList :records="runningRecords" running />
|
||||||
</div>
|
</section>
|
||||||
<div v-if="finishedProjects.length > 0">
|
<section>
|
||||||
<h2 class="center">Projects</h2>
|
<h2 class="center">Projects</h2>
|
||||||
<JugglProjectsPanel :projects="finishedProjects"/>
|
<div v-if="finishedProjects.length > 0">
|
||||||
</div>
|
<JugglProjectsPanel :projects="finishedProjects" />
|
||||||
|
</div>
|
||||||
|
<div id="add-project-form">
|
||||||
|
<FormProjectAdd/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section v-if="finishedRecords.length > 0">
|
||||||
|
<h2 class="center">Finished</h2>
|
||||||
|
<JugglRecordsList :records="finishedRecords" />
|
||||||
|
</section>
|
||||||
</LayoutNavbarPrivate>
|
</LayoutNavbarPrivate>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -15,6 +24,7 @@
|
||||||
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
|
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
|
||||||
import JugglProjectsPanel from "@/components/juggl/JugglProjectsPanel";
|
import JugglProjectsPanel from "@/components/juggl/JugglProjectsPanel";
|
||||||
import JugglRecordsList from "@/components/juggl/JugglRecordsList";
|
import JugglRecordsList from "@/components/juggl/JugglRecordsList";
|
||||||
|
import FormProjectAdd from "@/components/forms/FormProjectAdd";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -22,12 +32,16 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
LayoutNavbarPrivate,
|
LayoutNavbarPrivate,
|
||||||
JugglProjectsPanel,
|
JugglProjectsPanel,
|
||||||
JugglRecordsList
|
JugglRecordsList,
|
||||||
|
FormProjectAdd
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
finishedProjects: () => {
|
finishedProjects: () => {
|
||||||
return store.getters.finishedProjects;
|
return store.getters.finishedProjects;
|
||||||
},
|
},
|
||||||
|
finishedRecords: () => {
|
||||||
|
return store.getters.finishedRecords;
|
||||||
|
},
|
||||||
runningRecords: () => {
|
runningRecords: () => {
|
||||||
return store.getters.runningRecords;
|
return store.getters.runningRecords;
|
||||||
}
|
}
|
||||||
|
@ -35,12 +49,20 @@ export default {
|
||||||
created: () => {
|
created: () => {
|
||||||
store.dispatch("loadProjects");
|
store.dispatch("loadProjects");
|
||||||
store.dispatch("loadRunningRecords");
|
store.dispatch("loadRunningRecords");
|
||||||
|
store.dispatch("loadRecords", { limit: 25, finished: true });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
.center
|
.center
|
||||||
text-align: center
|
text-align: center
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
</style>
|
|
||||||
|
section
|
||||||
|
margin-bottom: 4rem
|
||||||
|
|
||||||
|
#add-project-form
|
||||||
|
margin-top: 1rem
|
||||||
|
text-align: center
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutMinimal title="Login" width="slimmer">
|
<LayoutMinimal title="Login" width="slimmer">
|
||||||
<FormLogin/>
|
<FormLogin />
|
||||||
</LayoutMinimal>
|
</LayoutMinimal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,4 +16,4 @@ export default {
|
||||||
FormLogin
|
FormLogin
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
43
src/views/RecordDetails.vue
Normal file
43
src/views/RecordDetails.vue
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<LayoutNavbarPrivate title="Record details" :center="loading || !!error">
|
||||||
|
<b-spinner v-if="loading" center />
|
||||||
|
<p class="danger" v-if="error">{{error}}</p>
|
||||||
|
<FormRecordDetails :record="record" v-if="record !== undefined"/>
|
||||||
|
</LayoutNavbarPrivate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
|
||||||
|
import FormRecordDetails from "@/components/forms/FormRecordDetails";
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "RecordDetails",
|
||||||
|
components: {
|
||||||
|
LayoutNavbarPrivate,
|
||||||
|
FormRecordDetails
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
record: {},
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
loading: function() {
|
||||||
|
return this.record == {} && !this.error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function() {
|
||||||
|
var recordId = this.$route.params.id;
|
||||||
|
this.record = store.getters.getRecordById(recordId);
|
||||||
|
|
||||||
|
if (this.record === undefined) {
|
||||||
|
this.error = "Could not load record with id '" + recordId + "'.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
</style>
|
Loading…
Reference in a new issue