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