Ext.require([ '*' ]); Ext.BLANK_IMAGE_URL = '../libs/ext-4.0/resources/themes/images/default/tree/s.gif'; Ext.onReady(function() { // Employee Data Model Ext.regModel('Employee', { fields: [ {name:'id', type:'int'}, {name:'first_name', type:'string'}, {name:'last_name', type:'string'}, {name:'title', type:'string'} ], hasMany: {model:'Review', name:'reviews'} }); // Review Data Model Ext.regModel('Review', { fields: [ {name:'review_date', label:'Date', type:'date', dateFormat:'d-m-Y'}, {name:'attendance', label:'Attendance', type:'int'}, {name:'attitude', label:'Attitude', type:'int'}, {name:'communication', label:'Communication', type:'int'}, {name:'excellence', label:'Excellence', type:'int'}, {name:'skills', label:'Skills', type:'int'}, {name:'teamwork', label:'Teamwork', type:'int'}, {name:'employee_id', label:'Employee ID', type:'int'} ], belongsTo: 'Employee' }); // Instance of a Data Store to hold Employee records var employeeStore = new Ext.data.Store({ storeId:'employeeStore', model:'Employee', data:[ {id:1, first_name:'Michael', last_name:'Scott', title:'Regional Manager'}, {id:2, first_name:'Dwight', last_name:'Schrute', title:'Sales Rep'}, {id:3, first_name:'Jim', last_name:'Halpert', title:'Sales Rep'}, {id:4, first_name:'Pam', last_name:'Halpert', title:'Office Administrator'}, {id:5, first_name:'Andy', last_name:'Bernard', title:'Sales Rep'}, {id:6, first_name:'Stanley', last_name:'Hudson', title:'Sales Rep'}, {id:7, first_name:'Phyllis', last_name:'Lapin-Vance', title:'Sales Rep'}, {id:8, first_name:'Kevin', last_name:'Malone', title:'Accountant'}, {id:9, first_name:'Angela', last_name:'Martin', title:'Senior Accountant'}, {id:10, first_name:'Meredith', last_name:'Palmer', title:'Supplier Relations Rep'} ], autoLoad:true }); /** * App.RadarStore * @extends Ext.data.Store * This is a specialized Data Store with dynamically generated fields * data reformating capabilities to transform Employee and Review data * into the format required by the Radar Chart. * * The constructor demonstrates dynamically generating store fields. * populateReviewScores() populates the store using records from * the reviewStore which holds all the employee review scores. * * calculateAverageScores() iterates through each metric in the * review and calculates an average across all available reviews. * * Most of the actual data population and updates done by * addUpdateRecordFromReviews() and removeRecordFromReviews() * called when add/update/delete events are triggered on the ReviewStore. */ Ext.define('App.RadarStore', { extend: 'Ext.data.Store', constructor: function(config) { config = config || {}; var dynamicFields = ['metric', 'avg']; // initalize the non-dynamic fields first employeeStore.each(function(record){ // loops through all the employees to setup the dynamic fields dynamicFields.push('eid_' + record.get('id')); }); Ext.apply(config, { storeId:'radarStore', // let's us look it up later using Ext.data.StoreMgr.lookup('radarStore') fields:dynamicFields, data:[] }); App.RadarStore.superclass.constructor.call(this, config); }, addUpdateRecordFromReviews: function(reviews) { var me = this; Ext.Array.each(reviews, function(review, recordIndex, all) { // add a new radarStore record for each review record var eid = 'eid_' + review.get('employee_id'); // creates a unique id for each employee column in the store review.fields.each(function(field) { if(field.name !== "employee_id" && field.name !== "review_date") { // filter out the fields we don't need var metricRecord = me.findRecord('metric', field.name); // checks for an existing metric record in the store if(metricRecord) { metricRecord.set(eid, review.get(field.name)); // updates existing record with field value from review } else { var newRecord = {}; // creates a new object we can populate with dynamic keys and values to create a new record newRecord[eid] = review.get(field.name); newRecord['metric'] = field.label; me.add(newRecord); } } }); }); this.calculateAverageScores(); // update average scores }, /** * Calculates an average for each metric across all employees. * We use this to create the average series always shown in the Radar Chart. */ calculateAverageScores: function() { var me = this; // keeps the store in scope during Ext.Array.each var reviewStore = Ext.data.StoreMgr.lookup('reviewStore'); var Review = Ext.ModelMgr.getModel('Review'); Ext.Array.each(Review.prototype.fields.keys, function(fieldName) { // loop through the Review model fields and calculate average scores if(fieldName !== "employee_id" && fieldName !== "review_date") { // ignore non-score fields var avgScore = Math.round(reviewStore.average(fieldName)); // takes advantage of Ext.data.Store.average() var record = me.findRecord('metric', fieldName); if(record) { record.set('avg', avgScore); } else { me.add({metric:fieldName, avg:avgScore}); } } }); }, populateReviewScores: function() { var reviewStore = Ext.data.StoreMgr.lookup('reviewStore'); this.addUpdateRecordFromReviews(reviewStore.data.items); // add all the review records to this store }, removeRecordFromReviews: function(reviews) { var me = this; Ext.Array.each(reviews, function(review, recordIndex, all) { var eid = 'eid_' + review.get('employee_id'); me.each(function(record) { delete record.data[eid]; }); }); // upate average scores this.calculateAverageScores(); } }); // end App.RadarStore definition /** Creates an instance of App.RadarStore here so we * here so we can re-use it during the life of the app. * Otherwise we'd have to create a new instance everytime * refreshRadarChart() is run. */ var radarStore = new App.RadarStore(); var reviewStore = new Ext.data.Store({ storeId:'reviewStore', model:'Review', data:[ {review_date:'01-04-2011', attendance:10, attitude:6, communication:6, excellence:3, skills:3, teamwork:3, employee_id:1}, {review_date:'01-04-2011', attendance:6, attitude:5, communication:2, excellence:8, skills:9, teamwork:5, employee_id:2}, {review_date:'01-04-2011', attendance:5, attitude:4, communication:3, excellence:5, skills:6, teamwork:2, employee_id:3}, {review_date:'01-04-2011', attendance:8, attitude:2, communication:4, excellence:2, skills:5, teamwork:6, employee_id:4}, {review_date:'01-04-2011', attendance:4, attitude:1, communication:5, excellence:7, skills:5, teamwork:5, employee_id:5}, {review_date:'01-04-2011', attendance:5, attitude:2, communication:4, excellence:7, skills:9, teamwork:8, employee_id:6}, {review_date:'01-04-2011', attendance:10, attitude:7, communication:8, excellence:7, skills:3, teamwork:4, employee_id:7}, {review_date:'01-04-2011', attendance:10, attitude:8, communication:8, excellence:4, skills:8, teamwork:7, employee_id:8}, {review_date:'01-04-2011', attendance:6, attitude:4, communication:9, excellence:7, skills:6, teamwork:5, employee_id:9}, {review_date:'01-04-2011', attendance:7, attitude:5, communication:9, excellence:4, skills:2, teamwork:4, employee_id:10} ], listeners: { add:function(store, records, storeIndex) { var radarStore = Ext.data.StoreMgr.lookup('radarStore'); if(radarStore) { // only add records if an instance of the rardarStore already exists radarStore.addUpdateRecordFromReviews(records); // add a new radarStore records for new review records } }, // end add listener update: function(store, record, operation) { radarStore.addUpdateRecordFromReviews([record]); refreshRadarChart(); }, remove: function(store, records, storeIndex) { // update the radarStore and regenerate the radarChart Ext.data.StoreMgr.lookup('radarStore').removeRecordFromReviews(records); refreshRadarChart(); } // end remove listener } }); /** * App.PerformanceRadar * @extends Ext.chart.Chart * This is a specialized Radar Chart which we use to display employee * performance reviews. * * The class will be registered with an xtype of 'performanceradar' */ Ext.define('App.PerformanceRadar', { extend: 'Ext.chart.Chart', alias: 'widget.performanceradar', // register xtype performanceradar constructor: function(config) { config = config || {}; this.setAverageSeries(config); // make sure average is always present Ext.apply(config, { id:'radarchart', theme:'Category2', animate:true, store: Ext.data.StoreMgr.lookup('radarStore'), margin:'0 0 50 0', width:350, height:500, insetPadding:80, legend:{ position: 'bottom' }, axes: [{ type:'Radial', position:'radial', label:{ display: true } }] }); // end Ext.apply App.PerformanceRadar.superclass.constructor.call(this, config); }, // end constructor setAverageSeries: function(config) { var avgSeries = { type: 'radar', xField: 'metric', yField: 'avg', title: 'Avg', labelDisplay:'over', showInLegend: true, showMarkers: true, markerCfg: { radius: 5, size: 5, stroke:'#0677BD', fill:'#0677BD' }, style: { 'stroke-width': 2, 'stroke':'#0677BD', fill: 'none' } } if(config.series) { config.series.push(avgSeries); // if a series is passed in then append the average to it } else { config.series = [avgSeries]; // if a series isn't passed just create average } } }); // end Ext.ux.Performance radar definition /** * App.EmployeeDetail * @extends Ext.Panel * This is a specialized Panel which is used to show information about * an employee and the reviews we have on record for them. * * This demonstrates adding 2 custom properties (tplMarkup and * startingMarkup) to the class. It also overrides the initComponent * method and adds a new method called updateDetail. * * The class will be registered with an xtype of 'employeedetail' */ Ext.define('App.EmployeeDetail', { extend: 'Ext.panel.Panel', // register the App.EmployeeDetail class with an xtype of employeedetail alias: 'widget.employeedetail', // add tplMarkup as a new property tplMarkup: [ '{first_name} {last_name}  ', 'Title: {title}

', 'Last Review  ', 'Attendance: {attendance}  ', 'Attitude: {attitude}  ', 'Communication: {communication}  ', 'Excellence: {excellence}  ', 'Skills: {skills}  ', 'Teamwork: {teamwork}' ], height:90, bodyPadding: 7, // override initComponent to create and compile the template // apply styles to the body of the panel initComponent: function() { this.tpl = new Ext.Template(this.tplMarkup); // call the superclass's initComponent implementation App.EmployeeDetail.superclass.initComponent.call(this); } }); Ext.define('App.ReviewWindow', { extend: 'Ext.window.Window', constructor: function(config) { config = config || {}; Ext.apply(config, { title:'Employee Performance Review', width:320, height:420, layout:'fit', items:[{ xtype:'form', id:'employeereviewcomboform', fieldDefaults: { labelAlign: 'left', labelWidth: 90, anchor: '100%' }, bodyPadding:5, items:[{ xtype:'fieldset', title:'Employee Info', items:[{ xtype:'hiddenfield', name:'employee_id' },{ xtype:'textfield', name:'first_name', fieldLabel:'First Name', allowBlank:false },{ xtype:'textfield', name:'last_name', fieldLabel:'Last Name', allowBlank:false },{ xtype:'textfield', name:'title', fieldLabel:'Title', allowBlank:false }] },{ xtype:'fieldset', title:'Performance Review', items:[{ xtype:'datefield', name:'review_date', fieldLabel:'Review Date', format:'d-m-Y', maxValue: new Date(), value: new Date(), allowBlank:false },{ xtype:'slider', name:'attendance', fieldLabel:'Attendance', value:5, increment:1, minValue:1, maxValue:10 },{ xtype:'slider', name:'attitude', fieldLabel:'Attitude', value:5, minValue: 1, maxValue: 10 },{ xtype:'slider', name:'communication', fieldLabel:'Communication', value:5, increment:1, minValue:1, maxValue:10 },{ xtype:'numberfield', name:'excellence', fieldLabel:'Excellence', value:5, minValue: 1, maxValue: 10 },{ xtype:'numberfield', name:'skills', fieldLabel:'Skills', value:5, minValue: 1, maxValue: 10 },{ xtype:'numberfield', name:'teamwork', fieldLabel:'Teamwork', value:5, minValue: 1, maxValue: 10 }] }] }], buttons:[{ text:'Cancel', width:80, handler:function() { this.up('window').close(); } }, { text:'Save', width:80, handler:function(btn, eventObj) { var window = btn.up('window'); var form = window.down('form').getForm(); if (form.isValid()) { window.getEl().mask('saving data...'); var vals = form.getValues(); var employeeStore = Ext.data.StoreMgr.lookup('employeeStore'); var currentEmployee = employeeStore.findRecord('id', vals['employee_id']); // look up id for this employee to see if they already exist if(vals['employee_id'] && currentEmployee) { currentEmployee.set('first_name', vals['first_name']); currentEmployee.set('last_name', vals['last_name']); currentEmployee.set('title', vals['title']); var currentReview = Ext.data.StoreMgr.lookup('reviewStore').findRecord('employee_id', vals['employee_id']); currentReview.set('review_date', vals['review_date']); currentReview.set('attendance', vals['attendance']); currentReview.set('attitude', vals['attitude']); currentReview.set('communication', vals['communication']); currentReview.set('excellence', vals['excellence']); currentReview.set('skills', vals['skills']); currentReview.set('teamwork', vals['teamwork']); } else { var newId = employeeStore.getCount() + 1; employeeStore.add({ id: newId, first_name: vals['first_name'], last_name: vals['last_name'], title: vals['title'] }); Ext.data.StoreMgr.lookup('reviewStore').add({ review_date: vals['review_date'], attendance: vals['attendance'], attitude: vals['attitude'], communication: vals['communication'], excellence: vals['excellence'], skills: vals['skills'], teamwork: vals['teamwork'], employee_id: newId }); } window.getEl().unmask(); window.close(); } } }] }); // end Ext.apply App.ReviewWindow.superclass.constructor.call(this, config); } // end constructor }); // adds a record to the radar chart store and // creates a series in the chart for selected employees function refreshRadarChart(employees) { employees = employees || []; // in case its called with nothing we'll at least have an empty array var existingRadarChart = Ext.getCmp('radarchart'); // grab the radar chart component (used down below) var reportsPanel = Ext.getCmp('reportspanel'); // grab the reports panel component (used down below) var dynamicSeries = []; // setup an array of chart series that we'll create dynamically for(var index = 0; index < employees.length; index++) { var fullName = employees[index].get('first_name') + ' ' + employees[index].get('last_name'); var eid = 'eid_' + employees[index].get('id'); // add to the dynamic series we're building dynamicSeries.push({ type: 'radar', title: fullName, xField: 'metric', yField: eid, labelDisplay: 'over', showInLegend: true, showMarkers: true, markerCfg: { radius: 5, size: 5 }, style: { 'stroke-width': 2, fill: 'none' } }); } // end for loop // destroy the existing chart existingRadarChart.destroy(); // create the new chart using the dynamic series we just made var newRadarChart = new App.PerformanceRadar({series:dynamicSeries}); // mask the panel while we switch out charts reportsPanel.getEl().mask('updating chart...'); // display the new one reportsPanel.add(newRadarChart); // un mask the reports panel reportsPanel.getEl().unmask(); } function refreshEmployeeDetails(employees) { var detailsPanel = Ext.getCmp('detailspanel'); var reviewStore = Ext.data.StoreMgr.lookup('reviewStore'); var items = []; for(var index = 0; index < employees.length; index++) { var templateData = Ext.applyIf(employees[index].data, reviewStore.findRecord('employee_id', employees[index].get('id')).data); var employeePanel = new App.EmployeeDetail({ title:employees[index].get('first_name') + ' ' + employees[index].get('last_name'), data:templateData // combined employee and latest review dataTransfer }); items.push(employeePanel); } detailsPanel.getEl().mask('updating details...'); detailsPanel.removeAll(); detailsPanel.add(items); detailsPanel.getEl().unmask(); } // sets Up Checkbox Selection Model for the Employee Grid var checkboxSelModel = new Ext.selection.CheckboxModel(); var viewport = new Ext.container.Viewport({ id:'mainviewport', layout: 'border', // sets up Ext.layout.container.Border items: [{ xtype:'panel', region:'center', layout:'auto', autoScroll:true, title:'Employee Performance Manager', tbar:[{ text:'Add Employee', tooltip:'Add a new employee', iconCls:'add', handler:function() { // display a window to add a new employee new App.ReviewWindow().show(); } }], items:[{ xtype:'grid', store:Ext.data.StoreMgr.lookup('employeeStore'), height:300, columns:[{ text:'First Name', dataIndex:'first_name', flex:2 }, { text:'Last Name', dataIndex:'last_name', flex:2 }, { text:'Title', dataIndex:'title', flex:3 }, { xtype:'actioncolumn', width:45, items:[{ icon:'images/edit.png', tooltip:'Edit Employee', handler:function(grid, rowIndex, colIndex) { var employee = grid.getStore().getAt(rowIndex); var review = reviewStore.findRecord('employee_id', employee.get('id')); var win = new App.ReviewWindow({hidden:true}); var form = win.down('form').getForm(); form.loadRecord(employee); form.loadRecord(review); win.show(); } }, { icon:'images/delete.png', tooltip:'Delete Employee', width:75, handler:function(grid, rowIndex, colIndex) { Ext.Msg.confirm('Remove Employee?', 'Are you sure you want to remove this employee?', function(choice) { if(choice === 'yes') { var reviewStore = Ext.data.StoreMgr.lookup('reviewStore'); var employee = grid.getStore().getAt(rowIndex); var reviewIndex = reviewStore.find('employee_id', employee.get('id')); reviewStore.removeAt(reviewIndex); grid.getStore().removeAt(rowIndex); } } ); } }] }], selModel: new Ext.selection.CheckboxModel(), columnLines: true, viewConfig: {stripeRows:true}, listeners:{ selectionchange:function(selModel, selected) { refreshRadarChart(selected); refreshEmployeeDetails(selected); } } },{ xtype:'container', id:'detailspanel', layout:{ type:'vbox', align:'stretch', autoSize:true } }] },{ xtype:'panel', // sets up the chart panel (starts collapsed) region:'east', id:'reportspanel', title:'Performance Report', width:350, layout: 'fit', items:[{ xtype:'performanceradar' // this instantiates a App.PerformanceRadar object }] }] // mainviewport items array ends here }); });