fe5799d3aff1e167de2cd8fe56aa87643fdfe6f5
[odoo/odoo.git] / addons / web_calendar / static / src / js / calendar.js
1 /*---------------------------------------------------------
2  * OpenERP web_calendar
3  *---------------------------------------------------------*/
4
5 openerp.web_calendar = function(openerp) {
6 var _t = openerp.web._t,
7    _lt = openerp.web._lt;
8 var QWeb = openerp.web.qweb;
9 openerp.web.views.add('calendar', 'openerp.web_calendar.CalendarView');
10 openerp.web_calendar.CalendarView = openerp.web.View.extend({
11     display_name: _lt('Calendar'),
12 // Dhtmlx scheduler ?
13     init: function(parent, dataset, view_id, options) {
14         this._super(parent);
15         this.ready = $.Deferred();
16         this.set_default_options(options);
17         this.dataset = dataset;
18         this.model = dataset.model;
19         this.fields_view = {};
20         this.view_id = view_id;
21         this.has_been_loaded = $.Deferred();
22         this.creating_event_id = null;
23         this.dataset_events = [];
24         this.form_dialog = new openerp.web_calendar.CalendarFormDialog(this, {
25                 destroy_on_close: false,
26                 width: '80%',
27                 min_width: 850
28             }, this.options.action_views_ids.form, dataset);
29         this.form_dialog.start();
30         this.COLOR_PALETTE = ['#f57900', '#cc0000', '#d400a8', '#75507b', '#3465a4', '#73d216', '#c17d11', '#edd400',
31              '#fcaf3e', '#ef2929', '#ff00c9', '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f',
32              '#ff8e00', '#ff0000', '#b0008c', '#9000ff', '#0078ff', '#00ff00', '#e6ff00', '#ffff00',
33              '#905000', '#9b0000', '#840067', '#510090', '#0000c9', '#009b00', '#9abe00', '#ffc900' ];
34         this.color_map = {};
35         this.last_search = [];
36         this.range_start = null;
37         this.range_stop = null;
38         this.update_range_dates(Date.today());
39         this.selected_filters = [];
40     },
41     start: function() {
42         this._super();
43         return this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type":"calendar", 'toolbar': true}, this.on_loaded);
44     },
45     stop: function() {
46         scheduler.clearAll();
47         this._super();
48     },
49     on_loaded: function(data) {
50         this.fields_view = data;
51         this.calendar_fields = {};
52         this.ids = this.dataset.ids;
53         this.color_values = [];
54         this.info_fields = [];
55
56         this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
57         this.view_id = this.fields_view.view_id;
58
59         // mode, one of month, week or day
60         this.mode = this.fields_view.arch.attrs.mode;
61
62         // date_start is mandatory, date_delay and date_stop are optional
63         this.date_start = this.fields_view.arch.attrs.date_start;
64         this.date_delay = this.fields_view.arch.attrs.date_delay;
65         this.date_stop = this.fields_view.arch.attrs.date_stop;
66
67         this.day_length = this.fields_view.arch.attrs.day_length || 8;
68         this.color_field = this.fields_view.arch.attrs.color;
69
70         if (this.color_field && this.selected_filters.length === 0) {
71             var default_filter;
72             if (default_filter = this.dataset.context['calendar_default_' + this.color_field]) {
73                 this.selected_filters.push(default_filter + '');
74             }
75         }
76         this.fields =  this.fields_view.fields;
77
78         if (!this.date_start) {
79             throw new Error("Calendar view has not defined 'date_start' attribute.");
80         }
81
82         //* Calendar Fields *
83         this.calendar_fields.date_start = {'name': this.date_start, 'kind': this.fields[this.date_start].type};
84
85         if (this.date_delay) {
86             if (this.fields[this.date_delay].type != 'float') {
87                 throw new Error("Calendar view has a 'date_delay' type != float");
88             }
89             this.calendar_fields.date_delay = {'name': this.date_delay, 'kind': this.fields[this.date_delay].type};
90         }
91         if (this.date_stop) {
92             this.calendar_fields.date_stop = {'name': this.date_stop, 'kind': this.fields[this.date_stop].type};
93         }
94
95         for (var fld = 0; fld < this.fields_view.arch.children.length; fld++) {
96             this.info_fields.push(this.fields_view.arch.children[fld].attrs.name);
97         }
98         this.$element.html(QWeb.render("CalendarView", {"fields_view": this.fields_view}));
99
100         if (this.options.sidebar && this.options.sidebar_id) {
101             this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
102             this.sidebar.start();
103             this.sidebar.navigator = new openerp.web_calendar.SidebarNavigator(this.sidebar, this);
104             this.sidebar.responsible = new openerp.web_calendar.SidebarResponsible(this.sidebar, this);
105             this.sidebar.add_toolbar(this.fields_view.toolbar);
106             this.set_common_sidebar_sections(this.sidebar);
107             this.sidebar.do_unfold();
108             this.sidebar.do_fold.add_last(this.refresh_scheduler);
109             this.sidebar.do_unfold.add_last(this.refresh_scheduler);
110             this.sidebar.do_toggle.add_last(this.refresh_scheduler);
111         }
112
113         this.init_scheduler();
114         this.has_been_loaded.resolve();
115     },
116     init_scheduler: function() {
117         var self = this;
118         scheduler.clearAll();
119         if (this.fields[this.date_start]['type'] == 'time') {
120             scheduler.config.xml_date = "%H:%M:%S";
121         } else {
122             scheduler.config.xml_date = "%Y-%m-%d %H:%i";
123         }
124         scheduler.config.api_date = "%Y-%m-%d %H:%i";
125         scheduler.config.multi_day = true; //Multi day events are not rendered in daily and weekly views
126         scheduler.config.start_on_monday = true;
127         scheduler.config.time_step = 30;
128         scheduler.config.scroll_hour = 8;
129         scheduler.config.drag_resize = true;
130         scheduler.config.drag_create = true;
131         scheduler.config.mark_now = true;
132         scheduler.config.day_date = '%l %j';
133
134         scheduler.init('openerp_scheduler', null, this.mode || 'month');
135
136         // Remove hard coded style attributes from dhtmlx scheduler
137         this.$element.find(".dhx_cal_navline div").removeAttr('style');
138
139         scheduler.detachAllEvents();
140         scheduler.attachEvent('onEventAdded', this.do_create_event);
141         scheduler.attachEvent('onEventDeleted', this.do_delete_event);
142         scheduler.attachEvent('onEventChanged', this.do_save_event);
143         scheduler.attachEvent('onClick', this.do_edit_event);
144         scheduler.attachEvent('onLightbox', this.do_edit_event);
145
146         scheduler.attachEvent('onViewChange', this.on_view_changed);
147         this.refresh_scheduler();
148
149         if (this.options.sidebar) {
150             this.mini_calendar = scheduler.renderCalendar({
151                 container: this.sidebar.navigator.element_id,
152                 navigation: true,
153                 date: scheduler._date,
154                 handler: function(date, calendar) {
155                     scheduler.setCurrentView(date, 'day');
156                 }
157             });
158         }
159     },
160     on_view_changed: function(mode, date) {
161         this.$element.removeClass('oe_cal_day oe_cal_week oe_cal_month').addClass('oe_cal_' + mode);
162         if (!date.between(this.range_start, this.range_stop)) {
163             this.update_range_dates(date);
164             this.do_ranged_search();
165         }
166         this.ready.resolve();
167     },
168     update_range_dates: function(date) {
169         this.range_start = date.clone().moveToFirstDayOfMonth();
170         this.range_stop = this.range_start.clone().addMonths(1).addSeconds(-1);
171     },
172     refresh_scheduler: function() {
173         scheduler.setCurrentView(scheduler._date);
174     },
175     refresh_minical: function() {
176         if (this.options.sidebar) {
177             scheduler.updateCalendar(this.mini_calendar);
178         }
179     },
180     reload_event: function(id) {
181         this.dataset.read_ids([id], _.keys(this.fields)).then(this.on_events_loaded);
182     },
183     get_color: function(key) {
184         if (this.color_map[key]) {
185             return this.color_map[key];
186         }
187         var index = _.keys(this.color_map).length % this.COLOR_PALETTE.length;
188         var color = this.COLOR_PALETTE[index];
189         this.color_map[key] = color;
190         return color;
191     },
192     on_events_loaded: function(events, fn_filter, no_filter_reload) {
193         var self = this;
194
195         //To parse Events we have to convert date Format
196         var res_events = [],
197             sidebar_items = {};
198         for (var e = 0; e < events.length; e++) {
199             var evt = events[e];
200             if (!evt[this.date_start]) {
201                 break;
202             }
203
204             if (this.color_field) {
205                 var filter = evt[this.color_field];
206                 if (filter) {
207                     var filter_value = (typeof filter === 'object') ? filter[0] : filter;
208                     if (typeof(fn_filter) === 'function' && !fn_filter(filter_value)) {
209                         continue;
210                     }
211                     var filter_item = {
212                         value: filter_value,
213                         label: (typeof filter === 'object') ? filter[1] : filter,
214                         color: this.get_color(filter_value)
215                     };
216                     if (!sidebar_items[filter_value]) {
217                         sidebar_items[filter_value] = filter_item;
218                     }
219                     evt.color = filter_item.color;
220                     evt.textColor = '#ffffff';
221                 }
222             }
223
224             if (this.fields[this.date_start]['type'] == 'date') {
225                 evt[this.date_start] = openerp.web.auto_str_to_date(evt[this.date_start]).set({hour: 9}).toString('yyyy-MM-dd HH:mm:ss');
226             }
227             if (this.date_stop && evt[this.date_stop] && this.fields[this.date_stop]['type'] == 'date') {
228                 evt[this.date_stop] = openerp.web.auto_str_to_date(evt[this.date_stop]).set({hour: 17}).toString('yyyy-MM-dd HH:mm:ss');
229             }
230             res_events.push(this.convert_event(evt));
231         }
232         scheduler.parse(res_events, 'json');
233         this.refresh_scheduler();
234         this.refresh_minical();
235         if (!no_filter_reload && this.options.sidebar) {
236             this.sidebar.responsible.on_events_loaded(sidebar_items);
237         }
238     },
239     convert_event: function(evt) {
240         var date_start = openerp.web.str_to_datetime(evt[this.date_start]),
241             date_stop = this.date_stop ? openerp.web.str_to_datetime(evt[this.date_stop]) : null,
242             date_delay = evt[this.date_delay] || 1.0,
243             res_text = '',
244             res_description = [];
245
246         if (this.info_fields) {
247             var fld = evt[this.info_fields[0]];
248             res_text = (typeof fld == 'object') ? fld[fld.length -1] : res_text = fld;
249
250             var sliced_info_fields = this.info_fields.slice(1);
251             for (var sl_fld in sliced_info_fields) {
252                 var slc_fld = evt[sliced_info_fields[sl_fld]];
253                 if (typeof slc_fld == 'object') {
254                     res_description.push(slc_fld[slc_fld.length - 1]);
255                 } else if (slc_fld) {
256                     res_description.push(slc_fld);
257                 }
258             }
259         }
260         if (!date_stop && date_delay) {
261             date_stop = date_start.clone().addHours(date_delay);
262         }
263         var r = {
264             'start_date': date_start.toString('yyyy-MM-dd HH:mm:ss'),
265             'end_date': date_stop.toString('yyyy-MM-dd HH:mm:ss'),
266             'text': res_text,
267             'id': evt.id,
268             'title': res_description.join()
269         };
270         if (evt.color) {
271             r.color = evt.color;
272         }
273         if (evt.textColor) {
274             r.textColor = evt.textColor;
275         }
276         return r;
277     },
278     do_create_event: function(event_id, event_obj) {
279         var self = this,
280             data = this.get_event_data(event_obj);
281         this.dataset.create(data, function(r) {
282             var id = r.result;
283             self.dataset.ids.push(id);
284             scheduler.changeEventId(event_id, id);
285             self.refresh_minical();
286             self.reload_event(id);
287         }, function(r, event) {
288             event.preventDefault();
289             self.do_create_event_with_formdialog(event_id, event_obj);
290         });
291     },
292     do_create_event_with_formdialog: function(event_id, event_obj) {
293         if (!event_obj) {
294             event_obj = scheduler.getEvent(event_id);
295         }
296         var self = this,
297             data = this.get_event_data(event_obj),
298             form = self.form_dialog.form,
299             fields_to_fetch = _(form.fields_view.fields).keys(),
300             set_values = [], 
301             fields_names = [];
302         this.dataset.index = null;
303         self.creating_event_id = event_id;
304         this.form_dialog.form.do_show().then(function() {
305             form.show_invalid = false;
306             _.each(['date_start', 'date_stop', 'date_delay'], function(field) {
307                 var field_name = self[field];
308                 if (field_name && form.fields[field_name]) {
309                     var ffield = form.fields[field_name];
310                     ffield.reset();
311                     var result = ffield.set_value(data[field_name]);
312                     set_values.push(result);
313                     fields_names.push(field_name);
314                     $.when(result).then(function() {
315                         ffield.validate();
316                     });
317                 }
318             });
319
320             $.when(set_values).then(function() {
321                 _.each(fields_names, function(fn) {
322                     var field = form.fields[fn];
323                     field.dirty = true;
324                     form.do_onchange(field);
325                 });
326                 form.show_invalid = true;
327                 self.form_dialog.open();
328             });
329         });
330     },
331     do_save_event: function(event_id, event_obj) {
332         var self = this,
333             data = this.get_event_data(event_obj),
334             index = this.dataset.get_id_index(event_id);
335         if (index != null) {
336             event_id = this.dataset.ids[index];
337             this.dataset.write(event_id, data, {}, function() {
338                 self.refresh_minical();
339             });
340         }
341     },
342     do_delete_event: function(event_id, event_obj) {
343         // dhtmlx sends this event even when it does not exist in openerp.
344         // Eg: use cancel in dhtmlx new event dialog
345         var self = this,
346             index = this.dataset.get_id_index(event_id);
347         if (index !== null) {
348             this.dataset.unlink(event_id, function() {
349                 self.refresh_minical();
350             });
351         }
352     },
353     do_edit_event: function(event_id) {
354         var self = this;
355         var index = this.dataset.get_id_index(event_id);
356         if (index !== null) {
357             this.dataset.index = index;
358             this.do_switch_view('page');
359         } else if (scheduler.getState().mode === 'month') {
360             var event_obj = scheduler.getEvent(event_id);
361             if (event_obj._length === 1) {
362                 event_obj['start_date'].addHours(8);
363                 event_obj['end_date'] = new Date(event_obj['start_date']);
364                 event_obj['end_date'].addHours(1);
365             }
366             this.do_create_event_with_formdialog(event_id, event_obj);
367             // return false;
368             // Theorically, returning false should prevent the lightbox to open.
369             // It works, but then the scheduler is in a buggy state where drag'n drop
370             // related internal Event won't be fired anymore.
371             // I tried scheduler.editStop(event_id); but doesn't work either
372             // After losing one hour on this, here's a quick and very dirty fix :
373             $(".dhx_cancel_btn").click();
374         }
375     },
376     get_event_data: function(event_obj) {
377         var data = {
378             name: event_obj.text
379         };
380         data[this.date_start] = openerp.web.datetime_to_str(event_obj.start_date);
381         if (this.date_stop) {
382             data[this.date_stop] = openerp.web.datetime_to_str(event_obj.end_date);
383         }
384         if (this.date_delay) {
385             var diff_seconds = Math.round((event_obj.end_date.getTime() - event_obj.start_date.getTime()) / 1000);
386             data[this.date_delay] = diff_seconds / 3600;
387         }
388         return data;
389     },
390     do_search: function(domain, context, group_by) {
391         this.last_search = arguments;
392         this.do_ranged_search();
393     },
394     do_ranged_search: function() {
395         var self = this
396         scheduler.clearAll();
397         $.when(this.has_been_loaded, this.ready).then(function() {
398             self.dataset.read_slice(_.keys(self.fields), {
399                 offset: 0,
400                 domain: self.get_range_domain(),
401                 context: self.last_search[1]
402             }).then(function(events) {
403                 self.dataset_events = events;
404                 self.on_events_loaded(events);
405             });
406         });
407     },
408     get_range_domain: function() {
409         var format = openerp.web.date_to_str,
410             domain = this.last_search[0].slice(0);
411         domain.unshift([this.date_start, '>=', format(this.range_start.clone().addDays(-6))]);
412         domain.unshift([this.date_start, '<=', format(this.range_stop.clone().addDays(6))]);
413         return domain;
414     },
415     do_show: function () {
416         var self = this;
417         $.when(this.has_been_loaded).then(function() {
418             self.$element.show();
419             if (self.sidebar) {
420                 self.sidebar.$element.show();
421             }
422             self.do_push_state({});
423         });
424     },
425     do_hide: function () {
426         this._super();
427         if (this.sidebar) {
428             this.sidebar.$element.hide();
429         }
430     },
431     get_selected_ids: function() {
432         // no way to select a record anyway
433         return [];
434     }
435 });
436
437 openerp.web_calendar.CalendarFormDialog = openerp.web.Dialog.extend({
438     init: function(view, options, view_id, dataset) {
439         this._super(view, options);
440         this.dataset = dataset;
441         this.view_id = view_id;
442         this.view = view;
443     },
444     start: function() {
445         var self = this;
446         this._super();
447         this.form = new openerp.web.FormView(this, this.dataset, this.view_id, {
448             sidebar: false,
449             pager: false
450         });
451         this.form.appendTo(this.$element);
452         this.form.on_created.add_last(this.on_form_dialog_saved);
453         this.form.on_saved.add_last(this.on_form_dialog_saved);
454         this.form.on_button_cancel = function() {
455             self.close();
456         }
457     },
458     on_form_dialog_saved: function() {
459         var id = this.dataset.ids[this.dataset.index];
460         if (this.view.creating_event_id) {
461             scheduler.changeEventId(this.view.creating_event_id, id);
462             this.view.creating_event_id = null;
463         }
464         this.view.reload_event(id);
465         this.close();
466     },
467     on_close: function() {
468         if (this.view.creating_event_id) {
469             scheduler.deleteEvent(this.view.creating_event_id);
470             this.view.creating_event_id = null;
471         }
472     }
473 });
474
475 openerp.web_calendar.SidebarResponsible = openerp.web.Widget.extend({
476     init: function(parent, view) {
477         var $section = parent.add_section(_t('Responsible'), 'responsible');
478         this.$div = $('<div></div>');
479         $section.append(this.$div);
480         this._super(parent, $section.attr('id'));
481         this.view = view;
482         this.$element.delegate('input:checkbox', 'change', this.on_filter_click);
483     },
484     on_events_loaded: function(filters) {
485         var selected_filters = this.view.selected_filters.slice(0);
486         this.$div.html(QWeb.render('CalendarView.sidebar.responsible', { filters: filters }));
487         this.$element.find('div.oe_calendar_responsible input').each(function() {
488             if (_.indexOf(selected_filters, $(this).val()) > -1) {
489                 $(this).click();
490             }
491         });
492     },
493     on_filter_click: function(e) {
494         var self = this,
495             responsibles = [],
496             $e = $(e.target);
497         this.view.selected_filters = [];
498         this.$element.find('div.oe_calendar_responsible input:checked').each(function() {
499             responsibles.push($(this).val());
500             self.view.selected_filters.push($(this).val());
501         });
502         scheduler.clearAll();
503         if (responsibles.length) {
504             this.view.on_events_loaded(this.view.dataset_events, function(filter_value) {
505                 return _.indexOf(responsibles, filter_value.toString()) > -1;
506             }, true);
507         } else {
508             this.view.on_events_loaded(this.view.dataset_events, false, true);
509         }
510     }
511 });
512
513 openerp.web_calendar.SidebarNavigator = openerp.web.Widget.extend({
514     init: function(parent, view) {
515         var $section = parent.add_section(_t('Navigator'), 'navigator');
516         this._super(parent, $section.attr('id'));
517         this.view = view;
518     },
519     on_events_loaded: function(events) {
520     }
521 });
522 };
523
524 // DEBUG_RPC:rpc.request:('execute', 'addons-dsh-l10n_us', 1, '*', ('ir.filters', 'get_filters', u'res.partner'))
525 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: