Basic site working
This commit is contained in:
parent
1be0761358
commit
045eb7ecc9
16 changed files with 494 additions and 256 deletions
|
@ -12,7 +12,7 @@
|
||||||
"bootstrap": "^4.5.3",
|
"bootstrap": "^4.5.3",
|
||||||
"bootstrap-vue": "^2.21.1",
|
"bootstrap-vue": "^2.21.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.12",
|
||||||
"vue-router": "^3.2.0",
|
"vue-router": "^3.2.0",
|
||||||
"vuex": "^3.4.0"
|
"vuex": "^3.4.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<b-img
|
<b-img
|
||||||
:src="require('../../assets/logo.png')"
|
:src="require('../../assets/logo.png')"
|
||||||
alt="Juggl"
|
alt="Juggl"
|
||||||
:height="heightSize"
|
:width="widthSize"
|
||||||
:center="center"
|
:center="center"
|
||||||
/>
|
/>
|
||||||
</b-link>
|
</b-link>
|
||||||
|
@ -25,13 +25,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
heightSize: function() {
|
widthSize: function() {
|
||||||
let sizes = {
|
let sizes = {
|
||||||
mini: "35px",
|
mini: "35px",
|
||||||
normal: "64px",
|
|
||||||
tiny: "80px",
|
tiny: "80px",
|
||||||
smaller: "110px",
|
smaller: "110px",
|
||||||
small: "150px",
|
small: "150px",
|
||||||
|
normal: "175px",
|
||||||
medium: "300px",
|
medium: "300px",
|
||||||
large: "450px",
|
large: "450px",
|
||||||
big: "600px",
|
big: "600px",
|
||||||
|
|
22
juggl-vue/src/components/base/BaseUserDropdown.vue
Normal file
22
juggl-vue/src/components/base/BaseUserDropdown.vue
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<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,10 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<b-form @submit="submitForm">
|
<b-form @submit="submitForm">
|
||||||
<b-form-group id="email-group" label-for="email">
|
<b-form-group id="username-group" label-for="username">
|
||||||
<b-form-input
|
<b-form-input
|
||||||
id="email"
|
id="username"
|
||||||
v-model="form.user_id"
|
v-model="form.user_id"
|
||||||
type="number"
|
|
||||||
required
|
required
|
||||||
placeholder="User ID"
|
placeholder="User ID"
|
||||||
trim
|
trim
|
||||||
|
@ -24,7 +23,8 @@
|
||||||
<b-form-invalid-feedback :state="!failed">
|
<b-form-invalid-feedback :state="!failed">
|
||||||
Password or email invalid.
|
Password or email invalid.
|
||||||
</b-form-invalid-feedback>
|
</b-form-invalid-feedback>
|
||||||
<b-button variant="primary" type="submit" block>
|
<b-button variant="primary" type="submit" block :disabled="working">
|
||||||
|
<b-spinner v-if="working" small/>
|
||||||
Log in
|
Log in
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-form>
|
</b-form>
|
||||||
|
@ -41,7 +41,8 @@ export default {
|
||||||
user_id: null,
|
user_id: null,
|
||||||
api_key: null
|
api_key: null
|
||||||
},
|
},
|
||||||
failed: false
|
failed: false,
|
||||||
|
working: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -50,14 +51,16 @@ export default {
|
||||||
*/
|
*/
|
||||||
submitForm: function(e) {
|
submitForm: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
this.failed = false;
|
||||||
|
this.working = true;
|
||||||
|
|
||||||
// Try to login
|
// Try to login
|
||||||
store.dispatch("login")
|
store.dispatch("login", { userId: this.form.user_id, apiKey: this.form.api_key})
|
||||||
.login(this.form.user_id, this.form.api_key)
|
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r !== true) {
|
if (r !== true) {
|
||||||
this.failed = true;
|
this.failed = true;
|
||||||
return;
|
this.working = false;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On success redirect to target or dashboard
|
// On success redirect to target or dashboard
|
||||||
|
@ -68,6 +71,7 @@ export default {
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
this.failed = true;
|
this.failed = true;
|
||||||
|
this.working = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
74
juggl-vue/src/components/juggl/JugglProjectsPanel.vue
Normal file
74
juggl-vue/src/components/juggl/JugglProjectsPanel.vue
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<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>
|
97
juggl-vue/src/components/juggl/JugglRecordsList.vue
Normal file
97
juggl-vue/src/components/juggl/JugglRecordsList.vue
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<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,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BaseLogo id="logo" size="smaller" center class="space-top" />
|
<BaseLogo id="logo" size="medium" center class="space-top" />
|
||||||
<BaseContainer :width="width" center class="space-bottom">
|
<BaseContainer :width="width" center class="space-bottom">
|
||||||
<BaseTitle v-if="title" center size="huge" class="centered">
|
<BaseTitle v-if="title" center size="huge" class="centered">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
|
@ -4,23 +4,11 @@
|
||||||
<BaseContainer width="wide" center>
|
<BaseContainer width="wide" center>
|
||||||
<ul id="header-container">
|
<ul id="header-container">
|
||||||
<li>
|
<li>
|
||||||
<BaseLogo size="normal"/>
|
<BaseLogo id="logo" size="normal"/>
|
||||||
|
</li>
|
||||||
|
<li class="right">
|
||||||
|
<BaseUserDropdown/>
|
||||||
</li>
|
</li>
|
||||||
<!-- <li style="float: right">
|
|
||||||
<input
|
|
||||||
id="user-id"
|
|
||||||
class="public hidden"
|
|
||||||
type="text"
|
|
||||||
placeholder="User ID"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
id="api-key"
|
|
||||||
class="public hidden"
|
|
||||||
type="password"
|
|
||||||
placeholder="API Key"
|
|
||||||
/>
|
|
||||||
<button id="auth-btn" onclick="handleAuthBtn()">Log In</button>
|
|
||||||
</li> -->
|
|
||||||
</ul>
|
</ul>
|
||||||
</BaseContainer>
|
</BaseContainer>
|
||||||
</header>
|
</header>
|
||||||
|
@ -35,12 +23,14 @@
|
||||||
<script>
|
<script>
|
||||||
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";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LayoutNavbarPrivate",
|
name: "LayoutNavbarPrivate",
|
||||||
components: {
|
components: {
|
||||||
BaseContainer,
|
BaseContainer,
|
||||||
BaseLogo
|
BaseLogo,
|
||||||
|
BaseUserDropdown
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -62,12 +52,11 @@ header
|
||||||
list-style: none
|
list-style: none
|
||||||
line-height: navbar-height
|
line-height: navbar-height
|
||||||
|
|
||||||
img
|
.right
|
||||||
margin-top: 0.5rem
|
float: right
|
||||||
height: 3rem
|
|
||||||
width: auto
|
|
||||||
|
|
||||||
*
|
*
|
||||||
|
line-height: $navbar-height
|
||||||
vertical-align: baseline
|
vertical-align: baseline
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,34 @@
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import VueRouter from "vue-router";
|
import VueRouter from "vue-router";
|
||||||
|
import store from "../store";
|
||||||
import Login from "../views/Login.vue";
|
import Login from "../views/Login.vue";
|
||||||
import Home from "../views/Home.vue";
|
import Home from "../views/Home.vue";
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
redirect: "/home"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: "Login",
|
name: "Login",
|
||||||
component: Login,
|
component: Login,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/home",
|
||||||
name: "Home",
|
name: "Home",
|
||||||
component: Home,
|
component: Home,
|
||||||
// beforeEnter: requireAuth,
|
beforeEnter: requireAuth,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/logout",
|
||||||
|
name: "Logout",
|
||||||
|
beforeEnter: (to, from, next) => {
|
||||||
|
store.dispatch("logout");
|
||||||
|
next("/");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -28,16 +41,15 @@ const router = new VueRouter({
|
||||||
/**
|
/**
|
||||||
* Checks authentication before proceeding
|
* Checks authentication before proceeding
|
||||||
*/
|
*/
|
||||||
// TODO: Authentication required in router
|
function requireAuth(to, from, next) {
|
||||||
// function requireAuth(to, from, next) {
|
if (!store.getters.isLoggedIn) {
|
||||||
// if (!userService.isLoggedIn()) {
|
next({
|
||||||
// next({
|
path: "/login",
|
||||||
// path: "/login",
|
query: { redirect: to.fullPath },
|
||||||
// query: { redirect: to.fullPath },
|
});
|
||||||
// });
|
} else {
|
||||||
// } else {
|
next();
|
||||||
// next();
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { store } from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper for the used fetch API, currently axios.
|
* A wrapper for the used fetch API, currently axios.
|
||||||
|
@ -15,11 +15,19 @@ export const apiService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
post(resource, json, options) {
|
post(resource, json, options) {
|
||||||
return this.getApi().post(resource, json, options);
|
return this.getApi().post(
|
||||||
|
resource,
|
||||||
|
{ ...this.getDefaultJson(), ...json },
|
||||||
|
options
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
put(resource, json, options) {
|
put(resource, json, options) {
|
||||||
return this.getApi().put(resource, json, options);
|
return this.getApi().put(
|
||||||
|
resource,
|
||||||
|
{ ...this.getDefaultJson(), ...json },
|
||||||
|
options
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,4 +40,10 @@ export const apiService = {
|
||||||
|
|
||||||
return axios.create(options);
|
return axios.create(options);
|
||||||
},
|
},
|
||||||
|
getDefaultJson() {
|
||||||
|
return {
|
||||||
|
user_id: store.getters.auth.userId,
|
||||||
|
api_key: store.getters.auth.apiKey,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// TODO: Do I need this file?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains a bunch of helper functions.
|
* Contains a bunch of helper functions.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -11,212 +11,88 @@ export const jugglService = {
|
||||||
* @returns A promise
|
* @returns A promise
|
||||||
*/
|
*/
|
||||||
getUser() {
|
getUser() {
|
||||||
return apiService.post("/getUser.php");
|
return apiService.post("/getUser.php").then((r) => {
|
||||||
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getProjects() {
|
getProjects() {
|
||||||
return apiService.post("/getProjects.php");
|
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() {
|
getRecords() {
|
||||||
return apiService.post("/getRecords.php");
|
return apiService.post("/getRecords.php").then((r) => {
|
||||||
|
return {
|
||||||
|
data: processRecords(r.data),
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getRunningRecords() {
|
getRunningRecords() {
|
||||||
return apiService.post("/getRunningRecords.php");
|
return apiService.post("/getRunningRecords.php").then((r) => {
|
||||||
|
return {
|
||||||
|
data: processRecords(r.data),
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
startRecord(projectId, startTime = null) {
|
startRecord(projectId, startTime = null) {
|
||||||
if (startTime == null) startTime = new Date();
|
if (startTime == null) startTime = new Date();
|
||||||
return apiService.post("/startRecord.php", {
|
return apiService
|
||||||
|
.post("/startRecord.php", {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
start_time: helperService.dateToString(startTime),
|
start_time: helperService.dateToString(startTime),
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests a list of all semesters from the current user.
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
getSemesterList() {
|
|
||||||
return apiService.get("/semester");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests all the data from a specific semester from the current user.
|
|
||||||
*
|
|
||||||
* @param {*} semesterNr Nr starting at 1 from the desired semester to gather data from
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
getSemester(semesterNr) {
|
|
||||||
var path = "/semester/" + semesterNr;
|
|
||||||
return apiService.get(path).then((semester) => {
|
|
||||||
// Add semester nr to modules
|
|
||||||
semester.data.modules.forEach((module) => {
|
|
||||||
module.semester_nr = semester.nr;
|
|
||||||
});
|
|
||||||
|
|
||||||
return semester;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new semester to the current user.
|
|
||||||
*
|
|
||||||
* @param {*} data An object with {nr, startdate, enddate}
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
addSemester(data) {
|
|
||||||
var path = "/semester";
|
|
||||||
return apiService.put(path, data);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new module to the current user.
|
|
||||||
*
|
|
||||||
* @param {*} semesterNr Associated semester nr
|
|
||||||
* @param {*} data An object with {name, required_total, required_per_sheet}
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
addModule(semesterNr, data) {
|
|
||||||
var path = "/semester/" + semesterNr + "/module";
|
|
||||||
return apiService.put(path, data);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new sheet to the current user.
|
|
||||||
*
|
|
||||||
* @param {*} semesterNr Associated semester nr
|
|
||||||
* @param {*} semesterNr Associated module name
|
|
||||||
* @param {*} data An object with {date, points, total, is_draft, percentage}
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
addSheet(semesterNr, moduleName, data) {
|
|
||||||
var path =
|
|
||||||
"/semester/" + semesterNr + "/module/" + moduleName + "/sheet";
|
|
||||||
return apiService.put(path, data);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a new user.
|
|
||||||
*
|
|
||||||
* @param {*} data An object with {date, points, total, is_draft, percentage}
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
signUp(data) {
|
|
||||||
var path = "/user/signup";
|
|
||||||
return apiService
|
|
||||||
.post(path, data)
|
|
||||||
.then((r) => {
|
|
||||||
return { success: true, msg: r.data.msg };
|
|
||||||
// Later
|
|
||||||
// if (r.status === 204) return { success: true, msg: "" };
|
|
||||||
// else return { success: false, msg: r.data.msg };
|
|
||||||
})
|
})
|
||||||
.catch((r) => {
|
|
||||||
return { success: false, msg: r.response.data.msg };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies a user with a token.
|
|
||||||
*
|
|
||||||
* @param {*} token Server generated token
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
verify(token) {
|
|
||||||
var path = "/user/verify";
|
|
||||||
return apiService
|
|
||||||
.post(path, { token: token })
|
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
return { success: true, msg: r.data.msg };
|
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),
|
||||||
})
|
})
|
||||||
.catch((r) => {
|
.then((r) => {
|
||||||
return { success: false, msg: r.response.data.msg };
|
return {
|
||||||
|
data: r.data,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
function processRecords(data) {
|
||||||
* Requests all the data from a specific semester from the current user.
|
Object.values(data.records).forEach(rec => {
|
||||||
*
|
rec.running = rec.end_time === null;
|
||||||
* @param {*} semesterNr Nr starting at 1 from the desired semester to gather data from
|
|
||||||
*
|
|
||||||
* @returns A promise
|
|
||||||
*/
|
|
||||||
getModule(semesterNr, moduleName) {
|
|
||||||
var path = "/semester/" + semesterNr + "/module/" + moduleName;
|
|
||||||
|
|
||||||
return apiService.get(path).then((module) => {
|
|
||||||
module.data.semester_nr = semesterNr;
|
|
||||||
|
|
||||||
// Add semester nr and module name to sheets
|
|
||||||
module.data.sheets.forEach((sheet) => {
|
|
||||||
sheet.semester_nr = semesterNr;
|
|
||||||
sheet.module_name = moduleName;
|
|
||||||
});
|
});
|
||||||
|
return data;
|
||||||
return module;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends credentials to the server in case of successful authentication, saves the jwt in localStorage.
|
|
||||||
*
|
|
||||||
* @param {*} email Email of user credentials
|
|
||||||
* @param {*} pass Password of user credentials
|
|
||||||
* @returns A promise with on parameter, which is true in case of successful authentication
|
|
||||||
*/
|
|
||||||
login(email, pass) {
|
|
||||||
// Is already logged in?
|
|
||||||
if (this.isLoggedIn()) {
|
|
||||||
this.logout();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to log in
|
|
||||||
var user = {
|
|
||||||
email: email,
|
|
||||||
password: pass,
|
|
||||||
};
|
|
||||||
|
|
||||||
return apiService
|
|
||||||
.post("/user/login", user)
|
|
||||||
.then((r) => {
|
|
||||||
localStorage.token = r.data;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the jwt.
|
|
||||||
*/
|
|
||||||
getToken() {
|
|
||||||
return localStorage.token;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the stored jwt and therefor prevents access to any page that requires authentication.
|
|
||||||
*/
|
|
||||||
logout() {
|
|
||||||
delete localStorage.token;
|
|
||||||
apiService.get("/user/logout");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current authentication state.
|
|
||||||
*/
|
|
||||||
isLoggedIn() {
|
|
||||||
// This !! converts expression to boolean with unchanged value
|
|
||||||
// First ! converts to boolean and inverts value and second ! inverts again
|
|
||||||
return !!localStorage.token;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -7,10 +7,10 @@ Vue.use(Vuex);
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
apiUrl: "https://juggl.giller.dev/api",
|
apiUrl: "https://juggl.giller.dev/api",
|
||||||
apiKey: undefined,
|
projects: {},
|
||||||
|
records: {},
|
||||||
user: undefined,
|
user: undefined,
|
||||||
projects: [],
|
auth: undefined,
|
||||||
records: [],
|
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setKey(state, key) {
|
setKey(state, key) {
|
||||||
|
@ -22,35 +22,145 @@ export default new Vuex.Store({
|
||||||
setRecords(state, records) {
|
setRecords(state, records) {
|
||||||
state.records = records;
|
state.records = records;
|
||||||
},
|
},
|
||||||
|
setUser(state, user) {
|
||||||
|
state.user = user;
|
||||||
|
},
|
||||||
|
logout(state) {
|
||||||
|
state.auth = undefined;
|
||||||
|
// TODO: Doesn't work apparently
|
||||||
|
localStorage.removeItem("apiKey");
|
||||||
|
localStorage.removeItem("userId");
|
||||||
|
},
|
||||||
|
login(state, { apiKey, userId }) {
|
||||||
|
state.auth = { apiKey: apiKey, userId: userId };
|
||||||
|
localStorage.setItem("apiKey", apiKey);
|
||||||
|
localStorage.setItem("userId", userId);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
runningRecords: (state) => {
|
runningRecords: (state) => {
|
||||||
return state.records.filter((record) => record.running);
|
return Object.values(state.records).filter((record) => record.running);
|
||||||
},
|
},
|
||||||
finishedRecords: (state) => {
|
finishedRecords: (state) => {
|
||||||
return state.records.filter((record) => !record.running);
|
return Object.values(state.records).filter(
|
||||||
|
(record) => !record.running
|
||||||
|
);
|
||||||
},
|
},
|
||||||
apiKey: (state) => state.apiKey,
|
auth: (state) => state.auth,
|
||||||
user: (state) => state.user,
|
|
||||||
apiUrl: (state) => state.apiUrl,
|
apiUrl: (state) => state.apiUrl,
|
||||||
isLoggedIn: (state) => !!state.apiKey,
|
user: (state) => state.user,
|
||||||
|
isLoggedIn: (state) => !!state.auth,
|
||||||
records: (state) => state.records,
|
records: (state) => state.records,
|
||||||
projects: (state) => state.projects,
|
projects: (state) => state.projects,
|
||||||
|
projectIds: (state) => {
|
||||||
|
var projectIds = [];
|
||||||
|
Object.values(state.projects).forEach((project) => {
|
||||||
|
projectIds.push(project.project_id);
|
||||||
|
});
|
||||||
|
return projectIds;
|
||||||
|
},
|
||||||
|
runningProjectIds: (state, getters) => {
|
||||||
|
var runningProjectIds = [];
|
||||||
|
Object.values(getters.runningRecords).forEach((record) => {
|
||||||
|
var projectId = record.project_id;
|
||||||
|
if (runningProjectIds.includes(runningProjectIds) === false) {
|
||||||
|
runningProjectIds.push(projectId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return runningProjectIds;
|
||||||
|
},
|
||||||
|
finishedProjectIds: (state, getters) => {
|
||||||
|
var runningProjectIds = getters.runningProjectIds;
|
||||||
|
return getters.projectIds.filter(
|
||||||
|
(id) => !runningProjectIds.includes(id)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
finishedProjects: (state, getters) => {
|
||||||
|
var ids = getters.finishedProjectIds;
|
||||||
|
return Object.values(state.projects).filter((project) =>
|
||||||
|
ids.includes(project.project_id)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
runningProjects: (state, getters) => {
|
||||||
|
var ids = getters.runningProjectIds;
|
||||||
|
return Object.values(state.projects).filter((project) =>
|
||||||
|
ids.includes(project.project_id)
|
||||||
|
);
|
||||||
|
},
|
||||||
getProjectById: (state, getters) => (id) => {
|
getProjectById: (state, getters) => (id) => {
|
||||||
return getters.projects.find(
|
return Object.values(getters.projects).find(
|
||||||
(project) => project.project_id === id
|
(project) => project.project_id === id
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getRecordById: (state, getters) => (id) => {
|
getRecordById: (state, getters) => (id) => {
|
||||||
return 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 }) {
|
||||||
commit("setProjects");
|
jugglService.getProjects().then((r) => {
|
||||||
|
commit("setProjects", r.data.projects);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
login({ commit, userId, apiKey }) {
|
loadAllRecords({ commit }) {
|
||||||
return jugglService.login();
|
jugglService.getRecords().then((r) => {
|
||||||
|
commit("setRecords", r.data.records);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
loadRunningRecords({ commit, getters }) {
|
||||||
|
jugglService.getRunningRecords().then((r) => {
|
||||||
|
var allRecords = {
|
||||||
|
...getters.finishedRecords,
|
||||||
|
...r.data.records,
|
||||||
|
};
|
||||||
|
commit("setRecords", allRecords);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
login({ commit, getters }, { userId, apiKey }) {
|
||||||
|
// Is already logged in?
|
||||||
|
if (getters.isLoggedIn) {
|
||||||
|
this.dispatch("logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
commit("login", { apiKey: apiKey, userId: userId });
|
||||||
|
|
||||||
|
return jugglService
|
||||||
|
.getUser()
|
||||||
|
.catch(() => {
|
||||||
|
this.dispatch("logout");
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
commit("setUser", r.data.users[0]);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
logout({ commit }) {
|
||||||
|
commit("setUser", undefined);
|
||||||
|
commit("logout");
|
||||||
|
},
|
||||||
|
endRecord(context, recordId) {
|
||||||
|
return jugglService
|
||||||
|
.endRecord(recordId)
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.dispatch("loadRunningRecords");
|
||||||
|
this.dispatch("loadProjects");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
startRecord(context, projectId) {
|
||||||
|
return jugglService
|
||||||
|
.startRecord(projectId)
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.dispatch("loadRunningRecords");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@ $black: #000
|
||||||
$white: #fff
|
$white: #fff
|
||||||
$primary: #F00
|
$primary: #F00
|
||||||
$secondary: $grey
|
$secondary: $grey
|
||||||
|
$red: $primary
|
||||||
|
|
||||||
$background-primary: #222
|
$background-primary: #222
|
||||||
$background-secondary: #000
|
$background-secondary: #000
|
||||||
|
@ -53,6 +54,23 @@ body
|
||||||
a
|
a
|
||||||
color: $font-link !important
|
color: $font-link !important
|
||||||
|
|
||||||
|
.b-dropdown, .dropdown
|
||||||
|
border-radius: 0
|
||||||
|
background: #0000
|
||||||
|
|
||||||
|
ul.dropdown-menu
|
||||||
|
background-color: $background-primary
|
||||||
|
border: 1px solid $primary
|
||||||
|
color: $primary
|
||||||
|
|
||||||
|
*:hover
|
||||||
|
color: $white !important
|
||||||
|
background: darken($primary, 20)
|
||||||
|
|
||||||
|
*:active
|
||||||
|
color: $white !important
|
||||||
|
background: $primary
|
||||||
|
|
||||||
// .custom-checkbox
|
// .custom-checkbox
|
||||||
// label
|
// label
|
||||||
// color: $font-primary !important
|
// color: $font-primary !important
|
||||||
|
|
|
@ -1,21 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutNavbarPrivate>
|
<LayoutNavbarPrivate>
|
||||||
Hey there
|
<div v-if="runningRecords.length > 0">
|
||||||
<b-link to="/login">Go to login</b-link>
|
<h2 class="center">Tracking</h2>
|
||||||
|
<JugglRecordsList :records="runningRecords"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="finishedProjects.length > 0">
|
||||||
|
<h2 class="center">Projects</h2>
|
||||||
|
<JugglProjectsPanel :projects="finishedProjects"/>
|
||||||
|
</div>
|
||||||
</LayoutNavbarPrivate>
|
</LayoutNavbarPrivate>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
|
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
|
||||||
|
import JugglProjectsPanel from "@/components/juggl/JugglProjectsPanel";
|
||||||
|
import JugglRecordsList from "@/components/juggl/JugglRecordsList";
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Home",
|
name: "Home",
|
||||||
components: {
|
components: {
|
||||||
LayoutNavbarPrivate
|
LayoutNavbarPrivate,
|
||||||
|
JugglProjectsPanel,
|
||||||
|
JugglRecordsList
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
finishedProjects: () => {
|
||||||
|
return store.getters.finishedProjects;
|
||||||
|
},
|
||||||
|
runningRecords: () => {
|
||||||
|
return store.getters.runningRecords;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: () => {
|
||||||
|
store.dispatch("loadProjects");
|
||||||
|
store.dispatch("loadRunningRecords");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="sass">
|
||||||
|
.center
|
||||||
|
text-align: center
|
||||||
|
font-weight: bold
|
||||||
</style>
|
</style>
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutMinimal title="Login" width="slimmer">
|
<LayoutMinimal title="Login" width="slimmer">
|
||||||
<FormLogin/>
|
<FormLogin/>
|
||||||
<b-link to="/">Go to home</b-link>
|
|
||||||
</LayoutMinimal>
|
</LayoutMinimal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue