Added manage structure page

This commit is contained in:
Maximilian Giller 2021-01-07 23:19:18 +01:00
parent e1154ec33b
commit 37701a0484
17 changed files with 818 additions and 5 deletions

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

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

View file

@ -222,6 +222,46 @@ function updateRecord($user_id, $record)
$db->execute();
}
function updateProject($user_id, $project)
{
$project_id = $project["project_id"];
// Update given parameters
$data = [];
$props = ["name", "start_date"];
foreach ($props as $p) {
if (array_key_exists ($p, $project)) {
$data[$p] = $project[$p];
}
}
$db = new DbOperations();
$db->update("projects", $data);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("project_id", Comparison::EQUAL, $project_id);
$db->execute();
}
function updateRecordTag($user_id, $tag)
{
$record_tag_id = $tag["record_tag_id"];
// Update given parameters
$data = [];
$props = ["name"];
foreach ($props as $p) {
if (array_key_exists ($p, $tag)) {
$data[$p] = $tag[$p];
}
}
$db = new DbOperations();
$db->update("record_tags", $data);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_tag_id", Comparison::EQUAL, $record_tag_id);
$db->execute();
}
function removeRecord($user_id, $params)
{
$record_id = $params->get("record_id");
@ -233,6 +273,49 @@ function removeRecord($user_id, $params)
$db->execute();
}
function removeProject($user_id, $params)
{
$project_id = $params->get("project_id");
$db = new DbOperations();
$db->delete("projects");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("project_id", Comparison::EQUAL, $project_id);
$db->execute();
removeRecordsOfProject($user_id, $project_id);
}
function removeRecordsOfProject($user_id, $project_id)
{
$db = new DbOperations();
$db->delete("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("project_id", Comparison::EQUAL, $project_id);
$db->execute();
}
function removeRecordTag($user_id, $params)
{
$record_tag_id = $params->get("record_tag_id");
$db = new DbOperations();
$db->delete("record_tags");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_tag_id", Comparison::EQUAL, $record_tag_id);
$db->execute();
removeRecordTagFromAssociations($record_tag_id);
}
function removeRecordTagFromAssociations($record_tag_id)
{
$db = new DbOperations();
$db->delete("tags_on_records");
$db->where("record_tag_id", Comparison::EQUAL, $record_tag_id);
$db->execute();
}
function updateTimeRecord($user_id, $params)
{
$data = [];

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

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

View file

@ -0,0 +1,72 @@
<template>
<div>
<b-button
:pressed.sync="triggered"
:size="size"
class="inline"
:variant="variant"
>
<slot />
</b-button>
<div id="confirm-popover" v-if="triggered" class="inline">
<div class="inline">{{ msg }}</div>
<b-button
@click="triggered = false"
:size="size"
class="inline"
variant="outline-secondary"
>
Cancel
</b-button>
<b-button
@click="() => confirm()"
:size="size"
class="inline"
variant="outline-danger"
>
Yes
</b-button>
</div>
</div>
</template>
<script>
export default {
name: "BaseConfirmButton",
props: {
size: {
required: false,
type: String
},
variant: {
required: false,
type: String,
default: "outline-danger"
},
msg: {
required: false,
type: String,
default: "Sure?"
}
},
data() {
return {
triggered: false
};
},
methods: {
confirm: function() {
this.$emit("click");
}
}
};
</script>
<style lang="sass" scoped>
.inline
display: inline-block
#confirm-popover *
margin-left: 1rem
</style>

View file

@ -0,0 +1,111 @@
<template>
<b-form @submit="submitForm">
<b-form-group id="id-group" label-for="id" label="Record ID">
<b-form-input id="id" v-model="form.project_id" required trim disabled>
</b-form-input>
</b-form-group>
<b-form-group id="name-group" label-for="name" label="Name">
<b-form-input id="name" v-model="form.name" required trim> </b-form-input>
</b-form-group>
<b-form-group id="startdate-group" label="Start date" label-for="startdate">
<b-form-datepicker
id="startdate"
v-model="form.start_date"
required
placeholder="Choose a 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-button variant="outline-secondary" @click="() => cancel()" class="left">
Cancel
</b-button>
<BaseConfirmButton
class="left"
@click="() => deleteProject()"
variant="outline-danger"
>
<b-icon class="icon-btn" icon="trash" />
</BaseConfirmButton>
</b-form>
</template>
<script>
import BaseConfirmButton from "@/components/base/BaseConfirmButton";
import store from "@/store";
export default {
name: "FormProjectDetails",
components: {
BaseConfirmButton
},
props: {
project: {
type: Object,
required: true
}
},
data() {
return {
failed: false,
working: false,
form: {
project_id: undefined,
start_date: undefined,
name: undefined
}
};
},
methods: {
/**
* Submits the form. Assupmtion: Form is valid, based on required flags.
*/
submitForm: function(e) {
e.preventDefault();
this.failed = false;
this.working = true;
store
.dispatch("updateProject", this.form)
.then(() => {
this.working = false;
this.$emit("submit");
})
.catch(() => {
this.failed = true;
this.working = false;
});
return false;
},
cancel: function() {
this.$emit("cancel");
},
deleteProject: function() {
store.dispatch("removeProject", this.form.project_id);
}
},
created: function() {
this.form.project_id = this.project.project_id;
this.form.name = this.project.name;
this.form.start_date = this.project.start_date;
}
};
</script>
<style lang="sass">
.left
float: left !important
margin-right: 1rem
.right
float: right !important
margin-left: 1rem
</style>

View file

@ -106,22 +106,26 @@
<b-button variant="outline-secondary" @click="() => cancel()" class="left">
Cancel
</b-button>
<b-button
<BaseConfirmButton
class="left"
@click="() => deleteRecord()"
variant="outline-danger"
>
<b-icon class="icon-btn" icon="trash" />
</b-button>
</BaseConfirmButton>
</b-form>
</template>
<script>
import { helperService } from "@/services/helper.service.js";
import BaseConfirmButton from "@/components/base/BaseConfirmButton";
import store from "@/store";
export default {
name: "FormRecordDetails",
components: {
BaseConfirmButton
},
props: {
record: {
type: Object,

View file

@ -0,0 +1,99 @@
<template>
<b-form @submit="submitForm">
<b-form-group id="id-group" label-for="id" label="Record ID">
<b-form-input id="id" v-model="form.record_tag_id" required trim disabled>
</b-form-input>
</b-form-group>
<b-form-group id="name-group" label-for="name" label="Name">
<b-form-input id="name" v-model="form.name" required trim> </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
</b-button>
<b-button variant="outline-secondary" @click="() => cancel()" class="left">
Cancel
</b-button>
<BaseConfirmButton
class="left"
@click="() => deleteTag()"
variant="outline-danger"
>
<b-icon class="icon-btn" icon="trash" />
</BaseConfirmButton>
</b-form>
</template>
<script>
import BaseConfirmButton from "@/components/base/BaseConfirmButton";
import store from "@/store";
export default {
name: "FormTagDetails",
components: {
BaseConfirmButton
},
props: {
tag: {
type: Object,
required: true
}
},
data() {
return {
failed: false,
working: false,
form: {
record_tag_id: undefined,
name: undefined
}
};
},
methods: {
/**
* Submits the form. Assupmtion: Form is valid, based on required flags.
*/
submitForm: function(e) {
e.preventDefault();
this.failed = false;
this.working = true;
store
.dispatch("updateTag", this.form)
.then(() => {
this.working = false;
this.$emit("submit");
})
.catch(() => {
this.failed = true;
this.working = false;
});
return false;
},
cancel: function() {
this.$emit("cancel");
},
deleteTag: function() {
store.dispatch("removeTag", this.form.record_tag_id);
}
},
created: function() {
this.form.record_tag_id = this.tag.record_tag_id;
this.form.name = this.tag.name;
}
};
</script>
<style lang="sass">
.left
float: left !important
margin-right: 1rem
.right
float: right !important
margin-left: 1rem
</style>

View file

@ -33,6 +33,9 @@
<hr />
<FormTagAdd @submit="() => reloadTags()" />
<b-link to="/manage">
Manage tags
</b-link>
</div>
</b-popover>
</div>

View file

@ -5,6 +5,7 @@ import Login from "../views/Login.vue";
import NotFound from "../views/NotFound.vue";
import Home from "../views/Home.vue";
import History from "../views/History.vue";
import Manage from "../views/Manage.vue";
Vue.use(VueRouter);
@ -24,6 +25,12 @@ const routes = [
component: History,
beforeEnter: requireAuth
},
{
path: "/manage",
name: "Manage",
component: Manage,
beforeEnter: requireAuth
},
{
path: "/home",
name: "Home",

View file

@ -63,6 +63,32 @@ export const jugglService = {
});
},
removeProject(projectId) {
return apiService
.post("/removeProject.php", {
project_id: projectId
})
.then(r => {
return {
data: r.data,
msg: ""
};
});
},
removeRecordTag(tagId) {
return apiService
.post("/removeRecordTag.php", {
record_tag_id: tagId
})
.then(r => {
return {
data: r.data,
msg: ""
};
});
},
getRecords(options = {}) {
var payload = {};
@ -94,6 +120,32 @@ export const jugglService = {
});
},
updateProject(project) {
var payload = {
project: project
};
return apiService.post("/updateProject.php", payload).then(r => {
return {
data: r.data,
msg: ""
};
});
},
updateRecordTag(tag) {
var payload = {
record_tag: tag
};
return apiService.post("/updateRecordTag.php", payload).then(r => {
return {
data: r.data,
msg: ""
};
});
},
getRunningRecords() {
return apiService.post("/getRunningRecords.php").then(r => {
return {

View file

@ -101,6 +101,16 @@ export const juggl = {
return Object.values(getters.records).filter(
record => record.record_id !== id
);
},
getProjectsExceptId: (state, getters) => id => {
return Object.values(getters.projects).filter(
project => project.project_id !== id
);
},
getTagsExceptId: (state, getters) => id => {
return Object.values(getters.tags).filter(
tag => tag.record_tag_id !== id
);
}
},
actions: {
@ -272,6 +282,36 @@ export const juggl = {
return true;
});
},
removeProject({ commit, getters }, projectId) {
if (projectId === undefined) {
return false;
}
return jugglService
.removeProject(projectId)
.catch(() => {
return false;
})
.then(() => {
commit("setProjects", getters.getProjectsExceptId(projectId));
return true;
});
},
removeTag({ commit, getters }, tagId) {
if (tagId === undefined) {
return false;
}
return jugglService
.removeRecordTag(tagId)
.catch(() => {
return false;
})
.then(() => {
commit("setTags", getters.getTagsExceptId(tagId));
return true;
});
},
loadSavedLogin() {
var userId = localStorage.getItem("userId");
var apiKey = localStorage.getItem("apiKey");
@ -302,6 +342,42 @@ export const juggl = {
commit("setRecords", records);
return true;
});
},
updateTag({ commit, getters }, tag) {
if (tag.record_tag_id === undefined) {
return;
}
return jugglService
.updateRecordTag(tag)
.catch(() => {
return false;
})
.then(() => {
// TODO: Return updated tag from API
var tags = getters.getTagsExceptId(tag.record_tag_id);
tags.push(tag);
commit("setTags", tags);
return true;
});
},
updateProject({ commit, getters }, project) {
if (project.project_id === undefined) {
return;
}
return jugglService
.updateProject(project)
.catch(() => {
return false;
})
.then(() => {
// TODO: Return updated project from API
var projects = getters.getProjectsExceptId(project.project_id);
projects.push(project);
commit("setProjects", projects);
return true;
});
}
}
};

View file

@ -55,8 +55,11 @@ body
a
color: $font-link !important
&:hover
&.btn:hover
color: $background-primary !important
&:hover
color: $primary !important
.b-dropdown, .dropdown
border-radius: 0

View file

@ -5,7 +5,14 @@
</div>
<section v-if="!working">
<div class="controls">
<b-button :download="downloadFilename" :href="'data:text/plain;charset=utf-8,' + encodeURIComponent(fileOutput)" variant="outline-secondary">Download data</b-button>
<b-button
:download="downloadFilename"
:href="
'data:text/plain;charset=utf-8,' + encodeURIComponent(fileOutput)
"
variant="outline-secondary"
>Download data</b-button
>
</div>
<JugglRecordsList :records="finishedRecords" />
</section>
@ -34,7 +41,9 @@ export default {
return store.getters.finishedRecords;
},
downloadFilename: function() {
return "juggl_data_" + helperService.dateAsFilenameString(new Date()) + ".json";
return (
"juggl_data_" + helperService.dateAsFilenameString(new Date()) + ".json"
);
},
fileOutput: function() {
var content = {

View file

@ -11,6 +11,9 @@
</div>
<div id="add-project-form">
<FormProjectAdd />
<b-link to="/manage">
Manage projects
</b-link>
</div>
</section>
<section v-if="finishedRecords.length > 0">

175
src/views/Manage.vue Normal file
View file

@ -0,0 +1,175 @@
<template>
<LayoutNavbarPrivate title="Manage structures">
<section id="projects">
<h1>Projects</h1>
<FormProjectAdd class="bottom-space" />
<b-table
:items="allProjects"
hover
:busy="working"
:fields="project_fields"
sort-by="name"
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(duration)="data">
{{ getDurationTimestamp(data.item.duration) }}
</template>
<template #cell(details)="data">
<b-button
size="sm"
@click="data.toggleDetails"
variant="outline-secondary"
>
<b-icon class="icon-btn" icon="gear" />
</b-button>
</template>
<template #row-details="data">
<b-card>
<FormProjectDetails
:project="data.item"
@cancel="data.toggleDetails"
@submit="data.toggleDetails"
/>
</b-card>
</template>
</b-table>
</section>
<section id="tags">
<h1>Tags</h1>
<FormTagAdd class="bottom-space" />
<b-table
:items="allTags"
hover
:busy="working"
:fields="tag_fields"
sort-by="name"
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(details)="data">
<b-button
size="sm"
@click="data.toggleDetails"
variant="outline-secondary"
>
<b-icon class="icon-btn" icon="gear" />
</b-button>
</template>
<template #row-details="data">
<b-card>
<FormTagDetails
:tag="data.item"
@cancel="data.toggleDetails"
@submit="data.toggleDetails"
/>
</b-card>
</template>
</b-table>
</section>
</LayoutNavbarPrivate>
</template>
<script>
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
import FormProjectDetails from "@/components/forms/FormProjectDetails";
import FormProjectAdd from "@/components/forms/FormProjectAdd";
import FormTagDetails from "@/components/forms/FormTagDetails";
import { helperService } from "@/services/helper.service.js";
import FormTagAdd from "@/components/forms/FormTagAdd";
import store from "@/store";
export default {
name: "Home",
components: {
LayoutNavbarPrivate,
FormProjectDetails,
FormProjectAdd,
FormTagDetails,
FormTagAdd
},
data: () => {
return {
working: true,
project_fields: [
{
key: "name",
label: "Name"
},
{
key: "start_date",
label: "Start date"
},
{
key: "duration",
label: "Duration"
},
{
key: "record_count",
label: "Records"
},
{
key: "details",
label: "Details"
}
],
tag_fields: [
{
key: "name",
label: "Name"
},
{
key: "details",
label: "Details"
}
]
};
},
created: function() {
store.dispatch("loadProjects");
store
.dispatch("loadTags")
.then(() => {
this.working = false;
})
.catch(() => {
this.working = false;
});
},
computed: {
allTags: function() {
return Object.values(store.getters.tags);
},
allProjects: function() {
return Object.values(store.getters.projects);
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style lang="sass">
.center
text-align: center
.bottom-space
margin-bottom: 1rem
section
margin-bottom: 4rem
</style>