diff --git a/.eslintrc.js b/.eslintrc.js index 25a4edf..c26d88a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,18 +7,8 @@ module.exports = { parserOptions: { parser: 'babel-eslint' }, - extends: [ - 'plugin:vue/recommended', - 'plugin:prettier/recommended' - ], - // required to lint *.vue files - plugins: [ - 'vue', - 'prettier' - ], + extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'], + plugins: ['vue', 'prettier'], // add your custom rules here - rules: { - 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' - } + rules: {} } diff --git a/.eslintrc.json b/.eslintrc1.json similarity index 100% rename from .eslintrc.json rename to .eslintrc1.json diff --git a/.prettierrc b/.prettierrc index b2095be..a896288 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,6 @@ { "semi": false, - "singleQuote": true + "singleQuote": true, + "printWidth": 70, + "endOfLine": "auto" } diff --git a/.vscode/settings.json b/.vscode/settings.json index acd9554..3deaabe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,6 @@ }, "editor.codeActionsOnSave": { "source.fixAll.eslint": true - } + }, + "editor.formatOnSave": true } \ No newline at end of file diff --git a/README.md b/README.md index 7c2099e..4204988 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ [Demo](https://hoogkamer.github.io/vue-org-chart/) -![Screenshot](/assets/img/Screenshot1.PNG?raw=true 'Screenshot') +![Chart](/assets/img/Screenshot1.PNG?raw=true 'chart') +![Profile](/assets/img/profile.png?raw=true 'profile') \ **Do you want to show your (Agile) teams instead of an orgchart? Try: [Teamviewer](https://github.com/Hoogkamer/TeamViewer) open source.** @@ -20,10 +21,10 @@ - Panzoom and interactive expansion of subdepartments - Deeplinks to departments - Save as image -- Search for departments and managers +- Search for departments and people - Add employees to departments - Use photo's from api (not included), or local folder -- Click on employee to link to api (not included) or local folder +- Click on employee to link to api (not included) or show profile information (**new**) ## Use as static website @@ -70,7 +71,8 @@ The config file is in /config.js For these locations it is fetched from "prefix" + photo + "suffix". So if you have photo P0001, it will be fetched from "photos/P0001.png". If you have an api or other locations which delivers photo's based on the photo field you can change that here. - linkUrl - It will open a new tab to navigate to that page when clicked in the sidescreen on a person. In this example it will just fetch the photo in a new tab, but if you have an api which shows a user profile page you can enter the location here. + It will open a new tab to navigate to that page when clicked in the sidescreen on a person. If you have an api which shows a user profile page you can enter the location here. + Don't specify this object if you want to see the profile information from this application (default) - startView Sets the inital options (the user can change them in the menu bar) diff --git a/assets/img/profile.png b/assets/img/profile.png new file mode 100644 index 0000000..c68b17b Binary files /dev/null and b/assets/img/profile.png differ diff --git a/components/DepartmentDetails.vue b/components/DepartmentDetails.vue new file mode 100644 index 0000000..8e5e541 --- /dev/null +++ b/components/DepartmentDetails.vue @@ -0,0 +1,203 @@ + + div + .property + span.label Name + br + input.name(v-if='editMode' :class="{error:!activeDepartment_name}" v-model="activeDepartment_name") + span(v-else).text {{activeDepartment_name}} + + .property + span.label Manager + i.material-icons.edit(v-if='editMode' v-on:click='personPicker="manager"') edit + br + span.text(v-if='activeDepartment.manager.name') {{activeDepartment.manager.name}} + span.untext(v-else) No manager + .property + span.label Description + br + textarea.description(v-if='editMode' v-model="activeDepartment_description") + span(v-else).text {{activeDepartment_description}} + + .property(v-for="field in activeDepartment.dataFields" v-if="field.value") + span.label {{field.name}} + br + template(v-if='editMode') + // todo edit extra properties + templage(v-else) + template(v-if='field.type==="url"') + a(:href="field.value" target="_blank") Link + template(v-else) + span.text {{field.value}} + + .property + template(v-if='!editMode') + span.label Department type + br + span(v-if='activeDepartment_isStaff') Staff department + span(v-else) Normal department + template(v-else) + span.label Staff department: + input.isstaff(type='checkbox' v-model="activeDepartment_isStaff" :disabled="!editMode") + .property + span.label Hierarchy + ul + li.clickable(v-for='(parent, pnr) in parents' v-on:click="setActiveDepartment(parent)") + span(v-for="n in pnr")   + i(v-if="pnr !==0").material-icons.sub subdirectory_arrow_right + span {{parent.name}} + li.clickable(v-on:click="setActiveDepartment(activeDepartment)") + span(v-for="n in parents.length")   + i(v-if="parents.length").material-icons.sub subdirectory_arrow_right + span.this-department {{activeDepartment.name}} + li.clickable(v-for='child in activeDepartment.children' v-on:click="setActiveDepartment(child)") + span(v-for="n in parents.length+5")   + span {{child.name}} + img.profile(:src='config.photoUrl.prefix+activeDepartment.manager.photo+config.photoUrl.suffix' v-on:click='visitProfile(activeDepartment.manager)' title='Goto profile') + person-picker(v-if='personPicker' type='manager' v-on:close='personPicker=null') + + + + + diff --git a/components/DeptBox.vue b/components/DeptBox.vue index 6918585..5db9dd5 100644 --- a/components/DeptBox.vue +++ b/components/DeptBox.vue @@ -17,7 +17,7 @@ div.hidden_dept.down(v-if='!departmentData.showChildren' v-on:click="doShowChildren(true)" title='Nr of subdepartments') {{departmentData.children.length}} div.hidden_dept.up(v-if='departmentData.showChildren' v-on:click="doShowChildren(false)" title='Nr of subdepartments') {{departmentData.children.length}} template(v-if="showNrPeople") - div.ppl_count(v-if='count_department_people(departmentData)' title='Nr of people in department') {{count_department_people(departmentData)}} + div.ppl_count(v-if='departmentData.employees.length' title='Nr of people in department') {{departmentData.employees.length}} i.material-icons.view_button(v-if="displaySiblingIcon" v-on:click="showViewMenu(departmentData, $event)" title="view options") visibility i.material-icons.hidden_parents1(v-if="hiddenParents" v-on:click="setHideParents(false)") more_vert @@ -42,7 +42,7 @@ div.hidden_dept.down(v-if='!departmentData.showChildren' v-on:click="doShowChildren(true)" title='Nr of subdepartments') {{departmentData.children.length}} div.hidden_dept.up(v-if='departmentData.showChildren' v-on:click="doShowChildren(false)" title='Nr of subdepartments') {{departmentData.children.length}} template(v-if="showNrPeople") - div.ppl_count(v-if='count_department_people(departmentData)' title='Nr of people in department') {{count_department_people(departmentData)}} + div.ppl_count(v-if='departmentData.employees.length' title='Nr of people in department') {{departmentData.employees.length}} i.material-icons.view_button(v-if="displaySiblingIcon" v-on:click="showViewMenu(departmentData, $event)" title="view options") visibility i.material-icons.hidden_parents(v-if="hiddenParents" v-on:click="setHideParents(false)") more_vert @@ -86,7 +86,6 @@ export default { 'editMode', 'config', 'chart', - 'assignments', 'showNrPeople', 'showNrDepartments' ]), @@ -155,10 +154,6 @@ export default { }, hideSiblings() { this.setHideSiblings(this.departmentData) - }, - count_department_people: function(dept) { - var person_ids = this.assignments.filter(a => a.department_id == dept.id) - return person_ids.length } } } diff --git a/components/EditMenu.vue b/components/EditMenu.vue index 3add7dc..2789ca4 100644 --- a/components/EditMenu.vue +++ b/components/EditMenu.vue @@ -18,22 +18,34 @@ import { mapState, mapActions } from 'vuex' export default { computed: { - ...mapState(['chart', 'showEditMenu', 'activeDepartment', 'moveDepartment']) + ...mapState([ + 'chart', + 'showEditMenu', + 'activeDepartment', + 'moveDepartment' + ]) }, mounted: function() { var d = document.getElementById('edit_menu') - var chartpos = document.getElementById('chart').getBoundingClientRect() + var chartpos = document + .getElementById('chart') + .getBoundingClientRect() var x = document.getElementById('chart') var scalex = 1 / (x.getBoundingClientRect().width / x.offsetWidth) d.style.display = 'inline-block' - d.style.left = this.showEditMenu.clientX - 0 * chartpos.left + 'px' + d.style.left = + this.showEditMenu.clientX - 0 * chartpos.left + 'px' d.style.top = this.showEditMenu.clientY - 0 * chartpos.top + 'px' console.log(scalex, this.showEditMenu.clientX, chartpos.left) }, methods: { - ...mapActions(['deleteDepartment', 'addDepartment', 'doMoveDepartment']), + ...mapActions([ + 'deleteDepartment', + 'addDepartment', + 'doMoveDepartment' + ]), removeDept: function() { var confirmed = true if (this.activeDepartment.children.length) { diff --git a/components/EmployeeList.vue b/components/EmployeeList.vue new file mode 100644 index 0000000..00e0b00 --- /dev/null +++ b/components/EmployeeList.vue @@ -0,0 +1,131 @@ + + div + .assignment(v-for='(person, p_idx) in activeDepartment.employees' v-on:click='visitProfile(person.person)' title='Goto profile') + table + tr + td + img.photo(:src='photoURL(person)' @error="markPhotoNotFound(person)") + td + .name + span {{person.person.name}} + .role(v-if='!editMode') {{person.role}} + .role(v-else) + input(:value="person.role" @input="updateThisRole(person, $event)" v-on:click.stop title='Edit role') + template(v-if='editMode') + i.material-icons.delete(title='remove from department' v-on:click.stop='removeDeptAssignment(person)') delete + button.btn(v-if='editMode' v-on:click='personPicker="person"') Add person + person-picker(v-if='personPicker' :type='personPicker' v-on:close='personPicker=null') + + + + + diff --git a/components/FileMenu.vue b/components/FileMenu.vue index 1fabbc1..e61c062 100644 --- a/components/FileMenu.vue +++ b/components/FileMenu.vue @@ -23,7 +23,13 @@ export default { } }, computed: { - ...mapState(['chart', 'people', 'assignments', 'editMode', 'config']) + ...mapState([ + 'chart', + 'people', + 'assignments', + 'editMode', + 'config' + ]) }, watch: { editMode: function(val) { @@ -34,7 +40,6 @@ export default { }, methods: { generateInputFile: function() { - //var chartTable = this.tree2arrayJSON(this.chart, []) var chartTable = this.tree2JSON(this.chart) var today = new Date() var dd = today.getDate() @@ -50,17 +55,38 @@ export default { } var updated = '"' + dd + '-' + mm + '-' + yyyy + '"' + let people = [] + let assignments = [] + this.people.forEach((p, i) => { + let person = Object.assign({}, p) + delete person.departments + people.push(person) + if (p.departments) { + p.departments.forEach(d => { + if (d.role !== 'Manager') { + assignments.push({ + department_id: d.department.id, + id: i, + person_id: p.id, + role: d.role + }) + } + }) + } + }) var json = 'var INPUT_DATA=' + JSON.stringify({ api_version: '1.0', chart: chartTable, - people: this.people, - assignments: this.assignments + people: people, + assignments: assignments }) + ';var UPDATED_ON=' + updated - var blob = new Blob([json], { type: 'text/plain;charset=utf-8' }) + var blob = new Blob([json], { + type: 'text/plain;charset=utf-8' + }) FileSaver.saveAs(blob, 'data.js') }, importData: function(infile) { @@ -70,12 +96,18 @@ export default { reader.onload = function(e) { var data = e.target.result var workbook = XLSX.read(data, { type: 'binary' }) - var chartdata = XLSX.utils.sheet_to_json(workbook.Sheets['chart'], { - defval: '' - }) - var people = XLSX.utils.sheet_to_json(workbook.Sheets['people'], { - defval: '' - }) + var chartdata = XLSX.utils.sheet_to_json( + workbook.Sheets['chart'], + { + defval: '' + } + ) + var people = XLSX.utils.sheet_to_json( + workbook.Sheets['people'], + { + defval: '' + } + ) var assignments = XLSX.utils.sheet_to_json( workbook.Sheets['assignment'], { @@ -91,14 +123,21 @@ export default { if (x.hasOwnProperty(property)) { if (property.indexOf('DATA_') === 0) { var name = property.substring(5).replace(/_/g, ' ') - var fnd = that.config.dataFields.find(d => d.name === name) + var fnd = that.config.dataFields.find( + d => d.name === name + ) var type = fnd ? fnd.type : '' - dataFields.push({ name: name, value: x[property], type }) + dataFields.push({ + name: name, + value: x[property], + type + }) } } } newchart.push({ showChildren: false, + employees: [], parent: null, parentId: x.parent_id, children: null, @@ -111,13 +150,40 @@ export default { }) }) that.$store.commit('createTree', newchart) + that.$store.commit('processAssignments', { + departments: newchart, + people: people, + assignments: assignments + }) + that.$store.commit('setPeople', people) that.$store.commit('setAssignments', assignments) + that.$store.commit('setActiveDepartment', null) alert('Data is imported') } reader.readAsBinaryString(f) }, doExportXls: function(a) { + let people = [] + let assignments = [] + this.people.forEach((p, i) => { + let person = Object.assign({}, p) + delete person.departments + people.push(person) + if (p.departments) { + p.departments.forEach(d => { + if (d.role !== 'Manager') { + assignments.push({ + department_id: d.department.id, + id: i, + person_id: p.id, + role: d.role + }) + } + }) + } + }) + var chartTable = this.tree2array(this.chart, []) var wb = XLSX.utils.book_new() XLSX.utils.book_append_sheet( @@ -127,12 +193,12 @@ export default { ) XLSX.utils.book_append_sheet( wb, - XLSX.utils.json_to_sheet(this.people), + XLSX.utils.json_to_sheet(people), 'people' ) XLSX.utils.book_append_sheet( wb, - XLSX.utils.json_to_sheet(this.assignments), + XLSX.utils.json_to_sheet(assignments), 'assignment' ) XLSX.writeFile(wb, 'chart_data.xlsx') @@ -188,7 +254,9 @@ export default { manager_id: chart.manager.id, dataFields: chart.dataFields }) - chart.children.forEach(child => this.tree2arrayJSON(child, array)) + chart.children.forEach(child => + this.tree2arrayJSON(child, array) + ) return array } } diff --git a/components/OrgChart.vue b/components/OrgChart.vue index 5ab3dca..0d8d388 100644 --- a/components/OrgChart.vue +++ b/components/OrgChart.vue @@ -3,11 +3,7 @@ .chart_container show-dept(v-if="chart" :parent="chart" :level="1") draw-lines(v-if="chart") - //edit-menu(v-if="showEditMenu") view-menu(v-if="showViewMenu") - //#move_dept(v-if="moveDepartment" :style="{ left: page.left + 'px', top: page.top + 'px' }") - // div {{moveDepartment.name}} - // i.material-icons.arrow.down(v-if='moveDepartment.children.length') arrow_drop_down + diff --git a/components/SideScreen.vue b/components/SideScreen.vue index 73a5e15..e3126c8 100644 --- a/components/SideScreen.vue +++ b/components/SideScreen.vue @@ -1,95 +1,27 @@ - .side-screen(v-if="showSideScreen") - button.right(v-on:click="$store.commit('closeSideScreen')") - i.material-icons.arrow keyboard_arrow_left - template(v-if="activeDepartment") - .title {{activeDepartment.name}} - .tabs - button.tab(:class='{active:activeTab===1}' v-on:click='activeTab=1') Details - button.tab(:class='{active:activeTab===2}' v-on:click='activeTab=2') People - template(v-if="activeTab===1") - .property - span.label Name - br - input.name(v-if='editMode' :class="{error:!activeDepartment_name}" v-model="activeDepartment_name") - span(v-else).text {{activeDepartment_name}} - - .property - span.label Manager - i.material-icons.edit(v-if='editMode' v-on:click='personPicker="manager"') edit - br - span.text(v-if='activeDepartment.manager.name') {{activeDepartment.manager.name}} - span.untext(v-else) No manager - .property - span.label Description - br - textarea.description(v-if='editMode' v-model="activeDepartment_description") - span(v-else).text {{activeDepartment_description}} - - .property(v-for="field in activeDepartment.dataFields" v-if="field.value") - span.label {{field.name}} - br - template(v-if='editMode') - // todo edit extra properties - templage(v-else) - template(v-if='field.type==="url"') - a(:href="field.value" target="_blank") Link - template(v-else) - span.text {{field.value}} - - .property - template(v-if='!editMode') - span.label Department type - br - span(v-if='activeDepartment_isStaff') Staff department - span(v-else) Normal department - template(v-else) - span.label Staff department: - input.isstaff(type='checkbox' v-model="activeDepartment_isStaff" :disabled="!editMode") - .property - span.label Hierarchy - ul - li.clickable(v-for='(parent, pnr) in parents' v-on:click="setActiveDepartment(parent)") - span(v-for="n in pnr")   - i(v-if="pnr !==0").material-icons.sub subdirectory_arrow_right - span {{parent.name}} - li.clickable(v-on:click="setActiveDepartment(activeDepartment)") - span(v-for="n in parents.length")   - i(v-if="parents.length").material-icons.sub subdirectory_arrow_right - span.this-department {{activeDepartment.name}} - li.clickable(v-for='child in activeDepartment.children' v-on:click="setActiveDepartment(child)") - span(v-for="n in parents.length+5")   - span {{child.name}} - img.profile(:src='config.photoUrl.prefix+activeDepartment.manager.photo+config.photoUrl.suffix' v-on:click='visitProfile(activeDepartment.manager)' v-if="activeDepartment.manager.photo") - template(v-if='activeTab===2') - .assignment(v-for='person in department_people' v-on:click='visitProfile(person.person)') - table - tr - td - img.photo(v-if="person.photoURL" :src='person.photoURL' @error="markPhotoNotFound(person)") - i(v-else).material-icons.nophoto account_box - td - .name - span {{person.person.name}} - .role(v-if='!editMode') {{person.assignment.role}} - .role(v-else) - input(:value="person.assignment.role" @input="updateRole(person, $event)") - template(v-if='editMode') - i.material-icons.delete(title='remove from department' v-on:click='removeFromDepartment(person)') delete - button(v-if='editMode' v-on:click='personPicker="person"') Add person - person-picker(v-if='personPicker' :type='personPicker' v-on:close='personPicker=null') - - .noside-screen(v-else) - button.right(v-on:click="$store.commit('openSideScreen')") - i.material-icons.arrow keyboard_arrow_right + .side-screen(v-if="showSideScreen") + button.right(v-on:click="$store.commit('closeSideScreen')") + i.material-icons.arrow keyboard_arrow_left + template(v-if="activeDepartment") + .title {{activeDepartment.name}} + .tabs + button.tab(:class='{active:activeTab===1}' v-on:click='activeTab=1') Details + button.tab(:class='{active:activeTab===2}' v-on:click='activeTab=2') People + department-details(v-if="activeTab===1") + employee-list(v-if='activeTab===2') + + .noside-screen(v-else) + button.right(v-on:click="$store.commit('openSideScreen')") + i.material-icons.arrow keyboard_arrow_right diff --git a/components/ViewMenu.vue b/components/ViewMenu.vue index 455678b..5e26c61 100644 --- a/components/ViewMenu.vue +++ b/components/ViewMenu.vue @@ -22,7 +22,9 @@ export default { }, mounted: function() { var d = document.getElementById('view_menu') - var chartpos = document.getElementById('chart').getBoundingClientRect() + var chartpos = document + .getElementById('chart') + .getBoundingClientRect() d.style.display = 'inline-block' d.style.left = this.showViewMenu.clientX - chartpos.left + 'px' d.style.top = this.showViewMenu.clientY + -chartpos.top + 'px' diff --git a/docs/200.html b/docs/200.html index 9095719..bc315c1 100644 --- a/docs/200.html +++ b/docs/200.html @@ -1,9 +1,9 @@
-