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"; import { darkBlue, darkBrown, darkGreen, lightBlue, lightBrown, lightGreen } from './colors'; 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 age = getAge(person) age = (age === '')? '' : ' - ' + age 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}${person.deathdate ? ' (‡)' : ''}*\n_${getGender(person.gender)}${age}_${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: lightGreen, font: { color: darkGreen }, 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.id}`, id: `${household.id}-person_${m.person.id}`, arrows: 'from', color: lightBrown, font: { color: darkBrown }, dashes: (getHouseholdWidth(m) === 1)? [0,4] : false, //edge style: [dash, gap, dash, gap] //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: lightBlue, font: { color: darkBlue }, }) 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: lightGreen, font: { color: darkGreen }, 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 }