Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / examples / personel-review / reviewapp.js
1 Ext.require([
2     '*'
3 ]);
4
5 Ext.BLANK_IMAGE_URL = '../libs/ext-4.0/resources/themes/images/default/tree/s.gif';
6
7 Ext.onReady(function() {
8
9     // Employee Data Model
10     Ext.regModel('Employee', {
11         fields: [
12             {name:'id', type:'int'},
13             {name:'first_name', type:'string'},
14             {name:'last_name', type:'string'},
15             {name:'title', type:'string'}
16         ],
17         
18         hasMany: {model:'Review', name:'reviews'}
19     });
20     
21     // Review Data Model    
22     Ext.regModel('Review', {
23         fields: [
24             {name:'review_date', label:'Date', type:'date', dateFormat:'d-m-Y'},
25             {name:'attendance', label:'Attendance', type:'int'},
26             {name:'attitude', label:'Attitude', type:'int'},
27             {name:'communication', label:'Communication', type:'int'},
28             {name:'excellence', label:'Excellence', type:'int'},
29             {name:'skills', label:'Skills', type:'int'},
30             {name:'teamwork', label:'Teamwork', type:'int'},
31             {name:'employee_id', label:'Employee ID', type:'int'}
32         ],
33         
34         belongsTo: 'Employee'
35     });
36     
37     // Instance of a Data Store to hold Employee records
38     var employeeStore = new Ext.data.Store({
39         storeId:'employeeStore',
40         model:'Employee',
41         data:[
42             {id:1, first_name:'Michael', last_name:'Scott', title:'Regional Manager'},
43             {id:2, first_name:'Dwight', last_name:'Schrute', title:'Sales Rep'},
44             {id:3, first_name:'Jim', last_name:'Halpert', title:'Sales Rep'},
45             {id:4, first_name:'Pam', last_name:'Halpert', title:'Office Administrator'},
46             {id:5, first_name:'Andy', last_name:'Bernard', title:'Sales Rep'},
47             {id:6, first_name:'Stanley', last_name:'Hudson', title:'Sales Rep'},
48             {id:7, first_name:'Phyllis', last_name:'Lapin-Vance', title:'Sales Rep'},
49             {id:8, first_name:'Kevin', last_name:'Malone', title:'Accountant'},
50             {id:9, first_name:'Angela', last_name:'Martin', title:'Senior Accountant'},             
51             {id:10, first_name:'Meredith', last_name:'Palmer', title:'Supplier Relations Rep'}                                                                                   
52         ],
53         autoLoad:true        
54     });    
55       
56    /**
57      * App.RadarStore
58      * @extends Ext.data.Store
59      * This is a specialized Data Store with dynamically generated fields
60      * data reformating capabilities to transform Employee and Review data
61      * into the format required by the Radar Chart.
62      *
63      * The constructor demonstrates dynamically generating store fields.
64      * populateReviewScores() populates the store using records from 
65      * the reviewStore which holds all the employee review scores.
66      *
67      * calculateAverageScores() iterates through each metric in the
68      * review and calculates an average across all available reviews.
69      * 
70      * Most of the actual data population and updates done by
71      * addUpdateRecordFromReviews() and removeRecordFromReviews()
72      * called when add/update/delete events are triggered on the ReviewStore.      
73      */      
74     Ext.define('App.RadarStore', {
75         extend: 'Ext.data.Store',
76         
77         constructor: function(config) {
78             config = config || {};
79             var dynamicFields = ['metric', 'avg'];  // initalize the non-dynamic fields first
80
81             employeeStore.each(function(record){    // loops through all the employees to setup the dynamic fields
82                 dynamicFields.push('eid_' + record.get('id'));
83             });
84                         
85             Ext.apply(config, {
86                 storeId:'radarStore',   // let's us look it up later using Ext.data.StoreMgr.lookup('radarStore')
87                 fields:dynamicFields,
88                 data:[]
89             });
90             
91             App.RadarStore.superclass.constructor.call(this, config);
92         },
93         
94         addUpdateRecordFromReviews: function(reviews) {
95             var me = this;
96             
97             Ext.Array.each(reviews, function(review, recordIndex, all) {    // add a new radarStore record for each review record 
98                 var eid = 'eid_' + review.get('employee_id');   // creates a unique id for each employee column in the store    
99                     
100                 review.fields.each(function(field) {
101                 
102                     if(field.name !== "employee_id" && field.name !== "review_date") {  // filter out the fields we don't need
103                         var metricRecord = me.findRecord('metric', field.name); // checks for an existing metric record in the store
104                         if(metricRecord) {
105                             metricRecord.set(eid, review.get(field.name));  // updates existing record with field value from review
106                         } else {
107                             var newRecord = {};    // creates a new object we can populate with dynamic keys and values to create a new record
108                             newRecord[eid] = review.get(field.name);
109                             newRecord['metric'] = field.label;
110                             me.add(newRecord);
111                         }
112                     }
113                 });
114             });
115             
116             this.calculateAverageScores();  // update average scores
117         },
118         
119        /**
120          * Calculates an average for each metric across all employees.
121          * We use this to create the average series always shown in the Radar Chart.      
122          */              
123         calculateAverageScores: function() {
124             var me = this; // keeps the store in scope during Ext.Array.each
125             var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
126             
127             var Review = Ext.ModelMgr.getModel('Review');
128             
129             Ext.Array.each(Review.prototype.fields.keys, function(fieldName) {  // loop through the Review model fields and calculate average scores
130                 if(fieldName !== "employee_id" && fieldName !== "review_date") {  // ignore non-score fields
131                     var avgScore = Math.round(reviewStore.average(fieldName));  // takes advantage of Ext.data.Store.average()
132                     var record = me.findRecord('metric', fieldName);
133                         
134                     if(record) {
135                         record.set('avg', avgScore);
136                     } else {
137                         me.add({metric:fieldName, avg:avgScore});
138                     }
139                 }
140             });
141         },
142         
143         populateReviewScores: function() {
144             var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
145             this.addUpdateRecordFromReviews(reviewStore.data.items); // add all the review records to this store
146         },
147            
148         removeRecordFromReviews: function(reviews) {
149             var me = this;
150             Ext.Array.each(reviews, function(review, recordIndex, all) {
151                 var eid = 'eid_' + review.get('employee_id');
152                 
153                 me.each(function(record) {
154                     delete record.data[eid];
155                 });
156             });
157             
158             // upate average scores
159             this.calculateAverageScores(); 
160         }
161     }); // end App.RadarStore definition
162       
163     
164    /** Creates an instance of App.RadarStore here so we
165      * here so we can re-use it during the life of the app.
166      * Otherwise we'd have to create a new instance everytime
167      * refreshRadarChart() is run.
168      */
169     var radarStore = new App.RadarStore();
170             
171     var reviewStore = new Ext.data.Store({
172         storeId:'reviewStore',
173         model:'Review',
174         data:[
175             {review_date:'01-04-2011', attendance:10, attitude:6, communication:6, excellence:3, skills:3, teamwork:3, employee_id:1},
176             {review_date:'01-04-2011', attendance:6, attitude:5, communication:2, excellence:8, skills:9, teamwork:5, employee_id:2},
177             {review_date:'01-04-2011', attendance:5, attitude:4, communication:3, excellence:5, skills:6, teamwork:2, employee_id:3},
178             {review_date:'01-04-2011', attendance:8, attitude:2, communication:4, excellence:2, skills:5, teamwork:6, employee_id:4},
179             {review_date:'01-04-2011', attendance:4, attitude:1, communication:5, excellence:7, skills:5, teamwork:5, employee_id:5},
180             {review_date:'01-04-2011', attendance:5, attitude:2, communication:4, excellence:7, skills:9, teamwork:8, employee_id:6},
181             {review_date:'01-04-2011', attendance:10, attitude:7, communication:8, excellence:7, skills:3, teamwork:4, employee_id:7},                        
182             {review_date:'01-04-2011', attendance:10, attitude:8, communication:8, excellence:4, skills:8, teamwork:7, employee_id:8},
183             {review_date:'01-04-2011', attendance:6, attitude:4, communication:9, excellence:7, skills:6, teamwork:5, employee_id:9},
184             {review_date:'01-04-2011', attendance:7, attitude:5, communication:9, excellence:4, skills:2, teamwork:4, employee_id:10}            
185         ],
186         listeners: {
187             add:function(store, records, storeIndex) {
188                 var radarStore = Ext.data.StoreMgr.lookup('radarStore');
189                 
190                 if(radarStore) {    // only add records if an instance of the rardarStore already exists
191                     radarStore.addUpdateRecordFromReviews(records);   // add a new radarStore records for new review records                              
192                 }
193             }, // end add listener
194             update: function(store, record, operation) {
195                 radarStore.addUpdateRecordFromReviews([record]);
196                 refreshRadarChart();
197             },
198             remove: function(store, records, storeIndex) {
199                 // update the radarStore and regenerate the radarChart
200                 Ext.data.StoreMgr.lookup('radarStore').removeRecordFromReviews(records);
201                 refreshRadarChart();
202             } // end remove listener
203         }
204     });
205         
206    /**
207      * App.PerformanceRadar
208      * @extends Ext.chart.Chart
209      * This is a specialized Radar Chart which we use to display employee 
210      * performance reviews.
211      *
212      * The class will be registered with an xtype of 'performanceradar'
213      */      
214     Ext.define('App.PerformanceRadar', {
215         extend: 'Ext.chart.Chart',
216         alias: 'widget.performanceradar',           // register xtype performanceradar
217         constructor: function(config) {
218             config = config || {};
219             
220             this.setAverageSeries(config);    // make sure average is always present
221             
222             Ext.apply(config, {
223                 id:'radarchart',
224                 theme:'Category2',
225                 animate:true,
226                 store: Ext.data.StoreMgr.lookup('radarStore'),
227                 margin:'0 0 50 0',
228                 width:350,
229                 height:500,
230                 insetPadding:80,
231                 legend:{
232                     position: 'bottom'
233                 },
234                 axes: [{
235                     type:'Radial',
236                     position:'radial',
237                     label:{
238                         display: true
239                     }
240                 }]
241             }); // end Ext.apply
242             
243             App.PerformanceRadar.superclass.constructor.call(this, config);
244         
245         }, // end constructor
246         
247         setAverageSeries: function(config) {
248             var avgSeries = {
249                 type: 'radar',
250                 xField: 'metric',
251                 yField: 'avg',
252                 title: 'Avg',
253                 labelDisplay:'over',
254                 showInLegend: true,
255                 showMarkers: true,
256                 markerCfg: {
257                     radius: 5,
258                     size: 5,
259                     stroke:'#0677BD',
260                     fill:'#0677BD'
261                 },
262                 style: {
263                     'stroke-width': 2,
264                     'stroke':'#0677BD',
265                     fill: 'none'
266                 }
267             }
268             
269             if(config.series) {        
270                 config.series.push(avgSeries);     // if a series is passed in then append the average to it
271             } else {                    
272                 config.series = [avgSeries];    // if a series isn't passed just create average
273             }
274         } 
275     
276     }); // end Ext.ux.Performance radar definition
277     
278    /**
279      * App.EmployeeDetail
280      * @extends Ext.Panel
281      * This is a specialized Panel which is used to show information about
282      * an employee and the reviews we have on record for them.
283      *
284      * This demonstrates adding 2 custom properties (tplMarkup and
285      * startingMarkup) to the class. It also overrides the initComponent
286      * method and adds a new method called updateDetail.
287      *
288      * The class will be registered with an xtype of 'employeedetail'
289      */
290     Ext.define('App.EmployeeDetail', {
291         extend: 'Ext.panel.Panel',
292         // register the App.EmployeeDetail class with an xtype of employeedetail
293         alias: 'widget.employeedetail',
294         // add tplMarkup as a new property
295         tplMarkup: [
296             '<b>{first_name}&nbsp;{last_name}</b>&nbsp;&nbsp;',
297             'Title: {title}<br/><br/>',
298             '<b>Last Review</b>&nbsp;&nbsp;',
299             'Attendance:&nbsp;{attendance}&nbsp;&nbsp;',
300             'Attitude:&nbsp;{attitude}&nbsp;&nbsp;',
301             'Communication:&nbsp;{communication}&nbsp;&nbsp;',
302             'Excellence:&nbsp;{excellence}&nbsp;&nbsp;',
303             'Skills:&nbsp;{skills}&nbsp;&nbsp;',
304             'Teamwork:&nbsp;{teamwork}' 
305         ],
306         
307         height:90,
308         bodyPadding: 7,
309         // override initComponent to create and compile the template
310         // apply styles to the body of the panel
311         initComponent: function() {
312             this.tpl = new Ext.Template(this.tplMarkup);
313                                    
314             // call the superclass's initComponent implementation
315             App.EmployeeDetail.superclass.initComponent.call(this);
316         }
317     });
318             
319     Ext.define('App.ReviewWindow', {
320         extend: 'Ext.window.Window',
321
322         constructor: function(config) {        
323             config = config || {};
324             Ext.apply(config, {        
325                 title:'Employee Performance Review',
326                 width:320,
327                 height:420,
328                 layout:'fit',        
329                 items:[{
330                     xtype:'form',
331                     id:'employeereviewcomboform',
332                     fieldDefaults: {
333                         labelAlign: 'left',
334                         labelWidth: 90,
335                         anchor: '100%'
336                     },            
337                     bodyPadding:5,
338                     items:[{
339                         xtype:'fieldset',
340                         title:'Employee Info',
341                         items:[{
342                             xtype:'hiddenfield',
343                             name:'employee_id'
344                         },{
345                             xtype:'textfield',
346                             name:'first_name',
347                             fieldLabel:'First Name',
348                             allowBlank:false
349                         },{
350                             xtype:'textfield',
351                             name:'last_name',
352                             fieldLabel:'Last Name',
353                             allowBlank:false                               
354                         },{
355                             xtype:'textfield',
356                             name:'title',
357                             fieldLabel:'Title',
358                             allowBlank:false                               
359                         }]
360                     },{
361                         xtype:'fieldset',
362                         title:'Performance Review',
363                         items:[{
364                             xtype:'datefield',
365                             name:'review_date',
366                             fieldLabel:'Review Date',
367                             format:'d-m-Y',                   
368                             maxValue: new Date(),
369                             value: new Date(),
370                             allowBlank:false
371                         },{
372                             xtype:'slider',
373                             name:'attendance',
374                             fieldLabel:'Attendance',                    
375                             value:5,
376                             increment:1,
377                             minValue:1,
378                             maxValue:10
379                         },{
380                             xtype:'slider',
381                             name:'attitude',
382                             fieldLabel:'Attitude',
383                             value:5,
384                             minValue: 1,
385                             maxValue: 10
386                         },{
387                             xtype:'slider',
388                             name:'communication',
389                             fieldLabel:'Communication',                    
390                             value:5,
391                             increment:1,
392                             minValue:1,
393                             maxValue:10
394                         },{
395                             xtype:'numberfield',
396                             name:'excellence',
397                             fieldLabel:'Excellence',
398                             value:5,
399                             minValue: 1,
400                             maxValue: 10                
401                         },{
402                             xtype:'numberfield',
403                             name:'skills',
404                             fieldLabel:'Skills',
405                             value:5,
406                             minValue: 1,
407                             maxValue: 10                
408                         },{
409                             xtype:'numberfield',
410                             name:'teamwork',
411                             fieldLabel:'Teamwork',
412                             value:5,
413                             minValue: 1,
414                             maxValue: 10                
415                         }]
416                     }]
417                 }],
418                 buttons:[{
419                     text:'Cancel',
420                     width:80,
421                     handler:function() {
422                         this.up('window').close();
423                     }
424                 },
425                 {
426                     text:'Save',
427                     width:80,
428                     handler:function(btn, eventObj) {
429                         var window = btn.up('window');
430                         var form = window.down('form').getForm();
431                         
432                         if (form.isValid()) {
433                             window.getEl().mask('saving data...');
434                             var vals = form.getValues();
435                             var employeeStore = Ext.data.StoreMgr.lookup('employeeStore');
436                             var currentEmployee = employeeStore.findRecord('id', vals['employee_id']);
437                             
438                             // look up id for this employee to see if they already exist
439                             if(vals['employee_id'] && currentEmployee) {
440                                 currentEmployee.set('first_name', vals['first_name']);
441                                 currentEmployee.set('last_name', vals['last_name']);
442                                 currentEmployee.set('title', vals['title']);
443                                 
444                                 var currentReview = Ext.data.StoreMgr.lookup('reviewStore').findRecord('employee_id', vals['employee_id']);
445                                 currentReview.set('review_date', vals['review_date']);
446                                 currentReview.set('attendance', vals['attendance']);
447                                 currentReview.set('attitude', vals['attitude']);
448                                 currentReview.set('communication', vals['communication']);
449                                 currentReview.set('excellence', vals['excellence']);
450                                 currentReview.set('skills', vals['skills']);
451                                 currentReview.set('teamwork', vals['teamwork']);                                                                                                                                                                
452                             } else {
453                                 var newId = employeeStore.getCount() + 1; 
454                                                                                            
455                                 employeeStore.add({
456                                     id: newId,
457                                     first_name: vals['first_name'],
458                                     last_name: vals['last_name'],
459                                     title: vals['title']
460                                 });
461     
462                                 Ext.data.StoreMgr.lookup('reviewStore').add({
463                                     review_date: vals['review_date'],
464                                     attendance: vals['attendance'],
465                                     attitude: vals['attitude'],
466                                     communication: vals['communication'],
467                                     excellence: vals['excellence'],
468                                     skills: vals['skills'],
469                                     teamwork: vals['teamwork'],
470                                     employee_id: newId
471                                 });
472                             }
473                             window.getEl().unmask();
474                             window.close();
475                         }
476                     }
477                 }]
478             }); // end Ext.apply
479             
480             App.ReviewWindow.superclass.constructor.call(this, config);
481             
482         } // end constructor
483         
484     });
485
486             
487     // adds a record to the radar chart store and 
488     // creates a series in the chart for selected employees
489     function refreshRadarChart(employees) {       
490         employees = employees || []; // in case its called with nothing we'll at least have an empty array
491         var existingRadarChart = Ext.getCmp('radarchart'); // grab the radar chart component (used down below)
492         var reportsPanel = Ext.getCmp('reportspanel'); // grab the reports panel component (used down below)
493         var dynamicSeries = []; // setup an array of chart series that we'll create dynamically
494        
495         for(var index = 0; index < employees.length; index++) {
496             var fullName = employees[index].get('first_name') + ' ' + employees[index].get('last_name');
497             var eid = 'eid_' + employees[index].get('id');
498                        
499             // add to the dynamic series we're building
500             dynamicSeries.push({
501                 type: 'radar',
502                 title: fullName,
503                 xField: 'metric',
504                 yField: eid,
505                 labelDisplay: 'over',
506                 showInLegend: true,
507                 showMarkers: true,
508                 markerCfg: {
509                     radius: 5,
510                     size: 5
511                 },
512                 style: {
513                     'stroke-width': 2,
514                     fill: 'none'
515                 }
516             });
517             
518         } // end for loop
519         
520         // create the new chart using the dynamic series we just made
521         var newRadarChart = new App.PerformanceRadar({series:dynamicSeries});
522         // mask the panel while we switch out charts
523         reportsPanel.getEl().mask('updating chart...');
524         // destroy the existing chart
525         existingRadarChart.destroy();
526         // display the new one
527         reportsPanel.add(newRadarChart);
528         // un mask the reports panel
529         reportsPanel.getEl().unmask();
530     }
531     
532     function refreshEmployeeDetails(employees) {
533         var detailsPanel = Ext.getCmp('detailspanel');
534         var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
535         var items = [];
536                
537         for(var index = 0; index < employees.length; index++) {
538             var templateData = Ext.merge(employees[index].data, reviewStore.findRecord('employee_id', employees[index].get('id')).data);
539             var employeePanel = new App.EmployeeDetail({
540                 title:employees[index].get('first_name') + ' ' + employees[index].get('last_name'),
541                 data:templateData // combined employee and latest review dataTransfer
542             });
543             items.push(employeePanel);
544         }
545         
546         detailsPanel.getEl().mask('updating details...');
547         detailsPanel.removeAll();
548         detailsPanel.add(items);
549         detailsPanel.getEl().unmask();
550     }
551     
552     // sets Up Checkbox Selection Model for the Employee Grid
553     var checkboxSelModel = new Ext.selection.CheckboxModel();
554     
555     var viewport = new Ext.container.Viewport({
556         id:'mainviewport',
557         layout: 'border',            // sets up Ext.layout.container.Border
558         items: [{
559             xtype:'panel',
560             region:'center',
561             layout:'auto',
562             autoScroll:true,
563             title:'Employee Performance Manager',
564             tbar:[{
565                 text:'Add Employee',
566                 tooltip:'Add a new employee',
567                 iconCls:'add',
568                 handler:function() {       // display a window to add a new employee
569                     new App.ReviewWindow().show();
570                 }
571             }],
572             items:[{
573                 xtype:'grid',
574                 store:Ext.data.StoreMgr.lookup('employeeStore'),
575                 height:300,
576                 columns:[{
577                     text:'First Name',
578                     dataIndex:'first_name',
579                     flex:2
580                 },
581                 {
582                     text:'Last Name',
583                     dataIndex:'last_name',
584                     flex:2
585                 },
586                 {
587                     text:'Title',
588                     dataIndex:'title',
589                     flex:3
590                 },
591                 {
592                     xtype:'actioncolumn',
593                     width:45,
594                     items:[{
595                         icon:'images/edit.png',
596                         tooltip:'Edit Employee',
597                         handler:function(grid, rowIndex, colIndex) {
598                               var employee = grid.getStore().getAt(rowIndex);
599                               var review = reviewStore.findRecord('employee_id', employee.get('id'));
600                               var win = new App.ReviewWindow({hidden:true});
601                               var form = win.down('form').getForm();
602                               form.loadRecord(employee);
603                               form.loadRecord(review);
604                               win.show();
605                         }
606                     },
607                     {
608                         icon:'images/delete.png',
609                         tooltip:'Delete Employee',
610                         width:75,
611                         handler:function(grid, rowIndex, colIndex) {
612                             Ext.Msg.confirm('Remove Employee?', 'Are you sure you want to remove this employee?',
613                                 function(choice) {
614                                     if(choice === 'yes') {
615                                         var reviewStore = Ext.data.StoreMgr.lookup('reviewStore');
616                                     
617                                         var employee = grid.getStore().getAt(rowIndex);
618                                         var reviewIndex = reviewStore.find('employee_id', employee.get('id'));
619                                         reviewStore.removeAt(reviewIndex);
620                                         grid.getStore().removeAt(rowIndex);
621                                     }
622                                 }
623                             );   
624                         }
625                     }]
626                 }],
627                 selModel: new Ext.selection.CheckboxModel(),
628                 columnLines: true,
629                 viewConfig: {stripeRows:true},
630                 listeners:{
631                     selectionchange:function(selModel, selected) {
632                         refreshRadarChart(selected);
633                         refreshEmployeeDetails(selected);
634                     }
635                 }
636             },{
637                 xtype:'container',
638                 id:'detailspanel',
639                 layout:{
640                     type:'vbox',
641                     align:'stretch',
642                     autoSize:true
643                 }
644             }]
645         },{
646             xtype:'panel',          // sets up the chart panel (starts collapsed)
647             region:'east',
648             id:'reportspanel',
649             title:'Performance Report',
650             width:350,
651             layout: 'fit',
652             items:[{
653                 xtype:'performanceradar'  // this instantiates a App.PerformanceRadar object
654             }]
655         }]  // mainviewport items array ends here
656     });
657         
658 });