--- /dev/null
+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: [
+ '<b>{first_name} {last_name}</b> ',
+ 'Title: {title}<br/><br/>',
+ '<b>Last Review</b> ',
+ '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
+
+ // 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...');
+ // destroy the existing chart
+ existingRadarChart.destroy();
+ // 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.merge(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
+ });
+
+});
\ No newline at end of file