diff --git a/addons/web/static/lib/jquery.ganttView/README.markdown b/addons/web/static/lib/jquery.ganttView/README.markdown new file mode 100644 index 00000000..aa5bfc57 --- /dev/null +++ b/addons/web/static/lib/jquery.ganttView/README.markdown @@ -0,0 +1,73 @@ +jQuery.ganttView +================ + +The jQuery.ganttView plugin is a very lightweight plugin for creating a Gantt chart in plain HTML...no vector graphics or images required. The plugin supports dragging and resizing the Gantt blocks and callbacks to trap the updated data. + +[![Sample Gantt](https://raw.githubusercontent.com/thegrubbsian/jquery.ganttView/master/example/jquery-ganttview.png) A sample chart](http://thegrubbsian.github.io/jquery.ganttView/example/index.html) + + +Browser Compatibility +--------------------- +Currently the plugin has been tested, and is working in: FF 3.5+, Chrome 5+, Safari 4+, IE8+. There are minor issues in IE7 and I haven't even attempted to use it in IE6. If you encounter any issues with any version of Internet Explorer and would like to contribute CSS fixes please do so, several people have asked for IE6 support. + + +Dependencies +------------ +The plugin depends on the following libraries: + +- jQuery 1.7 or higher +- jQuery-UI 1.8 or higher +- date.js + + +Documentation +------------- +Forthcoming... + + +Contribution Guidelines +------------ +The internal roadmap for the plugin is detailed in the project wiki. If you're interested in features outside of what's described there, we'd be interested to discuss pull requests that would add these features. If you like the plugin, feel free to fork it and submit your patches back. + +**Guidelines:** If you'd like to offer a new feature please help us out by submitting the pull request with only the fewest changes necessary. + +Ideal: Fork the project, apply just the individual changes to the individual files effected, submit pull request. + +Those pull requests can usually be automatically merged and closed through the site here. + +If your pull request includes things like: + +- changes to dependencies or where they're hosted +- stylistic modifications +- moving project files to different directories +- more than one new feature / functional change + +one of us will have to do the work of carving out just the feature being pulled. Your request is likely to sit unmerged for a while if that's the case. + + +License +------- +The jQuery.ganttView plugin may be used free of charge under the conditions +of the following license: + +The MIT License + +Copyright (c) 2010 JC Grubbs - jc.grubbs@devmynd.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/web/static/lib/jquery.ganttView/date.js b/addons/web/static/lib/jquery.ganttView/date.js new file mode 100644 index 00000000..3cb003fa --- /dev/null +++ b/addons/web/static/lib/jquery.ganttView/date.js @@ -0,0 +1,145 @@ +/** + * @version: 1.0 Alpha-1 + * @author: Coolite Inc. http://www.coolite.com/ + * @date: 2008-05-13 + * @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved. + * @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. + * @website: http://www.datejs.com/ + */ +Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|aft(er)?|from|hence)/i,subtract:/^(\-|bef(ore)?|ago)/i,yesterday:/^yes(terday)?/i,today:/^t(od(ay)?)?/i,tomorrow:/^tom(orrow)?/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^mn|min(ute)?s?/i,hour:/^h(our)?s?/i,week:/^w(eek)?s?/i,month:/^m(onth)?s?/i,day:/^d(ay)?s?/i,year:/^y(ear)?s?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a(?!u|p)|p)/i},timezones:[{name:"UTC",offset:"-000"},{name:"GMT",offset:"-000"},{name:"EST",offset:"-0500"},{name:"EDT",offset:"-0400"},{name:"CST",offset:"-0600"},{name:"CDT",offset:"-0500"},{name:"MST",offset:"-0700"},{name:"MDT",offset:"-0600"},{name:"PST",offset:"-0800"},{name:"PDT",offset:"-0700"}]}; +(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,p=function(s,l){if(!l){l=2;} +return("000"+s).slice(l*-1);};$P.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};$P.setTimeToNow=function(){var n=new Date();this.setHours(n.getHours());this.setMinutes(n.getMinutes());this.setSeconds(n.getSeconds());this.setMilliseconds(n.getMilliseconds());return this;};$D.today=function(){return new Date().clearTime();};$D.compare=function(date1,date2){if(isNaN(date1)||isNaN(date2)){throw new Error(date1+" - "+date2);}else if(date1 instanceof Date&&date2 instanceof Date){return(date1date2)?1:0;}else{throw new TypeError(date1+" - "+date2);}};$D.equals=function(date1,date2){return(date1.compareTo(date2)===0);};$D.getDayNumberFromName=function(name){var n=$C.dayNames,m=$C.abbreviatedDayNames,o=$C.shortestDayNames,s=name.toLowerCase();for(var i=0;i=start.getTime()&&this.getTime()<=end.getTime();};$P.isAfter=function(date){return this.compareTo(date||new Date())===1;};$P.isBefore=function(date){return(this.compareTo(date||new Date())===-1);};$P.isToday=function(){return this.isSameDay(new Date());};$P.isSameDay=function(date){return this.clone().clearTime().equals(date.clone().clearTime());};$P.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};$P.addSeconds=function(value){return this.addMilliseconds(value*1000);};$P.addMinutes=function(value){return this.addMilliseconds(value*60000);};$P.addHours=function(value){return this.addMilliseconds(value*3600000);};$P.addDays=function(value){this.setDate(this.getDate()+value);return this;};$P.addWeeks=function(value){return this.addDays(value*7);};$P.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,$D.getDaysInMonth(this.getFullYear(),this.getMonth())));return this;};$P.addYears=function(value){return this.addMonths(value*12);};$P.add=function(config){if(typeof config=="number"){this._orient=config;return this;} +var x=config;if(x.milliseconds){this.addMilliseconds(x.milliseconds);} +if(x.seconds){this.addSeconds(x.seconds);} +if(x.minutes){this.addMinutes(x.minutes);} +if(x.hours){this.addHours(x.hours);} +if(x.weeks){this.addWeeks(x.weeks);} +if(x.months){this.addMonths(x.months);} +if(x.years){this.addYears(x.years);} +if(x.days){this.addDays(x.days);} +return this;};var $y,$m,$d;$P.getWeek=function(){var a,b,c,d,e,f,g,n,s,w;$y=(!$y)?this.getFullYear():$y;$m=(!$m)?this.getMonth()+1:$m;$d=(!$d)?this.getDate():$d;if($m<=2){a=$y-1;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=0;f=$d-1+(31*($m-1));}else{a=$y;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=s+1;f=$d+((153*($m-3)+2)/5)+58+s;} +g=(a+b)%7;d=(f+g-e)%7;n=(f+3-d)|0;if(n<0){w=53-((g-s)/5|0);}else if(n>364+s){w=1;}else{w=(n/7|0)+1;} +$y=$m=$d=null;return w;};$P.getISOWeek=function(){$y=this.getUTCFullYear();$m=this.getUTCMonth()+1;$d=this.getUTCDate();return p(this.getWeek());};$P.setWeek=function(n){return this.moveToDayOfWeek(1).addWeeks(n-this.getWeek());};$D._validate=function(n,min,max,name){if(typeof n=="undefined"){return false;}else if(typeof n!="number"){throw new TypeError(n+" is not a Number.");}else if(nmax){throw new RangeError(n+" is not a valid value for "+name+".");} +return true;};$D.validateMillisecond=function(value){return $D._validate(value,0,999,"millisecond");};$D.validateSecond=function(value){return $D._validate(value,0,59,"second");};$D.validateMinute=function(value){return $D._validate(value,0,59,"minute");};$D.validateHour=function(value){return $D._validate(value,0,23,"hour");};$D.validateDay=function(value,year,month){return $D._validate(value,1,$D.getDaysInMonth(year,month),"day");};$D.validateMonth=function(value){return $D._validate(value,0,11,"month");};$D.validateYear=function(value){return $D._validate(value,0,9999,"year");};$P.set=function(config){if($D.validateMillisecond(config.millisecond)){this.addMilliseconds(config.millisecond-this.getMilliseconds());} +if($D.validateSecond(config.second)){this.addSeconds(config.second-this.getSeconds());} +if($D.validateMinute(config.minute)){this.addMinutes(config.minute-this.getMinutes());} +if($D.validateHour(config.hour)){this.addHours(config.hour-this.getHours());} +if($D.validateMonth(config.month)){this.addMonths(config.month-this.getMonth());} +if($D.validateYear(config.year)){this.addYears(config.year-this.getFullYear());} +if($D.validateDay(config.day,this.getFullYear(),this.getMonth())){this.addDays(config.day-this.getDate());} +if(config.timezone){this.setTimezone(config.timezone);} +if(config.timezoneOffset){this.setTimezoneOffset(config.timezoneOffset);} +if(config.week&&$D._validate(config.week,0,53,"week")){this.setWeek(config.week);} +return this;};$P.moveToFirstDayOfMonth=function(){return this.set({day:1});};$P.moveToLastDayOfMonth=function(){return this.set({day:$D.getDaysInMonth(this.getFullYear(),this.getMonth())});};$P.moveToNthOccurrence=function(dayOfWeek,occurrence){var shift=0;if(occurrence>0){shift=occurrence-1;} +else if(occurrence===-1){this.moveToLastDayOfMonth();if(this.getDay()!==dayOfWeek){this.moveToDayOfWeek(dayOfWeek,-1);} +return this;} +return this.moveToFirstDayOfMonth().addDays(-1).moveToDayOfWeek(dayOfWeek,+1).addWeeks(shift);};$P.moveToDayOfWeek=function(dayOfWeek,orient){var diff=(dayOfWeek-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};$P.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};$P.getOrdinalNumber=function(){return Math.ceil((this.clone().clearTime()-new Date(this.getFullYear(),0,1))/86400000)+1;};$P.getTimezone=function(){return $D.getTimezoneAbbreviation(this.getUTCOffset());};$P.setTimezoneOffset=function(offset){var here=this.getTimezoneOffset(),there=Number(offset)*-6/10;return this.addMinutes(there-here);};$P.setTimezone=function(offset){return this.setTimezoneOffset($D.getTimezoneOffset(offset));};$P.hasDaylightSavingTime=function(){return(Date.today().set({month:0,day:1}).getTimezoneOffset()!==Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.isDaylightSavingTime=function(){return(this.hasDaylightSavingTime()&&new Date().getTimezoneOffset()===Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r.charAt(0)+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};$P.getElapsed=function(date){return(date||new Date())-this;};if(!$P.toISOString){$P.toISOString=function(){function f(n){return n<10?'0'+n:n;} +return'"'+this.getUTCFullYear()+'-'+ +f(this.getUTCMonth()+1)+'-'+ +f(this.getUTCDate())+'T'+ +f(this.getUTCHours())+':'+ +f(this.getUTCMinutes())+':'+ +f(this.getUTCSeconds())+'Z"';};} +$P._toString=$P.toString;$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}} +var ord=function(n){switch(n*1){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};return format?format.replace(/(\\)?(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|S)/g,function(m){if(m.charAt(0)==="\\"){return m.replace("\\","");} +x.h=x.getHours;switch(m){case"hh":return p(x.h()<13?(x.h()===0?12:x.h()):(x.h()-12));case"h":return x.h()<13?(x.h()===0?12:x.h()):(x.h()-12);case"HH":return p(x.h());case"H":return x.h();case"mm":return p(x.getMinutes());case"m":return x.getMinutes();case"ss":return p(x.getSeconds());case"s":return x.getSeconds();case"yyyy":return p(x.getFullYear(),4);case"yy":return p(x.getFullYear());case"dddd":return $C.dayNames[x.getDay()];case"ddd":return $C.abbreviatedDayNames[x.getDay()];case"dd":return p(x.getDate());case"d":return x.getDate();case"MMMM":return $C.monthNames[x.getMonth()];case"MMM":return $C.abbreviatedMonthNames[x.getMonth()];case"MM":return p((x.getMonth()+1));case"M":return x.getMonth()+1;case"t":return x.h()<12?$C.amDesignator.substring(0,1):$C.pmDesignator.substring(0,1);case"tt":return x.h()<12?$C.amDesignator:$C.pmDesignator;case"S":return ord(x.getDate());default:return m;}}):this._toString();};}()); +(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,$N=Number.prototype;$P._orient=+1;$P._nth=null;$P._is=false;$P._same=false;$P._isSecond=false;$N._dateElement="day";$P.next=function(){this._orient=+1;return this;};$D.next=function(){return $D.today().next();};$P.last=$P.prev=$P.previous=function(){this._orient=-1;return this;};$D.last=$D.prev=$D.previous=function(){return $D.today().last();};$P.is=function(){this._is=true;return this;};$P.same=function(){this._same=true;this._isSecond=false;return this;};$P.today=function(){return this.same().day();};$P.weekday=function(){if(this._is){this._is=false;return(!this.is().sat()&&!this.is().sun());} +return false;};$P.at=function(time){return(typeof time==="string")?$D.parse(this.toString("d")+" "+time):this.set(time);};$N.fromNow=$N.after=function(date){var c={};c[this._dateElement]=this;return((!date)?new Date():date.clone()).add(c);};$N.ago=$N.before=function(date){var c={};c[this._dateElement]=this*-1;return((!date)?new Date():date.clone()).add(c);};var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),pxf=("Milliseconds Seconds Minutes Hours Date Week Month FullYear").split(/\s/),nth=("final first second third fourth fifth").split(/\s/),de;$P.toObject=function(){var o={};for(var i=0;itemp){throw new RangeError($D.getDayName(n)+" does not occur "+ntemp+" times in the month of "+$D.getMonthName(temp.getMonth())+" "+temp.getFullYear()+".");} +return this;} +return this.moveToDayOfWeek(n,this._orient);};};var sdf=function(n){return function(){var t=$D.today(),shift=n-t.getDay();if(n===0&&$C.firstDayOfWeek===1&&t.getDay()!==0){shift=shift+7;} +return t.addDays(shift);};};for(var i=0;i-1;m--){v=px[m].toLowerCase();if(o1[v]!=o2[v]){return false;} +if(k==v){break;}} +return true;} +if(j.substring(j.length-1)!="s"){j+="s";} +return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;} +if(!last&&q[1].length===0){last=true;} +if(!last){var qx=[];for(var j=0;j0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}} +if(rx[1].length1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];} +if(args){for(var i=0,px=args.shift();i2)?n:(n+(((n+2000)<$C.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];for(var i=0;i$D.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");} +var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});} +return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;} +for(var i=0;i", { "class": "ganttview" }); + new Chart(div, opts).render(); + container.append(div); + + var w = jQuery("div.ganttview-vtheader", container).outerWidth() + + jQuery("div.ganttview-slide-container", container).outerWidth(); + container.css("width", (w + 2) + "px"); + + new Behavior(container, opts).apply(); + }); + } + } + + function handleMethod(method, value) { + + if (method == "setSlideWidth") { + var div = $("div.ganttview", this); + div.each(function () { + var vtWidth = $("div.ganttview-vtheader", div).outerWidth(); + $(div).width(vtWidth + value + 1); + $("div.ganttview-slide-container", this).width(value); + }); + } + } + + var Chart = function(div, opts) { + + function render() { + addVtHeader(div, opts.data, opts.cellHeight); + + var slideDiv = jQuery("
", { + "class": "ganttview-slide-container", + "css": { "width": opts.slideWidth + "px" } + }); + + dates = getDates(opts.start, opts.end); + addHzHeader(slideDiv, dates, opts.cellWidth); + addGrid(slideDiv, opts.data, dates, opts.cellWidth, opts.showWeekends, opts.showToday); + addBlockContainers(slideDiv, opts.data); + addBlocks(slideDiv, opts.data, opts.cellWidth, opts.start); + div.append(slideDiv); + applyLastClass(div.parent()); + } + + var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + // Creates a 3 dimensional array [year][month][day] of every day + // between the given start and end dates + function getDates(start, end) { + var end = end.clone().addDays(6); + var dates = []; + dates[start.getFullYear()] = []; + dates[start.getFullYear()][start.getMonth()] = [start] + var last = start; + while (last.compareTo(end) == -1) { + var next = last.clone().addDays(1); + if (!dates[next.getFullYear()]) { dates[next.getFullYear()] = []; } + if (!dates[next.getFullYear()][next.getMonth()]) { + dates[next.getFullYear()][next.getMonth()] = []; + } + dates[next.getFullYear()][next.getMonth()].push(next); + last = next; + } + return dates; + } + + function addVtHeader(div, data, cellHeight) { + var headerDiv = jQuery("
", { "class": "ganttview-vtheader" }); + for (var i = 0; i < data.length; i++) { + var itemDiv = jQuery("
", { "class": "ganttview-vtheader-item" }); + if ($.trim(data[i].name).length > 0) + itemDiv.append(jQuery("
", { + "class": "ganttview-vtheader-item-name", + "css": { "height": (data[i].series.length * cellHeight) + "px" } + }).append(data[i].name)); + var seriesDiv = jQuery("
", { "class": "ganttview-vtheader-series" }); + for (var j = 0; j < data[i].series.length; j++) { + seriesDiv.append(jQuery("
", { "class": "ganttview-vtheader-series-name" }) + .append(data[i].series[j].name)); + } + itemDiv.append(seriesDiv); + headerDiv.append(itemDiv); + } + div.append(headerDiv); + } + + function addHzHeader(div, dates, cellWidth) { + var headerDiv = jQuery("
", { "class": "ganttview-hzheader" }); + var monthsDiv = jQuery("
", { "class": "ganttview-hzheader-months" }); + var daysDiv = jQuery("
", { "class": "ganttview-hzheader-days" }); + var totalW = 0; + for (var y in dates) { + for (var m in dates[y]) { + var w = dates[y][m].length * cellWidth; + totalW = totalW + w; + monthsDiv.append(jQuery("
", { + "class": "ganttview-hzheader-month", + "css": { "width": w + "px" } + }).append(monthNames[m] + "/" + y)); + for (var d in dates[y][m]) { + daysDiv.append(jQuery("
", { "class": "ganttview-hzheader-day" }) + .append(dates[y][m][d].getDate())); + } + } + } + monthsDiv.css("width", totalW + "px"); + daysDiv.css("width", totalW + "px"); + headerDiv.append(monthsDiv).append(daysDiv); + div.append(headerDiv); + } + + function addGrid(div, data, dates, cellWidth, showWeekends, showToday) { + var gridDiv = jQuery("
", { "class": "ganttview-grid" }); + var rowDiv = jQuery("
", { "class": "ganttview-grid-row" }); + for (var y in dates) { + for (var m in dates[y]) { + for (var d in dates[y][m]) { + var cellDiv = jQuery("
", { "class": "ganttview-grid-row-cell" }); + if (DateUtils.isWeekend(dates[y][m][d]) && showWeekends) { + cellDiv.addClass("ganttview-weekend"); + } + if (DateUtils.isToday(dates[y][m][d]) && showToday) { + cellDiv.addClass("ganttview-today"); + } + rowDiv.append(cellDiv); + } + } + } + var w = jQuery("div.ganttview-grid-row-cell", rowDiv).length * cellWidth; + rowDiv.css("width", w + "px"); + gridDiv.css("width", w + "px"); + for (var i = 0; i < data.length; i++) { + for (var j = 0; j < data[i].series.length; j++) { + gridDiv.append(rowDiv.clone()); + } + } + div.append(gridDiv); + } + + function addBlockContainers(div, data) { + var blocksDiv = jQuery("
", { "class": "ganttview-blocks" }); + for (var i = 0; i < data.length; i++) { + for (var j = 0; j < data[i].series.length; j++) { + blocksDiv.append(jQuery("
", { "class": "ganttview-block-container" })); + } + } + div.append(blocksDiv); + } + + function addBlocks(div, data, cellWidth, start) { + var rows = jQuery("div.ganttview-blocks div.ganttview-block-container", div); + var rowIdx = 0; + for (var i = 0; i < data.length; i++) { + for (var j = 0; j < data[i].series.length; j++) { + var series = data[i].series[j]; + var size = DateUtils.daysBetween(series.start, series.end) ? DateUtils.daysBetween(series.start, series.end) : 1; + var offset = DateUtils.daysBetween(start, series.start) == 0 ? DateUtils.daysBetween(start, series.start) : DateUtils.daysBetween(start, series.start) - 1; + var block = jQuery("
", { + "class": "ganttview-block", + "title": series.name + ", " + size + " days", + "css": { + "width": ((size * cellWidth) - 7) + "px", + "margin-left": ((offset * cellWidth) + 3) + "px" + } + }); + addBlockData(block, data[i], series); + if (data[i].series[j].color) { + block.css("background-color", data[i].series[j].color); + } + block.append(jQuery("
", { "class": "ganttview-block-text" }).text(size)); + jQuery(rows[rowIdx]).append(block); + rowIdx = rowIdx + 1; + } + } + } + + function addBlockData(block, data, series) { + // This allows custom attributes to be added to the series data objects + // and makes them available to the 'data' argument of click, resize, and drag handlers + var blockData = { id: data.id, name: data.name }; + jQuery.extend(blockData, series); + block.data("block-data", blockData); + } + + function applyLastClass(div) { + jQuery("div.ganttview-grid-row div.ganttview-grid-row-cell:last-child", div).addClass("last"); + jQuery("div.ganttview-hzheader-days div.ganttview-hzheader-day:last-child", div).addClass("last"); + jQuery("div.ganttview-hzheader-months div.ganttview-hzheader-month:last-child", div).addClass("last"); + } + + return { + render: render + }; + } + + var Behavior = function (div, opts) { + + function apply() { + + if (opts.behavior.clickable) { + bindBlockClick(div, opts.behavior.onClick); + } + + if (opts.behavior.resizable) { + bindBlockResize(div, opts.cellWidth, opts.start, opts.behavior.onResize); + } + + if (opts.behavior.draggable) { + bindBlockDrag(div, opts.cellWidth, opts.start, opts.behavior.onDrag); + } + } + + function bindBlockClick(div, callback) { + jQuery("div.ganttview-block", div).on("click", function () { + if (callback) { callback(jQuery(this).data("block-data")); } + }); + } + + function bindBlockResize(div, cellWidth, startDate, callback) { + jQuery("div.ganttview-block", div).resizable({ + grid: cellWidth, + handles: "e,w", + stop: function () { + var block = jQuery(this); + updateDataAndPosition(div, block, cellWidth, startDate); + if (callback) { callback(block.data("block-data")); } + } + }); + } + + function bindBlockDrag(div, cellWidth, startDate, callback) { + jQuery("div.ganttview-block", div).draggable({ + axis: "x", + grid: [cellWidth, cellWidth], + stop: function () { + var block = jQuery(this); + updateDataAndPosition(div, block, cellWidth, startDate); + if (callback) { callback(block.data("block-data")); } + } + }); + } + + function updateDataAndPosition(div, block, cellWidth, startDate) { + var container = jQuery("div.ganttview-slide-container", div); + var scroll = container.scrollLeft(); + var offset = block.offset().left - container.offset().left - 1 + scroll; + + // Set new start date + var daysFromStart = Math.round(offset / cellWidth); + var newStart = startDate.clone().addDays(daysFromStart); + block.data("block-data").start = newStart; + + // Set new end date + var width = block.outerWidth(); + var numberOfDays = Math.round(width / cellWidth) - 1; + block.data("block-data").end = newStart.clone().addDays(numberOfDays); + jQuery("div.ganttview-block-text", block).text(numberOfDays + 1); + + // Remove top and left properties to avoid incorrect block positioning, + // set position to relative to keep blocks relative to scrollbar when scrolling + block.css("top", "").css("left", "") + .css("position", "relative").css("margin-left", offset + "px"); + } + + return { + apply: apply + }; + } + + var ArrayUtils = { + + contains: function (arr, obj) { + var has = false; + for (var i = 0; i < arr.length; i++) { if (arr[i] == obj) { has = true; } } + return has; + } + }; + + var DateUtils = { + + daysBetween: function (start, end) { + if (!start || !end) { return 0; } + start = Date.parse(start); end = Date.parse(end); + if (start.getYear() == 1901 || end.getYear() == 8099) { return 0; } + var count = 0, date = start.clone(); + while (date.compareTo(end) == -1) { count = count + 1; date.addDays(1); } + return count; + }, + + isWeekend: function (date) { + return date.getDay() % 6 == 0; + }, + + isToday: function (date) { + return date.isToday(); + }, + + getBoundaryDatesFromData: function (data, minDays) { + var minStart = new Date(); maxEnd = new Date(); + for (var i = 0; i < data.length; i++) { + for (var j = 0; j < data[i].series.length; j++) { + var start = Date.parse(data[i].series[j].start); + var end = Date.parse(data[i].series[j].end) + if (i == 0 && j == 0) { minStart = start; maxEnd = end; } + if (minStart.compareTo(start) == 1) { minStart = start; } + if (maxEnd.compareTo(end) == -1) { maxEnd = end; } + } + } + + // Insure that the width of the chart is at least the slide width to avoid empty + // whitespace to the right of the grid + if (DateUtils.daysBetween(minStart, maxEnd) < minDays) { + maxEnd = minStart.clone().addDays(minDays); + } + + return [minStart, maxEnd]; + } + }; + +})(jQuery); diff --git a/addons/web/static/src/js/views/gantt/gantt_controller.js b/addons/web/static/src/js/views/gantt/gantt_controller.js new file mode 100644 index 00000000..8089cd1d --- /dev/null +++ b/addons/web/static/src/js/views/gantt/gantt_controller.js @@ -0,0 +1,28 @@ +flectra.define('web.GanttController', function (require) { +"use strict"; +/*--------------------------------------------------------- + * Flectra Gantt view + *---------------------------------------------------------*/ + +var AbstractController = require('web.AbstractController'); +var core = require('web.core'); + +var qweb = core.qweb; + +var GanttController = AbstractController.extend({ + template: "GanttView", + /** + * @override + * @param {Widget} parent + * @param {GanttModel} model + * @param {AbstractRenderer} renderer + * @param {Object} params + */ + init: function(parent, model, renderer, params) { + this._super.apply(this, arguments); + }, +}); + +return GanttController; + +}); diff --git a/addons/web/static/src/js/views/gantt/gantt_model.js b/addons/web/static/src/js/views/gantt/gantt_model.js new file mode 100644 index 00000000..5b709c75 --- /dev/null +++ b/addons/web/static/src/js/views/gantt/gantt_model.js @@ -0,0 +1,174 @@ +flectra.define('web.GanttModel', function (require) { +"use strict"; + +/** + * The gantt model is responsible for fetching and processing data from the + * server. It basically just do a search_read and format/normalize data. + */ + +var core = require('web.core'); +var AbstractModel = require('web.AbstractModel'); + +var _t = core._t; + +return AbstractModel.extend({ + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + }, + /** + * @override + * @param {any} params + * @returns {Deferred} + */ + load: function(params) { + var self = this; + this.modelName = params.modelName; + this.gantt = { + data: [], + domain: params.domain, + groupBy: params.groupedBy, + context: params.context, + arch: params.arch.attrs, + }; + return this._loadGantt(); + }, + /** + * @override + * @param {any} handle ignored! + * @param {Object} params + * @param {string[]} [params.domain] + * @param {string[]} [params.groupBy] + * @returns {Deferred} + */ + reload: function(handle, params) { + if (params.domain) { + this.gantt.domain = params.domain; + } + if (params.groupBy) { + this.gantt.groupBy = params.groupBy; + } + return this._loadGantt(); + }, + /** + * Fetch and process gantt data. It is basically a read_group with correct + * fields. + * + * @returns {Deferred} + */ + _loadGantt: function() { + var self = this; + this.gantt.data = []; + return this._rpc({ + model: this.modelName, + method: 'search_read', + context: this.gantt.context, + domain: this.gantt.domain, + groupBy: this.gantt.groupBy, + }).then(function(raw_datas) { + /** + * GroupBy is only supported till 1st level ! + * @todo Flectra: Support Multi level GroupBy + */ + if(self.gantt.groupBy.length) { + _.each(raw_datas, function(raw_data) { + var grpByStr = raw_data[self.gantt.groupBy[0]] ? raw_data[self.gantt.groupBy[0]] : 'Undefined'; + if(grpByStr && grpByStr instanceof Array) { + grpByStr = raw_data[self.gantt.groupBy[0]] ? raw_data[self.gantt.groupBy[0]][1] : 'Undefined'; + } + var keyCheck = _.findKey(self.gantt.data, {name: grpByStr}); + if(!keyCheck) { + self.gantt.data.push({ + name: grpByStr, + series: [], + }); + } + keyCheck = _.findKey(self.gantt.data, {name: grpByStr}); + if(self.gantt.data[keyCheck]) { + if(raw_data[self.gantt.arch['date_stop']]) { + self.gantt.data[keyCheck].series.push({ + id: raw_data['id'], name: raw_data['display_name'], + start: raw_data[self.gantt.arch['date_start']], end: raw_data[self.gantt.arch['date_stop']] + }); + } else { + self.gantt.data[keyCheck].series.push({ + id: raw_data['id'], name: raw_data['display_name'], + start: raw_data[self.gantt.arch['date_start']], end: raw_data[self.gantt.arch['date_start']] + }); + } + } + }); + } else { + _.each(raw_datas, function(raw_data) { + if(raw_data[self.gantt.arch['date_stop']]) { + self.gantt.data.push({ + series: [ + {id: raw_data['id'], name: raw_data['display_name'], + start: raw_data[self.gantt.arch['date_start']], end: raw_data[self.gantt.arch['date_stop']]}, + ], + }); + } else { + self.gantt.data.push({ + series: [ + {id: raw_data['id'], name: raw_data['display_name'], + start: raw_data[self.gantt.arch['date_start']], end: raw_data[self.gantt.arch['date_start']]}, + ], + }); + } + }); + } + + /** + * Render the Gantt view. + * + * Note that This method is synchronous, but the actual rendering is done + * asynchronously (in a setTimeout). + * + */ + setTimeout(function() { + $(".o_gantt_view_container").empty(); + $(".o_gantt_view_container").ganttView({ + data: self.gantt.data, + slideWidth: 'auto', + cellWidth: 20, + behavior: { + clickable: false, + draggable: false, + resizable: false, + /** + * @todo Flectra: + * Turn-On below events & related behavior/functions + */ +// onClick: function(data) {}, +// +// onResize: function(data) { +// self.updateRecord(data); +// }, +// +// onDrag: function(data) { +// self.updateRecord(data); +// }, + } + }); + }, 0); + }); + }, + +// updateRecord: function(data) { +// var self = this; +// return this._rpc({ +// model: self.modelName, +// method: 'write', +// args: [[data.id], { +// [self.gantt.arch['date_start']]: data.start, +// [self.gantt.arch['date_stop']]: data.end, +// }], +// context: self.gantt.context, +// }); +// }, + +}); + +}); diff --git a/addons/web/static/src/js/views/gantt/gantt_view.js b/addons/web/static/src/js/views/gantt/gantt_view.js new file mode 100644 index 00000000..f294babd --- /dev/null +++ b/addons/web/static/src/js/views/gantt/gantt_view.js @@ -0,0 +1,34 @@ +flectra.define('web.GanttView', function (require) { +"use strict"; + +var AbstractView = require('web.AbstractView'); +var core = require('web.core'); +var GanttModel = require('web.GanttModel'); +var Controller = require('web.GanttController'); +var AbstractRenderer = require('web.AbstractRenderer'); + +var _t = core._t; +var _lt = core._lt; + +var GanttView = AbstractView.extend({ + display_name: _lt('Gantt'), + icon: 'fa-tasks', + config: { + Model: GanttModel, + Controller: Controller, + Renderer: AbstractRenderer, + }, + /** + * @override + */ + init: function(viewInfo) { + this._super.apply(this, arguments); + var arch = viewInfo.arch; + var fields = viewInfo.fields; + this.loadParams.arch = arch; + }, +}); + +return GanttView; + +}); diff --git a/addons/web/static/src/js/views/view_registry.js b/addons/web/static/src/js/views/view_registry.js index bacc448d..6f6c33eb 100644 --- a/addons/web/static/src/js/views/view_registry.js +++ b/addons/web/static/src/js/views/view_registry.js @@ -31,6 +31,7 @@ var KanbanView = require('web.KanbanView'); var ListView = require('web.ListView'); var PivotView = require('web.PivotView'); var CalendarView = require('web.CalendarView'); +var GanttView = require('web.GanttView'); var view_registry = require('web.view_registry'); view_registry @@ -39,6 +40,7 @@ view_registry .add('kanban', KanbanView) .add('graph', GraphView) .add('pivot', PivotView) - .add('calendar', CalendarView); + .add('calendar', CalendarView) + .add('gantt', GanttView); }); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 6c44b787..5c3b0142 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -528,6 +528,10 @@

+ +
+ +
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index f7dc2fa4..a03111fb 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -236,6 +236,17 @@