mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-21 23:23:51 +00:00
Merge remote-tracking branch 'origin/master' into issue185_ACCent_createdBy_updatedBy
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
import vis from 'vis-network/dist/vis-network.min';
|
||||
|
||||
require('./scss/vis.scss');
|
||||
|
||||
// create an array with nodes
|
||||
let nodes = new vis.DataSet([
|
||||
{ id: 1, label: "Node 1" },
|
||||
{ id: 2, label: "Node 2" },
|
||||
{ id: 3, label: "Node 3" },
|
||||
{ id: 4, label: "Node 4" },
|
||||
{ id: 5, label: "Node 5", cid: 1 },
|
||||
]);
|
||||
|
||||
// create an array with edges
|
||||
let edges = new vis.DataSet([
|
||||
{ from: 1, to: 3 },
|
||||
{ from: 1, to: 2 },
|
||||
{ from: 2, to: 4 },
|
||||
{ from: 2, to: 5 },
|
||||
{ from: 3, to: 3 },
|
||||
]);
|
||||
|
||||
// create a network
|
||||
let container = document.getElementById("graph-relationship");
|
||||
let data = {
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
};
|
||||
let options = {};
|
||||
|
||||
//
|
||||
let network = new vis.Network(container, data, options);
|
@@ -1,5 +0,0 @@
|
||||
div#graph-relationship {
|
||||
margin: 2em auto;
|
||||
height: 500px;
|
||||
border: 1px solid lightgray;
|
||||
}
|
@@ -16,16 +16,15 @@
|
||||
<comment v-if="accompanyingCourse.step === 'DRAFT'"></comment>
|
||||
<confirm v-if="accompanyingCourse.step === 'DRAFT'"></confirm>
|
||||
|
||||
<div v-for="error in errorMsg" class="vue-component errors alert alert-danger">
|
||||
<!-- <div v-for="error in errorMsg" v-bind:key="error.id" class="vue-component errors alert alert-danger">
|
||||
<p>
|
||||
<span>{{ error.sta }} {{ error.txt }}</span><br>
|
||||
<span>{{ $t(error.msg) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import Banner from './components/Banner.vue';
|
||||
import StickyNav from './components/StickyNav.vue';
|
||||
import OriginDemand from './components/OriginDemand.vue';
|
||||
@@ -55,11 +54,12 @@ export default {
|
||||
Comment,
|
||||
Confirm,
|
||||
},
|
||||
computed: mapState([
|
||||
'accompanyingCourse',
|
||||
'addressContext',
|
||||
'errorMsg'
|
||||
])
|
||||
computed: {
|
||||
...mapState([
|
||||
'accompanyingCourse',
|
||||
'addressContext'
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@@ -86,7 +86,8 @@ const postParticipation = (id, payload, method) => {
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
throw { msg: 'Error while sending AccompanyingPeriod Course participation.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
|
||||
// TODO: adjust message according to status code? Or how to access the message from the violation array?
|
||||
throw { msg: 'Error while sending AccompanyingPeriod Course participation', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -10,13 +10,13 @@
|
||||
<VueMultiselect
|
||||
name="selectOrigin"
|
||||
label="text"
|
||||
v-bind:custom-label="transText"
|
||||
:custom-label="transText"
|
||||
track-by="id"
|
||||
v-bind:multiple="false"
|
||||
v-bind:searchable="true"
|
||||
v-bind:placeholder="$t('origin.placeholder')"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="$t('origin.placeholder')"
|
||||
v-model="value"
|
||||
v-bind:options="options"
|
||||
:options="options"
|
||||
@select="updateOrigin">
|
||||
</VueMultiselect>
|
||||
|
||||
@@ -47,18 +47,18 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getOptions() {
|
||||
//console.log('loading origins list');
|
||||
getListOrigins().then(response => new Promise((resolve, reject) => {
|
||||
this.options = response.results;
|
||||
resolve();
|
||||
}));
|
||||
},
|
||||
updateOrigin(value) {
|
||||
//console.log('value', value);
|
||||
console.log('value', value);
|
||||
this.$store.dispatch('updateOrigin', value);
|
||||
},
|
||||
transText ({ text }) {
|
||||
return text.fr //TODO multilang
|
||||
const parsedText = JSON.parse(text);
|
||||
return parsedText.fr;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -77,7 +77,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
|
||||
},
|
||||
mutations: {
|
||||
catchError(state, error) {
|
||||
console.log('### mutation: a new error have been catched and pushed in store !', error);
|
||||
// console.log('### mutation: a new error have been catched and pushed in store !', error);
|
||||
state.errorMsg.push(error);
|
||||
},
|
||||
removeParticipation(state, participation) {
|
||||
|
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingSocialActions">
|
||||
<p>spinner</p>
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i>
|
||||
</div>
|
||||
|
||||
<div v-if="hasSocialActionPicked" id="persons">
|
||||
@@ -72,7 +72,7 @@
|
||||
{{ $t('action.save') }}
|
||||
</button>
|
||||
<button class="btn btn-save" v-show="isPostingWork" disabled>
|
||||
{{ $t('Save') }}
|
||||
{{ $t('action.save') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -0,0 +1,508 @@
|
||||
<template>
|
||||
|
||||
<div id="visgraph"></div>
|
||||
|
||||
<teleport to="#visgraph-legend">
|
||||
<div class="post-menu">
|
||||
<div class="list-group mt-4">
|
||||
<button type="button" class="list-group-item list-group-item-action btn btn-create" @click="createRelationship">
|
||||
{{ $t('visgraph.add_link') }}
|
||||
</button>
|
||||
<a type="button" class="list-group-item list-group-item-action btn btn-misc" id="exportCanvasBtn" @click="exportCanvasAsImage">
|
||||
<i class="fa fa-camera fa-fw"></i> {{ $t('visgraph.screenshot') }}
|
||||
</a>
|
||||
<button type="button" class="list-group-item list-group-item-action btn btn-light" @click="refreshNetwork">
|
||||
<i class="fa fa-refresh fa-fw"></i> {{ $t('visgraph.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="displayHelpMessage" class="alert alert-info mt-3">
|
||||
{{ $t('visgraph.create_link_help') }}
|
||||
</div>
|
||||
|
||||
<div class="my-4 legend">
|
||||
<h3>{{ $t('visgraph.Legend') }}</h3>
|
||||
<div class="list-group">
|
||||
<label class="list-group-item" v-for="layer in legendLayers">
|
||||
<input
|
||||
class="form-check-input me-1"
|
||||
type="checkbox"
|
||||
:value="layer.id"
|
||||
v-model="checkedLayers"
|
||||
@change="toggleLayer"
|
||||
/>
|
||||
{{ layer.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<teleport to="body">
|
||||
<modal v-if="modal.showModal" :modalDialogClass="modal.modalDialogClass" @close="modal.showModal = false">
|
||||
<template v-slot:header>
|
||||
<h2 class="modal-title">{{ $t(modal.title) }}</h2>
|
||||
<!-- {{ modal.data.id }} -->
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div v-if="modal.action === 'delete'">
|
||||
<p>{{ $t('visgraph.delete_confirmation_text') }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">{{ $t('visgraph.between') }}<br>{{ $t('visgraph.and') }}</div>
|
||||
<div class="col">
|
||||
<h4>{{ getPerson(modal.data.from).text }}</h4>
|
||||
<p class="text-start" v-if="relation && relation.title">
|
||||
<span v-if="reverse">
|
||||
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.from).text, getPerson(modal.data.to).text, relation.reverseTitle.fr.toLowerCase() ])}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.from).text, getPerson(modal.data.to).text, relation.title.fr.toLowerCase() ])}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<h4>{{ getPerson(modal.data.to).text }}</h4>
|
||||
<p class="text-end" v-if="relation && relation.title">
|
||||
<span v-if="reverse">
|
||||
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.to).text, getPerson(modal.data.from).text, relation.title.fr.toLowerCase() ])}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.to).text, getPerson(modal.data.from).text, relation.reverseTitle.fr.toLowerCase() ])}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<VueMultiselect
|
||||
id="relation"
|
||||
label="title"
|
||||
track-by="id"
|
||||
:custom-label="customLabel"
|
||||
:placeholder="$t('visgraph.choose_relation')"
|
||||
:close-on-select="true"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:options="relations"
|
||||
v-model="relation"
|
||||
:value="relation"
|
||||
>
|
||||
</VueMultiselect>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="reverse"
|
||||
v-model="reverse"
|
||||
>
|
||||
<label class="form-check-label" for="reverse">{{ $t('visgraph.reverse_relation') }}</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button class="btn" :class="modal.button.class" @click="submitRelationship">
|
||||
{{ $t(modal.button.text)}}
|
||||
</button>
|
||||
<button class="btn btn-delete" v-if="modal.action === 'edit'" @click="dropRelationship"></button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import vis from 'vis-network/dist/vis-network'
|
||||
import { mapState, mapGetters } from "vuex"
|
||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
|
||||
import VueMultiselect from 'vue-multiselect'
|
||||
import { getRelationsList, postRelationship, patchRelationship, deleteRelationship } from "./api";
|
||||
import { splitId } from "./vis-network";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Modal,
|
||||
VueMultiselect
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
container: '',
|
||||
checkedLayers: [],
|
||||
relations: [],
|
||||
displayHelpMessage: false,
|
||||
listenPersonFlag: 'normal',
|
||||
newEdgeData: {},
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-md",
|
||||
title: null,
|
||||
action: null,
|
||||
data: {
|
||||
type: 'relationship',
|
||||
from: null,
|
||||
to: null,
|
||||
relation: null,
|
||||
reverse: false
|
||||
},
|
||||
button: {
|
||||
class: null,
|
||||
text: null
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['nodes', 'edges',
|
||||
// not used 'isInWhitelist', 'isHouseholdLoading', 'isCourseLoaded', 'isRelationshipLoaded', 'isPersonLoaded', 'isExcludedNode', 'countLinksByNode', 'getParticipationsByCourse', 'getMembersByHousehold', 'getPersonsGroup',
|
||||
]),
|
||||
...mapState(['persons', 'households', 'courses', 'excludedNodesIds', 'updateHack',
|
||||
// not used 'links', 'relationships', 'whitelistIds', 'personLoadedIds', 'householdLoadingIds', 'courseLoadedIds', 'relationshipLoadedIds',
|
||||
]),
|
||||
|
||||
visgraph_data() {
|
||||
console.log('::: visgraph_data :::', this.nodes.length, 'nodes,', this.edges.length, 'edges')
|
||||
return {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges
|
||||
}
|
||||
},
|
||||
|
||||
refreshNetwork() {
|
||||
console.log('--- refresh network')
|
||||
window.network.setData(this.visgraph_data)
|
||||
},
|
||||
|
||||
legendLayers() {
|
||||
console.log('--- refresh legend and rebuild checked Layers')
|
||||
this.checkedLayers = []
|
||||
let layersDisplayed = [
|
||||
...this.nodes.filter(n => n.id.startsWith('household')),
|
||||
...this.nodes.filter(n => n.id.startsWith('accompanying'))
|
||||
]
|
||||
layersDisplayed.forEach(layer => {
|
||||
this.checkedLayers.push(layer.id)
|
||||
})
|
||||
return [
|
||||
...this.households,
|
||||
...this.courses
|
||||
]
|
||||
},
|
||||
|
||||
checkedLayers() { // required to refresh data checkedLayers
|
||||
console.log('--- checkedLayers')
|
||||
return this.checkedLayers
|
||||
},
|
||||
|
||||
relation: {
|
||||
get() {
|
||||
return this.modal.data.relation
|
||||
},
|
||||
set(value) {
|
||||
this.modal.data.relation = value
|
||||
}
|
||||
},
|
||||
|
||||
reverse: {
|
||||
get() {
|
||||
return this.modal.data.reverse
|
||||
},
|
||||
set(value) {
|
||||
this.modal.data.reverse = value
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
updateHack(newValue, oldValue) {
|
||||
console.log(`--- updateHack ${oldValue} <> ${newValue}`)
|
||||
if (oldValue !== newValue) {
|
||||
this.forceUpdateComponent()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
//console.log('=== mounted: init graph')
|
||||
this.initGraph()
|
||||
this.listenOnGraph()
|
||||
this.getRelationsList()
|
||||
},
|
||||
methods: {
|
||||
|
||||
initGraph() {
|
||||
this.container = document.getElementById('visgraph')
|
||||
// Instanciate vis objects in separate window variables, see vis-network.js
|
||||
window.network = new vis.Network(this.container, this.visgraph_data, window.options)
|
||||
},
|
||||
forceUpdateComponent() {
|
||||
//console.log('!! forceUpdateComponent !!')
|
||||
this.refreshNetwork
|
||||
this.$forceUpdate()
|
||||
},
|
||||
|
||||
// events
|
||||
listenOnGraph() {
|
||||
window.network.on('selectNode', (data) => {
|
||||
if (data.nodes.length > 1) {
|
||||
throw 'Multi selection is not allowed. Disable it in options.interaction !'
|
||||
}
|
||||
let node = data.nodes[0]
|
||||
let nodeType = splitId(node, 'type')
|
||||
switch (nodeType) {
|
||||
|
||||
case 'person':
|
||||
let person = this.nodes.filter(n => n.id === node)[0]
|
||||
console.log('@@@@@@ event on selected Node', person.id)
|
||||
if (this.listenPersonFlag === 'normal') {
|
||||
if (person.folded === true) {
|
||||
console.log(' @@> expand mode event')
|
||||
this.$store.commit('unfoldPerson', person)
|
||||
}
|
||||
} else {
|
||||
console.log(' @@> create link mode event')
|
||||
this.listenStepsToAddRelationship(person)
|
||||
}
|
||||
break
|
||||
|
||||
case 'household':
|
||||
let household = this.nodes.filter(n => n.id === node)[0]
|
||||
console.log('@@@@@@ event on selected Node', household.id)
|
||||
this.$store.dispatch('unfoldPersonsByHousehold', household)
|
||||
break
|
||||
|
||||
case 'accompanying_period':
|
||||
let course = this.nodes.filter(n => n.id === node)[0]
|
||||
console.log('@@@@@@ event on selected Node', course.id)
|
||||
this.$store.dispatch('unfoldPersonsByCourse', course)
|
||||
break
|
||||
|
||||
default:
|
||||
throw 'event is undefined for this type of node'
|
||||
}
|
||||
this.forceUpdateComponent()
|
||||
})
|
||||
window.network.on('selectEdge', (data) => {
|
||||
if (data.nodes.length !== 0 || data.edges.length !== 1) {
|
||||
return false //we don't want to trigger nodeEdge or multiselect !
|
||||
}
|
||||
let link = data.edges[0]
|
||||
let linkType = splitId(link, 'link')
|
||||
console.log('@@@@@ event on selected Edge', data.edges.length, linkType, data)
|
||||
|
||||
if (linkType.startsWith('relationship')) {
|
||||
//console.log('linkType relationship')
|
||||
|
||||
let relationships = this.edges.filter(l => l.id === link)
|
||||
if (relationships.length > 1) {
|
||||
throw 'error: only one link is allowed between two person!'
|
||||
}
|
||||
|
||||
let relationship = relationships[0]
|
||||
//console.log(relationship)
|
||||
|
||||
this.editRelationshipModal({
|
||||
from: relationship.from,
|
||||
to: relationship.to,
|
||||
id: relationship.id,
|
||||
relation: relationship.relation,
|
||||
reverse: relationship.reverse
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
listenStepsToAddRelationship(person) {
|
||||
console.log(' @@> listenStep', this.listenPersonFlag)
|
||||
if (this.listenPersonFlag === 'step2') {
|
||||
//console.log(' @@> person 2', person)
|
||||
this.newEdgeData.to = person.id
|
||||
this.addRelationshipModal(this.newEdgeData)
|
||||
this.displayHelpMessage = false
|
||||
this.listenPersonFlag = 'normal'
|
||||
this.newEdgeData = {}
|
||||
}
|
||||
if (this.listenPersonFlag === 'step1') {
|
||||
//console.log(' @@> person 1', person)
|
||||
this.newEdgeData.from = person.id
|
||||
this.listenPersonFlag = 'step2'
|
||||
}
|
||||
},
|
||||
|
||||
/// control Layers
|
||||
toggleLayer(value) {
|
||||
let id = value.target.value
|
||||
console.log('@@@@@@ toggle Layer', id)
|
||||
this.forceUpdateComponent()
|
||||
if (this.checkedLayers.includes(id)) {
|
||||
this.removeLayer(id)
|
||||
} else {
|
||||
this.addLayer(id)
|
||||
}
|
||||
},
|
||||
addLayer(id) {
|
||||
//console.log('+ addLayer', id)
|
||||
this.checkedLayers.push(id)
|
||||
this.$store.dispatch('excludedNode', ['remove', id])
|
||||
},
|
||||
removeLayer(id) {
|
||||
//console.log('- removeLayer', id)
|
||||
this.checkedLayers = this.checkedLayers.filter(i => i !== id)
|
||||
this.$store.dispatch('excludedNode', ['add', id])
|
||||
},
|
||||
|
||||
/// control Modal
|
||||
addRelationshipModal(edgeData) {
|
||||
//console.log('==- addRelationshipModal', edgeData)
|
||||
this.modal = {
|
||||
data: { from: edgeData.from, to: edgeData.to },
|
||||
action: 'create',
|
||||
showModal: true,
|
||||
title: 'visgraph.add_relationship_link',
|
||||
button: { class: 'btn-create', text: 'action.create' }
|
||||
}
|
||||
},
|
||||
editRelationshipModal(edgeData) {
|
||||
//console.log('==- editRelationshipModal', edgeData)
|
||||
this.modal = {
|
||||
data: edgeData,
|
||||
action: 'edit',
|
||||
showModal: true,
|
||||
title: 'visgraph.edit_relationship_link',
|
||||
button: { class: 'btn-edit', text: 'action.edit' }
|
||||
}
|
||||
},
|
||||
|
||||
// form
|
||||
resetForm() {
|
||||
this.modal = {
|
||||
data: { type: 'relationship', from: null, to: null, relation: null, reverse: false },
|
||||
action: null,
|
||||
title: null,
|
||||
button: { class: null, text: null, }
|
||||
}
|
||||
console.log('==- reset Form', this.modal.data)
|
||||
},
|
||||
getRelationsList() {
|
||||
//console.log('fetch relationsList')
|
||||
return getRelationsList().then(relations => new Promise(resolve => {
|
||||
//console.log('+ relations list', relations.results.length)
|
||||
this.relations = relations.results.filter(r => r.isActive === true)
|
||||
resolve()
|
||||
})).catch()
|
||||
},
|
||||
customLabel(value) {
|
||||
//console.log('customLabel', value)
|
||||
return (value.title && value.reverseTitle) ? `${value.title.fr} ↔ ${value.reverseTitle.fr}` : ''
|
||||
},
|
||||
getPerson(id) {
|
||||
let person = this.persons.filter(p => p.id === id)
|
||||
return person[0]
|
||||
},
|
||||
|
||||
// actions
|
||||
createRelationship() {
|
||||
this.displayHelpMessage = true
|
||||
this.listenPersonFlag = 'step1' // toggle listener in create link mode
|
||||
console.log(' @@> switch listener to create link mode:', this.listenPersonFlag)
|
||||
},
|
||||
dropRelationship() {
|
||||
//console.log('delete', this.modal.data)
|
||||
deleteRelationship(this.modal.data)
|
||||
.catch()
|
||||
this.$store.commit('removeLink', this.modal.data.id)
|
||||
this.modal.showModal = false
|
||||
this.resetForm()
|
||||
},
|
||||
submitRelationship() {
|
||||
console.log('submitRelationship', this.modal.action)
|
||||
switch (this.modal.action) {
|
||||
|
||||
case 'create':
|
||||
return postRelationship(this.modal.data)
|
||||
.then(relationship => new Promise(resolve => {
|
||||
console.log('post relationship response', relationship)
|
||||
this.$store.dispatch('addLinkFromRelationship', relationship)
|
||||
this.modal.showModal = false
|
||||
this.resetForm()
|
||||
resolve()
|
||||
}))
|
||||
.catch()
|
||||
|
||||
case 'edit':
|
||||
return patchRelationship(this.modal.data)
|
||||
.then(relationship => new Promise(resolve => {
|
||||
console.log('patch relationship response', relationship)
|
||||
this.$store.commit('updateLink', relationship)
|
||||
this.modal.showModal = false
|
||||
this.resetForm()
|
||||
resolve()
|
||||
}))
|
||||
.catch()
|
||||
|
||||
default:
|
||||
throw "uncaught action"
|
||||
}
|
||||
},
|
||||
|
||||
// export image
|
||||
exportCanvasAsImage() {
|
||||
const canvas = document.getElementById('visgraph')
|
||||
.querySelector('canvas')
|
||||
console.log(canvas)
|
||||
|
||||
let link = document.getElementById('exportCanvasBtn')
|
||||
link.download = "filiation.png"
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
console.log(blob)
|
||||
link.href = URL.createObjectURL(blob)
|
||||
}, 'image/png')
|
||||
|
||||
/*
|
||||
TODO improve feature
|
||||
|
||||
// 1. fonctionne, mais pas de contrôle sur le nom
|
||||
if (canvas && canvas.getContext('2d')) {
|
||||
let img = canvas.toDataURL('image/png;base64;')
|
||||
img = img.replace('image/png','image/octet-stream')
|
||||
window.open(img, '', 'width=1000, height=1000')
|
||||
}
|
||||
|
||||
// 2. fonctionne, mais 2 click et pas compatible avec tous les browsers
|
||||
let link = document.getElementById('exportCanvasBtn')
|
||||
link.download = "image.png"
|
||||
canvas.toBlob(blob => {
|
||||
link.href = URL.createObjectURL(blob)
|
||||
}, 'image/png')
|
||||
*/
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="vis-network/dist/dist/vis-network.min.css"></style>
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
div#visgraph {
|
||||
height: 700px;
|
||||
margin: auto;
|
||||
}
|
||||
div#visgraph-legend {
|
||||
div.post-menu.legend {
|
||||
}
|
||||
}
|
||||
.modal-mask {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.debug {
|
||||
margin: 1em; padding: 1em;
|
||||
color: dimgray;
|
||||
font-style: italic;
|
||||
font-size: 80%;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,195 @@
|
||||
import { splitId } from './vis-network'
|
||||
|
||||
/**
|
||||
* @function makeFetch
|
||||
* @param method
|
||||
* @param url
|
||||
* @param body
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const makeFetch = (method, url, body) => {
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8'
|
||||
},
|
||||
body: (body !== null) ? JSON.stringify(body) : null
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
return response.json().then(violations => {
|
||||
throw ValidationException(violations)
|
||||
});
|
||||
}
|
||||
|
||||
throw {
|
||||
msg: 'Error while updating AccompanyingPeriod Course.',
|
||||
sta: response.status,
|
||||
txt: response.statusText,
|
||||
err: new Error(),
|
||||
body: response.body
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param violations
|
||||
* @constructor
|
||||
*/
|
||||
const ValidationException = (violations) => {
|
||||
this.violations = violations
|
||||
this.name = 'ValidationException'
|
||||
}
|
||||
|
||||
/**
|
||||
* @function getFetch
|
||||
* @param url
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const getFetch = (url) => {
|
||||
return makeFetch('GET', url, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function postFetch
|
||||
* @param url
|
||||
* @param body
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const postFetch = (url, body) => {
|
||||
return makeFetch('POST', url, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function patchFetch
|
||||
* @param url
|
||||
* @param body
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const patchFetch = (url, body) => {
|
||||
return makeFetch('PATCH', url, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function deleteFetch
|
||||
* @param url
|
||||
* @param body
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const deleteFetch = (url, body) => {
|
||||
return makeFetch('DELETE', url, null)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @function getHouseholdByPerson
|
||||
* @param person
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const getHouseholdByPerson = (person) => {
|
||||
//console.log('getHouseholdByPerson', person.id)
|
||||
if (person.current_household_id === null) {
|
||||
throw 'Currently the person has not household!'
|
||||
}
|
||||
return getFetch(
|
||||
`/api/1.0/person/household/${person.current_household_id}.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function getCoursesByPerson
|
||||
* @param person
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const getCoursesByPerson = (person) => {
|
||||
//console.log('getCoursesByPerson', person._id)
|
||||
return getFetch(
|
||||
`/api/1.0/person/accompanying-course/by-person/${person._id}.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function getRelationshipsByPerson
|
||||
* @param person
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const getRelationshipsByPerson = (person) => {
|
||||
//console.log('getRelationshipsByPerson', person.id)
|
||||
return getFetch(
|
||||
`/api/1.0/relations/relationship/by-person/${person._id}.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of relations
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const getRelationsList = () => {
|
||||
return getFetch(`/api/1.0/relations/relation.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function postRelationship
|
||||
* @param relationship
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const postRelationship = (relationship) => {
|
||||
//console.log(relationship)
|
||||
return postFetch(
|
||||
`/api/1.0/relations/relationship.json`,
|
||||
{
|
||||
type: 'relationship',
|
||||
fromPerson: { type: 'person', id: splitId(relationship.from, 'id') },
|
||||
toPerson: { type: 'person', id: splitId(relationship.to, 'id') },
|
||||
relation: { type: 'relation', id: relationship.relation.id },
|
||||
reverse: relationship.reverse
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function patchRelationship
|
||||
* @param relationship
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const patchRelationship = (relationship) => {
|
||||
//console.log(relationship)
|
||||
let linkType = splitId(relationship.id, 'link')
|
||||
let id = splitId(linkType, 'id')
|
||||
return patchFetch(
|
||||
`/api/1.0/relations/relationship/${id}.json`,
|
||||
{
|
||||
type: 'relationship',
|
||||
fromPerson: { type: 'person', id: splitId(relationship.from, 'id') },
|
||||
toPerson: { type: 'person', id: splitId(relationship.to, 'id') },
|
||||
relation: { type: 'relation', id: relationship.relation.id },
|
||||
reverse: relationship.reverse
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @function deleteRelationship
|
||||
* @param relationship
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
const deleteRelationship = (relationship) => {
|
||||
//console.log(relationship)
|
||||
let linkType = splitId(relationship.id, 'link')
|
||||
let id = splitId(linkType, 'id')
|
||||
return deleteFetch(
|
||||
`/api/1.0/relations/relationship/${id}.json`
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
getHouseholdByPerson,
|
||||
getCoursesByPerson,
|
||||
getRelationshipsByPerson,
|
||||
getRelationsList,
|
||||
postRelationship,
|
||||
patchRelationship,
|
||||
deleteRelationship
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
const visMessages = {
|
||||
fr: {
|
||||
visgraph: {
|
||||
Course: 'Parcours',
|
||||
Household: 'Ménage',
|
||||
Holder: 'Titulaire',
|
||||
Legend: 'Calques',
|
||||
concerned: 'concerné',
|
||||
both: 'neutre, non binaire',
|
||||
woman: 'féminin',
|
||||
man: 'masculin',
|
||||
years: 'ans',
|
||||
click_to_expand: 'cliquez pour étendre',
|
||||
add_relationship_link: "Créer un lien de filiation",
|
||||
edit_relationship_link: "Modifier le lien de filiation",
|
||||
delete_relationship_link: "Êtes-vous sûr ?",
|
||||
delete_confirmation_text: "Vous allez supprimer le lien entre ces 2 usagers.",
|
||||
reverse_relation: "Inverser la relation",
|
||||
relation_from_to_like: "{2} de {1}", // disable {0}
|
||||
between: "entre",
|
||||
and: "et",
|
||||
add_link: "Créer un lien de filiation",
|
||||
create_link_help: "Pour créer un lien de filiation, cliquez d'abord sur un usager, puis sur un second ; précisez ensuite la nature du lien dans le formulaire d'édition.",
|
||||
refresh: "Rafraîchir",
|
||||
screenshot: "Prendre une photo",
|
||||
choose_relation: "Choisissez le lien de parenté",
|
||||
},
|
||||
edit: 'Éditer',
|
||||
del: 'Supprimer',
|
||||
back: 'Revenir en arrière',
|
||||
addNode: 'Ajouter un noeuds',
|
||||
addEdge: 'Ajouter un lien de filiation',
|
||||
editNode: 'Éditer le noeuds',
|
||||
editEdge: 'Éditer le lien',
|
||||
addDescription: 'Cliquez dans un espace vide pour créer un nouveau nœud.',
|
||||
edgeDescription: 'Cliquez sur un usager et faites glisser le lien vers un autre usager pour les connecter.',
|
||||
editEdgeDescription: 'Cliquez sur les points de contrôle et faites-les glisser vers un nœud pour les relier.',
|
||||
createEdgeError: 'Il est impossible de relier des arêtes à un cluster.',
|
||||
deleteClusterError: 'Les clusters ne peuvent pas être supprimés.',
|
||||
editClusterError: 'Les clusters ne peuvent pas être modifiés.'
|
||||
},
|
||||
en: {
|
||||
edit: 'Edit',
|
||||
del: 'Delete selected',
|
||||
back: 'Back',
|
||||
addNode: 'Add Node',
|
||||
addEdge: 'Add Link',
|
||||
editNode: 'Edit Switch',
|
||||
editEdge: 'Edit Link',
|
||||
addDescription: 'Click in an empty space to place a new node.',
|
||||
edgeDescription: 'Click on a node and drag the link to another node to connect them.',
|
||||
editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.',
|
||||
createEdgeError: 'Cannot link edges to a cluster.',
|
||||
deleteClusterError: 'Clusters cannot be deleted.',
|
||||
editClusterError: 'Clusters cannot be edited.'
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
visMessages
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { createApp } from "vue"
|
||||
import { store } from "./store.js"
|
||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
|
||||
import { visMessages } from './i18n'
|
||||
import App from './App.vue'
|
||||
|
||||
import './vis-network'
|
||||
|
||||
const i18n = _createI18n(visMessages)
|
||||
const container = document.getElementById('relationship-graph')
|
||||
const persons = JSON.parse(container.dataset.persons)
|
||||
|
||||
persons.forEach(person => {
|
||||
store.dispatch('addPerson', person)
|
||||
store.commit('markInWhitelist', person)
|
||||
})
|
||||
|
||||
const app = createApp({
|
||||
template: `<app></app>`
|
||||
})
|
||||
.use(store)
|
||||
.use(i18n)
|
||||
.component('app', App)
|
||||
.mount('#relationship-graph')
|
@@ -0,0 +1,534 @@
|
||||
import { createStore } from 'vuex'
|
||||
import { getHouseholdByPerson, getCoursesByPerson, getRelationshipsByPerson } from './api'
|
||||
import { getHouseholdLabel, getHouseholdWidth, getRelationshipLabel, getRelationshipTitle, getRelationshipDirection, splitId, getGender, getAge } from './vis-network'
|
||||
import {visMessages} from "./i18n";
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production'
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
state: {
|
||||
persons: [],
|
||||
households: [],
|
||||
courses: [],
|
||||
relationships: [],
|
||||
links: [],
|
||||
whitelistIds: [],
|
||||
personLoadedIds: [],
|
||||
householdLoadingIds: [],
|
||||
courseLoadedIds: [],
|
||||
relationshipLoadedIds: [],
|
||||
excludedNodesIds: [],
|
||||
updateHack: 0
|
||||
},
|
||||
getters: {
|
||||
nodes(state) {
|
||||
let nodes = []
|
||||
state.persons.forEach(p => {
|
||||
nodes.push(p)
|
||||
})
|
||||
state.households.forEach(h => {
|
||||
nodes.push(h)
|
||||
})
|
||||
state.courses.forEach(c => {
|
||||
nodes.push(c)
|
||||
})
|
||||
// except excluded nodes (unchecked layers)
|
||||
state.excludedNodesIds.forEach(excluded => {
|
||||
nodes = nodes.filter(n => n.id !== excluded)
|
||||
})
|
||||
return nodes
|
||||
},
|
||||
edges(state) {
|
||||
return state.links
|
||||
},
|
||||
isInWhitelist: (state) => (person_id) => {
|
||||
return state.whitelistIds.includes(person_id)
|
||||
},
|
||||
isHouseholdLoading: (state) => (household_id) => {
|
||||
return state.householdLoadingIds.includes(household_id)
|
||||
},
|
||||
isCourseLoaded: (state) => (course_id) => {
|
||||
return state.courseLoadedIds.includes(course_id)
|
||||
},
|
||||
isRelationshipLoaded: (state) => (relationship_id) => {
|
||||
return state.relationshipLoadedIds.includes(relationship_id)
|
||||
},
|
||||
isPersonLoaded: (state) => (person_id) => {
|
||||
return state.personLoadedIds.includes(person_id)
|
||||
},
|
||||
isExcludedNode: (state) => (id) => {
|
||||
return state.excludedNodesIds.includes(id)
|
||||
},
|
||||
|
||||
countLinksByNode: (state) => (node_id) => {
|
||||
let array = []
|
||||
state.links.filter(link => ! link.id.startsWith('relationship'))
|
||||
.forEach(link => {
|
||||
if (link.from === node_id || link.to === node_id) {
|
||||
if (state.excludedNodesIds.indexOf(splitId(link.id, 'link')) === -1) {
|
||||
array.push(link)
|
||||
}
|
||||
//console.log(link.id, state.excludedNodesIds.indexOf(splitId(link.id, 'link')))
|
||||
}
|
||||
})
|
||||
//console.log('count links', array.length, array.map(i => i.id))
|
||||
return array.length
|
||||
},
|
||||
|
||||
getParticipationsByCourse: (state) => (course_id) => {
|
||||
const course = state.courses.filter(c => c.id === course_id)[0]
|
||||
const currentParticipations = course.participations.filter(p => p.endDate === null)
|
||||
//console.log('get persons in', course_id, currentParticipations.map(p => p.person.id),
|
||||
// 'with folded', currentParticipations.filter(p => p.person.folded === true).map(p => p.person.id))
|
||||
return currentParticipations
|
||||
},
|
||||
|
||||
getMembersByHousehold: (state) => (household_id) => {
|
||||
const household = state.households.filter(h => h.id === household_id)[0]
|
||||
const currentMembers = household.members.filter(m => household.current_members_id.includes(m.id))
|
||||
//console.log('get persons in', household_id, currentMembers.map(m => m.person.id),
|
||||
// 'with folded', currentMembers.filter(m => m.person.folded === true).map(m => m.person.id))
|
||||
return currentMembers
|
||||
},
|
||||
|
||||
/**
|
||||
* This getter is a little bit mysterious :
|
||||
* The 2 previous getters return complete array, but folded (missing) persons are not taken into consideration and are not displayed (!?!)
|
||||
* This getter compare input array (participations|members) to personLoadedIds array
|
||||
* and return complete array with folded persons taken into consideration
|
||||
*
|
||||
* @param state
|
||||
* @param array - An array of persons from course or household.
|
||||
* This array is dirty, melting persons adapted (or not) to vis, with _id and _label.
|
||||
* @return array - An array of persons mapped and taken in state.persons
|
||||
*/
|
||||
getPersonsGroup: (state) => (array) => {
|
||||
let group = []
|
||||
array.forEach(item => {
|
||||
let id = splitId(item.person.id, 'id')
|
||||
if (state.personLoadedIds.includes(id)) {
|
||||
group.push(state.persons.filter(person => person._id === id)[0])
|
||||
}
|
||||
})
|
||||
//console.log('array', array.map(item => item.person.id))
|
||||
console.log('get persons group', group.map(f => f.id))
|
||||
return group
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
mutations: {
|
||||
addPerson(state, [person, options]) {
|
||||
let debug = ''
|
||||
/// Debug mode: uncomment to display person_id on visgraph
|
||||
//debug = `\nid ${person.id}`
|
||||
person.group = person.type
|
||||
person._id = person.id
|
||||
person.id = `person_${person.id}`
|
||||
person.label = `*${person.text}*\n_${getGender(person.gender)} - ${getAge(person.birthdate)}_${debug}` //
|
||||
person.folded = false
|
||||
// folded is used for missing persons
|
||||
if (options.folded) {
|
||||
person.title = visMessages.fr.visgraph.click_to_expand
|
||||
person._label = person.label // keep label
|
||||
person.label = null
|
||||
person.folded = true
|
||||
}
|
||||
state.persons.push(person)
|
||||
},
|
||||
addHousehold(state, household) {
|
||||
household.group = household.type
|
||||
household._id = household.id
|
||||
household.label = `${visMessages.fr.visgraph.Household} n° ${household.id}`
|
||||
household.id = `household_${household.id}`
|
||||
state.households.push(household)
|
||||
},
|
||||
addCourse(state, course) {
|
||||
course.group = course.type
|
||||
course._id = course.id
|
||||
course.label = `${visMessages.fr.visgraph.Course} n° ${course.id}`
|
||||
course.id = `accompanying_period_${course.id}`
|
||||
state.courses.push(course)
|
||||
},
|
||||
addRelationship(state, relationship) {
|
||||
relationship.group = relationship.type
|
||||
relationship._id = relationship.id
|
||||
relationship.id = `relationship_${relationship.id}`
|
||||
state.relationships.push(relationship)
|
||||
},
|
||||
addLink(state, link) {
|
||||
state.links.push(link)
|
||||
},
|
||||
updateLink(state, link) {
|
||||
console.log('updateLink', link)
|
||||
let link_ = {
|
||||
from: `person_${link.fromPerson.id}`,
|
||||
to: `person_${link.toPerson.id}`,
|
||||
id: 'relationship_' + splitId(link.id,'id')
|
||||
+ '-person_' + link.fromPerson.id + '-person_' + link.toPerson.id,
|
||||
arrows: getRelationshipDirection(link),
|
||||
color: 'lightblue',
|
||||
font: { color: '#33839d' },
|
||||
dashes: true,
|
||||
label: getRelationshipLabel(link),
|
||||
title: getRelationshipTitle(link),
|
||||
relation: link.relation,
|
||||
reverse: link.reverse
|
||||
}
|
||||
// find row position and replace by updatedLink
|
||||
state.links.splice(
|
||||
state.links.findIndex(item => item.id === link_.id), 1, link_
|
||||
)
|
||||
},
|
||||
removeLink(state, link_id) {
|
||||
state.links = state.links.filter(l => l.id !== link_id)
|
||||
},
|
||||
|
||||
//// id markers
|
||||
markInWhitelist(state, person) {
|
||||
state.whitelistIds.push(person.id)
|
||||
},
|
||||
markPersonLoaded(state, id) {
|
||||
state.personLoadedIds.push(id)
|
||||
},
|
||||
unmarkPersonLoaded(state, id) {
|
||||
state.personLoadedIds = state.personLoadedIds.filter(i => i !== id)
|
||||
},
|
||||
markHouseholdLoading(state, id) {
|
||||
//console.log('..loading household', id)
|
||||
state.householdLoadingIds.push(id)
|
||||
},
|
||||
unmarkHouseholdLoading(state, id) {
|
||||
state.householdLoadingIds = state.householdLoadingIds.filter(i => i !== id)
|
||||
},
|
||||
markCourseLoaded(state, id) {
|
||||
state.courseLoadedIds.push(id)
|
||||
},
|
||||
unmarkCourseLoaded(state, id) {
|
||||
state.courseLoadedIds = state.courseLoadedIds.filter(i => i !== id)
|
||||
},
|
||||
markRelationshipLoaded(state, id) {
|
||||
state.relationshipLoadedIds.push(id)
|
||||
},
|
||||
unmarkRelationshipLoaded(state, id) {
|
||||
state.relationshipLoadedIds = state.relationshipLoadedIds.filter(i => i !== id)
|
||||
},
|
||||
|
||||
//// excluded
|
||||
addExcludedNode(state, id) {
|
||||
//console.log('==> exclude list: +', id)
|
||||
state.excludedNodesIds.push(id)
|
||||
},
|
||||
removeExcludedNode(state, id) {
|
||||
//console.log('<== exclude list: -', id)
|
||||
state.excludedNodesIds = state.excludedNodesIds.filter(e => e !== id)
|
||||
},
|
||||
|
||||
//// unfold
|
||||
unfoldPerson(state, person) {
|
||||
//console.log('unfoldPerson', person)
|
||||
person.label = person._label
|
||||
delete person._label
|
||||
delete person.title
|
||||
person.folded = false
|
||||
},
|
||||
|
||||
//// force update hack
|
||||
updateHack(state) {
|
||||
state.updateHack = state.updateHack + 1
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* Expand loop (steps 1->10), always start from a person.
|
||||
* Fetch household, courses, relationships, and others persons.
|
||||
* These persons are "missing" and will be first display in fold mode.
|
||||
*
|
||||
* 1) Add a new person
|
||||
* @param object
|
||||
* @param person
|
||||
*/
|
||||
addPerson({ commit, dispatch }, person) {
|
||||
commit('markPersonLoaded', person.id)
|
||||
commit('addPerson', [person, { folded: false }])
|
||||
commit('updateHack')
|
||||
dispatch('fetchInfoForPerson', person)
|
||||
},
|
||||
|
||||
/**
|
||||
* 2) Fetch infos for this person (hub)
|
||||
* @param object
|
||||
* @param person
|
||||
*/
|
||||
fetchInfoForPerson({ dispatch }, person) {
|
||||
// TODO enfants hors ménages
|
||||
// example: household 61
|
||||
// console.log(person.text, 'household', person.current_household_id)
|
||||
if (null !== person.current_household_id) {
|
||||
dispatch('fetchHouseholdForPerson', person)
|
||||
}
|
||||
dispatch('fetchCoursesByPerson', person)
|
||||
dispatch('fetchRelationshipByPerson', person)
|
||||
},
|
||||
|
||||
/**
|
||||
* 3) Fetch person current household (if it is not already loading)
|
||||
* check first isHouseholdLoading to fetch household once
|
||||
* @param object
|
||||
* @param person
|
||||
*/
|
||||
fetchHouseholdForPerson({ commit, getters, dispatch }, person) {
|
||||
//console.log(' isHouseholdLoading ?', getters.isHouseholdLoading(person.current_household_id))
|
||||
if (! getters.isHouseholdLoading(person.current_household_id)) {
|
||||
commit('markHouseholdLoading', person.current_household_id)
|
||||
getHouseholdByPerson(person)
|
||||
.then(household => new Promise(resolve => {
|
||||
commit('addHousehold', household)
|
||||
// DISABLED: in init or expand loop, layer is uncheck when added
|
||||
//commit('addExcludedNode', household.id)
|
||||
//commit('updateHack')
|
||||
dispatch('addLinkFromPersonsToHousehold', household)
|
||||
commit('updateHack')
|
||||
resolve()
|
||||
})
|
||||
).catch( () => {
|
||||
commit('unmarkHouseholdLoading', person.current_household_id)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 4) Add an edge for each household member (household -> person)
|
||||
* @param object
|
||||
* @param household
|
||||
*/
|
||||
addLinkFromPersonsToHousehold({ commit, getters, dispatch }, household) {
|
||||
let members = getters.getMembersByHousehold(household.id)
|
||||
console.log('add link for', members.length, 'members')
|
||||
members.forEach(m => {
|
||||
commit('addLink', {
|
||||
from: `${m.person.type}_${m.person.id}`,
|
||||
to: `household_${m.person.current_household_id}`,
|
||||
id: `household_${m.person.current_household_id}-person_${m.person.id}`,
|
||||
arrows: 'from',
|
||||
color: 'pink',
|
||||
font: { color: '#D04A60' },
|
||||
label: getHouseholdLabel(m),
|
||||
width: getHouseholdWidth(m),
|
||||
})
|
||||
if (!getters.isPersonLoaded(m.person.id)) {
|
||||
dispatch('addMissingPerson', [m.person, household])
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 5) Fetch AccompanyingCourses for the person
|
||||
* @param object
|
||||
* @param person
|
||||
*/
|
||||
fetchCoursesByPerson({ commit, dispatch }, person) {
|
||||
getCoursesByPerson(person)
|
||||
.then(courses => new Promise(resolve => {
|
||||
dispatch('addCourses', courses)
|
||||
resolve()
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* 6) Add each distinct course (a person can have multiple courses)
|
||||
* @param object
|
||||
* @param courses
|
||||
*/
|
||||
addCourses({ commit, getters, dispatch }, courses) {
|
||||
let currentCourses = courses.filter(c => c.closingDate === null)
|
||||
currentCourses.forEach(course => {
|
||||
//console.log(' isCourseLoaded ?', getters.isCourseLoaded(course.id))
|
||||
if (! getters.isCourseLoaded(course.id)) {
|
||||
commit('markCourseLoaded', course.id)
|
||||
commit('addCourse', course)
|
||||
commit('addExcludedNode', course.id) // in init or expand loop, layer is uncheck when added
|
||||
dispatch('addLinkFromPersonsToCourse', course)
|
||||
commit('updateHack')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 7) Add an edge for each course participation (course <- person)
|
||||
* @param object
|
||||
* @param course
|
||||
*/
|
||||
addLinkFromPersonsToCourse({ commit, getters, dispatch }, course) {
|
||||
const participations = getters.getParticipationsByCourse(course.id)
|
||||
console.log('add link for', participations.length, 'participations')
|
||||
participations.forEach(p => {
|
||||
//console.log(p.person.id)
|
||||
commit('addLink', {
|
||||
from: `${p.person.type}_${p.person.id}`,
|
||||
to: `${course.id}`,
|
||||
id: `accompanying_period_${splitId(course.id,'id')}-person_${p.person.id}`,
|
||||
arrows: 'from',
|
||||
color: 'orange',
|
||||
font: { color: 'darkorange' },
|
||||
})
|
||||
if (!getters.isPersonLoaded(p.person.id)) {
|
||||
dispatch('addMissingPerson', [p.person, course])
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 8) Fetch Relationship
|
||||
* @param object
|
||||
* @param person
|
||||
*/
|
||||
fetchRelationshipByPerson({ dispatch }, person) {
|
||||
//console.log('fetchRelationshipByPerson', person)
|
||||
getRelationshipsByPerson(person)
|
||||
.then(relationships => new Promise(resolve => {
|
||||
dispatch('addRelationships', relationships)
|
||||
resolve()
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* 9) Add each distinct relationship
|
||||
* @param object
|
||||
* @param relationships
|
||||
*/
|
||||
addRelationships({ commit, getters, dispatch }, relationships) {
|
||||
relationships.forEach(relationship => {
|
||||
//console.log(' isRelationshipLoaded ?', getters.isRelationshipLoaded(relationship.id))
|
||||
if (! getters.isRelationshipLoaded(relationship.id)) {
|
||||
commit('markRelationshipLoaded', relationship.id)
|
||||
commit('addRelationship', relationship)
|
||||
dispatch('addLinkFromRelationship', relationship)
|
||||
commit('updateHack')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 10) Add an edge for each relationship (person -> person)
|
||||
* @param object
|
||||
* @param relationship
|
||||
*/
|
||||
addLinkFromRelationship({ commit, getters, dispatch }, relationship) {
|
||||
//console.log('-> addLink from person', relationship.fromPerson.id, 'to person', relationship.toPerson.id)
|
||||
commit('addLink', {
|
||||
from: `person_${relationship.fromPerson.id}`,
|
||||
to: `person_${relationship.toPerson.id}`,
|
||||
id: 'relationship_' + splitId(relationship.id,'id')
|
||||
+ '-person_' + relationship.fromPerson.id + '-person_' + relationship.toPerson.id,
|
||||
arrows: getRelationshipDirection(relationship),
|
||||
color: 'lightblue',
|
||||
font: { color: '#33839d' },
|
||||
dashes: true,
|
||||
label: getRelationshipLabel(relationship),
|
||||
title: getRelationshipTitle(relationship),
|
||||
relation: relationship.relation,
|
||||
reverse: relationship.reverse
|
||||
})
|
||||
for (let person of [relationship.fromPerson, relationship.toPerson]) {
|
||||
if (!getters.isPersonLoaded(person.id)) {
|
||||
dispatch('addMissingPerson', [person, relationship])
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add missing person. node is displayed without label (folded).
|
||||
* We stop here and listen on events to unfold person and expand its fetch infos
|
||||
* @param object
|
||||
* @param array
|
||||
*/
|
||||
addMissingPerson({ commit, getters, dispatch }, [person, parent]) {
|
||||
console.log('! add missing Person', person.id)
|
||||
commit('markPersonLoaded', person.id)
|
||||
commit('addPerson', [person, { folded: true }])
|
||||
if (getters.isExcludedNode(parent.id)) {
|
||||
// in init or expand loop, exclude too missing persons if parent have been excluded
|
||||
commit('addExcludedNode', person.id)
|
||||
}
|
||||
commit('updateHack')
|
||||
},
|
||||
|
||||
/**
|
||||
* ==================================================================
|
||||
* Triggered by a vis-network event when clicking on a Course Node.
|
||||
* Each folded node is unfold, then expanded with fetch infos
|
||||
* @param object
|
||||
* @param course
|
||||
*/
|
||||
unfoldPersonsByCourse({ getters, commit, dispatch }, course) {
|
||||
const participations = getters.getParticipationsByCourse(course.id)
|
||||
getters.getPersonsGroup(participations)
|
||||
.forEach(person => {
|
||||
if (person.folded === true) {
|
||||
console.log('-=. unfold and expand person', person.id)
|
||||
commit('unfoldPerson', person)
|
||||
dispatch('fetchInfoForPerson', person)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggered by a vis-network event when clicking on a Household Node.
|
||||
* Each folded node is unfold, then expanded with fetch infos
|
||||
* @param object
|
||||
* @param household
|
||||
*/
|
||||
unfoldPersonsByHousehold({ getters, commit, dispatch }, household) {
|
||||
const members = getters.getMembersByHousehold(household.id)
|
||||
getters.getPersonsGroup(members)
|
||||
.forEach(person => {
|
||||
if (person.folded === true) {
|
||||
console.log('-=. unfold and expand person', person.id)
|
||||
commit('unfoldPerson', person)
|
||||
dispatch('fetchInfoForPerson', person)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* ==================================================================
|
||||
* For an excluded node, add|remove relative persons excluded too
|
||||
* @param object
|
||||
* @param array (add|remove action, id)
|
||||
*/
|
||||
excludedNode({ getters, commit }, [action, id]) {
|
||||
const personGroup = () => {
|
||||
switch (splitId(id, 'type')) {
|
||||
case 'accompanying_period':
|
||||
return getters.getParticipationsByCourse(id)
|
||||
case 'household':
|
||||
return getters.getMembersByHousehold(id)
|
||||
default:
|
||||
throw 'undefined case with this id'
|
||||
}
|
||||
}
|
||||
let group = getters.getPersonsGroup(personGroup())
|
||||
if (action === 'add') {
|
||||
commit('addExcludedNode', id)
|
||||
group.forEach(person => {
|
||||
// countLinks < 2 but parent has just already been added !
|
||||
if (!getters.isInWhitelist(person.id) && getters.countLinksByNode(person.id) < 1) {
|
||||
commit('addExcludedNode', person.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (action === 'remove') {
|
||||
commit('removeExcludedNode', id)
|
||||
group.forEach(person => {
|
||||
commit('removeExcludedNode', person.id)
|
||||
})
|
||||
}
|
||||
commit('updateHack')
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
export { store }
|
@@ -0,0 +1,262 @@
|
||||
import { visMessages } from './i18n'
|
||||
|
||||
/**
|
||||
* Vis-network initial data/configuration script
|
||||
* Notes:
|
||||
* Use window.network and window.options to avoid conflict between vue and vis
|
||||
* cfr. https://github.com/almende/vis/issues/2524#issuecomment-307108271
|
||||
*/
|
||||
|
||||
window.network = {}
|
||||
|
||||
window.options = {
|
||||
locale: 'fr',
|
||||
locales: visMessages,
|
||||
/*
|
||||
configure: {
|
||||
enabled: true,
|
||||
filter: 'nodes,edges',
|
||||
//container: undefined,
|
||||
showButton: true
|
||||
},
|
||||
*/
|
||||
physics: {
|
||||
enabled: true,
|
||||
barnesHut: {
|
||||
theta: 0.5,
|
||||
gravitationalConstant: -2000,
|
||||
centralGravity: 0.08, //// 0.3
|
||||
springLength: 220, //// 95
|
||||
springConstant: 0.04,
|
||||
damping: 0.09,
|
||||
avoidOverlap: 0
|
||||
},
|
||||
forceAtlas2Based: {
|
||||
theta: 0.5,
|
||||
gravitationalConstant: -50,
|
||||
centralGravity: 0.01,
|
||||
springLength: 100,
|
||||
springConstant: 0.08,
|
||||
damping: 0.4,
|
||||
avoidOverlap: 0
|
||||
},
|
||||
repulsion: {
|
||||
centralGravity: 0.2,
|
||||
springLength: 200,
|
||||
springConstant: 0.05,
|
||||
nodeDistance: 100,
|
||||
damping: 0.09
|
||||
},
|
||||
hierarchicalRepulsion: {
|
||||
centralGravity: 0.0,
|
||||
springLength: 100,
|
||||
springConstant: 0.01,
|
||||
nodeDistance: 120,
|
||||
damping: 0.09,
|
||||
avoidOverlap: 0
|
||||
},
|
||||
maxVelocity: 50,
|
||||
minVelocity: 0.1,
|
||||
solver: 'forceAtlas2Based', //'barnesHut', //
|
||||
stabilization: {
|
||||
enabled: true,
|
||||
iterations: 1000,
|
||||
updateInterval: 100,
|
||||
onlyDynamicEdges: false,
|
||||
fit: true
|
||||
},
|
||||
timestep: 0.5,
|
||||
adaptiveTimestep: true,
|
||||
wind: { x: 0, y: 0 }
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
multiselect: true,
|
||||
navigationButtons: false,
|
||||
},
|
||||
manipulation: {
|
||||
enabled: false,
|
||||
initiallyActive: false,
|
||||
addNode: false,
|
||||
deleteNode: false
|
||||
},
|
||||
nodes: {
|
||||
borderWidth: 1,
|
||||
borderWidthSelected: 3,
|
||||
font: {
|
||||
multi: 'md'
|
||||
}
|
||||
},
|
||||
edges: {
|
||||
font: {
|
||||
color: '#b0b0b0',
|
||||
size: 9,
|
||||
face: 'arial',
|
||||
background: 'none',
|
||||
strokeWidth: 2, // px
|
||||
strokeColor: '#ffffff',
|
||||
align: 'middle',
|
||||
multi: false,
|
||||
vadjust: 0,
|
||||
},
|
||||
scaling:{
|
||||
label: true,
|
||||
},
|
||||
smooth: true,
|
||||
},
|
||||
groups: {
|
||||
person: {
|
||||
shape: 'box',
|
||||
shapeProperties: {
|
||||
borderDashes: false,
|
||||
borderRadius: 3,
|
||||
},
|
||||
color: {
|
||||
border: '#b0b0b0',
|
||||
background: 'rgb(193,229,222)',
|
||||
highlight: {
|
||||
border: '#89c9a9',
|
||||
background: 'rgb(156,213,203)'
|
||||
},
|
||||
hover: {
|
||||
border: '#89c9a9',
|
||||
background: 'rgb(156,213,203)'
|
||||
}
|
||||
},
|
||||
opacity: 0.85,
|
||||
shadow:{
|
||||
enabled: true,
|
||||
color: 'rgba(0,0,0,0.5)',
|
||||
size:10,
|
||||
x:5,
|
||||
y:5
|
||||
},
|
||||
},
|
||||
household: {
|
||||
color: 'pink'
|
||||
},
|
||||
accompanying_period: {
|
||||
color: 'orange',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param gender
|
||||
* @returns {string}
|
||||
*/
|
||||
const getGender = (gender) => {
|
||||
switch (gender) {
|
||||
case 'both':
|
||||
return visMessages.fr.visgraph.both
|
||||
case 'woman':
|
||||
return visMessages.fr.visgraph.woman
|
||||
case 'man':
|
||||
return visMessages.fr.visgraph.man
|
||||
default:
|
||||
throw 'gender undefined'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO Repeat getAge() in PersonRenderBox.vue
|
||||
* @param birthdate
|
||||
* @returns {string|null}
|
||||
*/
|
||||
const getAge = (birthdate) => {
|
||||
if (null === birthdate) {
|
||||
return null
|
||||
}
|
||||
const birthday = new Date(birthdate.datetime)
|
||||
const now = new Date()
|
||||
return (now.getFullYear() - birthday.getFullYear()) + ' '+ visMessages.fr.visgraph.years
|
||||
}
|
||||
|
||||
/**
|
||||
* Return member position in household
|
||||
* @param member
|
||||
* @returns string
|
||||
*/
|
||||
const getHouseholdLabel = (member) => {
|
||||
let position = member.position.label.fr
|
||||
let holder = member.holder ? ` ${visMessages.fr.visgraph.Holder}` : ''
|
||||
return position + holder
|
||||
}
|
||||
|
||||
/**
|
||||
* Return edge width for member (depends of position in household)
|
||||
* @param member
|
||||
* @returns integer (width)
|
||||
*/
|
||||
const getHouseholdWidth = (member) => {
|
||||
if (member.holder) {
|
||||
return 5
|
||||
}
|
||||
if (member.shareHousehold) {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Return direction edge
|
||||
* @param relationship
|
||||
* @returns string
|
||||
*/
|
||||
const getRelationshipDirection = (relationship) => {
|
||||
return (!relationship.reverse) ? 'to' : 'from'
|
||||
}
|
||||
|
||||
/**
|
||||
* Return label edge
|
||||
* !! always set label in title direction (arrow is reversed, see in previous method) !!
|
||||
* @param relationship
|
||||
* @returns string
|
||||
*/
|
||||
const getRelationshipLabel = (relationship) => {
|
||||
return relationship.relation.title.fr
|
||||
}
|
||||
|
||||
/**
|
||||
* Return title edge
|
||||
* @param relationship
|
||||
* @returns string
|
||||
*/
|
||||
const getRelationshipTitle = (relationship) => {
|
||||
return (!relationship.reverse) ?
|
||||
relationship.relation.title.fr + ': ' + relationship.fromPerson.text + '\n' + relationship.relation.reverseTitle.fr + ': ' + relationship.toPerson.text :
|
||||
relationship.relation.title.fr + ': ' + relationship.toPerson.text + '\n' + relationship.relation.reverseTitle.fr + ': ' + relationship.fromPerson.text
|
||||
}
|
||||
|
||||
/**
|
||||
* Split string id and return type|id substring
|
||||
* @param id
|
||||
* @param position
|
||||
* @returns string|integer
|
||||
*/
|
||||
const splitId = (id, position) => {
|
||||
//console.log(id, position)
|
||||
switch (position) {
|
||||
case 'type': // return 'accompanying_period'
|
||||
return /(.+)_/.exec(id)[1]
|
||||
case 'id': // return 124
|
||||
return parseInt(id.toString()
|
||||
.split("_")
|
||||
.pop())
|
||||
case 'link':
|
||||
return id.split("-")[0] // return first segment
|
||||
default:
|
||||
throw 'position undefined'
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getGender,
|
||||
getAge,
|
||||
getHouseholdLabel,
|
||||
getHouseholdWidth,
|
||||
getRelationshipDirection,
|
||||
getRelationshipLabel,
|
||||
getRelationshipTitle,
|
||||
splitId
|
||||
}
|
@@ -20,18 +20,25 @@
|
||||
v-bind:item="item">
|
||||
</suggestion-third-party>
|
||||
|
||||
<suggestion-user
|
||||
v-if="item.result.type === 'user'"
|
||||
v-bind:item="item">
|
||||
</suggestion-user>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SuggestionPerson from './TypePerson';
|
||||
import SuggestionThirdParty from './TypeThirdParty';
|
||||
import SuggestionUser from './TypeUser';
|
||||
|
||||
export default {
|
||||
name: 'PersonSuggestion',
|
||||
components: {
|
||||
SuggestionPerson,
|
||||
SuggestionThirdParty,
|
||||
SuggestionUser,
|
||||
},
|
||||
props: [
|
||||
'item',
|
||||
|
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="container usercontainer">
|
||||
<div class="user-identification">
|
||||
<span class="name">
|
||||
{{ item.result.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right_actions">
|
||||
<span class="badge rounded-pill bg-secondary">
|
||||
{{ $t('user')}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
fr: {
|
||||
user: 'Utilisateur' // TODO how to define other translations?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'SuggestionUser',
|
||||
props: ['item'],
|
||||
i18n,
|
||||
computed: {
|
||||
hasParent() {
|
||||
return this.$props.item.result.parent !== null;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.usercontainer {
|
||||
.userparent {
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-variant: all-small-caps;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user