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
$start_time = getTimeRecord($user_id, $record_id)["start_time"];
$data = [
$record = [
"record_id" => $record_id,
"end_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->update("time_records", $data);
$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
id="starttime"
v-model="times.start.time"
show-seconds
required
placeholder="Choose a start time"
dark
>
</b-form-timepicker>
</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-datepicker
id="enddate"
v-model="times.end.date"
required
:required="times.finished"
:disabled="!times.finished"
placeholder="Choose an end date"
:min="times.start.date"
dark
>
</b-form-datepicker>
</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">
Something went wrong.
</b-form-invalid-feedback>
<b-button variant="primary" type="submit" class="right" :disabled="working">
<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-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 { helperService } from "@/services/helper.service.js";
import store from "@/store";
export default {
@ -75,14 +110,50 @@ export default {
data() {
return {
times: {
start: {},
end: {}
start: { date: undefined, time: undefined },
end: { date: undefined, time: undefined },
finished: false,
duration: 0
},
selectableProjects: [],
failed: 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: {
/**
* Submits the form. Assupmtion: Form is valid, based on required flags.
@ -92,7 +163,21 @@ export default {
this.failed = false;
this.working = true;
store.dispatch("updateRecord", this.resultRecord).then(() => {
this.working = false;
this.$emit("submit");
}).catch(() => {
this.failed = true;
this.working = false;
});
return false;
},
cancel: function() {
this.$emit("cancel");
},
deleteRecord: function() {
store.dispatch("removeRecord", this.record.record_id);
}
},
created: function() {
@ -104,6 +189,9 @@ export default {
this.selectableProjects = projects;
// Load record times
this.times.finished = !this.record.running;
this.times.duration = Math.round(this.record.duration);
let dateAndTime = /([0-9-]*)\s([0-9:]*)/;
var startMatch = String(this.record.start_time).match(dateAndTime);
this.times.start.date = startMatch[1];
@ -115,7 +203,7 @@ export default {
this.times.end.time = endMatch[2];
} else {
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">
.left
float: left !important
margin-right: 1rem
.right
float: right !important
margin-left: 1rem
</style>

View file

@ -27,25 +27,11 @@
</template>
<template #cell(details)="data">
<b-button
size="sm"
@click="() => detailsRecord(data.item.record_id)"
variant="outline-dark"
>
<b-button size="sm" @click="data.toggleDetails" 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"
@ -55,15 +41,29 @@
<b-icon class="icon-btn" icon="check" />
</b-button>
</template>
<template #row-details="data">
<b-card>
<FormRecordDetails
:record="data.item"
@cancel="data.toggleDetails"
@submit="data.toggleDetails"
/>
</b-card>
</template>
</b-table>
</template>
<script>
import store from "@/store";
import FormRecordDetails from "@/components/forms/FormRecordDetails";
import { helperService } from "@/services/helper.service.js";
export default {
name: "JugglRecordsList",
components: {
FormRecordDetails
},
props: {
records: {
required: true,
@ -72,7 +72,7 @@ export default {
sortDesc: {
required: false,
type: Boolean,
default: false
default: true
},
running: {
required: false,
@ -102,10 +102,6 @@ export default {
}
],
runningFields: [
{
key: "abort",
label: "Abort"
},
{
key: "stop",
label: "Stop"
@ -141,12 +137,8 @@ export default {
stopRecord: function(id) {
store.dispatch("endRecord", id);
},
abortRecord: function(id) {
store.dispatch("removeRecord", 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 Login from "../views/Login.vue";
import Home from "../views/Home.vue";
import History from "../views/History.vue";
import RecordDetails from "../views/RecordDetails.vue";
Vue.use(VueRouter);
@ -17,6 +18,12 @@ const routes = [
name: "Login",
component: Login
},
{
path: "/history",
name: "History",
component: History,
beforeEnter: requireAuth
},
{
path: "/home",
name: "Home",
@ -49,6 +56,8 @@ const router = new VueRouter({
* Checks authentication before proceeding
*/
function requireAuth(to, from, next) {
store.dispatch("loadSavedLogin");
if (!store.getters.isLoggedIn) {
next({
path: "/login",

View file

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

View file

@ -136,5 +136,15 @@ export const helperService = {
timestamp = days + ":" + 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 = {};
if (limit !== undefined && limit > 0) {
payload.limit = limit;
if (options.limit !== undefined && options.limit > 0) {
payload.limit = options.limit;
}
if (finished !== undefined) {
payload.finished = finished;
if (options.finished !== undefined) {
payload.finished = options.finished;
}
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() {
return apiService.post("/getRunningRecords.php").then(r => {
return {
@ -105,7 +118,7 @@ export const jugglService = {
})
.then(r => {
return {
data: r.data,
data: processRecords(r.data),
msg: ""
};
});
@ -120,7 +133,7 @@ export const jugglService = {
})
.then(r => {
return {
data: r.data,
data: processRecords(r.data),
msg: ""
};
});

View file

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

View file

@ -22,15 +22,6 @@ export const juggl = {
setRecords(state, 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;
},
@ -103,13 +94,17 @@ export const juggl = {
return Object.values(getters.records).find(
record => record.record_id === id
);
},
getRecordsExceptId: (state, getters) => id => {
return Object.values(getters.records).filter(
record => record.record_id !== id
);
}
},
actions: {
loadProjects({ commit }) {
return jugglService.getProjects().then(r => {
commit("setProjects", r.data.projects);
commit("usingProjects", true);
});
},
loadUser({ commit }) {
@ -124,14 +119,15 @@ export const juggl = {
});
},
loadRecords({ commit, state }, { limit, finished }) {
commit("setRecordsLimit", limit);
return jugglService
.getRecords({ limit: state.recordsLimit, finished: finished })
.then(r => {
commit("setRecords", r.data.records);
commit("usingFinishedRecords", true);
commit("usingRunningRecords", true);
});
if (limit !== undefined) {
commit("setRecordsLimit", limit);
}
var payload = { limit: state.recordsLimit, finished: finished };
return jugglService.getRecords(payload).then(r => {
commit("setRecords", r.data.records);
});
},
loadRunningRecords({ commit, getters }) {
return jugglService.getRunningRecords().then(r => {
@ -140,7 +136,6 @@ export const juggl = {
...r.data.records
};
commit("setRecords", allRecords);
commit("usingRunningRecords", true);
});
},
login({ commit, getters }, { userId, apiKey }) {
@ -169,14 +164,20 @@ export const juggl = {
commit("setUser", undefined);
commit("logout");
},
endRecord(context, recordId) {
endRecord({ getters }, recordId) {
if (recordId === undefined) {
return false;
}
return jugglService
.endRecord(recordId)
.catch(() => {
return false;
})
.then(() => {
this.dispatch("updateState");
// TODO: Return ended record from API
var record = getters.getRecordById(recordId);
record.running = false;
return true;
});
},
@ -187,45 +188,40 @@ export const juggl = {
return false;
})
.then(() => {
this.dispatch("updateState");
this.dispatch("loadProjects");
return true;
});
},
startRecord(context, projectId) {
if (projectId === undefined) {
return false;
}
return jugglService
.startRecord(projectId)
.catch(() => {
return false;
})
.then(() => {
this.dispatch("updateState");
this.dispatch("loadRunningRecords");
return true;
});
},
removeRecord(context, recordId) {
removeRecord({ commit, getters }, recordId) {
if (recordId === undefined) {
return false;
}
return jugglService
.removeRecord(recordId)
.catch(() => {
return false;
})
.then(() => {
this.dispatch("updateState");
commit("setRecords", getters.getRecordsExceptId(recordId));
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");
@ -235,6 +231,27 @@ export const juggl = {
}
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
$background-primary: #222
$background-secondary: #000
$background-secondary: #191919
// $background-gradient: linear-gradient(165deg, $background-primary 65%, $background-secondary 100%)
$font-primary: $white
$font-inactive: $grey
$font-secondary: $primary
$font-inverted: $black
$font-link: $secondary
@ -53,6 +54,9 @@ body
a
color: $font-link !important
&:hover
color: $background-primary !important
.b-dropdown, .dropdown
border-radius: 0
@ -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)
color: $font-primary !important
input:disabled
color: $font-secondary !important
border-color: $grey !important
input:disabled, .b-form-timepicker[aria-disabled=true], .b-form-datepicker[aria-disabled=true]
color: $font-inactive !important
border-color: $background-primary !important
input
color: $font-primary !important
@ -123,6 +127,13 @@ select
background-color: $background-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/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>
<LayoutNavbarPrivate>
<section v-if="runningRecords.length > 0">
<h2 class="center">Tracking</h2>
<h2 class="center bold">Tracking</h2>
<JugglRecordsList :records="runningRecords" running />
</section>
<section>
<h2 class="center">Projects</h2>
<h2 class="center bold">Projects</h2>
<div v-if="finishedProjects.length > 0">
<JugglProjectsPanel :projects="finishedProjects" />
</div>
@ -14,8 +14,13 @@
</div>
</section>
<section v-if="finishedRecords.length > 0">
<h2 class="center">Finished</h2>
<h2 class="center bold">History</h2>
<JugglRecordsList :records="finishedRecords" />
<div class="center">
<b-button to="/history" variant="outline-secondary" center
>Show all</b-button
>
</div>
</section>
</LayoutNavbarPrivate>
</template>
@ -49,7 +54,7 @@ export default {
created: () => {
store.dispatch("loadProjects");
store.dispatch("loadRunningRecords");
store.dispatch("loadRecords", { limit: 25, finished: true });
store.dispatch("loadRecords", { limit: 10, finished: true });
}
};
</script>
@ -57,6 +62,8 @@ export default {
<style lang="sass">
.center
text-align: center
.bold
font-weight: bold
section