Commit b3529f54 authored by yova's avatar yova

Merge branch 'develop' into 'master'

Develop

See merge request !18
parents 28c5d3ed 7ecc5f3f
VUE_APP_I18N_LOCALE=en
VUE_APP_I18N_FALLBACK_LOCALE=en
VUE_APP_POD="https://base.solid.community/public/budget.ttl"
VUE_APP_SUBFOLDER="public/solidbase/"
......@@ -61,6 +61,64 @@ We use the [vuex](https://vuex.vuejs.org/) store as interface between the applic
To learn about representing semantic, linked open data in a machine-readable form, refer to the [Solid platform notes](https://github.com/solid/solid#solid-platform-notes), read the [Turtle intro](https://linkeddata.github.io/rdflib.js/Documentation/turtle-intro.html) and the [tutorial for using rdflib.js](https://github.com/solid/solid-tutorial-rdflib.js#tutorial-for-using-rdflibjs) for more details.
### Vocabulary of meta data
The meta data of this application has been designed following this ontology:
```mermaid
graph LR
ME("solid:webId")-- "rdf:type" -->INI("essglobal:SSEInitiative")
ME-- "essglobal:totalOfMemebers" -->l6(literal)
ME-- "vf:name" --> l5(literal)
BGT("#budget")-- "dc:valid" --> l1(literal)
BGT -- "vf:budgetUnit" --> qudt:unit
BGT -- "vf:preparedFor" --> ME
PROC("vf:Process") -- "vf:estimatedDemand" --> INT("vf:Intent")
PROC -- "vf:name" --> l2(literal)
PROC -- "vf:includedIn" --> PROC
INT -- "vf:includedIn" --> INT
INT -- "vf:name" --> l3(literal)
INT -- "vf:intendedQuantity" --> QNT(qudt:QuantityValue)
QNT -- "qudt:numericValue" --> l4(literal)
```
We are using third-party vocabularies and prefixes as context:
```
@prefix dc "http://purl.org/dc/terms/"
@prefix essglobal "http://purl.org/essglobal/vocab/v1.1/"
@prefix qudt "http://qudt.org/schema/qudt/"
@prefix rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
@prefix vf "https://lab.allmende.io/solidbase/valueflows/raw/plan/release-doc-in-process/budget.ttl#"
```
#### `#budget`
The RDF descriptor `#budget` carries resource-global properties of the budgeting data. As for now we only want to enable annual budgets, `dc:valid` should carry the number of the year the budget is prepared for.
We also want to deal with single currency budgets only, so the currency can be stored as a `qudt:unit` on the global level.
#### solid:WebId
The WebID is the SoLiD authenticated agent for which the `#budget` is prepared. It is of type `essglobal:SSEInitiative`. Its (planned) total members for the year can be defined, and a name can be set.
#### `vf:Process`
The `process` is all economic activity for achieving results. It transforms inputs into outputs. In its planning stage it carries `intents` with it. `Intents` for planned output, but also `intents` for planned input, of which we are most interested in its monetary value.
To enable different levels of abstraction, the *processes* can be nested (`vf:includedIn`) into each other. One level could be *business branch*, another *production procedure*.
[*Processes* on vaueflo.ws](https://www.valueflo.ws/introduction/processes.html)
#### `vf:Intent`
In our app we want to organize and present planned *expenses*. These are intended inputs for the processes which carry an economic value, in our case always the *price* for the expense.
Intents can also be nested. This allows for categorization of costs in higher level processes.
Intents are automatically inherited from a less abstract to a more abstract process.
## Authors
Produced by
......
import { Pie, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: Pie,
mixins: [ reactiveProp ],
props: [ 'data', 'options' ],
mounted () {
this.renderChart( this.data, this.options )
}
}
<template>
<div>
<button @click="changeMembers()">change members</button>
<button @click="saveData()">Save Data</button>
<button @click="resetBudget()">reset Budget</button>
<hr>
<div>
<button class="button is-small is-dark" @click="isProcessModalActive = true">add Process</button>
<button class="button is-small is-dark" @click="isIntentModalActive = true">add Expense</button>
<div class="buttons" style="align-items: center;justify-content: center;">
<button class="button is-primary" @click="changeMembers()">change members</button>
<button class="button is-primary" @click="isSaveModalActive = true">
<span class="icon is-small"><i class="fas fa-check"></i></span>
<span>Save</span>
</button>
<button class="button is-primary" @click="isLoadModalActive = true">Load</button>
<button class="button is-primary" @click="isProcessModalActive = true">add Activity</button>
<button class="button is-primary" @click="isIntentModalActive = true">add Expense</button>
</div>
<editPintModalButton type="process" :item="focus" @valueChanged="$emit('repaintChart',$event)"/>
<b-modal :active.sync="isProcessModalActive" has-modal-card>
<addProcessModal :focus="this.focus" @repaintChart="$emit('repaintChart',$event)"></addProcessModal>
</b-modal>
<b-modal :active.sync="isIntentModalActive" has-modal-card>
<addIntentModal :focus="this.focus" @repaintChart="$emit('repaintChart',$event)"></addIntentModal>
</b-modal>
<b-modal :active.sync="isSaveModalActive" has-modal-card>
<saveModal/>
</b-modal>
<b-modal :active.sync="isLoadModalActive" has-modal-card>
<loadBudgetModal @repaintChart="$emit('repaintChart',$event)"/>
</b-modal>
</div>
</div>
</template>
......@@ -23,18 +34,27 @@
import addIntentModal from '@/components/addIntentModal'
import addProcessModal from '@/components/addProcessModal'
import saveModal from '@/components/saveModal'
import loadBudgetModal from '@/components/loadBudgetModal'
import editPintModalButton from '@/components/editPintModalButton.vue'
export default {
name: 'ControlField',
props: ['focus'],
components: {
addIntentModal,
addProcessModal
addProcessModal,
saveModal,
loadBudgetModal,
editPintModalButton
},
data: function () {
return {
isProcessModalActive: false,
isIntentModalActive: false,
isSaveModalActive: false,
isLoadModalActive: false,
isEditModalActive: false,
processName: '',
returnProcess: null,
intentName: '',
......@@ -57,54 +77,6 @@ export default {
this.$emit('repaintChart')
}.bind(this)
})
},
resetBudget: function() { // create datastructure budgetline
var farmlandId= {}
var farmworkId={}
var gardenlandId= {}
var gardenworkId={}
var CSAId={}
var farmId={}
var gardenId={}
if (! this.$store.state.webId) {
this.$toast.open(`Please login first!`)
return false
}
this.$store.commit('initGraph')
this.$store.commit('addProcess', {name: 'farming', ID: farmId})
this.$store.commit('createIntent', {name: 'farmland', value: 30, ID: farmlandId})
this.$store.dispatch('addIntentId', {intentId: farmlandId.value, processId: farmId.value })
this.$store.commit('createIntent', {name: 'farmwork', value: 10, ID: farmworkId })
this.$store.dispatch('addIntentId', {intentId: farmworkId.value, processId: farmId.value })
this.$store.commit('addProcess', {name: 'market garden' , ID: gardenId})
this.$store.commit('createIntent', {name: 'gardenland', value: 20, ID: gardenlandId})
this.$store.dispatch('addIntentId', { intentId: gardenlandId.value, processId: gardenId.value })
this.$store.commit('createIntent', {name: 'gardenwork', value: 10, ID: gardenworkId})
this.$store.dispatch('addIntentId', {intentId: gardenworkId.value, processId: gardenId.value })
this.$store.commit('addProcess', {name: 'CSA', value: 0 , ID: CSAId})
// this.$store.commit('addProcess', {name: 'market garden'})
console.log('IDs: ' + farmlandId.value + ',' + farmworkId.value + '\ngraph:\n' + this.$store.state.graph);
// this.$store.commit('addRelation', {higher: farmId.value,lower: farmlandId.value})
// this.$store.commit('addRelation', {higher: farmId.value,lower: farmworkId.value})
// this.$store.commit('addRelation', {higher: gardenId.value,lower: gardenlandId.value})
// this.$store.commit('addRelation', {higher: gardenId.value,lower: gardenworkId.value})
this.$store.dispatch('addRelation', {higher: CSAId.value,lower: farmId.value})
this.$store.dispatch('addRelation', {higher: CSAId.value,lower: gardenId.value})
this.$store.dispatch('saveuri')
console.log('IDs: ' + JSON.stringify(farmId) + ',' + JSON.stringify(farmId.value) + '\ngraph:\n' + this.$store.state.graph);
this.$emit('repaintChart')
},
saveData () {
this.$store.dispatch('saveuri')
}
}
}
......
<template>
<section>
<InputField v-for="item in data" :key="item.ID.value" :label="item.name" :value="parseInt(item.value)" :ID="item.ID" :type="type" @valueChanged="$emit('repaintChart',$event)"/>
</section>
</template>
<script>
import InputField from '@/components/InputField.vue'
export default {
name: 'Form',
components: {
InputField
},
props: ['data', 'type'],
methods: {
}
}
</script>
<template>
<b-field :label="label">
<b-input v-model.number="returnValue" type="number" v-if="!displayValue"
min="0"
max="10000000">
</b-input>
<div v-if="displayValue">:&nbsp;{{this.value}}&nbsp;{{this.$store.getters.currency}}&nbsp;</div>
<button v-if="this.type=='process'" class="button is-small is-dark" @click="setFocus()">Focus</button>
<button class="button is-small is-dark" @click="isEditModalActive = true">Edit {{this.type == 'intent' ? 'expense' : 'process'}}</button>
<b-modal :active.sync="isEditModalActive" has-modal-card>
<editPintModal :type="this.type" :label="this.label" :ID="this.ID" @valueChanged="$emit('valueChanged',$event)"></editPintModal>
</b-modal>
</b-field>
</template>
<script>
import editPintModal from '@/components/editPintModal.vue'
export default {
name: 'InputField',
props: [ 'label', 'value' , 'ID', 'type'],
data: function () {
return {
returnValue: this.value,
returnLabel: this.label,
isEditModalActive: false
}
},
components: {
editPintModal
},
watch: {
returnValue: function (newValue) {
this.$store.commit('changeIntentValue',{ID: this.ID, value: newValue})
this.$emit('valueChanged', this.$store.getters.processOfIntent(this.ID))
}
},
computed: {
displayValue: function() { // display only value instead of input form
return (this.type=='process') || (this.$store.getters.hasIncludes(this.ID))
}
},
methods: {
setFocus: function() {
this.$emit('valueChanged', this.ID)
}
}
}
</script>
<template>
<section>
<div v-if="groupName"><b>{{ groupName }}</b><br><br></div>
<b-field v-for="item in data" :key="item.ID.value" @valueChanged="$emit('repaintChart',$event)">
<b-field style="flex-wrap:wrap;justify-content:flex-end;align-items:center;">
<b>{{ item.processName ? '' : item.name + ':' }}</b>
<div v-if="item.processName != undefined">
<span>from <a @click="$emit('valueChanged',item.processId)">{{ item.processName }}</a> comes <b>{{item.name}}</b>:</span>
</div>
&nbsp;
<b-input v-if="!displayValue(item)"
@input="valueChanged(item)"
v-model.number="item.value"
type="number"
min="0"
max="10000000">
</b-input>
<div v-if="displayValue(item)">&nbsp;{{parseInt(item.value)}}</div>
&nbsp;&nbsp;{{$store.getters.currency}}&nbsp;
<div class="field is-grouped" style="flex-wrap:wrap;justify-content:flex-end;align-items:center;">
<p class="control">
<button v-if="type=='process'" class="button" @click="setFocus(item.ID)">Focus</button>
</p>
<p class="control">
<editPintModalButton :type="type" :item="item" @valueChanged="$emit('valueChanged',$event)"/>
</p>
<p class="control">
<b-collapse v-if="type=='intent' && $store.getters.hasIncludes(item.ID)" :open="false">
<button class="button" slot="trigger">Toggle details</button>
<div class="notification">
<div class="content">
<InputForm :data='includes(item.ID)'
type="intent"
@valueChanged="$emit('valueChanged',$event)"/>
</div>
</div>
</b-collapse>
</p>
</div>
</b-field>
</b-field>
</section>
</template>
<script>
/* eslint-disable no-console */
import editPintModalButton from '@/components/editPintModalButton.vue'
import IntentSelector from '@/components/IntentSelector.vue'
export default {
name: 'InputForm',
props: [ 'data', 'type', 'groupName'],
components: {
editPintModalButton,
IntentSelector
},
data: function () {
return {
label: null,
value: null,
ID: null,
processName: null,
returnValue: this.value,
returnLabel: this.label,
selected: null,
includedIntents: null,
}
},
watch: {
selected: function () {
this.$emit('valueChanged', this.selected.processId)
}
},
computed: {
},
methods: {
valueChanged: function (item) {
this.$store.commit('changeIntentValue',{ID: item.ID, value: item.value})
this.$emit('valueChanged', this.$store.getters.processOfIntent(item.ID))
},
displayValue: function(item) { // display only value instead of input form
return (this.type=='process') || (this.$store.getters.hasIncludes(item.ID))
},
setFocus: function(ID) {
console.log('setFocus:');
console.log(ID);
this.$emit('valueChanged', ID)
},
includes: function(ID){
var descs=this.$store.getters.descendants(ID)
var names =[]
var i,proc,procname
for (i=0;i<descs.length;i++) {
proc= this.$store.getters.processOfIntent(descs[i])
if (proc == undefined) procname="none"
else procname = this.$store.getters.processName(proc)
names.push({processName: procname,name: this.$store.getters.processName(descs[i]), ID: descs[i], processId: proc, value: this.$store.getters.intentValue(descs[i])})
}
this.includedIntents = names
return names
}
},
mounted () {
this.includes()
},
}
</script>
<template>
<b-select v-model="selectedIntent" @input="$emit('input',$event)">
<option
v-for="intent in siblings()"
:value="intent"
:key="intent.ID.value">
{{ intent.name}}
</option>
</b-select>
</template>
<script>
/* eslint-disable no-console*/
export default {
name: 'IntentSelector',
props: ['currentIntent', 'currentProc'],
data () {
return {
selectedIntent: null,
proc: null
}
},
methods: {
siblings: function () {
var i,index
if (this.currentProc) {
console.log('intentselector:');
console.log(this.currentProc);
return this.$store.getters.intentsList(this.currentProc)
} else {
this.proc = this.$store.getters.processOfIntent(this.currentIntent)
var intents = this.$store.getters.intentsList(this.proc)
var intentIds = intents.map(a => a.ID)
index = intentIds.indexOf(this.currentIntent)
intents.splice(index,1)
return intents
}
}
}
}
</script>
<!-- extended login Button with status report -->
<template>
<div>
<button @click="login()" >{{ loggedIn ? 'Log out' : 'Log in'}} </button><br>
<button @click="login()" >{{ this.$store.getters.isLoggedIn ? 'Log out' : 'Log in'}} </button><br>
</div>
</template>
<script>
/* eslint-disable no-console */
import auth from 'solid-auth-client'
const popupUri = 'https://solid.community/common/popup.html'
export default {
name: 'LoginButton',
mounted () {
auth.trackSession(session => { this.loggedIn= session }) // Tell me, why does this work?
},
data: function () {
return {
loggedIn: false
}
if (!this.$store.getters.isLoggedIn) this.trackSession()
},
watch: {
loggedIn: function () {
this.$store.commit('setLoggedIn',this.loggedIn)
this.$store.dispatch('trackSession')
}
},
methods: {
login () {
(this.loggedIn) ? auth.logout().then(this.$store.dispatch('trackSession')) : auth.popupLogin({ popupUri });
(this.$store.getters.isLoggedIn) ? auth.logout().then(()=>{this.trackSession()}) : auth.popupLogin({ popupUri }).then(() => {this.trackSession()}) //TODO: Beautify, still apparently multiple calls to tracksession
},
trackSession() {
if (!this.$store.getters.isLoggedIn) this.$store.dispatch('trackSession').then(
session => {
this.$store.commit('setSession',session)
},
() => {
console.log('You are currently logged off')
this.$toast.open({
message:'To use this app please log in',
type: 'is-danger',
duration: 1000
})
}
)
}
}
}
......
......@@ -11,7 +11,7 @@ export default {
name: 'PersonalInfo',
computed: {
loggedIn: function () {
if (this.$store.state.loggedIn) {
if (this.$store.getters.isLoggedIn) {
return 'You are logged in as<br><b>' + this.$store.state.name +'</b><br>Your WebID is:<br><code><a href=' + this.$store.state.webId +' >' + this.$store.state.webId + '</code></a>'
} else {
return ""
......
<template>
<b-field label="Set associated process to: ">
<b-field label="Set associated activity to: ">
<b-select v-model="selectedProcess" @input="$emit('input',$event)">
<option
v-for="process in procs"
......
<template>
<div class="modal-card" style="width: auto">
<div class="modal-card" style="width: auto" @keyup.enter.capture="saveIntent()">
<header class="modal-card-head">
<p class="modal-card-title">Add expense</p>
</header>
<section class="modal-card-body">
<b-field label="Name">
<b-input v-model="intentName" placeholder='Enter Name of expense'></b-input>
<b-field label="Name" >
<b-input v-model="intentName"
placeholder="Enter Name of expense"
ref="nameField">
</b-input>
</b-field>
<b-field label="Amount">
<b-input v-model.number="intentValue"></b-input>
<b-input v-model.number="intentValue" @keyup.enter="saveIntent()"></b-input>
</b-field>
<ProcessSelector :currentProc="this.focus.ID" @input="newParentProcess($event)"/>
<b-field label="Include in expense">
<IntentSelector :currentProc="this.returnProcess" @input="newIntInc($event)"/>
</b-field>
</section>
<footer class="modal-card-foot">
<button class="button is-small is-dark" @click="saveIntent()">Save!</button>
<footer class="modal-card-foot" style="justify-content:space-between">
<button class="button is-success is-rounded" @click="saveIntent()">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>Add</span>
</button>
<button class="button is-warning is-rounded" @click="$parent.close()">
<span class="icon is-small">
<i class="fas fa-times"></i>
</span>
<span>Cancel</span>
</button>
</footer>
</div>
</template>
<script>
/* eslint-disable no-console */
import ProcessSelector from '@/components/ProcessSelector.vue'
import IntentSelector from '@/components/IntentSelector.vue'
export default {
name: 'addIntentModal',
props: ['focus'],
......@@ -27,15 +49,23 @@ export default {
return {
intentName: '',
intentValue: 5555,
returnProcess: null
returnProcess: this.focus.ID,
IntInc: null // include the new intent into
}
},
components: {
ProcessSelector
ProcessSelector,
IntentSelector
},
mounted() {
this.$refs.nameField.focus()
},
methods: {
newParentProcess: function(process) {
this.returnProcess=process
newParentProcess: function(proc) {
this.returnProcess=proc
},
newIntInc(intent) {
this.IntInc=intent
},
saveIntent() {
var ID={}
......@@ -43,7 +73,11 @@ export default {
if (this.returnProcess==null) this.returnProcess=this.focus.ID
this.$store.commit('createIntent', {name: this.intentName, value: this.intentValue, ID: ID})
this.$store.dispatch('addIntentId', {intentId: ID.value, processId: this.returnProcess })
if (this.IntInc!=null) {
this.$store.dispatch('add2ndIntent', {IntInc:this.IntInc, ID: ID, proc: this.returnProcess})
} else {
this.$store.dispatch('addIntentId', {intentId: ID.value, processId: this.returnProcess })
}
this.$parent.close()
this.$emit('repaintChart', this.returnProcess)
}
......
<template>
<div class="modal-card" style="width: auto">
<div class="modal-card" style="width: auto" @keyup.enter.capture="saveProcess()">
<header class="modal-card-head">
<p class="modal-card-title">Add process</p>
<p class="modal-card-title">Add activity</p>
</header>
<section class="modal-card-body">
<b-field label="Name">
<b-input v-model="lowerProcessName" placeholder='Enter process name'></b-input>
<b-input v-model="lowerProcessName"
placeholder='Enter activity name'
ref="nameField">
</b-input>
</b-field>
<ProcessSelector :currentProc="this.focus.ID" @input="newParentProcess($event)"/>
</section>
<footer class="modal-card-foot">
<button class="button is-small is-dark" @click="saveProcess()">Save!</button>
<footer class="modal-card-foot" style="justify-content:space-between">
<button class="button is-success is-rounded" @click="saveProcess()">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>Add</span>
</button>
<button class="button is-warning is-rounded" @click="$parent.close()">
<span class="icon is-small">
<i class="fas fa-times"></i>
</span>
<span>Cancel</span>
</button>
</footer>
</div>
</template>
<script>
/* eslint-disable no-console*/
import ProcessSelector from '@/components/ProcessSelector.vue'
export default {
name: 'addIntentModal',
......@@ -29,6 +45,9 @@ export default {
components: {
ProcessSelector
},
mounted() {
this.$refs.nameField.focus()
},
methods: {
newParentProcess: function(proc) {
this.higherProcessId=proc
......
<template>
<div class="modal-card" style="width: auto">
<div class="modal-card" style="width: auto" @keyup.enter.capture="save()">
<header class="modal-card-head">
<p class="modal-card-title">Edit properties of {{this.type}} {{this.label}}.</p>
<p class="modal-card-title">Edit properties of {{this.type == 'process' ? 'activity' : 'expense'}} {{this.label}}.</p>
</header>
<section class="modal-card-body">
......@@ -10,19 +10,45 @@
</b-field>
<div v-if="relChange">
<ProcessSelector :currentProc="parentProcess" :exclude="[this.ID]" @input="newParentProcess($event)"/>
<div v-if="this.type=='intent' && !this.$store.getters.hasIncludes(this.ID)" >
<button class="button is-small is-dark" @click="deleteIntent()">Delete</button>
<div v-if="type=='process' || !this.$store.getters.isIntent2nd(this.ID)">
<ProcessSelector :currentProc="parentProcess" :exclude="[this.ID]" @input="newParentProcess($event)"/>
</div>
<div v-if="this.type=='process'">
<button class="button is-small is-dark" @click="deleteProcess()">Delete</button>
</div>
</div>
<div v-else-if="this.type=='intent'">
Include in expense:<br>
<IntentSelector :currentIntent="this.ID" @input="newLabel=$event"/>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-small is-dark" @click="save()">Save!</button>
<footer class="modal-card-foot" style="justify-content:space-between">
<button class="button is-success is-rounded" @click="save()">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>Save</span>
</button>
<div v-if="this.type=='intent' && !this.$store.getters.hasIncludes(this.ID)" >
<button class="button is-danger is-rounded" @click="deleteIntent()">
<span class="icon is-small">
<i class="fas fa-times"></i>
</span>
<span>Delete</span>
</button>
</div>
<div v-if="this.type=='process'">
<button class="button is-danger is-rounded" @click="deleteProcess()">
<span class="icon is-small">
<i class="fas fa-times"></i>
</span>
<span>Delete</span>