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
|
|
// 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
|
});
|
|
});
|