diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c85f102b..a308e03cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,12 +12,45 @@ and this project adheres to
+* [task] Select2 field in task form to allow search for a user (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/167)
+* remove "search by phone configuration option": search by phone is now executed by default
+* remplacer le classement par ordre alphabétique par un classement par ordre de pertinence, qui tient compte:
+ * de la présence d'une string avec le nom de la ville;
+ * de la similarité;
+ * du fait que la recherche commence par une partie du mot recherché
+* ajouter la recherche par numéro de téléphone directement dans la barre de recherche et dans le formulaire recherche avancée;
+* ajouter la recherche par date de naissance directement dans la barre de recherche;
+* ajouter la recherche par ville dans la recherche avancée
+* ajouter un lien vers le ménage dans les résultats de recherche
+* ajouter l'id du parcours dans les résultats de recherche
+* ajouter le demandeur dans les résultats de recherche
+* ajout d'un bouton "recherche avancée" sur la page d'accueil
+* [person] create an accompanying course: add client-side validation if no origin (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/210)
+* [person] fix bounds for computing current person address: the new address appears immediatly
+* [docgen] create a normalizer and serializer for normalization on doc format
## Test releases
+### Test release 2021-11-15
+
+* [main] fix adding multiple AddresseDeRelais (combine PickAddressType with ChillCollection)
+* [person]: do not suggest the current household of the person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/51)
+* [person]: display other phone numbers in view + add message in case no others phone numbers (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/184)
+* unnecessary whitespace removed from person banner after person-id + double parentheses removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/290)
+* [person]: delete accompanying period work, including related objects (cascade) (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/36)
+* [address]: Display of incomplete address adjusted.
+* [household]: improve relationship graph
+ * add form to create/edit/delete relationship link,
+ * improve graph refresh mechanism
+ * add feature to export canvas as image (png)
+* [person suggest] In widget "add person", improve the pertinence of persons when one of the names starts with the pattern;
+* [person] do not ask for center any more on person creation
+* [3party] do not ask for center any more on 3party creation
+
### Test release 2021-11-08
+* [person]: Display the name of a user when searching after a User (TMS)
* [person]: Add civility to the person
* [person]: Various improvements on the edit person form
* [person]: Set available_languages and available_countries as parameters for use in the edit person form
@@ -42,10 +75,9 @@ and this project adheres to
* [tasks]: different layout for task list / my tasks, and fix link to tasks in alert or in warning
* [admin]: links to activity admin section added again.
* [household]: household addresses ordered by ValidFrom date and by id to show the last created address on top.
-* [socialWorkAction]: display of social issue and parent issues + banner context added.
+* [socialWorkAction]: display of social issue and parent issues + banner context added.
* [DBAL dependencies] Upgrade to DBAL 3.1
-
### Test release 2021-10-27
* [person]: delete double actions buttons on search person page
@@ -63,7 +95,10 @@ and this project adheres to
* [3party]: fix address creation
* [household members editor] finalisation of editor
* [AccompanyingCourse banner]: replace translation referrer (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/70)
-* [Location]: add location system in activity and RV (calendar). User can choose in location list or create a new location.
+* [Location]: add location system in activity and RV (calendar). User can choose in location list or create a new location.
+* [household]: add relationship page with dynamic data visualisation graph
+
+## Test releases
### Test release 2021-10-11
@@ -130,7 +165,7 @@ and this project adheres to
## Test released
-
-
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js
index 52f3f6c36..c372ac7a7 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js
@@ -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 };
});
};
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue
index 84e7fb2c6..7cce4d900 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Banner.vue
@@ -22,20 +22,22 @@
{{ $t('course.open_at') }}{{ $d(accompanyingCourse.openingDate.datetime, 'text') }}
- {{ $t('course.referrer') }}: {{ accompanyingCourse.user.username }}
+ {{ $t('course.referrer') }}: {{ accompanyingCourse.user.username }}
-
-
-
-
+
+
+
+
+
+
@@ -43,12 +45,14 @@
+
+
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue
index f23a7c9e5..4142b4f50 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Confirm.vue
@@ -10,7 +10,7 @@
{{ $t('confirm.alert_validation') }}
- -
+
-
{{ $t(notValidMessages[k].msg) }}
@@ -83,7 +83,11 @@ export default {
},
location: {
msg: 'confirm.location_not_valid',
- anchor: '#section-20' //
+ anchor: '#section-20'
+ },
+ origin: {
+ msg: 'confirm.origin_not_valid',
+ anchor: '#section-30'
},
socialIssue: {
msg: 'confirm.socialIssue_not_valid',
@@ -103,6 +107,7 @@ export default {
...mapGetters([
'isParticipationValid',
'isSocialIssueValid',
+ 'isOriginValid',
'isLocationValid',
'validationKeys',
'isValidToBeConfirmed'
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue
index 30001028c..b88bf0747 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue
@@ -10,24 +10,27 @@
-
+
+
+ {{ $t('origin.not_valid') }}
+
+
+
+
+
+
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/api.js
new file mode 100644
index 000000000..448ff6633
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/api.js
@@ -0,0 +1,195 @@
+import { splitId } from './vis-network'
+
+/**
+ * @function makeFetch
+ * @param method
+ * @param url
+ * @param body
+ * @returns {Promise}
+ */
+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}
+ */
+const getFetch = (url) => {
+ return makeFetch('GET', url, null)
+}
+
+/**
+ * @function postFetch
+ * @param url
+ * @param body
+ * @returns {Promise}
+ */
+const postFetch = (url, body) => {
+ return makeFetch('POST', url, body)
+}
+
+/**
+ * @function patchFetch
+ * @param url
+ * @param body
+ * @returns {Promise}
+ */
+const patchFetch = (url, body) => {
+ return makeFetch('PATCH', url, body)
+}
+
+/**
+ * @function deleteFetch
+ * @param url
+ * @param body
+ * @returns {Promise}
+ */
+const deleteFetch = (url, body) => {
+ return makeFetch('DELETE', url, null)
+}
+
+
+/**
+ * @function getHouseholdByPerson
+ * @param person
+ * @returns {Promise}
+ */
+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}
+ */
+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}
+ */
+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}
+ */
+const getRelationsList = () => {
+ return getFetch(`/api/1.0/relations/relation.json`)
+}
+
+/**
+ * @function postRelationship
+ * @param relationship
+ * @returns {Promise}
+ */
+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}
+ */
+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}
+ */
+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
+}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js
new file mode 100644
index 000000000..c2ee09960
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js
@@ -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
+}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js
new file mode 100644
index 000000000..ca76f283b
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js
@@ -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: ``
+})
+.use(store)
+.use(i18n)
+.component('app', App)
+.mount('#relationship-graph')
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js
new file mode 100644
index 000000000..119a7b29c
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js
@@ -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 }
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js
new file mode 100644
index 000000000..e95bc0d0b
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js
@@ -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
+}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue
index 931830fd3..e3b4901d7 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons/PersonSuggestion.vue
@@ -20,18 +20,25 @@
v-bind:item="item">
+
+
+
+
+
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_join_household.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_join_household.html.twig
index 7ef2de466..fda36da85 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_join_household.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_join_household.html.twig
@@ -6,7 +6,7 @@
- Corriger
+ {{ 'fix it'|trans }}
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig
index f04a8a376..2febc7967 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig
@@ -8,7 +8,7 @@
- Corriger
+ {{ 'fix it'|trans }}
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig
index e55b39f64..ded5590a2 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/banner.html.twig
@@ -23,11 +23,28 @@
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig
index 0351d0ba1..fb06c4d5e 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig
@@ -216,13 +216,23 @@ This view should receive those arguments:
{%- if chill_person.fields.mobilenumber == 'visible' -%}
- {{ 'Mobilenumber'|trans }} :
- - {% if person.mobilenumber is not empty %}
{{ person.mobilenumber|chill_format_phonenumber }}
{% else %}{{ 'No data given'|trans }}{% endif %}
+ - {% if person.mobilenumber is not empty %}{{ person.mobilenumber|chill_format_phonenumber }}{% else %}{{ 'No data given'|trans }}{% endif %}
{% endif %}
- {# TODO
- display collection of others phonenumbers
- #}
+ {%- if chill_person.fields.mobilenumber == 'visible' -%}
+ {% if person.otherPhoneNumbers is not empty %}
+
+ - {{ 'Others phone numbers'|trans }} :
+ {% for el in person.otherPhoneNumbers %}
+ {% if el.phonenumber is not empty %}
+ - {% if el.description is not empty %}{{ el.description }} : {% endif %}{{ el.phonenumber|chill_format_phonenumber }}
+ {% endif %}
+ {% endfor %}
+
+
+ {% endif %}
+ {% endif %}
{%- if chill_person.fields.contact_info == 'visible' -%}
@@ -259,6 +269,21 @@ This view should receive those arguments:
{% endif %}
+
+ {% if person.createdBy %}
+
+ {{ 'Created by'|trans}}: {{ person.createdBy|chill_entity_render_box }},
+ {{ 'on'|trans ~ person.createdAt|format_datetime('long', 'short') }}
+
+ {% endif %}
+ {% if person.updatedBy %}
+
+ {{ 'Last updated by'|trans}}: {{ person.updatedBy|chill_entity_render_box }},
+ {{ 'on'|trans ~ person.updatedAt|format_datetime('long', 'short') }}
+
+ {% endif %}
+
+
{% if is_granted('CHILL_PERSON_UPDATE', person) %}