diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c8fecc02..6dc54473f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,12 +12,26 @@ and this project adheres to
+* [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 releases
### 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 +56,8 @@ 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
-* [person]: double parentheses removed around age in banner + whitespace
-
### Test release 2021-10-27
@@ -64,7 +76,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
@@ -131,7 +146,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/OriginDemand.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue
index 30001028c..30ad6afe0 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/OriginDemand.vue
@@ -10,13 +10,13 @@
@@ -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;
},
}
}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js
index 9b69845cf..41e341dd8 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js
@@ -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) {
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkCreate/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkCreate/App.vue
index fe7838c4d..7931a2a0d 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkCreate/App.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkCreate/App.vue
@@ -26,7 +26,7 @@
@@ -72,7 +72,7 @@
{{ $t('action.save') }}
- {{ $t('Save') }}
+ {{ $t('action.save') }}
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue
new file mode 100644
index 000000000..60db92628
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/App.vue
@@ -0,0 +1,508 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t(modal.title) }}
+
+
+
+
+
{{ $t('visgraph.delete_confirmation_text') }}
+
+
+
+
+
+ {{ $t(modal.button.text)}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/AccompanyingCourseWork/delete.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/delete.html.twig
new file mode 100644
index 000000000..c66e5aa3d
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/delete.html.twig
@@ -0,0 +1,34 @@
+{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
+
+{% set activeRouteKey = 'chill_person_accompanying_period_work_list' %}
+
+{% block title 'accompanying_course_work.remove'|trans %}
+
+{% block content %}
+
+
+
+ {{ 'accompanying_course_work.action'|trans }}
+ {{ work.socialAction|chill_entity_render_string }}
+
+
+
+
{{ "Associated peoples"|trans }}
+
+ {% for p in work.persons %}
+ {{ p|chill_entity_render_box }}
+ {% endfor %}
+
+
+
+
+
+ {{ include('@ChillMain/Util/confirmation_template.html.twig',
+ {
+ 'title' : 'accompanying_course_work.remove'|trans,
+ 'confirm_question' : 'Are you sure you want to remove this work of the accompanying period %name% ?'|trans({ '%name%' : accompanyingCourse.id } ),
+ 'cancel_route' : 'chill_person_accompanying_period_work_list',
+ 'cancel_parameters' : {'id' : accompanyingCourse.id},
+ 'form' : delete_form
+ } ) }}
+{% endblock %}
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_by_accompanying_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_by_accompanying_period.html.twig
index dd8beded5..797f1bcc8 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_by_accompanying_period.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_by_accompanying_period.html.twig
@@ -103,6 +103,11 @@
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"
>{% if buttonText is not defined or buttonText == true %}{{ 'Edit'|trans }}{% endif %}
+
+ {% if buttonText is not defined or buttonText == true %}{{ 'Delete'|trans }}{% endif %}
+
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig
index 5d5fc77af..56fcce85c 100644
--- a/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig
+++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig
@@ -2,26 +2,34 @@
{% block title 'household.Relationship'|trans %}
-{% block content %}
- {{ block('title') }}
-
+{#
+ Give more space to graph:
+ * use parent twig block (layout_wvm_content)
+ * hide title (d-none)
+ * apply negative margin-top
+#}
+{% block layout_wvm_content %}
+
- {% for m in household.members %}
- {% if m.endDate is null %}
- {{ dump(m) }}
- {% endif %}
- {% endfor %}
+
+
{{ block('title') }}
+
+
+
+
+{% endblock %}
+
+{% block block_post_menu %}
+
{% endblock %}
{% block js %}
- {{ parent() }}
- {{ encore_entry_script_tags('page_vis') }}
+ {{ encore_entry_script_tags('vue_visgraph') }}
{% endblock %}
{% block css %}
- {{ parent() }}
- {{ encore_entry_link_tags('page_vis') }}
+ {{ encore_entry_link_tags('vue_visgraph') }}
{% endblock %}
-
-{% block block_post_menu %}{% endblock %}
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/view.html.twig
index 807a0d894..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' -%}
diff --git a/src/Bundle/ChillPersonBundle/Search/SearchPersonApiProvider.php b/src/Bundle/ChillPersonBundle/Search/SearchPersonApiProvider.php
index dd0ae67c7..60122dbc2 100644
--- a/src/Bundle/ChillPersonBundle/Search/SearchPersonApiProvider.php
+++ b/src/Bundle/ChillPersonBundle/Search/SearchPersonApiProvider.php
@@ -2,29 +2,43 @@
namespace Chill\PersonBundle\Search;
+use Chill\MainBundle\Entity\Center;
+use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\MainBundle\Search\SearchApiInterface;
+use Chill\PersonBundle\Security\Authorization\PersonVoter;
+use Symfony\Component\Security\Core\Security;
class SearchPersonApiProvider implements SearchApiInterface
{
private PersonRepository $personRepository;
+ private Security $security;
+ private AuthorizationHelperInterface $authorizationHelper;
- public function __construct(PersonRepository $personRepository)
+ public function __construct(PersonRepository $personRepository, Security $security, AuthorizationHelperInterface $authorizationHelper)
{
$this->personRepository = $personRepository;
+ $this->security = $security;
+ $this->authorizationHelper = $authorizationHelper;
}
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
+ {
+ return $this->addAuthorizations($this->buildBaseQuery($pattern, $parameters));
+ }
+
+ public function buildBaseQuery(string $pattern, array $parameters): SearchApiQuery
{
$query = new SearchApiQuery();
$query
->setSelectKey("person")
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)")
- ->setSelectPertinence("GREATEST(".
- "STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical), ".
- "(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int".
- ")", [ $pattern, $pattern ])
+ ->setSelectPertinence("".
+ "STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
+ "(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
+ "(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"
+ , [ $pattern, $pattern, $pattern ])
->setFromClause("chill_person_person AS person")
->setWhereClauses("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ])
@@ -33,6 +47,28 @@ class SearchPersonApiProvider implements SearchApiInterface
return $query;
}
+ private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
+ {
+ $authorizedCenters = $this->authorizationHelper
+ ->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
+
+ if ([] === $authorizedCenters) {
+ return $query->andWhereClause("FALSE = TRUE", []);
+ }
+
+ return $query
+ ->andWhereClause(
+ strtr(
+ "person.center_id IN ({{ center_ids }})",
+ [
+ '{{ center_ids }}' => \implode(', ',
+ \array_fill(0, count($authorizedCenters), '?')),
+ ]
+ ),
+ \array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
+ );
+ }
+
public function supportsTypes(string $pattern, array $types, array $parameters): bool
{
return \in_array('person', $types);
diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php
index d0bdba9f5..019f3989e 100644
--- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php
+++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonNormalizer.php
@@ -86,7 +86,6 @@ class PersonNormalizer implements
'mobilenumber' => $person->getMobilenumber(),
'altNames' => $this->normalizeAltNames($person->getAltNames()),
'gender' => $person->getGender(),
- 'gender_numeric' => $person->getGenderNumeric(),
'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress()),
'current_household_id' => $household ? $this->normalizer->normalize($household->getId()) : null,
];
diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php
index 8a8c04538..7e18c7c65 100644
--- a/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php
+++ b/src/Bundle/ChillPersonBundle/Tests/Controller/PersonControllerUpdateTest.php
@@ -259,7 +259,8 @@ class PersonControllerUpdateTest extends WebTestCase
return array(
['firstName', 'random Value', function(Person $person) { return $person->getFirstName(); } ],
['lastName' , 'random Value', function(Person $person) { return $person->getLastName(); } ],
- ['placeOfBirth', 'none place', function(Person $person) { return $person->getPlaceOfBirth(); }],
+ // reminder: this value is capitalized
+ ['placeOfBirth', 'A PLACE', function(Person $person) { return $person->getPlaceOfBirth(); }],
['birthdate', '1980-12-15', function(Person $person) { return $person->getBirthdate()->format('Y-m-d'); }],
['phonenumber', '+32123456789', function(Person $person) { return $person->getPhonenumber(); }],
['memo', 'jfkdlmq jkfldmsq jkmfdsq', function(Person $person) { return $person->getMemo(); }],
diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php
new file mode 100644
index 000000000..ac785f781
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Tests/Controller/RelationshipApiControllerTest.php
@@ -0,0 +1,136 @@
+client = $this->getClientAuthenticated();
+ }
+
+ /**
+ * @dataProvider personProvider
+ */
+ public function testGetRelationshipByPerson($personId)
+ {
+ $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/relations/relationship/by-person/%d.json', $personId));
+
+ $response = $this->client->getResponse();
+ $this->assertEquals(200, $response->getStatusCode(), 'Test to see that API response returns a status code 200');
+ }
+
+ /**
+ * @dataProvider relationProvider
+ */
+ public function testPostRelationship($fromPersonId, $toPersonId, $relationId, $isReverse): void
+ {
+ $this->client->request(Request::METHOD_POST,
+ '/api/1.0/relations/relationship.json',
+ [],
+ [],
+ [],
+ \json_encode([
+ 'type' => 'relationship',
+ 'fromPerson' => ['id' => $fromPersonId, 'type' => 'person'],
+ 'toPerson' => ['id' => $toPersonId, 'type' => 'person'],
+ 'relation' => ['id' => $relationId, 'type' => 'relation'],
+ 'reverse' => $isReverse
+ ]));
+
+ $response = $this->client->getResponse();
+ $this->assertEquals(200, $response->getStatusCode());
+ }
+
+ public function relationProvider(): array
+ {
+ static::bootKernel();
+ $em = self::$container->get(EntityManagerInterface::class);
+ $countPersons = $em->createQueryBuilder()
+ ->select('count(p)')
+ ->from(Person::class, 'p')
+ ->join('p.center', 'c')
+ ->where('c.name LIKE :name')
+ ->setParameter('name', 'Center A')
+ ->getQuery()
+ ->getSingleScalarResult()
+ ;
+ $persons = $em->createQueryBuilder()
+ ->select('p')
+ ->from(Person::class, 'p')
+ ->join('p.center', 'c')
+ ->where('c.name LIKE :name')
+ ->setParameter('name', 'Center A')
+ ->getQuery()
+ ->setMaxResults(2)
+ ->setFirstResult(\random_int(0, $countPersons - 1))
+ ->getResult()
+ ;
+
+ return [
+ [$persons[0]->getId(), $persons[1]->getId(), $this->getRandomRelation($em)->getId(), true],
+ ];
+
+ }
+
+ private function getRandomRelation(EntityManagerInterface $em): Relation
+ {
+ if (null === $this->relations) {
+ $this->relations = $em->getRepository(Relation::class)
+ ->findAll();
+ }
+
+ return $this->relations[\array_rand($this->relations)];
+ }
+
+ public function personProvider(): array
+ {
+ static::bootKernel();
+ $em = self::$container->get(EntityManagerInterface::class);
+ $countPersons = $em->createQueryBuilder()
+ ->select('count(p)')
+ ->from(Person::class, 'p')
+ ->join('p.center', 'c')
+ ->where('c.name LIKE :name')
+ ->setParameter('name', 'Center A')
+ ->getQuery()
+ ->getSingleScalarResult()
+ ;
+ $person = $em->createQueryBuilder()
+ ->select('p')
+ ->from(Person::class, 'p')
+ ->join('p.center', 'c')
+ ->where('c.name LIKE :name')
+ ->setParameter('name', 'Center A')
+ ->getQuery()
+ ->setMaxResults(1)
+ ->setFirstResult(\random_int(0, $countPersons - 1))
+ ->getSingleResult()
+ ;
+
+ return [
+ [$person->getId()],
+ ];
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php
index 75c10ca75..b1b660596 100644
--- a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php
+++ b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php
@@ -11,7 +11,7 @@ class LocationValidity extends Constraint
{
public $messagePersonLocatedMustBeAssociated = "The person where the course is located must be associated to the course. Change course's location before removing the person.";
- public $messagePeriodMustRemainsLocated = "The period must remains located";
+ public $messagePeriodMustRemainsLocated = "The period must remain located";
public function getTargets()
{
diff --git a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ParticipationOverlap.php b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ParticipationOverlap.php
new file mode 100644
index 000000000..38d5516b5
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ParticipationOverlap.php
@@ -0,0 +1,15 @@
+getStartDate()->getTimezone());
+ $participationList = [];
+
+ foreach ($participations as $participation) {
+
+ if (!$participation instanceof AccompanyingPeriodParticipation) {
+ throw new UnexpectedTypeException($participation, AccompanyingPeriodParticipation::class);
+ }
+
+ $personId = $participation->getPerson()->getId();
+
+ $participationList[$personId][] = $participation;
+
+ }
+
+ foreach ($participationList as $group) {
+ if (count($group) > 1) {
+ foreach ($group as $p) {
+ $overlaps->add($p->getStartDate(), $p->getEndDate(), $p->getId());
+ }
+ }
+ }
+
+ $overlaps->compute();
+
+ if ($overlaps->hasIntersections()) {
+ foreach ($overlaps->getIntersections() as list($start, $end, $ids)) {
+ $msg = $end === null ? $constraint->message :
+ $constraint->message;
+
+ $this->context->buildViolation($msg)
+ ->setParameters([
+ '{{ start }}' => $start->format('d-m-Y'),
+ '{{ end }}' => $end === null ? null : $end->format('d-m-Y'),
+ '{{ ids }}' => $ids,
+ ])
+ ->addViolation();
+ }
+ }
+
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ResourceDuplicateCheck.php b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ResourceDuplicateCheck.php
new file mode 100644
index 000000000..bfc9d62af
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ResourceDuplicateCheck.php
@@ -0,0 +1,16 @@
+personRender = $personRender;
+ $this->thirdpartyRender = $thirdPartyRender;
+ }
+
+ public function validate($resources, Constraint $constraint)
+ {
+ if (!$constraint instanceof ResourceDuplicateCheck) {
+ throw new UnexpectedTypeException($constraint, ParticipationOverlap::class);
+ }
+
+ if (!$resources instanceof Collection) {
+ throw new UnexpectedTypeException($resources, Collection::class);
+ }
+
+ $resourceList = [];
+
+ foreach ($resources as $resource) {
+ $id = ($resource->getResource() instanceof Person ? 'p' :
+ 't').$resource->getResource()->getId();
+
+ if (\in_array($id, $resourceList, true)) {
+ $this->context->buildViolation($constraint->message)
+ ->setParameter('{{ name }}', $resource->getResource() instanceof Person ? $this->personRender->renderString($resource->getResource(), []) :
+ $this->thirdpartyRender->renderString($resource->getResource(), []))
+ ->addViolation();
+ }
+
+ $resourceList[] = $id;
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Bundle/ChillPersonBundle/Validator/Constraints/Person/PersonHasCenterValidator.php b/src/Bundle/ChillPersonBundle/Validator/Constraints/Person/PersonHasCenterValidator.php
index 1738e6ddc..825084448 100644
--- a/src/Bundle/ChillPersonBundle/Validator/Constraints/Person/PersonHasCenterValidator.php
+++ b/src/Bundle/ChillPersonBundle/Validator/Constraints/Person/PersonHasCenterValidator.php
@@ -2,6 +2,7 @@
namespace Chill\PersonBundle\Validator\Constraints\Person;
+use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Validator\Constraint;
@@ -10,10 +11,12 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class PersonHasCenterValidator extends \Symfony\Component\Validator\ConstraintValidator
{
private bool $centerRequired;
+ private CenterResolverDispatcher $centerResolverDispatcher;
- public function __construct(ParameterBagInterface $parameterBag)
+ public function __construct(ParameterBagInterface $parameterBag, CenterResolverDispatcher $centerResolverDispatcher)
{
$this->centerRequired = $parameterBag->get('chill_person')['validation']['center_required'];
+ $this->centerResolverDispatcher = $centerResolverDispatcher;
}
/**
@@ -29,7 +32,7 @@ class PersonHasCenterValidator extends \Symfony\Component\Validator\ConstraintVa
return;
}
- if (NULL === $person->getCenter()) {
+ if (NULL === $this->centerResolverDispatcher->resolveCenter($person)) {
$this
->context
->buildViolation($constraint->message)
diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml
index a87711de2..5e1f0d937 100644
--- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml
+++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml
@@ -274,6 +274,41 @@ components:
enum:
- "social_work_goal"
+ RelationById:
+ type: object
+ properties:
+ id:
+ type: integer
+ type:
+ type: string
+ enum:
+ - "relation"
+ required:
+ - id
+ - type
+ Relationship:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - "relationship"
+ id:
+ type: integer
+ readOnly: true
+ fromPerson:
+ anyOf:
+ - $ref: "#/components/schemas/PersonById"
+ toPerson:
+ anyOf:
+ - $ref: "#/components/schemas/PersonById"
+ relation:
+ anyOf:
+ - $ref: "#/components/schemas/RelationById"
+ reverse:
+ type: boolean
+
+
paths:
/1.0/person/person/{id}.json:
get:
@@ -1079,6 +1114,29 @@ paths:
400:
description: "transition cannot be applyed"
+ /1.0/person/accompanying-course/by-person/{person_id}.json:
+ get:
+ tags:
+ - accompanying period
+ summary: get a list of accompanying periods for a person
+ description: Returns a list of the current accompanying periods for a person
+ parameters:
+ - name: person_id
+ in: path
+ required: true
+ description: The person id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+
/1.0/person/accompanying-period/origin.json:
get:
tags:
@@ -1586,3 +1644,115 @@ paths:
description: "OK"
400:
description: "Bad Request"
+
+ /1.0/relations/relationship/by-person/{id}.json:
+ get:
+ tags:
+ - relationships
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The person's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 400:
+ description: "Bad Request"
+
+ /1.0/relations/relationship.json:
+ post:
+ tags:
+ - relationships
+ summary: Create a new relationship
+ requestBody:
+ description: "A relationship"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Relationship"
+ responses:
+ 200:
+ description: "OK"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Relationship"
+ 403:
+ description: "Unauthorized"
+ 422:
+ description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation"
+
+ /1.0/relations/relationship/{id}.json:
+ patch:
+ tags:
+ - relationships
+ summary: "Alter a relationship"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The relationship's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ requestBody:
+ description: "A relationship"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Relationship"
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "Object with validation errors"
+ delete:
+ tags:
+ - relationships
+ summary: "Remove the relationship"
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The relationship's id
+ schema:
+ type: integer
+ format: integer
+ minimum: 1
+ responses:
+ 401:
+ description: "Unauthorized"
+ 404:
+ description: "Not found"
+ 200:
+ description: "OK"
+ 422:
+ description: "object with validation errors"
+
+
+ /1.0/relations/relation.json:
+ get:
+ tags:
+ - relations
+ summary: get a list of relations
+ responses:
+ 401:
+ description: "Unauthorized"
+ 200:
+ description: "OK"
diff --git a/src/Bundle/ChillPersonBundle/chill.webpack.config.js b/src/Bundle/ChillPersonBundle/chill.webpack.config.js
index e8194b648..e033fc89d 100644
--- a/src/Bundle/ChillPersonBundle/chill.webpack.config.js
+++ b/src/Bundle/ChillPersonBundle/chill.webpack.config.js
@@ -12,9 +12,9 @@ module.exports = function(encore, entries)
encore.addEntry('vue_accourse', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
encore.addEntry('vue_accourse_work_create', __dirname + '/Resources/public/vuejs/AccompanyingCourseWorkCreate/index.js');
encore.addEntry('vue_accourse_work_edit', __dirname + '/Resources/public/vuejs/AccompanyingCourseWorkEdit/index.js');
+ encore.addEntry('vue_visgraph', __dirname + '/Resources/public/vuejs/VisGraph/index.js');
encore.addEntry('page_household_edit_metadata', __dirname + '/Resources/public/page/household_edit_metadata/index.js');
encore.addEntry('page_person', __dirname + '/Resources/public/page/person/index.js');
encore.addEntry('page_accompanying_course_index_person_locate', __dirname + '/Resources/public/page/accompanying_course_index/person_locate.js');
- //encore.addEntry('page_vis', __dirname + '/Resources/public/page/vis/index.js');
};
diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml
index 098313286..b03ccf966 100644
--- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml
+++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml
@@ -65,3 +65,7 @@ services:
Chill\PersonBundle\Controller\HouseholdApiController:
autowire: true
tags: ['controller.service_arguments']
+
+ Chill\PersonBundle\Controller\RelationshipApiController:
+ autowire: true
+ tags: ['controller.service_arguments']
diff --git a/src/Bundle/ChillPersonBundle/config/services/fixtures.yaml b/src/Bundle/ChillPersonBundle/config/services/fixtures.yaml
index 72bf899f4..753521ad7 100644
--- a/src/Bundle/ChillPersonBundle/config/services/fixtures.yaml
+++ b/src/Bundle/ChillPersonBundle/config/services/fixtures.yaml
@@ -1,6 +1,7 @@
services:
Chill\PersonBundle\DataFixtures\ORM\:
autowire: true
+ autoconfigure: true
resource: ../../DataFixtures/ORM
tags: [ 'doctrine.fixture.orm' ]
diff --git a/src/Bundle/ChillPersonBundle/config/services/validator.yaml b/src/Bundle/ChillPersonBundle/config/services/validator.yaml
new file mode 100644
index 000000000..435f2f5b5
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/config/services/validator.yaml
@@ -0,0 +1,6 @@
+services:
+ Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod:
+ autowire: true
+ autoconfigure: true
+ tags: ['validator.service_arguments']
+
\ No newline at end of file
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211020131133.php b/src/Bundle/ChillPersonBundle/migrations/Version20211020131133.php
new file mode 100644
index 000000000..031356cd3
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20211020131133.php
@@ -0,0 +1,31 @@
+addSql('CREATE UNIQUE INDEX person_unique ON chill_person_accompanying_period_resource (person_id, accompanyingperiod_id) WHERE person_id IS NOT NULL');
+ $this->addSql('CREATE UNIQUE INDEX thirdparty_unique ON chill_person_accompanying_period_resource (thirdparty_id, accompanyingperiod_id) WHERE thirdparty_id IS NOT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP INDEX person_unique');
+ $this->addSql('DROP INDEX thirdparty_unique');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211021125359.php b/src/Bundle/ChillPersonBundle/migrations/Version20211021125359.php
new file mode 100644
index 000000000..a4ed54254
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20211021125359.php
@@ -0,0 +1,36 @@
+addSql('ALTER TABLE chill_person_accompanying_period_participation ADD CONSTRAINT '.
+ "participations_no_overlap EXCLUDE USING GIST(
+ -- extension btree_gist required to include comparaison with integer
+ person_id WITH =, accompanyingperiod_id WITH =,
+ daterange(startdate, enddate) WITH &&
+ )
+ INITIALLY DEFERRED");
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('CREATE UNIQUE INDEX participation_unique ON chill_person_accompanying_period_participation (accompanyingperiod_id, person_id)');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211025141226.php b/src/Bundle/ChillPersonBundle/migrations/Version20211025141226.php
new file mode 100644
index 000000000..9d67b0f4d
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20211025141226.php
@@ -0,0 +1,49 @@
+addSql('CREATE SEQUENCE chill_person_relations_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
+ $this->addSql('CREATE SEQUENCE chill_person_relationships_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
+ $this->addSql('CREATE TABLE chill_person_relations (id INT NOT NULL, title JSON DEFAULT NULL, reverseTitle JSON DEFAULT NULL, PRIMARY KEY(id))');
+ $this->addSql('CREATE TABLE chill_person_relationships (id INT NOT NULL, relation_id INT NOT NULL, reverse BOOLEAN NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, fromPerson_id INT NOT NULL, toPerson_id INT NOT NULL, createdBy_id INT NOT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
+ $this->addSql('CREATE INDEX IDX_23D47C51CBA59C1E ON chill_person_relationships (fromPerson_id)');
+ $this->addSql('CREATE INDEX IDX_23D47C514013E22A ON chill_person_relationships (toPerson_id)');
+ $this->addSql('CREATE INDEX IDX_23D47C513256915B ON chill_person_relationships (relation_id)');
+ $this->addSql('CREATE INDEX IDX_23D47C513174800F ON chill_person_relationships (createdBy_id)');
+ $this->addSql('CREATE INDEX IDX_23D47C5165FF1AEC ON chill_person_relationships (updatedBy_id)');
+ $this->addSql('COMMENT ON COLUMN chill_person_relationships.createdAt IS \'(DC2Type:datetime_immutable)\'');
+ $this->addSql('COMMENT ON COLUMN chill_person_relationships.updatedAt IS \'(DC2Type:datetime_immutable)\'');
+ $this->addSql('ALTER TABLE chill_person_relationships ADD CONSTRAINT FK_23D47C51CBA59C1E FOREIGN KEY (fromPerson_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE chill_person_relationships ADD CONSTRAINT FK_23D47C514013E22A FOREIGN KEY (toPerson_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE chill_person_relationships ADD CONSTRAINT FK_23D47C513256915B FOREIGN KEY (relation_id) REFERENCES chill_person_relations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE chill_person_relationships ADD CONSTRAINT FK_23D47C513174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE chill_person_relationships ADD CONSTRAINT FK_23D47C5165FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ }
+
+ public function down(Schema $schema): void
+ {
+
+ $this->addSql('ALTER TABLE chill_person_relationships DROP CONSTRAINT FK_23D47C513256915B');
+ $this->addSql('DROP SEQUENCE chill_person_relations_id_seq CASCADE');
+ $this->addSql('DROP SEQUENCE chill_person_relationships_id_seq CASCADE');
+ $this->addSql('DROP TABLE chill_person_relations');
+ $this->addSql('DROP TABLE chill_person_relationships');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20211029075117.php b/src/Bundle/ChillPersonBundle/migrations/Version20211029075117.php
new file mode 100644
index 000000000..ef739dcb8
--- /dev/null
+++ b/src/Bundle/ChillPersonBundle/migrations/Version20211029075117.php
@@ -0,0 +1,29 @@
+addSql('ALTER TABLE chill_person_relations ADD isActive BOOLEAN DEFAULT true NOT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE chill_person_relations DROP isActive');
+ }
+}
diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml
index 7290bef31..e815153c8 100644
--- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml
+++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml
@@ -57,8 +57,8 @@ household:
Household summary: Résumé du ménage
Accompanying period: Parcours d'accompagnement
Addresses: Historique adresse
- Relationship: Composition familiale
- Household relationships: Composition du ménage
+ Relationship: Filiation
+ Household relationships: Filiations dans le ménage
Current address: Adresse actuelle
Household does not have any address currently: Le ménage n'a pas d'adresse renseignée actuellement
Edit household members: Modifier l'appartenance au ménage
diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
index af3ca6f05..7f9527b91 100644
--- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml
@@ -50,6 +50,8 @@ mobilenumber: numéro de téléphone portable
Accept short text message ?: La personne a donné l'autorisation d'utiliser ce no de téléphone pour l'envoi de rappel par SMS
Accept short text message: La personne a donné l'autorisation d'utiliser ce no de téléphone pour l'envoi de rappel par SMS
Other phonenumber: Autre numéro de téléphone
+Others phone numbers: Autres numéros de téléphone
+No additional phone numbers: Aucun numéro de téléphone supplémentaire
Description: description
Add new phone: Ajouter un numéro de téléphone
Remove phone: Supprimer
@@ -405,6 +407,8 @@ Back to household: Revenir au ménage
# accompanying course work
Accompanying Course Actions: Actions d'accompagnements
Accompanying Course Action: Action d'accompagnement
+Are you sure you want to remove this work of the accompanying period %name% ?: Êtes-vous sûr de vouloir supprimer cette action de la période d'accompagnement %name% ?
+The accompanying period work has been successfully removed.: L'action d'accompagnement a été supprimée.
accompanying_course_work:
create: Créer une action
Create accompanying course work: Créer une action d'accompagnement
@@ -419,6 +423,7 @@ accompanying_course_work:
results: Résultats - orientations
goal: Objectif - motif - dispositif
Any work: Aucune action d'accompagnement
+ remove: Supprimer une action d'accompagnement
#
Person addresses: Adresses de résidence
diff --git a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
index 0e77dae0c..6d2ccb3cb 100644
--- a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
+++ b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
@@ -41,3 +41,6 @@ household:
household_membership:
The end date must be after start date: La date de la fin de l'appartenance doit être postérieure à la date de début.
Person with membership covering: Une personne ne peut pas appartenir à deux ménages simultanément. Or, avec cette modification, %person_name% appartiendrait à %nbHousehold% ménages à partir du %from%.
+
+# Accompanying period
+'{{ name }} is already associated to this accompanying course.': '{{ name }} est déjà associé avec ce parcours.'
\ No newline at end of file
diff --git a/src/Bundle/ChillThirdPartyBundle/Form/ThirdPartyType.php b/src/Bundle/ChillThirdPartyBundle/Form/ThirdPartyType.php
index 2c7380116..e2ebfa6d9 100644
--- a/src/Bundle/ChillThirdPartyBundle/Form/ThirdPartyType.php
+++ b/src/Bundle/ChillThirdPartyBundle/Form/ThirdPartyType.php
@@ -18,6 +18,7 @@ use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -46,18 +47,22 @@ class ThirdPartyType extends AbstractType
protected EntityManagerInterface $om;
+ private bool $askCenter;
+
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
ThirdPartyTypeManager $typesManager,
TranslatableStringHelper $translatableStringHelper,
- EntityManagerInterface $om
+ EntityManagerInterface $om,
+ ParameterBagInterface $parameterBag
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
$this->typesManager = $typesManager;
$this->translatableStringHelper = $translatableStringHelper;
$this->om = $om;
+ $this->askCenter = $parameterBag->get('chill_main')['acl']['form_show_centers'];
}
/**
@@ -78,16 +83,19 @@ class ThirdPartyType extends AbstractType
])
->add('comment', ChillTextareaType::class, [
'required' => false
- ])
- ->add('centers', PickCenterType::class, [
- 'role' => (\array_key_exists('data', $options) && $this->om->contains($options['data'])) ?
- ThirdPartyVoter::UPDATE : ThirdPartyVoter::CREATE,
- 'choice_options' => [
- 'multiple' => true,
- 'attr' => ['class' => 'select2']
- ]
- ])
- ;
+ ]);
+
+ if ($this->askCenter) {
+ $builder
+ ->add('centers', PickCenterType::class, [
+ 'role' => (\array_key_exists('data', $options) && $this->om->contains($options['data'])) ?
+ ThirdPartyVoter::UPDATE : ThirdPartyVoter::CREATE,
+ 'choice_options' => [
+ 'multiple' => true,
+ 'attr' => ['class' => 'select2']
+ ]
+ ]);
+ }
// Contact Person ThirdParty (child)
if (ThirdParty::KIND_CONTACT === $options['kind'] || ThirdParty::KIND_CHILD === $options['kind']) {
diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/_form.html.twig b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/_form.html.twig
index 6bc4e3da7..c53764871 100644
--- a/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/_form.html.twig
+++ b/src/Bundle/ChillThirdPartyBundle/Resources/views/ThirdParty/_form.html.twig
@@ -30,38 +30,10 @@
{{ form_row(form.address) }}
-{#
-
- {{ form_label(form.address) }}
- {{ form_widget(form.address) }}
-
-
- {% if thirdParty.address %}
- {# include vue_address component #
- {% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
- targetEntity: { name: 'thirdparty', id: thirdParty.id },
- mode: 'edit',
- addressId: thirdParty.address.id,
- buttonSize: 'btn-sm',
- } %}
- {#
- backUrl: path('chill_3party_3party_new'),
- #
- {% else %}
- {# include vue_address component #
- {% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
- targetEntity: { name: 'thirdparty', id: thirdParty.id },
- mode: 'new',
- buttonSize: 'btn-sm',
- buttonText: 'Create a new address',
- modalTitle: 'Create a new address',
- } %}
- {% endif %}
-
-
-#}
-
{{ form_row(form.comment) }}
+
+{% if form.centers is defined %}
{{ form_row(form.centers) }}
+{% endif %}
{{ form_row(form.active) }}