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 @@
-

spinner

+
@@ -72,7 +72,7 @@ {{ $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 @@ + + + + + + + + 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 }}

+ +
+
+ + + {{ 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) }}