Added ability to edit records

This commit is contained in:
Maximilian Giller 2021-01-03 20:37:15 +01:00
parent a2473bd06d
commit 3ad1041a7b
13 changed files with 352 additions and 99 deletions

View file

@ -193,11 +193,28 @@ function updateEndRecord($user_id, $params)
// Get start instance to calculate duration // Get start instance to calculate duration
$start_time = getTimeRecord($user_id, $record_id)["start_time"]; $start_time = getTimeRecord($user_id, $record_id)["start_time"];
$data = [ $record = [
"record_id" => $record_id,
"end_time" => $params->get("end_time"), "end_time" => $params->get("end_time"),
"duration" => calcDuration($start_time, $params->get("end_time")) "duration" => calcDuration($start_time, $params->get("end_time"))
]; ];
updateRecord($user_id, $record);
}
function updateRecord($user_id, $record)
{
$record_id = $record["record_id"];
// Update given parameters
$data = [];
$props = ["end_time", "start_time", "duration", "project_id"];
foreach ($props as $p) {
if (array_key_exists ($p, $record)) {
$data[$p] = $record[$p];
}
}
$db = new DbOperations(); $db = new DbOperations();
$db->update("time_records", $data); $db->update("time_records", $data);
$db->where("user_id", Comparison::EQUAL, $user_id); $db->where("user_id", Comparison::EQUAL, $user_id);

View 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 UpdateRecordBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
if ($params->exists(["record"]) == false) {
respondStatus(400, "Missing parameter");
}
updateRecord($user_id, $params->get("record"));
respondStatus(200);
}
}
$branch = new UpdateRecordBranch();
$branch->execute();

View file

@ -28,40 +28,75 @@
<b-form-timepicker <b-form-timepicker
id="starttime" id="starttime"
v-model="times.start.time" v-model="times.start.time"
show-seconds
required required
placeholder="Choose a start time" placeholder="Choose a start time"
dark dark
> >
</b-form-timepicker> </b-form-timepicker>
</b-form-group> </b-form-group>
<b-form-checkbox id="running" v-model="times.finished" required dark>
Finished
</b-form-checkbox>
<b-form-group id="enddate-group" label="End date" label-for="enddate"> <b-form-group id="enddate-group" label="End date" label-for="enddate">
<b-form-datepicker <b-form-datepicker
id="enddate" id="enddate"
v-model="times.end.date" v-model="times.end.date"
required :required="times.finished"
:disabled="!times.finished"
placeholder="Choose an end date" placeholder="Choose an end date"
:min="times.start.date" :min="times.start.date"
dark dark
> >
</b-form-datepicker> </b-form-datepicker>
</b-form-group> </b-form-group>
<b-form-group id="endtime-group" label="End time" label-for="endtime">
<b-form-timepicker
id="endtime"
v-model="times.end.time"
:required="times.finished"
:disabled="!times.finished"
show-seconds
placeholder="Choose an end time"
dark
>
</b-form-timepicker>
</b-form-group>
<b-form-group id="duration-group" label="Duration [seconds]" label-for="duration">
<b-form-input
id="duration"
v-model="times.duration"
type="number"
:required="times.finished"
:disabled="!times.finished"
placeholder="Choose a duration"
min="0"
dark
>
</b-form-input>
</b-form-group>
<b-form-invalid-feedback :state="!failed"> <b-form-invalid-feedback :state="!failed">
Something went wrong. Something went wrong.
</b-form-invalid-feedback> </b-form-invalid-feedback>
<b-button variant="primary" type="submit" class="right" :disabled="working"> <b-button variant="primary" type="submit" class="right" :disabled="working">
<b-spinner v-if="working" small /> <b-spinner v-if="working" small />
Save {{ saveBtnText }}
</b-button>
<b-button variant="outline-secondary" @click="() => cancel()" class="left">
Cancel
</b-button>
<b-button
class="left"
@click="() => deleteRecord()"
variant="outline-danger"
>
<b-icon class="icon-btn" icon="trash" />
</b-button> </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> </b-form>
</template> </template>
<script> <script>
import { helperService } from "@/services/helper.service.js";
import store from "@/store"; import store from "@/store";
export default { export default {
@ -75,14 +110,50 @@ export default {
data() { data() {
return { return {
times: { times: {
start: {}, start: { date: undefined, time: undefined },
end: {} end: { date: undefined, time: undefined },
finished: false,
duration: 0
}, },
selectableProjects: [], selectableProjects: [],
failed: false, failed: false,
working: false working: false
}; };
}, },
computed: {
resultRecord: function() {
var resultRecord = {
record_id: this.record.record_id,
user_id: this.record.user_id,
project_id: this.record.project_id,
running: !this.times.finished,
start_time: this.times.start.date + " " + this.times.start.time,
end_time: this.times.end.date + " " + this.times.end.time,
duration: this.times.duration,
start_device_id: this.record.start_device_id // TODO: Remove at some time
};
if (resultRecord.running) {
resultRecord.end_time = null;
resultRecord.duration = null;
}
return resultRecord;
},
saveBtnText: function() {
var result = "Save";
if (this.resultRecord.running !== this.record.running) {
if (this.resultRecord.running) {
result += " and track";
} else {
result += " and finish";
}
}
return result;
}
},
methods: { methods: {
/** /**
* Submits the form. Assupmtion: Form is valid, based on required flags. * Submits the form. Assupmtion: Form is valid, based on required flags.
@ -92,7 +163,21 @@ export default {
this.failed = false; this.failed = false;
this.working = true; this.working = true;
store.dispatch("updateRecord", this.resultRecord).then(() => {
this.working = false;
this.$emit("submit");
}).catch(() => {
this.failed = true;
this.working = false;
});
return false; return false;
},
cancel: function() {
this.$emit("cancel");
},
deleteRecord: function() {
store.dispatch("removeRecord", this.record.record_id);
} }
}, },
created: function() { created: function() {
@ -104,6 +189,9 @@ export default {
this.selectableProjects = projects; this.selectableProjects = projects;
// Load record times // Load record times
this.times.finished = !this.record.running;
this.times.duration = Math.round(this.record.duration);
let dateAndTime = /([0-9-]*)\s([0-9:]*)/; let dateAndTime = /([0-9-]*)\s([0-9:]*)/;
var startMatch = String(this.record.start_time).match(dateAndTime); var startMatch = String(this.record.start_time).match(dateAndTime);
this.times.start.date = startMatch[1]; this.times.start.date = startMatch[1];
@ -115,7 +203,7 @@ export default {
this.times.end.time = endMatch[2]; this.times.end.time = endMatch[2];
} else { } else {
this.times.end.date = new Date().toISOString(); this.times.end.date = new Date().toISOString();
this.times.end.time = new Date().toTimeString(); this.times.end.time = helperService.toISOTime(new Date());
} }
} }
}; };
@ -124,7 +212,9 @@ export default {
<style lang="sass"> <style lang="sass">
.left .left
float: left !important float: left !important
margin-right: 1rem
.right .right
float: right !important float: right !important
margin-left: 1rem
</style> </style>

View file

@ -27,25 +27,11 @@
</template> </template>
<template #cell(details)="data"> <template #cell(details)="data">
<b-button <b-button size="sm" @click="data.toggleDetails" variant="outline-dark">
size="sm"
@click="() => detailsRecord(data.item.record_id)"
variant="outline-dark"
>
<b-icon class="icon-btn" icon="gear" /> <b-icon class="icon-btn" icon="gear" />
</b-button> </b-button>
</template> </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"> <template #cell(stop)="data">
<b-button <b-button
size="sm" size="sm"
@ -55,15 +41,29 @@
<b-icon class="icon-btn" icon="check" /> <b-icon class="icon-btn" icon="check" />
</b-button> </b-button>
</template> </template>
<template #row-details="data">
<b-card>
<FormRecordDetails
:record="data.item"
@cancel="data.toggleDetails"
@submit="data.toggleDetails"
/>
</b-card>
</template>
</b-table> </b-table>
</template> </template>
<script> <script>
import store from "@/store"; import store from "@/store";
import FormRecordDetails from "@/components/forms/FormRecordDetails";
import { helperService } from "@/services/helper.service.js"; import { helperService } from "@/services/helper.service.js";
export default { export default {
name: "JugglRecordsList", name: "JugglRecordsList",
components: {
FormRecordDetails
},
props: { props: {
records: { records: {
required: true, required: true,
@ -72,7 +72,7 @@ export default {
sortDesc: { sortDesc: {
required: false, required: false,
type: Boolean, type: Boolean,
default: false default: true
}, },
running: { running: {
required: false, required: false,
@ -102,10 +102,6 @@ export default {
} }
], ],
runningFields: [ runningFields: [
{
key: "abort",
label: "Abort"
},
{ {
key: "stop", key: "stop",
label: "Stop" label: "Stop"
@ -141,12 +137,8 @@ export default {
stopRecord: function(id) { stopRecord: function(id) {
store.dispatch("endRecord", id); store.dispatch("endRecord", id);
}, },
abortRecord: function(id) {
store.dispatch("removeRecord", id);
},
detailsRecord: function(id) { detailsRecord: function(id) {
return id; this.$router.push("/record/" + id);
// this.$router.push("/record/" + id);
} }
} }
}; };

View file

@ -3,6 +3,7 @@ import VueRouter from "vue-router";
import store from "../store"; 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";
import History from "../views/History.vue";
import RecordDetails from "../views/RecordDetails.vue"; import RecordDetails from "../views/RecordDetails.vue";
Vue.use(VueRouter); Vue.use(VueRouter);
@ -17,6 +18,12 @@ const routes = [
name: "Login", name: "Login",
component: Login component: Login
}, },
{
path: "/history",
name: "History",
component: History,
beforeEnter: requireAuth
},
{ {
path: "/home", path: "/home",
name: "Home", name: "Home",
@ -49,6 +56,8 @@ const router = new VueRouter({
* Checks authentication before proceeding * Checks authentication before proceeding
*/ */
function requireAuth(to, from, next) { function requireAuth(to, from, next) {
store.dispatch("loadSavedLogin");
if (!store.getters.isLoggedIn) { if (!store.getters.isLoggedIn) {
next({ next({
path: "/login", path: "/login",

View file

@ -10,11 +10,11 @@ import store from "@/store";
* Returns promises. * Returns promises.
*/ */
export const apiService = { export const apiService = {
get(resource, options) { get(resource, options = {}) {
return this.getApi().get(resource, options); return this.getApi().get(resource, options);
}, },
post(resource, json, options) { post(resource, json = {}, options = {}) {
return this.getApi().post( return this.getApi().post(
resource, resource,
{ ...this.getDefaultJson(), ...json }, { ...this.getDefaultJson(), ...json },
@ -22,7 +22,7 @@ export const apiService = {
); );
}, },
put(resource, json, options) { put(resource, json = {}, options = {}) {
return this.getApi().put( return this.getApi().put(
resource, resource,
{ ...this.getDefaultJson(), ...json }, { ...this.getDefaultJson(), ...json },

View file

@ -136,5 +136,15 @@ export const helperService = {
timestamp = days + ":" + timestamp; timestamp = days + ":" + timestamp;
} }
return timestamp; return timestamp;
},
toISOTime(dateTime) {
return (
dateTime.getHours() +
":" +
dateTime.getMinutes() +
":" +
dateTime.getSeconds()
);
} }
}; };

View file

@ -54,14 +54,14 @@ export const jugglService = {
}); });
}, },
getRecords({ limit = undefined, finished = undefined }) { getRecords(options = {}) {
var payload = {}; var payload = {};
if (limit !== undefined && limit > 0) { if (options.limit !== undefined && options.limit > 0) {
payload.limit = limit; payload.limit = options.limit;
} }
if (finished !== undefined) { if (options.finished !== undefined) {
payload.finished = finished; payload.finished = options.finished;
} }
return apiService.post("/getRecords.php", payload).then(r => { return apiService.post("/getRecords.php", payload).then(r => {
@ -72,6 +72,19 @@ export const jugglService = {
}); });
}, },
updateRecord(record) {
var payload = {
record: record
};
return apiService.post("/updateRecord.php", payload).then(r => {
return {
data: processRecords(r.data),
msg: ""
};
});
},
getRunningRecords() { getRunningRecords() {
return apiService.post("/getRunningRecords.php").then(r => { return apiService.post("/getRunningRecords.php").then(r => {
return { return {
@ -105,7 +118,7 @@ export const jugglService = {
}) })
.then(r => { .then(r => {
return { return {
data: r.data, data: processRecords(r.data),
msg: "" msg: ""
}; };
}); });
@ -120,7 +133,7 @@ export const jugglService = {
}) })
.then(r => { .then(r => {
return { return {
data: r.data, data: processRecords(r.data),
msg: "" msg: ""
}; };
}); });

View file

@ -1,7 +1,8 @@
import Vue from "vue"; import Vue from "vue";
import Vuex from "vuex"; import Vuex from "vuex";
import { juggl } from "./modules/juggl"; import { juggl } from "./modules/juggl";
import createPersistedState from "vuex-persistedstate"; // TODO: Remove dependency from packages
// import createPersistedState from "vuex-persistedstate";
Vue.use(Vuex); Vue.use(Vuex);
@ -9,9 +10,9 @@ export default new Vuex.Store({
modules: { modules: {
juggl juggl
}, },
plugins: [ // plugins: [
createPersistedState({ // createPersistedState({
storage: window.sessionStorage // storage: window.localStorage
}) // })
] // ]
}); });

View file

@ -22,15 +22,6 @@ export const juggl = {
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) { setRecordsLimit(state, limit) {
state.recordsLimit = limit; state.recordsLimit = limit;
}, },
@ -103,13 +94,17 @@ export const juggl = {
return Object.values(getters.records).find( return Object.values(getters.records).find(
record => record.record_id === id record => record.record_id === id
); );
},
getRecordsExceptId: (state, getters) => id => {
return Object.values(getters.records).filter(
record => record.record_id !== id
);
} }
}, },
actions: { actions: {
loadProjects({ commit }) { loadProjects({ commit }) {
return jugglService.getProjects().then(r => { return jugglService.getProjects().then(r => {
commit("setProjects", r.data.projects); commit("setProjects", r.data.projects);
commit("usingProjects", true);
}); });
}, },
loadUser({ commit }) { loadUser({ commit }) {
@ -124,13 +119,14 @@ export const juggl = {
}); });
}, },
loadRecords({ commit, state }, { limit, finished }) { loadRecords({ commit, state }, { limit, finished }) {
if (limit !== undefined) {
commit("setRecordsLimit", limit); commit("setRecordsLimit", limit);
return jugglService }
.getRecords({ limit: state.recordsLimit, finished: finished })
.then(r => { var payload = { limit: state.recordsLimit, finished: finished };
return jugglService.getRecords(payload).then(r => {
commit("setRecords", r.data.records); commit("setRecords", r.data.records);
commit("usingFinishedRecords", true);
commit("usingRunningRecords", true);
}); });
}, },
loadRunningRecords({ commit, getters }) { loadRunningRecords({ commit, getters }) {
@ -140,7 +136,6 @@ export const juggl = {
...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 }) {
@ -169,14 +164,20 @@ export const juggl = {
commit("setUser", undefined); commit("setUser", undefined);
commit("logout"); commit("logout");
}, },
endRecord(context, recordId) { endRecord({ getters }, recordId) {
if (recordId === undefined) {
return false;
}
return jugglService return jugglService
.endRecord(recordId) .endRecord(recordId)
.catch(() => { .catch(() => {
return false; return false;
}) })
.then(() => { .then(() => {
this.dispatch("updateState"); // TODO: Return ended record from API
var record = getters.getRecordById(recordId);
record.running = false;
return true; return true;
}); });
}, },
@ -187,45 +188,40 @@ export const juggl = {
return false; return false;
}) })
.then(() => { .then(() => {
this.dispatch("updateState"); this.dispatch("loadProjects");
return true; return true;
}); });
}, },
startRecord(context, projectId) { startRecord(context, projectId) {
if (projectId === undefined) {
return false;
}
return jugglService return jugglService
.startRecord(projectId) .startRecord(projectId)
.catch(() => { .catch(() => {
return false; return false;
}) })
.then(() => { .then(() => {
this.dispatch("updateState"); this.dispatch("loadRunningRecords");
return true; return true;
}); });
}, },
removeRecord(context, recordId) { removeRecord({ commit, getters }, recordId) {
if (recordId === undefined) {
return false;
}
return jugglService return jugglService
.removeRecord(recordId) .removeRecord(recordId)
.catch(() => { .catch(() => {
return false; return false;
}) })
.then(() => { .then(() => {
this.dispatch("updateState"); commit("setRecords", getters.getRecordsExceptId(recordId));
return true; 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 }) { loadSavedLogin({ commit }) {
var userId = localStorage.getItem("userId"); var userId = localStorage.getItem("userId");
var apiKey = localStorage.getItem("apiKey"); var apiKey = localStorage.getItem("apiKey");
@ -235,6 +231,27 @@ export const juggl = {
} }
commit("login", { apiKey: apiKey, userId: userId }); commit("login", { apiKey: apiKey, userId: userId });
},
removeLocalRecords({ commit }) {
commit("setRecords", []);
},
updateRecord({ commit, getters }, record) {
if (record.record_id === undefined) {
return;
}
return jugglService
.updateRecord(record)
.catch(() => {
return false;
})
.then(() => {
// TODO: Return updated record from API
var records = getters.getRecordsExceptId(record.record_id);
records.push(record);
commit("setRecords", records);
return true;
});
} }
} }
}; };

View file

@ -9,10 +9,11 @@ $secondary: $grey
$red: $primary $red: $primary
$background-primary: #222 $background-primary: #222
$background-secondary: #000 $background-secondary: #191919
// $background-gradient: linear-gradient(165deg, $background-primary 65%, $background-secondary 100%) // $background-gradient: linear-gradient(165deg, $background-primary 65%, $background-secondary 100%)
$font-primary: $white $font-primary: $white
$font-inactive: $grey
$font-secondary: $primary $font-secondary: $primary
$font-inverted: $black $font-inverted: $black
$font-link: $secondary $font-link: $secondary
@ -54,6 +55,9 @@ body
a a
color: $font-link !important color: $font-link !important
&:hover
color: $background-primary !important
.b-dropdown, .dropdown .b-dropdown, .dropdown
border-radius: 0 border-radius: 0
background: #0000 background: #0000
@ -110,9 +114,9 @@ header.b-calendar-grid-caption, .b-calendar-grid-weekdays *, header.b-calendar-h
.b-form-datepicker label:not(.text-muted), .b-form-timepicker label:not(.text-muted) .b-form-datepicker label:not(.text-muted), .b-form-timepicker label:not(.text-muted)
color: $font-primary !important color: $font-primary !important
input:disabled input:disabled, .b-form-timepicker[aria-disabled=true], .b-form-datepicker[aria-disabled=true]
color: $font-secondary !important color: $font-inactive !important
border-color: $grey !important border-color: $background-primary !important
input input
color: $font-primary !important color: $font-primary !important
@ -123,6 +127,13 @@ select
background-color: $background-primary !important background-color: $background-primary !important
color: $font-primary !important color: $font-primary !important
div.card
background-color: $background-secondary
border-color: $white
pre
color: $font-primary !important
// Import Bootstrap and BootstrapVue source SCSS files // Import Bootstrap and BootstrapVue source SCSS files
@import '~bootstrap/scss/bootstrap.scss' @import '~bootstrap/scss/bootstrap.scss'

57
src/views/History.vue Normal file
View file

@ -0,0 +1,57 @@
<template>
<LayoutNavbarPrivate>
<section>
<h2 class="center">History</h2>
<div class="center">
<b-spinner v-if="working"></b-spinner>
</div>
<JugglRecordsList :records="finishedRecords" v-if="!working" />
</section>
</LayoutNavbarPrivate>
</template>
<script>
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
import JugglRecordsList from "@/components/juggl/JugglRecordsList";
import store from "@/store";
export default {
name: "Home",
data: () => {
return {
working: true
};
},
components: {
LayoutNavbarPrivate,
JugglRecordsList
},
computed: {
finishedRecords: () => {
return store.getters.finishedRecords;
}
},
created: function() {
store.dispatch("loadProjects");
store
.dispatch("loadRecords", { limit: 0, finished: true })
.then(() => {
this.working = false;
})
.catch(() => {
this.working = false;
});
},
beforeDestroy: function() {
store.dispatch("removeLocalRecords");
}
};
</script>
<style lang="sass">
.center
text-align: center
section
margin-bottom: 4rem
</style>

View file

@ -1,11 +1,11 @@
<template> <template>
<LayoutNavbarPrivate> <LayoutNavbarPrivate>
<section v-if="runningRecords.length > 0"> <section v-if="runningRecords.length > 0">
<h2 class="center">Tracking</h2> <h2 class="center bold">Tracking</h2>
<JugglRecordsList :records="runningRecords" running /> <JugglRecordsList :records="runningRecords" running />
</section> </section>
<section> <section>
<h2 class="center">Projects</h2> <h2 class="center bold">Projects</h2>
<div v-if="finishedProjects.length > 0"> <div v-if="finishedProjects.length > 0">
<JugglProjectsPanel :projects="finishedProjects" /> <JugglProjectsPanel :projects="finishedProjects" />
</div> </div>
@ -14,8 +14,13 @@
</div> </div>
</section> </section>
<section v-if="finishedRecords.length > 0"> <section v-if="finishedRecords.length > 0">
<h2 class="center">Finished</h2> <h2 class="center bold">History</h2>
<JugglRecordsList :records="finishedRecords" /> <JugglRecordsList :records="finishedRecords" />
<div class="center">
<b-button to="/history" variant="outline-secondary" center
>Show all</b-button
>
</div>
</section> </section>
</LayoutNavbarPrivate> </LayoutNavbarPrivate>
</template> </template>
@ -49,7 +54,7 @@ export default {
created: () => { created: () => {
store.dispatch("loadProjects"); store.dispatch("loadProjects");
store.dispatch("loadRunningRecords"); store.dispatch("loadRunningRecords");
store.dispatch("loadRecords", { limit: 25, finished: true }); store.dispatch("loadRecords", { limit: 10, finished: true });
} }
}; };
</script> </script>
@ -57,6 +62,8 @@ export default {
<style lang="sass"> <style lang="sass">
.center .center
text-align: center text-align: center
.bold
font-weight: bold font-weight: bold
section section