From 9de6368be9f2f7c9ad1155a6b3cf2777b2460eb8 Mon Sep 17 00:00:00 2001 From: jfefe Date: Thu, 2 Jan 2014 07:07:43 +0100 Subject: [PATCH 01/57] First version of resource module --- class/actions_resource.class.php | 93 + class/html.formresource.class.php | 145 + class/resource.class.php | 598 +++ core/ajax/resource_action.json.php | 158 + core/modules/modResource.class.php | 437 ++ core/tpl/resource_add.tpl.php | 6 + core/tpl/resource_view.tpl.php | 111 + element_resource.php | 236 + index.php | 167 + js/fullcalendar/fullcalendar.css | 589 +++ js/fullcalendar/fullcalendar.js | 6110 ++++++++++++++++++++++++ js/fullcalendar/fullcalendar.min.js | 7 + js/fullcalendar/fullcalendar.print.css | 32 + js/fullcalendar/gcal.js | 107 + langs/en_US/resource.lang | 16 + resource_planning.php | 121 + resource_timeline.php | 134 + sql/llx_element_resources.sql | 28 + 18 files changed, 9095 insertions(+) create mode 100644 class/actions_resource.class.php create mode 100644 class/html.formresource.class.php create mode 100644 class/resource.class.php create mode 100644 core/ajax/resource_action.json.php create mode 100644 core/modules/modResource.class.php create mode 100644 core/tpl/resource_add.tpl.php create mode 100644 core/tpl/resource_view.tpl.php create mode 100644 element_resource.php create mode 100644 index.php create mode 100644 js/fullcalendar/fullcalendar.css create mode 100644 js/fullcalendar/fullcalendar.js create mode 100644 js/fullcalendar/fullcalendar.min.js create mode 100644 js/fullcalendar/fullcalendar.print.css create mode 100644 js/fullcalendar/gcal.js create mode 100755 langs/en_US/resource.lang create mode 100644 resource_planning.php create mode 100644 resource_timeline.php create mode 100644 sql/llx_element_resources.sql diff --git a/class/actions_resource.class.php b/class/actions_resource.class.php new file mode 100644 index 00000000000..16cf5e27d32 --- /dev/null +++ b/class/actions_resource.class.php @@ -0,0 +1,93 @@ + + * +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +/** + * \file resource/class/actions_resource.class.php + * \brief Place module actions + */ + +class ActionsResource +{ + + var $db; + var $error; + var $errors=array(); + + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + function __construct($db) + { + $this->db = $db; + } + + function doActions($parameters, &$object, &$action) { + + global $langs,$user; + $langs->load('resource@resource'); + + if (in_array('element_resource',explode(':',$parameters['context']))) + { + // Efface une ressource + if ($action == 'confirm_delete_resource' && $user->rights->resource->delete && GETPOST('confirm') == 'yes') + { + $res = $object->fetch(GETPOST('lineid')); + if($res) + { + $result = $object->delete_resource(GETPOST('lineid'),GETPOST('element')); + + if ($result >= 0) + { + setEventMessage($langs->trans('RessourceLineSuccessfullyDeleted')); + Header("Location: ".$_SERVER['PHP_SELF']."?element=".GETPOST('element')."&element_id=".GETPOST('element_id')); + exit; + } + else { + setEventMessage($object->error,'errors'); + } + } + } + + // Update ressource + if ($action == 'update_resource' && $user->rights->resource->write && !GETPOST('cancel') ) + { + $res = $object->fetch(GETPOST('lineid')); + if($res) + { + $object->id = GETPOST('lineid'); + $object->busy = GETPOST('busy'); + $object->mandatory = GETPOST('mandatory'); + + $result = $object->update(); + + if ($result >= 0) + { + setEventMessage($langs->trans('RessourceLineSuccessfullyUpdated')); + Header("Location: ".$_SERVER['PHP_SELF']."?element=".GETPOST('element')."&element_id=".GETPOST('element_id')); + exit; + } + else { + setEventMessage($object->error,'errors'); + } + } + } + } + + } +} diff --git a/class/html.formresource.class.php b/class/html.formresource.class.php new file mode 100644 index 00000000000..de7b3d23f3d --- /dev/null +++ b/class/html.formresource.class.php @@ -0,0 +1,145 @@ + +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +/** + * \file place/class/html.place.class.php + * \ingroup core + * \brief Fichier de la classe permettant la generation du formulaire html d'envoi de mail unitaire + */ +require_once(DOL_DOCUMENT_ROOT ."/core/class/html.form.class.php"); + + +/** + * + * Classe permettant la gestion des formulaire du module place + * + * @package resource + +* \remarks Utilisation: $formresource = new FormResource($db) +* \remarks $formplace->proprietes=1 ou chaine ou tableau de valeurs +*/ +class FormResource +{ + var $db; + + var $substit=array(); + var $param=array(); + + var $error; + + + /** + * Constructor + * + * @param DoliDB $DB Database handler + */ + function __construct($db) + { + $this->db = $db; + + return 1; + } + + + /** + * Output html form to select a location (place) + * + * @param string $selected Preselected type + * @param string $htmlname Name of field in form + * @param string $filter Optionnal filters criteras (example: 's.rowid <> x') + * @param int $showempty Add an empty field + * @param int $showtype Show third party type in combolist (customer, prospect or supplier) + * @param int $forcecombo Force to use combo box + * @param array $event Event options. Example: array(array('method'=>'getContacts', 'url'=>dol_buildpath('/core/ajax/contacts.php',1), 'htmlname'=>'contactid', 'params'=>array('add-customer-contact'=>'disabled'))) + * @param string $filterkey Filter on key value + * @param int $outputmode 0=HTML select string, 1=Array + * @param int $limit Limit number of answers + * @return string HTML string with + */ + function select_resource_list($selected='',$htmlname='fk_resource',$filter='',$showempty=0, $showtype=0, $forcecombo=0, $event=array(), $filterkey='', $outputmode=0, $limit=20) + { + global $conf,$user,$langs; + + $out=''; + $outarray=array(); + + $resourcestat = new Resource($this->db); + + $resources_used = $resourcestat->fetch_all_used('ASC', 't.rowid', $limit, $offset, $filter=''); + + $out = '
'; + $out.= ''; + //$out.= ''; + //$out.= ''; + + if ($resourcestat) + { + if ($conf->use_javascript_ajax && $conf->global->COMPANY_USE_SEARCH_TO_SELECT && ! $forcecombo) + { + //$minLength = (is_numeric($conf->global->COMPANY_USE_SEARCH_TO_SELECT)?$conf->global->COMPANY_USE_SEARCH_TO_SELECT:2); + $out.= ajax_combobox($htmlname, $event, $conf->global->COMPANY_USE_SEARCH_TO_SELECT); + } + + // Construct $out and $outarray + $out.= ''."\n"; + + + $out.= '     '; + + $out.= '
'; + } + else + { + dol_print_error($this->db); + } + + if ($outputmode) return $outarray; + return $out; + } + + +} + +?> diff --git a/class/resource.class.php b/class/resource.class.php new file mode 100644 index 00000000000..edbf4bc0ad6 --- /dev/null +++ b/class/resource.class.php @@ -0,0 +1,598 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file place/class/resource.class.php + * \ingroup place + * \brief Class file for resource object + + */ + +// Put here all includes required by your class file +require_once(DOL_DOCUMENT_ROOT."/core/class/commonobject.class.php"); + + +/** + * DAO Resource object + */ +class Resource extends CommonObject +{ + var $db; //!< To store db handler + var $error; //!< To return error code (or message) + var $errors=array(); //!< To return several error codes (or messages) + //var $element='resource'; //!< Id that identify managed objects + //var $table_element='llx_resource'; //!< Name of table without prefix where object is stored + + var $id; + + var $resource_id; + var $resource_type; + var $element_id; + var $element_type; + var $busy; + var $mandatory; + var $fk_user_create; + var $tms=''; + + + + + /** + * Constructor + * + * @param DoliDb $db Database handler + */ + function __construct($db) + { + $this->db = $db; + return 1; + } + + /** + * Load object in memory from database + * @param id id object + * @return int <0 if KO, >0 if OK + */ + function fetch($id) + { + global $langs; + $sql = "SELECT"; + $sql.= " t.rowid,"; + $sql.= " t.resource_id,"; + $sql.= " t.resource_type,"; + $sql.= " t.element_id,"; + $sql.= " t.element_type,"; + $sql.= " t.busy,"; + $sql.= " t.mandatory,"; + $sql.= " t.fk_user_create,"; + $sql.= " t.tms"; + $sql.= " FROM ".MAIN_DB_PREFIX."element_resources as t"; + $sql.= " WHERE t.rowid = ".$this->db->escape($id); + + dol_syslog(get_class($this)."::fetch sql=".$sql, LOG_DEBUG); + $resql=$this->db->query($sql); + if ($resql) + { + if ($this->db->num_rows($resql)) + { + $obj = $this->db->fetch_object($resql); + + $this->id = $obj->rowid; + $this->resource_id = $obj->resource_id; + $this->resource_type = $obj->resource_type; + $this->element_id = $obj->element_id; + $this->element_type = $obj->element_type; + $this->busy = $obj->busy; + $this->mandatory = $obj->mandatory; + $this->fk_user_create = $obj->fk_user_create; + + if($obj->resource_id && $obj->resource_type) + $this->objresource = $this->fetchObjectByElement($obj->resource_id,$obj->resource_type); + if($obj->element_id && $obj->element_type) + $this->objelement = $this->fetchObjectByElement($obj->element_id,$obj->element_type); + + } + $this->db->free($resql); + + return $this->id; + } + else + { + $this->error="Error ".$this->db->lasterror(); + dol_syslog(get_class($this)."::fetch ".$this->error, LOG_ERR); + return -1; + } + } + + /** + * Load all objects into $this->lines + * + * @param string $sortorder sort order + * @param string $sortfield sort field + * @param int $limit limit page + * @param int $offset page + * @param array $filter filter output + * @return int <0 if KO, >0 if OK + */ + function fetch_all($sortorder, $sortfield, $limit, $offset, $filter='') + { + global $conf; + $sql="SELECT "; + $sql.= " t.rowid,"; + $sql.= " t.resource_id,"; + $sql.= " t.resource_type,"; + $sql.= " t.element_id,"; + $sql.= " t.element_type,"; + $sql.= " t.busy,"; + $sql.= " t.mandatory,"; + $sql.= " t.fk_user_create,"; + $sql.= " t.tms"; + $sql.= ' FROM '.MAIN_DB_PREFIX .'element_resources as t '; + //$sql.= " WHERE t.entity IN (".getEntity('resource').")"; + + //Manage filter + if (!empty($filter)){ + foreach($filter as $key => $value) { + if (strpos($key,'date')) { + $sql.= ' AND '.$key.' = \''.$this->db->idate($value).'\''; + } + else { + $sql.= ' AND '.$key.' LIKE \'%'.$value.'%\''; + } + } + } + $sql.= " GROUP BY t.rowid"; + $sql.= " ORDER BY $sortfield $sortorder " . $this->db->plimit( $limit + 1 ,$offset); + dol_syslog(get_class($this)."::fetch_all sql=".$sql, LOG_DEBUG); + + $resql=$this->db->query($sql); + if ($resql) + { + $num = $this->db->num_rows($resql); + if ($num) + { + $i = 0; + while ($i < $num) + { + $obj = $this->db->fetch_object($resql); + $line = new Resource($this->db); + $line->id = $obj->rowid; + $line->resource_id = $obj->resource_id; + $line->resource_type = $obj->resource_type; + $line->element_id = $obj->element_id; + $line->element_type = $obj->element_type; + $line->busy = $obj->busy; + $line->mandatory = $obj->mandatory; + $line->fk_user_create = $obj->fk_user_create; + + if($obj->resource_id && $obj->resource_type) + $line->objresource = $this->fetchObjectByElement($obj->resource_id,$obj->resource_type); + if($obj->element_id && $obj->element_type) + $line->objelement = $this->fetchObjectByElement($obj->element_id,$obj->element_type); + $this->lines[$i] = $line; + + $i++; + } + $this->db->free($resql); + } + return $num; + } + else + { + $this->error = $this->db->lasterror(); + return -1; + } + + } + + /** + * Load all objects into $this->lines + * + * @param string $sortorder sort order + * @param string $sortfield sort field + * @param int $limit limit page + * @param int $offset page + * @param array $filter filter output + * @return int <0 if KO, >0 if OK + */ + function fetch_all_used($sortorder="ASC",$sortfield="t.rowid",$limit, $offset, $filter='') + { + global $conf; + $sql="SELECT "; + $sql.= " t.rowid,"; + $sql.= " t.resource_id,"; + $sql.= " t.resource_type,"; + $sql.= " t.element_id,"; + $sql.= " t.element_type,"; + $sql.= " t.busy,"; + $sql.= " t.mandatory,"; + $sql.= " t.fk_user_create,"; + $sql.= " t.tms"; + $sql.= ' FROM '.MAIN_DB_PREFIX .'element_resources as t '; + //$sql.= " WHERE t.entity IN (".getEntity('resource').")"; + + //Manage filter + if (!empty($filter)){ + foreach($filter as $key => $value) { + if (strpos($key,'date')) { + $sql.= ' AND '.$key.' = \''.$this->db->idate($value).'\''; + } + else { + $sql.= ' AND '.$key.' LIKE \'%'.$value.'%\''; + } + } + } + $sql.= " GROUP BY t.resource_id"; + $sql.= " ORDER BY $sortfield $sortorder " . $this->db->plimit( $limit + 1 ,$offset); + dol_syslog(get_class($this)."::fetch_all sql=".$sql, LOG_DEBUG); + + $resql=$this->db->query($sql); + if ($resql) + { + $num = $this->db->num_rows($resql); + if ($num) + { + $i = 0; + while ($i < $num) + { + $obj = $this->db->fetch_object($resql); + $line = new Resource($this->db); + $line->id = $obj->rowid; + $line->resource_id = $obj->resource_id; + $line->resource_type = $obj->resource_type; + $line->element_id = $obj->element_id; + $line->element_type = $obj->element_type; + $line->busy = $obj->busy; + $line->mandatory = $obj->mandatory; + $line->fk_user_create = $obj->fk_user_create; + + $this->lines[$i] = $this->fetchObjectByElement($obj->resource_id,$obj->resource_type); + + $i++; + } + $this->db->free($resql); + } + return $num; + } + else + { + $this->error = $this->db->lasterror(); + return -1; + } + + } + + /** + * Fetch all resources available, declared by modules + * + * Load available resource in array $this->available_resources + * + * + * @return int number of available resources declared by modules + */ + function fetch_all_available() { + global $conf; + + if (! empty($conf->modules_parts['resources'])) + { + $this->available_resources=(array) $conf->modules_parts['resources']; + + return count($this->available_resources); + } + return 0; + } + + + /** + * Update object into database + * + * @param User $user User that modifies + * @param int $notrigger 0=launch triggers after, 1=disable triggers + * @return int <0 if KO, >0 if OK + */ + function update($user=0, $notrigger=0) + { + global $conf, $langs; + $error=0; + + // Clean parameters + if (isset($this->resource_id)) $this->resource_id=trim($this->resource_id); + if (isset($this->resource_type)) $this->resource_type=trim($this->resource_type); + if (isset($this->element_id)) $this->element_id=trim($this->element_id); + if (isset($this->element_type)) $this->element_type=trim($this->element_type); + if (isset($this->busy)) $this->busy=trim($this->busy); + if (isset($this->mandatory)) $this->mandatory=trim($this->mandatory); + + + // Check parameters + // Put here code to add a control on parameters values + + // Update request + $sql = "UPDATE ".MAIN_DB_PREFIX."element_resources SET"; + $sql.= " resource_id=".(isset($this->resource_id)?"'".$this->db->escape($this->resource_id)."'":"null").","; + $sql.= " resource_type=".(isset($this->resource_type)?"'".$this->resource_type."'":"null").","; + $sql.= " element_id=".(isset($this->element_id)?$this->element_id:"null").","; + $sql.= " element_type=".(isset($this->element_type)?"'".$this->db->escape($this->element_type)."'":"null").","; + $sql.= " busy=".(isset($this->busy)?$this->busy:"null").","; + $sql.= " mandatory=".(isset($this->mandatory)?$this->mandatory:"null").","; + $sql.= " tms=".(dol_strlen($this->tms)!=0 ? "'".$this->db->idate($this->tms)."'" : 'null').""; + + + $sql.= " WHERE rowid=".$this->id; + + $this->db->begin(); + + dol_syslog(get_class($this)."::update sql=".$sql, LOG_DEBUG); + $resql = $this->db->query($sql); + if (! $resql) { $error++; $this->errors[]="Error ".$this->db->lasterror(); } + + if (! $error) + { + if (! $notrigger) + { + // Uncomment this and change MYOBJECT to your own tag if you + // want this action calls a trigger. + + // Call triggers + include_once DOL_DOCUMENT_ROOT . '/core/class/interfaces.class.php'; + $interface=new Interfaces($this->db); + $result=$interface->run_triggers('RESOURCE_MODIFY',$this,$user,$langs,$conf); + if ($result < 0) { $error++; $this->errors=$interface->errors; } + // End call triggers + } + } + + // Commit or rollback + if ($error) + { + foreach($this->errors as $errmsg) + { + dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR); + $this->error.=($this->error?', '.$errmsg:$errmsg); + } + $this->db->rollback(); + return -1*$error; + } + else + { + $this->db->commit(); + return 1; + } + } + + + /** + * + * + * @param string $element_type Element type project_task + * @return array + */ + function getElementProperties($element_type) + { + // Parse element/subelement (ex: project_task) + $module = $element = $subelement = $element_type; + + // If we ask an resource form external module (instead of default path) + if (preg_match('/^([^@]+)@([^@]+)$/i',$element_type,$regs)) + { + $element = $subelement = $regs[1]; + $module = $regs[2]; + } + + //print '
1. element : '.$element.' - module : '.$module .'
'; + + if ( preg_match('/^([^_]+)_([^_]+)/i',$element,$regs)) + { + $module = $element = $regs[1]; + $subelement = $regs[2]; + } + + $classfile = strtolower($subelement); + $classname = ucfirst($subelement); + $classpath = $module.'/class'; + + + // For compat + if($element_type == "action") { + $classpath = 'comm/action/class'; + $subelement = 'Actioncomm'; + $classfile = strtolower($subelement); + $classname = ucfirst($subelement); + $module = 'agenda'; + } + + + $element_properties = array( + 'module' => $module, + 'classpath' => $classpath, + 'element' => $element, + 'subelement' => $subelement, + 'classfile' => $classfile, + 'classname' => $classname + ); + return $element_properties; + } + + /** + * Fetch an object with element_type and his id + * Inclusion classes is automatic + * + * + */ + function fetchObjectByElement($element_id,$element_type) { + + global $conf; + + $element_prop = $this->getElementProperties($element_type); + + if (is_array($element_prop) && $conf->$element_prop['module']->enabled) + { + dol_include_once('/'.$element_prop['classpath'].'/'.$element_prop['classfile'].'.class.php'); + + $objectstat = new $element_prop['classname']($this->db); + $ret = $objectstat->fetch($element_id); + if ($ret >= 0) + { + return $objectstat; + } + } + return 0; + } + + /** + * Add resources to the actioncom object + * + * @param int $element_id Element id + * @param string $element_type Element type + * @param int $resource_id Resource id + * @param string $resource_type Resource type + * @param array $resource Resources linked with element + * @return int <=0 if KO, >0 if OK + */ + function add_element_resource($element_id,$element_type,$resource_id,$resource_element,$busy=0,$mandatory=0) + { + $this->db->begin(); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX."element_resources ("; + $sql.= "resource_id"; + $sql.= ", resource_type"; + $sql.= ", element_id"; + $sql.= ", element_type"; + $sql.= ", busy"; + $sql.= ", mandatory"; + $sql.= ") VALUES ("; + $sql.= $resource_id; + $sql.= ", '".$resource_element."'"; + $sql.= ", '".$element_id."'"; + $sql.= ", '".$element_type."'"; + $sql.= ", '".$busy."'"; + $sql.= ", '".$mandatory."'"; + $sql.= ")"; + + dol_syslog(get_class($this)."::add_element_resource sql=".$sql, LOG_DEBUG); + if ($this->db->query($sql)) + { + $this->db->commit(); + return 1; + } + else + { + $this->error=$this->db->lasterror(); + $this->db->rollback(); + return 0; + } + } + + + /* + * Return an array with resources linked to the element + * + * + */ + function getElementResources($element,$element_id,$resource_type='') + { + + // Links beetween objects are stored in this table + $sql = 'SELECT rowid, resource_id, resource_type, busy, mandatory'; + $sql.= ' FROM '.MAIN_DB_PREFIX.'element_resources'; + $sql.= " WHERE element_id='".$element_id."' AND element_type='".$element."'"; + if($resource_type) + $sql.=" AND resource_type LIKE '%".$resource_type."%'"; + $sql .= ' ORDER BY resource_type'; + + dol_syslog(get_class($this)."::getElementResources sql=".$sql); + $resql = $this->db->query($sql); + if ($resql) + { + $num = $this->db->num_rows($resql); + $i = 0; + while ($i < $num) + { + $obj = $this->db->fetch_object($resql); + + $resources[$i] = array( + 'rowid' => $obj->rowid, + 'resource_id' => $obj->resource_id, + 'resource_type'=>$obj->resource_type, + 'busy'=>$obj->busy, + 'mandatory'=>$obj->mandatory + ); + $i++; + } + } + + return $resources; + } + + function fetchElementResources($element,$element_id) + { + $resources = $this->getElementResources($element,$element_id); + $i=0; + foreach($resources as $nb => $resource) + { + $this->lines[$i] = $this->fetchObjectByElement($resource['resource_id'],$resource['resource_type']); + $i++; + } + return $i; + + } + + /** + * Delete a link to resource line + * TODO: move into commonobject class + * + * @param int $rowid Id of resource line to delete + * @param int $element element name (for trigger) TODO: use $this->element into commonobject class + * @param int $notrigger Disable all triggers + * @return int >0 if OK, <0 if KO + */ + function delete_resource($rowid, $element, $notrigger=0) + { + global $user,$langs,$conf; + + $error=0; + + $sql = "DELETE FROM ".MAIN_DB_PREFIX."element_resources"; + $sql.= " WHERE rowid =".$rowid; + + dol_syslog(get_class($this)."::delete_resource sql=".$sql); + if ($this->db->query($sql)) + { + if (! $notrigger) + { + // Call triggers + include_once DOL_DOCUMENT_ROOT . '/core/class/interfaces.class.php'; + $interface=new Interfaces($this->db); + $result=$interface->run_triggers(strtoupper($element).'_DELETE_RESOURCE',$this,$user,$langs,$conf); + if ($result < 0) { + $error++; $this->errors=$interface->errors; + } + // End call triggers + } + + return 1; + } + else + { + $this->error=$this->db->lasterror(); + dol_syslog(get_class($this)."::delete_resource error=".$this->error, LOG_ERR); + return -1; + } + } + +} +?> diff --git a/core/ajax/resource_action.json.php b/core/ajax/resource_action.json.php new file mode 100644 index 00000000000..da6a667e834 --- /dev/null +++ b/core/ajax/resource_action.json.php @@ -0,0 +1,158 @@ + + * Copyright (C) 2013 Jean-François Ferry + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file resource/core/ajax/resource_action.json.php + * \ingroup resource + * \brief This file is used for resource planning + */ + +if (! defined('NOREQUIREHTML')) define('NOREQUIREHTML','1'); // If we don't need to load the html.form.class.php +if (! defined('NOREQUIREAJAX')) define('NOREQUIREAJAX','1'); + +// Change this following line to use the correct relative path (../, ../../, etc) +$res=0; +if (! $res && file_exists("../../../main.inc.php")) $res=@include '../../../main.inc.php'; // to work if your module directory is into dolibarr root htdocs directory +if (! $res && file_exists("../../../../main.inc.php")) $res=@include '../../../../main.inc.php'; // to work if your module directory is into a subdir of root htdocs directory + +if (! $res) die("Include of main fails"); +// Change this following line to use the correct relative path from htdocs +require_once DOL_DOCUMENT_ROOT.'/comm/action/class/actioncomm.class.php'; + +dol_include_once('/resource/class/resource.class.php'); + +// Load traductions files requiredby by page +$langs->load("companies"); +$langs->load("other"); + +// Get parameters +$id = GETPOST('id','int'); +$action = GETPOST('action','alpha'); + +$start = GETPOST('start','int'); +$end = GETPOST('end','int'); +$fk_resource = GETPOST('fk_resource','int'); + + +// Get event in an array +$eventarray=array(); + +$sql = 'SELECT a.id,a.label,'; +$sql.= ' a.datep,'; +$sql.= ' a.datep2,'; +$sql.= ' a.datea,'; +$sql.= ' a.datea2,'; +$sql.= ' a.percent,'; +$sql.= ' a.fk_user_author,a.fk_user_action,a.fk_user_done,'; +$sql.= ' a.priority, a.fulldayevent, a.location,'; +$sql.= ' a.fk_soc, a.fk_contact,'; +$sql.= ' ca.code'; +$sql.= ' FROM ('.MAIN_DB_PREFIX.'c_actioncomm as ca,'; +$sql.= " ".MAIN_DB_PREFIX.'user as u,'; +$sql.= " ".MAIN_DB_PREFIX."actioncomm as a)"; +if($fk_resource > 0) { + $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'element_resources as r ON a.id = r.element_id '; +} +$sql.= ' WHERE a.fk_action = ca.id'; +if($fk_resource > 0) { + $sql.= " AND r.resource_id = '".$db->escape($fk_resource)."'"; +} +$sql.= ' AND a.fk_user_author = u.rowid'; +$sql.= ' AND a.entity IN ('.getEntity().')'; +if ($actioncode) $sql.=" AND ca.code='".$db->escape($actioncode)."'"; +if ($pid) $sql.=" AND a.fk_project=".$db->escape($pid); + + + + +if ($type) $sql.= " AND ca.id = ".$type; +if ($status == 'done') { $sql.= " AND (a.percent = 100 OR (a.percent = -1 AND a.datep2 <= '".$db->idate($now)."'))"; } +if ($status == 'todo') { $sql.= " AND ((a.percent >= 0 AND a.percent < 100) OR (a.percent = -1 AND a.datep2 > '".$db->idate($now)."'))"; } +if ($filtera > 0 || $filtert > 0 || $filterd > 0) +{ + $sql.= " AND ("; + if ($filtera > 0) $sql.= " a.fk_user_author = ".$filtera; + if ($filtert > 0) $sql.= ($filtera>0?" OR ":"")." a.fk_user_action = ".$filtert; + if ($filterd > 0) $sql.= ($filtera>0||$filtert>0?" OR ":"")." a.fk_user_done = ".$filterd; + $sql.= ")"; +} +$sql.= ' GROUP BY a.id'; +// Sort on date +$sql.= ' ORDER BY datep'; +//print $sql; + +dol_syslog("comm/action/index.php sql=".$sql, LOG_DEBUG); +$resql=$db->query($sql); +if ($resql) +{ + $num = $db->num_rows($resql); + $i=0; + while ($i < $num) + { + $obj = $db->fetch_object($resql); + + // Create a new object action + $event=new ActionComm($db); + $event->id=$obj->id; + $event->datep=$db->jdate($obj->datep); // datep and datef are GMT date + $event->datef=$db->jdate($obj->datep2); + $event->type_code=$obj->code; + $event->libelle=$obj->label; + $event->percentage=$obj->percent; + $event->author->id=$obj->fk_user_author; // user id of creator + $event->usertodo->id=$obj->fk_user_action; // user id of owner + $event->userdone->id=$obj->fk_user_done; // deprecated + // $event->userstodo=... with s after user, in future version, will be an array with all id of user assigned to event + $event->priority=$obj->priority; + $event->fulldayevent=$obj->fulldayevent; + $event->location=$obj->location; + + $event->societe->id=$obj->fk_soc; + $event->contact->id=$obj->fk_contact; + + + $eventarray[]=$event; + + $i++; + + } +} +else +{ + dol_print_error($db); +} + +//var_dump($eventarray); +foreach ($eventarray as $day => $event) { + $event_json[] = array( + 'id' => $event->id, + 'title' => $event->libelle, + 'start' => $event->datep, + 'end' => $event->datef, + 'end' => $event->datef, + 'allDay' => $event->fulldayevent?true:false, + 'url' => dol_buildpath("/comm/action/fiche.php",1).'?id='. $event->id + ); +} + +//var_dump($event_json); +echo json_encode($event_json); + + +$db->close(); +?> diff --git a/core/modules/modResource.class.php b/core/modules/modResource.class.php new file mode 100644 index 00000000000..e0ecf6654ea --- /dev/null +++ b/core/modules/modResource.class.php @@ -0,0 +1,437 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \defgroup resource Resource module + * \brief Resource module descriptor. + * \file core/modules/modResource.class.php + * \ingroup resource + * \brief Description and activation file for module Resource + */ +include_once DOL_DOCUMENT_ROOT . "/core/modules/DolibarrModules.class.php"; + +/** + * Description and activation class for module Resource + */ +class modResource extends DolibarrModules +{ + + /** + * Constructor. Define names, constants, directories, boxes, permissions + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + global $langs, $conf; + + $this->db = $db; + + // Id for module (must be unique). + // Use a free id here + // (See in Home -> System information -> Dolibarr for list of used modules id). + $this->numero = 110111; + // Key text used to identify module (for permissions, menus, etc...) + $this->rights_class = 'resource'; + + // Family can be 'crm','financial','hr','projects','products','ecm','technic','other' + // It is used to group modules in module setup page + $this->family = "hr"; + // Module label (no space allowed) + // used if translation string 'ModuleXXXName' not found + // (where XXX is value of numeric property 'numero' of module) + $this->name = preg_replace('/^mod/i', '', get_class($this)); + // Module description + // used if translation string 'ModuleXXXDesc' not found + // (where XXX is value of numeric property 'numero' of module) + $this->description = "Description of module Resource"; + // Possible values for version are: 'development', 'experimental' or version + $this->version = '0.1'; + // Key used in llx_const table to save module status enabled/disabled + // (where MYMODULE is value of property name of module in uppercase) + $this->const_name = 'MAIN_MODULE_' . strtoupper($this->name); + // Where to store the module in setup page + // (0=common,1=interface,2=others,3=very specific) + $this->special = 0; + // Name of image file used for this module. + // If file is in theme/yourtheme/img directory under name object_pictovalue.png + // use this->picto='pictovalue' + // If file is in module/img directory under name object_pictovalue.png + // use this->picto='pictovalue@module' + $this->picto = 'resource@resource'; // mypicto@resource + // Defined all module parts (triggers, login, substitutions, menus, css, etc...) + // for default path (eg: /resource/core/xxxxx) (0=disable, 1=enable) + // for specific path of parts (eg: /resource/core/modules/barcode) + // for specific css file (eg: /resource/css/resource.css.php) + $this->module_parts = array( + // Set this to 1 if module has its own trigger directory + //'triggers' => 1, + // Set this to 1 if module has its own login method directory + //'login' => 0, + // Set this to 1 if module has its own substitution function file + //'substitutions' => 0, + // Set this to 1 if module has its own menus handler directory + //'menus' => 0, + // Set this to 1 if module has its own barcode directory + //'barcode' => 0, + // Set this to 1 if module has its own models directory + //'models' => 0, + // Set this to relative path of css if module has its own css file + 'css' => '/resource/css/resource.css.php', + // Set here all hooks context managed by module + 'hooks' => array('actioncard','actioncommdao','element_resource') + // Set here all workflow context managed by module + //'workflow' => array('order' => array('WORKFLOW_ORDER_AUTOCREATE_INVOICE')) + ); + + // Data directories to create when module is enabled. + // Example: this->dirs = array("/resource/temp"); + $this->dirs = array("/resource"); + + // Config pages. Put here list of php pages + // stored into resource/admin directory, used to setup module. + $this->config_page_url = array("admin_resource.php@resource"); + + // Dependencies + // List of modules id that must be enabled if this module is enabled + $this->depends = array(); + // List of modules id to disable if this one is disabled + $this->requiredby = array('modPlace'); + // Minimum version of PHP required by module + $this->phpmin = array(5, 3); + // Minimum version of Dolibarr required by module + $this->need_dolibarr_version = array(3, 4); + $this->langfiles = array("resource@resource"); // langfiles@resource + // Constants + // List of particular constants to add when module is enabled + // (key, 'chaine', value, desc, visible, 'current' or 'allentities', deleteonunactive) + // Example: + $this->const = array( + 0=>array( + 'PLACE_DEFAULT_ZOOM_FOR_MAP', + 'chaine', + '1', + 'This is a constant to defined default zoom into link to OSM map', + 1 + ) + + ); + + // Array to add new pages in new tabs + // Example: + $this->tabs = array( + // // To add a new tab identified by code tabname1 + // 'objecttype:+tabname1:Title1:langfile@resource:$user->rights->resource->read:/resource/mynewtab1.php?id=__ID__', + // // To add another new tab identified by code tabname2 + // 'objecttype:+tabname2:Title2:langfile@resource:$user->rights->othermodule->read:/resource/mynewtab2.php?id=__ID__', + // // To remove an existing tab identified by code tabname + // 'objecttype:-tabname' + ); + // where objecttype can be + // 'thirdparty' to add a tab in third party view + // 'intervention' to add a tab in intervention view + // 'order_supplier' to add a tab in supplier order view + // 'invoice_supplier' to add a tab in supplier invoice view + // 'invoice' to add a tab in customer invoice view + // 'order' to add a tab in customer order view + // 'product' to add a tab in product view + // 'stock' to add a tab in stock view + // 'propal' to add a tab in propal view + // 'member' to add a tab in fundation member view + // 'contract' to add a tab in contract view + // 'user' to add a tab in user view + // 'group' to add a tab in group view + // 'contact' to add a tab in contact view + // 'categories_x' to add a tab in category view + // (reresource 'x' by type of category (0=product, 1=supplier, 2=customer, 3=member) + + $this->tabs = array( + 'action:+resources:Resources:resource@resource:$user->rights->resource->read:/resource/element_resource.php?element=action&element_id=__ID__', + 'thirdparty:+resources:Resources:resource@resource:$user->rights->resource->read:/resource/element_resource.php?element=societe&element_id=__ID__' + ); + + /* Example: + // This is to avoid warnings + if (! isset($conf->resource->enabled)) $conf->resource->enabled=0; + $this->dictionnaries=array( + 'langs'=>'resource@resource', + // List of tables we want to see into dictonnary editor + 'tabname'=>array( + MAIN_DB_PREFIX."table1", + MAIN_DB_PREFIX."table2", + MAIN_DB_PREFIX."table3" + ), + // Label of tables + 'tablib'=>array("Table1","Table2","Table3"), + // Request to select fields + 'tabsql'=>array( + 'SELECT f.rowid as rowid, f.code, f.label, f.active' + . ' FROM ' . MAIN_DB_PREFIX . 'table1 as f', + 'SELECT f.rowid as rowid, f.code, f.label, f.active' + . ' FROM ' . MAIN_DB_PREFIX . 'table2 as f', + 'SELECT f.rowid as rowid, f.code, f.label, f.active' + . ' FROM ' . MAIN_DB_PREFIX . 'table3 as f' + ), + // Sort order + 'tabsqlsort'=>array("label ASC","label ASC","label ASC"), + // List of fields (result of select to show dictionnary) + 'tabfield'=>array("code,label","code,label","code,label"), + // List of fields (list of fields to edit a record) + 'tabfieldvalue'=>array("code,label","code,label","code,label"), + // List of fields (list of fields for insert) + 'tabfieldinsert'=>array("code,label","code,label","code,label"), + // Name of columns with primary key (try to always name it 'rowid') + 'tabrowid'=>array("rowid","rowid","rowid"), + // Condition to show each dictionnary + 'tabcond'=>array( + $conf->resource->enabled, + $conf->resource->enabled, + $conf->resource->enabled + ) + ); + */ + + // Boxes + // Add here list of php file(s) stored in core/boxes that contains class to show a box. + $this->boxes = array(); // Boxes list + $r = 0; + // Example: + + //$this->boxes[$r][1] = "MyBox@resource"; + //$r ++; + /* + $this->boxes[$r][1] = "myboxb.php"; + $r++; + */ + + // Permissions + $this->rights = array(); // Permission array used by this module + $r = 0; + + $this->rights[$r][0] = 1101201; + $this->rights[$r][1] = 'See resources'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'read'; + $r++; + + $this->rights[$r][0] = 1101202; + $this->rights[$r][1] = 'Modify resources'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'write'; + $r++; + + $this->rights[$r][0] = 1101203; + $this->rights[$r][1] = 'Delete resources'; + $this->rights[$r][3] = 0; + $this->rights[$r][4] = 'delete'; + $r++; + + + // Add here list of permission defined by + // an id, a label, a boolean and two constant strings. + // Example: + //// Permission id (must not be already used) + //$this->rights[$r][0] = 2000; + //// Permission label + //$this->rights[$r][1] = 'Permision label'; + //// Permission by default for new user (0/1) + //$this->rights[$r][3] = 1; + //// In php code, permission will be checked by test + //// if ($user->rights->permkey->level1->level2) + //$this->rights[$r][4] = 'level1'; + //// In php code, permission will be checked by test + //// if ($user->rights->permkey->level1->level2) + //$this->rights[$r][5] = 'level2'; + //$r++; + // Main menu entries + $this->menu = array(); // List of menus to add + $r = 0; + + // Menus declaration + $this->menu[$r]=array( + 'fk_menu'=>'fk_mainmenu=tools', + 'type'=>'left', + 'titre'=> 'MenuResourceIndex', + 'mainmenu'=>'tools', + 'leftmenu'=> 'resource', + 'url'=> '/resource/index.php', + 'langs'=> 'resource@resource', + 'position'=> 100, + 'enabled'=> '1', + 'perms'=> '$user->rights->resource->read', + 'user'=> 0 + ); + $r++; + + // Menus declaration + $this->menu[$r]=array( + 'fk_menu'=>'fk_mainmenu=tools,fk_leftmenu=resource', + 'type'=>'left', + 'titre'=> 'MenuResourcePlanning', + 'mainmenu'=>'tools', + 'leftmenu'=> '', + 'url'=> '/resource/resource_planning.php', + 'langs'=> 'resource@resource', + 'position'=> 101, + 'enabled'=> '1', + 'perms'=> '$user->rights->resource->read', + 'user'=> 0 + ); + $r++; + + + // Exports + $r = 1; + + // Example: + //$this->export_code[$r]=$this->rights_class.'_'.$r; + //// Translation key (used only if key ExportDataset_xxx_z not found) + //$this->export_label[$r]='CustomersInvoicesAndInvoiceLines'; + //// Condition to show export in list (ie: '$user->id==3'). + //// Set to 1 to always show when module is enabled. + //$this->export_enabled[$r]='1'; + //$this->export_permission[$r]=array(array("facture","facture","export")); + //$this->export_fields_array[$r]=array( + // 's.rowid'=>"IdCompany", + // 's.nom'=>'CompanyName', + // 's.address'=>'Address', + // 's.cp'=>'Zip', + // 's.ville'=>'Town', + // 's.fk_pays'=>'Country', + // 's.tel'=>'Phone', + // 's.siren'=>'ProfId1', + // 's.siret'=>'ProfId2', + // 's.ape'=>'ProfId3', + // 's.idprof4'=>'ProfId4', + // 's.code_compta'=>'CustomerAccountancyCode', + // 's.code_compta_fournisseur'=>'SupplierAccountancyCode', + // 'f.rowid'=>"InvoiceId", + // 'f.facnumber'=>"InvoiceRef", + // 'f.datec'=>"InvoiceDateCreation", + // 'f.datef'=>"DateInvoice", + // 'f.total'=>"TotalHT", + // 'f.total_ttc'=>"TotalTTC", + // 'f.tva'=>"TotalVAT", + // 'f.paye'=>"InvoicePaid", + // 'f.fk_statut'=>'InvoiceStatus', + // 'f.note'=>"InvoiceNote", + // 'fd.rowid'=>'LineId', + // 'fd.description'=>"LineDescription", + // 'fd.price'=>"LineUnitPrice", + // 'fd.tva_tx'=>"LineVATRate", + // 'fd.qty'=>"LineQty", + // 'fd.total_ht'=>"LineTotalHT", + // 'fd.total_tva'=>"LineTotalTVA", + // 'fd.total_ttc'=>"LineTotalTTC", + // 'fd.date_start'=>"DateStart", + // 'fd.date_end'=>"DateEnd", + // 'fd.fk_product'=>'ProductId', + // 'p.ref'=>'ProductRef' + //); + //$this->export_entities_array[$r]=array('s.rowid'=>"company", + // 's.nom'=>'company', + // 's.address'=>'company', + // 's.cp'=>'company', + // 's.ville'=>'company', + // 's.fk_pays'=>'company', + // 's.tel'=>'company', + // 's.siren'=>'company', + // 's.siret'=>'company', + // 's.ape'=>'company', + // 's.idprof4'=>'company', + // 's.code_compta'=>'company', + // 's.code_compta_fournisseur'=>'company', + // 'f.rowid'=>"invoice", + // 'f.facnumber'=>"invoice", + // 'f.datec'=>"invoice", + // 'f.datef'=>"invoice", + // 'f.total'=>"invoice", + // 'f.total_ttc'=>"invoice", + // 'f.tva'=>"invoice", + // 'f.paye'=>"invoice", + // 'f.fk_statut'=>'invoice', + // 'f.note'=>"invoice", + // 'fd.rowid'=>'invoice_line', + // 'fd.description'=>"invoice_line", + // 'fd.price'=>"invoice_line", + // 'fd.total_ht'=>"invoice_line", + // 'fd.total_tva'=>"invoice_line", + // 'fd.total_ttc'=>"invoice_line", + // 'fd.tva_tx'=>"invoice_line", + // 'fd.qty'=>"invoice_line", + // 'fd.date_start'=>"invoice_line", + // 'fd.date_end'=>"invoice_line", + // 'fd.fk_product'=>'product', + // 'p.ref'=>'product' + //); + //$this->export_sql_start[$r] = 'SELECT DISTINCT '; + //$this->export_sql_end[$r] = ' FROM (' . MAIN_DB_PREFIX . 'facture as f, ' + // . MAIN_DB_PREFIX . 'facturedet as fd, ' . MAIN_DB_PREFIX . 'societe as s)'; + //$this->export_sql_end[$r] .= ' LEFT JOIN ' . MAIN_DB_PREFIX + // . 'product as p on (fd.fk_product = p.rowid)'; + //$this->export_sql_end[$r] .= ' WHERE f.fk_soc = s.rowid ' + // . 'AND f.rowid = fd.fk_facture'; + //$r++; + } + + /** + * Function called when module is enabled. + * The init function add constants, boxes, permissions and menus + * (defined in constructor) into Dolibarr database. + * It also creates data directories + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int 1 if OK, 0 if KO + */ + public function init($options = '') + { + $sql = array(); + + $result = $this->loadTables(); + + return $this->_init($sql, $options); + } + + /** + * Function called when module is disabled. + * Remove from database constants, boxes and permissions from Dolibarr database. + * Data directories are not deleted + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int 1 if OK, 0 if KO + */ + public function remove($options = '') + { + $sql = array(); + + return $this->_remove($sql, $options); + } + + /** + * Create tables, keys and data required by module + * Files llx_table1.sql, llx_table1.key.sql llx_data.sql with create table, create keys + * and create data commands must be stored in directory /resource/sql/ + * This function is called by this->init + * + * @return int <=0 if KO, >0 if OK + */ + private function loadTables() + { + return $this->_load_tables('/resource/sql/'); + } +} diff --git a/core/tpl/resource_add.tpl.php b/core/tpl/resource_add.tpl.php new file mode 100644 index 00000000000..fe4d0cdf94d --- /dev/null +++ b/core/tpl/resource_add.tpl.php @@ -0,0 +1,6 @@ +'.$langs->trans('NotAvailableYet').''; +print '
'; +// FIN DU TPL diff --git a/core/tpl/resource_view.tpl.php b/core/tpl/resource_view.tpl.php new file mode 100644 index 00000000000..dccc59a069c --- /dev/null +++ b/core/tpl/resource_view.tpl.php @@ -0,0 +1,111 @@ + 0) +{ + $var=false; + + // TODO: DEBUT DU TPL + if($mode == 'edit' ) + { + + print '
'; + print '
'; + print '
'.$langs->trans('Type').'
'; + print '
'.$langs->trans('Resource').'
'; + print '
'.$langs->trans('Busy').'
'; + print '
'.$langs->trans('Mandatory').'
'; + print '
'.$langs->trans('Edit').'
'; + print '
'; + //print '
'; + + } + else + { + + print '
'; + print '
'; + print '
'.$langs->trans('Type').'
'; + print '
'.$langs->trans('Resource').'
'; + print '
'.$langs->trans('Busy').'
'; + print '
'.$langs->trans('Mandatory').'
'; + print '
'.$langs->trans('Edit').'
'; + print '
'; + //print '
'; + + } + + + foreach ($linked_resources as $linked_resource) + { + $var=!$var; + $object_resource = $object->fetchObjectByElement($linked_resource['resource_id'],$linked_resource['resource_type']); + + if($mode == 'edit' && $linked_resource['rowid'] == GETPOST('lineid')) + { + + /*print '
action="" method="POST">';*/ + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + print '
'; + print '
'.$object_resource->getNomUrl(1).'
'; + print '
'.$form->selectyesno('busy',$linked_resource['busy']?1:0,1).'
'; + print '
'.$form->selectyesno('mandatory',$linked_resource['mandatory']?1:0,1).'
'; + print '
'; + print '
'; + + } + else + { + $style=''; + if($linked_resource['rowid'] == GETPOST('lineid')) + $style='style="background: orange;"'; + + print '
'; + + print '
'; + print $langs->trans(ucfirst($object_resource->element)); + print '
'; + + print '
'; + print $object_resource->getNomUrl(1); + print '
'; + + print '
'; + print $linked_resource['busy']?1:0; + print '
'; + + print '
'; + print $linked_resource['mandatory']?1:0; + print '
'; + + print '
'; + print ''.$langs->trans('Delete').''; + print ''.$langs->trans('Edit').''; + print '
'; + + print '
'; + } + + + } + print ''; + + + + +} +else { + print '
'.$langs->trans('NoResourceLinked').'
'; + +} +// FIN DU TPL diff --git a/element_resource.php b/element_resource.php new file mode 100644 index 00000000000..0a53522e6b2 --- /dev/null +++ b/element_resource.php @@ -0,0 +1,236 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file resource/element_resource.php + * \ingroup resource + * \brief Page to show and manage linked resources to an element + */ + + +$res=0; +$res=@include("../main.inc.php"); // For root directory +if (! $res) $res=@include("../../main.inc.php"); // For "custom" directory +if (! $res) die("Include of main fails"); + +require 'class/resource.class.php'; + +// Load traductions files requiredby by page +$langs->load("resource@resource"); +$langs->load("other"); + +// Get parameters +$id = GETPOST('id','int'); +$action = GETPOST('action','alpha'); +$mode = GETPOST('mode','alpha'); +$lineid = GETPOST('lineid','int'); +$element = GETPOST('element','alpha'); +$element_id = GETPOST('element_id','int'); +$resource_id = GETPOST('resource_id','int'); +$resource_type = GETPOST('resource_type','alpha'); + +/* +$sortorder = GETPOST('sortorder','alpha'); +$sortfield = GETPOST('sortfield','alpha'); +$page = GETPOST('page','int'); +*/ + +if( ! $user->rights->place->read) + accessforbidden(); + +$object=new Resource($db); + +$hookmanager->initHooks(array('element_resource')); + +$parameters=array('resource_id'=>$resource_id); +$reshook=$hookmanager->executeHooks('doActions',$parameters,$object,$action); // Note that $action and $object may have been modified by some hooks + + + +/*************************************************** + * VIEW +* +* Put here all code to build page +****************************************************/ + +$pagetitle=$langs->trans('ResourceElementPage'); +llxHeader('',$pagetitle,''); + + +$form=new Form($db); + + +// Load available resource, declared by modules +$ret = $object->fetch_all_available(); +if($ret == -1) { + dol_print_error($db,$object->error); + exit; +} +if(!$ret) { + print '
'.$langs->trans('NoResourceInDatabase').'
'; +} +else +{ + // Confirmation suppression resource line + if ($action == 'delete_resource') + { + print $form->formconfirm("element_resource.php?element=".$element."&element_id=".$element_id."&lineid=".$lineid,$langs->trans("DeleteResource"),$langs->trans("ConfirmDeleteResourceElement"),"confirm_delete_resource",'','',1); + } + + + /* + * Specific to agenda module + */ + if($element_id && $element == 'action') + { + require_once DOL_DOCUMENT_ROOT.'/core/lib/agenda.lib.php'; + + $act = $object->fetchObjectByElement($element_id,$element); + if(is_object($act)) { + + $head=actions_prepare_head($act); + + dol_fiche_head($head, 'resources', $langs->trans("Action"),0,'action'); + + // Affichage fiche action en mode visu + print ''; + + $linkback = ''.$langs->trans("BackToList").''; + + // Ref + print ''; + + // Type + if (! empty($conf->global->AGENDA_USE_EVENT_TYPE)) + { + print ''; + } + + // Title + print ''; + print '
'.$langs->trans("Ref").''; + print $form->showrefnav($act, 'id', $linkback, ($user->societe_id?0:1), 'id', 'ref', ''); + print '
'.$langs->trans("Type").''.$act->type.'
'.$langs->trans("Title").''.$act->label.'
'; + + print ''; + } + } + /* + * Specific to thirdparty module + */ + if($element_id && $element == 'societe') + { + $socstatic = $object->fetchObjectByElement($element_id,$element); + if(is_object($socstatic)) { + require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php'; + $head = societe_prepare_head($socstatic); + + dol_fiche_head($head, 'resources', $langs->trans("ThirdParty"),0,'company'); + + // Affichage fiche action en mode visu + print ''; + + //$linkback = ''.$langs->trans("BackToList").''; + + // Name + print ''; + print ''; + print ''; + print '
'.$langs->trans('ThirdPartyName').''; + print $form->showrefnav($socstatic, 'socid', '', ($user->societe_id?0:1), 'rowid', 'nom'); + print '
'; + + print ''; + } + } + + + + print_fiche_titre($langs->trans('ResourcesLinkedToElement'),'','resource_32@resource'); + + + foreach ($object->available_resources as $modresources => $resources) + { + $langs->load($modresources); + //print '

'.$modresources.'

'; + //var_dump($resources); + + $resources=(array) $resources; // To be sure $resources is an array + foreach($resources as $resource_obj) + { + $element_prop = $object->getElementProperties($resource_obj); + //var_dump($element_prop); + + print_titre($langs->trans(ucfirst($element_prop['element']).'Singular')); + + //print '/'.$modresources.'/class/'.$resource_obj.'.class.php
'; + + $linked_resources = $object->getElementResources($element,$element_id,$resource_obj); + + if ( $mode == 'add' && $resource_obj == $resource_type) + { + //print '/'.$element_prop['module'].'/core/tpl/resource_'.$element_prop['element'].'_'.$mode.'.tpl.php'; + + $path = ''; + if(strpos($element_prop['module'],'@')) + $path .= '/'.$element_prop['module']; + + // If we have a specific template we use it + if(file_exists(dol_buildpath($path.'/core/tpl/resource_'.$element_prop['element'].'_'.$mode.'.tpl.php'))) + { + $res=include dol_buildpath($path.'/core/tpl/resource_'.$element_prop['element'].'_'.$mode.'.tpl.php'); + + } + else + { + $res=@include dol_buildpath('/resource/core/tpl/resource_add.tpl.php'); + + } + } + else + { + //print '/'.$element_prop['module'].'/core/tpl/resource_'.$element_prop['element'].'_view.tpl.php'; + + // If we have a specific template we use it + if(file_exists(dol_buildpath('/'.$element_prop['module'].'/core/tpl/resource_'.$element_prop['element'].'_view.tpl.php'))) + { + $res=@include dol_buildpath('/'.$element_prop['module'].'/core/tpl/resource_'.$element_prop['element'].'_view.tpl.php'); + + } + else + { + $res=include dol_buildpath('/resource/core/tpl/resource_view.tpl.php'); + + } + } + + if($resource_obj!=$resource_type ) + { + print '
'; + print '
'; + print 'Add resource'; + print '
'; + print '
'; + } + } + } +} + +llxFooter(); + +$db->close(); diff --git a/index.php b/index.php new file mode 100644 index 00000000000..134f143a5f1 --- /dev/null +++ b/index.php @@ -0,0 +1,167 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * \file place/index.php + * \ingroup place + * \brief Page to manage place object + */ + + +// Change this following line to use the correct relative path (../, ../../, etc) +$res=0; +$res=@include("../main.inc.php"); // For root directory +if (! $res) $res=@include("../../main.inc.php"); // For "custom" directory +if (! $res) die("Include of main fails"); + +require 'class/resource.class.php'; + + + +// Load traductions files requiredby by page +$langs->load("resource@resource"); +$langs->load("companies"); +$langs->load("other"); + +// Get parameters +$id = GETPOST('id','int'); +$action = GETPOST('action','alpha'); + +$lineid = GETPOST('lineid','int'); +$element = GETPOST('element','alpha'); +$element_id = GETPOST('element_id','int'); +$resource_id = GETPOST('resource_id','int'); + +$sortorder = GETPOST('sortorder','alpha'); +$sortfield = GETPOST('sortfield','alpha'); +$page = GETPOST('page','int'); + +$object = new Resource($db); + +$hookmanager->initHooks(array('element_resource')); + +$parameters=array(); +$reshook=$hookmanager->executeHooks('doActions',$parameters,$object,$action); // Note that $action and $object may have been modified by some hooks + + + +if (empty($sortorder)) $sortorder="DESC"; +if (empty($sortfield)) $sortfield="t.rowid"; +if (empty($arch)) $arch = 0; + +if ($page == -1) { + $page = 0 ; +} + +$limit = $conf->liste_limit; +$offset = $limit * $page ; +$pageprev = $page - 1; +$pagenext = $page + 1; + +if( ! $user->rights->place->read) + accessforbidden(); + +/*************************************************** + * VIEW +* +* Put here all code to build page +****************************************************/ + +$pagetitle=$langs->trans('ResourcePageIndex'); +llxHeader('',$pagetitle,''); + + + +$form=new Form($db); + +print_fiche_titre($pagetitle,'','resource_32.png@resource'); + + // Confirmation suppression resource line + if ($action == 'delete_resource') + { + print $form->formconfirm($_SERVER['PHP_SELF']."?element=".$element."&element_id=".$element_id."&lineid=".$lineid,$langs->trans("DeleteResource"),$langs->trans("ConfirmDeleteResourceElement"),"confirm_delete_resource",'','',1); + } + +// Load object list +$ret = $object->fetch_all($sortorder, $sortfield, $limit, $offset); +if($ret == -1) { + dol_print_error($db,$object->error); + exit; +} +if(!$ret) { + print '
'.$langs->trans('NoResourceInDatabase').'
'; +} +else +{ + + $var=false; + + print ''."\n"; + print ''; + print_liste_field_titre($langs->trans('Resource'),$_SERVER['PHP_SELF'],'t.resource_id','',$param,'',$sortfield,$sortorder); + print_liste_field_titre($langs->trans('Element'),$_SERVER['PHP_SELF'],'t.element_id','',$param,'',$sortfield,$sortorder); + print_liste_field_titre($langs->trans('Edit')); + print ''; + + foreach ($object->lines as $resource) + { + $var=!$var; + + $style=''; + if($resource->id == GETPOST('lineid')) + $style='style="background: orange;"'; + + print ''; + + print ''; + + print ''; + + print ''; + } + + print '
'; + //print $resource->getNomUrl(1); + if(is_object($resource->objresource)) + print $resource->objresource->getNomUrl(1); + print ''; + if(is_object($resource->objelement)) + print $resource->objelement->getNomUrl(1); + print ''; + print ''.$langs->trans('Delete').''; + print '
'; + +} + + + +// Action Bar +print '
'; +print ''; +print '
'; + + + + +llxFooter(); + +$db->close(); + + diff --git a/js/fullcalendar/fullcalendar.css b/js/fullcalendar/fullcalendar.css new file mode 100644 index 00000000000..92fe47f2029 --- /dev/null +++ b/js/fullcalendar/fullcalendar.css @@ -0,0 +1,589 @@ +/*! + * FullCalendar v1.6.4 Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + + +.fc { + direction: ltr; + text-align: left; + } + +.fc table { + border-collapse: collapse; + border-spacing: 0; + } + +html .fc, +.fc table { + font-size: 1em; + } + +.fc td, +.fc th { + padding: 0; + vertical-align: top; + } + + + +/* Header +------------------------------------------------------------------------*/ + +.fc-header td { + white-space: nowrap; + } + +.fc-header-left { + width: 25%; + text-align: left; + } + +.fc-header-center { + text-align: center; + } + +.fc-header-right { + width: 25%; + text-align: right; + } + +.fc-header-title { + display: inline-block; + vertical-align: top; + } + +.fc-header-title h2 { + margin-top: 0; + white-space: nowrap; + } + +.fc .fc-header-space { + padding-left: 10px; + } + +.fc-header .fc-button { + margin-bottom: 1em; + vertical-align: top; + } + +/* buttons edges butting together */ + +.fc-header .fc-button { + margin-right: -1px; + } + +.fc-header .fc-corner-right, /* non-theme */ +.fc-header .ui-corner-right { /* theme */ + margin-right: 0; /* back to normal */ + } + +/* button layering (for border precedence) */ + +.fc-header .fc-state-hover, +.fc-header .ui-state-hover { + z-index: 2; + } + +.fc-header .fc-state-down { + z-index: 3; + } + +.fc-header .fc-state-active, +.fc-header .ui-state-active { + z-index: 4; + } + + + +/* Content +------------------------------------------------------------------------*/ + +.fc-content { + clear: both; + zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */ + } + +.fc-view { + width: 100%; + overflow: hidden; + } + + + +/* Cell Styles +------------------------------------------------------------------------*/ + +.fc-widget-header, /* , usually */ +.fc-widget-content { /* , usually */ + border: 1px solid #ddd; + } + +.fc-state-highlight { /* today cell */ /* TODO: add .fc-today to */ + background: #fcf8e3; + } + +.fc-cell-overlay { /* semi-transparent rectangle while dragging */ + background: #bce8f1; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ + } + + + +/* Buttons +------------------------------------------------------------------------*/ + +.fc-button { + position: relative; + display: inline-block; + padding: 0 .6em; + overflow: hidden; + height: 1.9em; + line-height: 1.9em; + white-space: nowrap; + cursor: pointer; + } + +.fc-state-default { /* non-theme */ + border: 1px solid; + } + +.fc-state-default.fc-corner-left { /* non-theme */ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + +.fc-state-default.fc-corner-right { /* non-theme */ + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } + +/* + Our default prev/next buttons use HTML entities like ‹ › « » + and we'll try to make them look good cross-browser. +*/ + +.fc-text-arrow { + margin: 0 .1em; + font-size: 2em; + font-family: "Courier New", Courier, monospace; + vertical-align: baseline; /* for IE7 */ + } + +.fc-button-prev .fc-text-arrow, +.fc-button-next .fc-text-arrow { /* for ‹ › */ + font-weight: bold; + } + +/* icon (for jquery ui) */ + +.fc-button .fc-icon-wrap { + position: relative; + float: left; + top: 50%; + } + +.fc-button .ui-icon { + position: relative; + float: left; + margin-top: -50%; + *margin-top: 0; + *top: -50%; + } + +/* + button states + borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) +*/ + +.fc-state-default { + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + color: #333; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + } + +.fc-state-hover, +.fc-state-down, +.fc-state-active, +.fc-state-disabled { + color: #333333; + background-color: #e6e6e6; + } + +.fc-state-hover { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; + } + +.fc-state-down, +.fc-state-active { + background-color: #cccccc; + background-image: none; + outline: 0; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + } + +.fc-state-disabled { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + box-shadow: none; + } + + + +/* Global Event Styles +------------------------------------------------------------------------*/ + +.fc-event-container > * { + z-index: 8; + } + +.fc-event-container > .ui-draggable-dragging, +.fc-event-container > .ui-resizable-resizing { + z-index: 9; + } + +.fc-event { + border: 1px solid #3a87ad; /* default BORDER color */ + background-color: #3a87ad; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ + font-size: .85em; + cursor: default; + } + +a.fc-event { + text-decoration: none; + } + +a.fc-event, +.fc-event-draggable { + cursor: pointer; + } + +.fc-rtl .fc-event { + text-align: right; + } + +.fc-event-inner { + width: 100%; + height: 100%; + overflow: hidden; + } + +.fc-event-time, +.fc-event-title { + padding: 0 1px; + } + +.fc .ui-resizable-handle { + display: block; + position: absolute; + z-index: 99999; + overflow: hidden; /* hacky spaces (IE6/7) */ + font-size: 300%; /* */ + line-height: 50%; /* */ + } + + + +/* Horizontal Events +------------------------------------------------------------------------*/ + +.fc-event-hori { + border-width: 1px 0; + margin-bottom: 1px; + } + +.fc-ltr .fc-event-hori.fc-event-start, +.fc-rtl .fc-event-hori.fc-event-end { + border-left-width: 1px; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + +.fc-ltr .fc-event-hori.fc-event-end, +.fc-rtl .fc-event-hori.fc-event-start { + border-right-width: 1px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + +/* resizable */ + +.fc-event-hori .ui-resizable-e { + top: 0 !important; /* importants override pre jquery ui 1.7 styles */ + right: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: e-resize; + } + +.fc-event-hori .ui-resizable-w { + top: 0 !important; + left: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: w-resize; + } + +.fc-event-hori .ui-resizable-handle { + _padding-bottom: 14px; /* IE6 had 0 height */ + } + + + +/* Reusable Separate-border Table +------------------------------------------------------------*/ + +table.fc-border-separate { + border-collapse: separate; + } + +.fc-border-separate th, +.fc-border-separate td { + border-width: 1px 0 0 1px; + } + +.fc-border-separate th.fc-last, +.fc-border-separate td.fc-last { + border-right-width: 1px; + } + +.fc-border-separate tr.fc-last th, +.fc-border-separate tr.fc-last td { + border-bottom-width: 1px; + } + +.fc-border-separate tbody tr.fc-first td, +.fc-border-separate tbody tr.fc-first th { + border-top-width: 0; + } + + + +/* Month View, Basic Week View, Basic Day View +------------------------------------------------------------------------*/ + +.fc-grid th { + text-align: center; + } + +.fc .fc-week-number { + width: 22px; + text-align: center; + } + +.fc .fc-week-number div { + padding: 0 2px; + } + +.fc-grid .fc-day-number { + float: right; + padding: 0 2px; + } + +.fc-grid .fc-other-month .fc-day-number { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ + } + +.fc-grid .fc-day-content { + clear: both; + padding: 2px 2px 1px; /* distance between events and day edges */ + } + +/* event styles */ + +.fc-grid .fc-event-time { + font-weight: bold; + } + +/* right-to-left */ + +.fc-rtl .fc-grid .fc-day-number { + float: left; + } + +.fc-rtl .fc-grid .fc-event-time { + float: right; + } + + + +/* Agenda Week View, Agenda Day View +------------------------------------------------------------------------*/ + +.fc-agenda table { + border-collapse: separate; + } + +.fc-agenda-days th { + text-align: center; + } + +.fc-agenda .fc-agenda-axis { + width: 50px; + padding: 0 4px; + vertical-align: middle; + text-align: right; + white-space: nowrap; + font-weight: normal; + } + +.fc-agenda .fc-week-number { + font-weight: bold; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 1px; + } + +/* make axis border take precedence */ + +.fc-agenda-days .fc-agenda-axis { + border-right-width: 1px; + } + +.fc-agenda-days .fc-col0 { + border-left-width: 0; + } + +/* all-day area */ + +.fc-agenda-allday th { + border-width: 0 1px; + } + +.fc-agenda-allday .fc-day-content { + min-height: 34px; /* TODO: doesnt work well in quirksmode */ + _height: 34px; + } + +/* divider (between all-day and slots) */ + +.fc-agenda-divider-inner { + height: 2px; + overflow: hidden; + } + +.fc-widget-header .fc-agenda-divider-inner { + background: #eee; + } + +/* slot rows */ + +.fc-agenda-slots th { + border-width: 1px 1px 0; + } + +.fc-agenda-slots td { + border-width: 1px 0 0; + background: none; + } + +.fc-agenda-slots td div { + height: 20px; + } + +.fc-agenda-slots tr.fc-slot0 th, +.fc-agenda-slots tr.fc-slot0 td { + border-top-width: 0; + } + +.fc-agenda-slots tr.fc-minor th, +.fc-agenda-slots tr.fc-minor td { + border-top-style: dotted; + } + +.fc-agenda-slots tr.fc-minor th.ui-widget-header { + *border-top-style: solid; /* doesn't work with background in IE6/7 */ + } + + + +/* Vertical Events +------------------------------------------------------------------------*/ + +.fc-event-vert { + border-width: 0 1px; + } + +.fc-event-vert.fc-event-start { + border-top-width: 1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + } + +.fc-event-vert.fc-event-end { + border-bottom-width: 1px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + } + +.fc-event-vert .fc-event-time { + white-space: nowrap; + font-size: 10px; + } + +.fc-event-vert .fc-event-inner { + position: relative; + z-index: 2; + } + +.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + opacity: .25; + filter: alpha(opacity=25); + } + +.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */ +.fc-select-helper .fc-event-bg { + display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */ + } + +/* resizable */ + +.fc-event-vert .ui-resizable-s { + bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ + width: 100% !important; + height: 8px !important; + overflow: hidden !important; + line-height: 8px !important; + font-size: 11px !important; + font-family: monospace; + text-align: center; + cursor: s-resize; + } + +.fc-agenda .ui-resizable-resizing { /* TODO: better selector */ + _overflow: hidden; + } + + diff --git a/js/fullcalendar/fullcalendar.js b/js/fullcalendar/fullcalendar.js new file mode 100644 index 00000000000..41c50856cf4 --- /dev/null +++ b/js/fullcalendar/fullcalendar.js @@ -0,0 +1,6110 @@ +/*! + * FullCalendar v1.6.4 + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +/* + * Use fullcalendar.css for basic styling. + * For event drag & drop, requires jQuery UI draggable. + * For event resizing, requires jQuery UI resizable. + */ + +(function($, undefined) { + + +;; + +var defaults = { + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + weekNumberCalculation: 'iso', + weekNumberTitle: 'W', + + // editing + //editable: false, + //disableDragging: false, + //disableResizing: false, + + allDayDefault: true, + ignoreTimezone: true, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + + // time formats + titleFormat: { + month: 'MMMM yyyy', + week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", + day: 'dddd, MMM d, yyyy' + }, + columnFormat: { + month: 'ddd', + week: 'ddd M/d', + day: 'dddd M/d' + }, + timeFormat: { // for event elements + '': 'h(:mm)t' // default + }, + + // locale + isRTL: false, + firstDay: 0, + monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], + dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], + buttonText: { + prev: "", + next: "", + prevYear: "«", + nextYear: "»", + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + // jquery-ui theming + theme: false, + buttonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e' + }, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + handleWindowResize: true + +}; + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonText: { + prev: "", + next: "", + prevYear: "»", + nextYear: "«" + }, + buttonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w' + } +}; + + + +;; + +var fc = $.fullCalendar = { version: "1.6.4" }; +var fcViews = fc.views = {}; + + +$.fn.fullCalendar = function(options) { + + + // method calling + if (typeof options == 'string') { + var args = Array.prototype.slice.call(arguments, 1); + var res; + this.each(function() { + var calendar = $.data(this, 'fullCalendar'); + if (calendar && $.isFunction(calendar[options])) { + var r = calendar[options].apply(calendar, args); + if (res === undefined) { + res = r; + } + if (options == 'destroy') { + $.removeData(this, 'fullCalendar'); + } + } + }); + if (res !== undefined) { + return res; + } + return this; + } + + options = options || {}; + + // would like to have this logic in EventManager, but needs to happen before options are recursively extended + var eventSources = options.eventSources || []; + delete options.eventSources; + if (options.events) { + eventSources.push(options.events); + delete options.events; + } + + + options = $.extend(true, {}, + defaults, + (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {}, + options + ); + + + this.each(function(i, _element) { + var element = $(_element); + var calendar = new Calendar(element, options, eventSources); + element.data('fullCalendar', calendar); // TODO: look into memory leak implications + calendar.render(); + }); + + + return this; + +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + $.extend(true, defaults, d); +} + + + +;; + + +function Calendar(element, options, eventSources) { + var t = this; + + + // exports + t.options = options; + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.rerenderEvents = rerenderEvents; + t.changeView = changeView; + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.formatDate = function(format, date) { return formatDate(format, date, options) }; + t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; + t.getDate = getDate; + t.getView = getView; + t.option = option; + t.trigger = trigger; + + + // imports + EventManager.call(t, options, eventSources); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + + + // locals + var _element = element[0]; + var header; + var headerElement; + var content; + var tm; // for making theme classes + var currentView; + var elementOuterWidth; + var suggestedViewHeight; + var resizeUID = 0; + var ignoreWindowResize = 0; + var date = new Date(); + var events = []; + var _dragElement; + + + + /* Main Rendering + -----------------------------------------------------------------------------*/ + + + setYMD(date, options.year, options.month, options.date); + + + function render(inc) { + if (!content) { + initialRender(); + } + else if (elementVisible()) { + // mainly for the public API + calcSize(); + _renderView(inc); + } + } + + + function initialRender() { + tm = options.theme ? 'ui' : 'fc'; + element.addClass('fc'); + if (options.isRTL) { + element.addClass('fc-rtl'); + } + else { + element.addClass('fc-ltr'); + } + if (options.theme) { + element.addClass('ui-widget'); + } + + content = $("
") + .prependTo(element); + + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } + + changeView(options.defaultView); + + if (options.handleWindowResize) { + $(window).resize(windowResize); + } + + // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize + if (!bodyVisible()) { + lateRender(); + } + } + + + // called when we know the calendar couldn't be rendered when it was initialized, + // but we think it's ready now + function lateRender() { + setTimeout(function() { // IE7 needs this so dimensions are calculated correctly + if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once + renderView(); + } + },0); + } + + + function destroy() { + + if (currentView) { + trigger('viewDestroy', currentView, currentView, currentView.element); + currentView.triggerEventDestroy(); + } + + $(window).unbind('resize', windowResize); + + header.destroy(); + content.remove(); + element.removeClass('fc fc-rtl ui-widget'); + } + + + function elementVisible() { + return element.is(':visible'); + } + + + function bodyVisible() { + return $('body').is(':visible'); + } + + + + /* View Rendering + -----------------------------------------------------------------------------*/ + + + function changeView(newViewName) { + if (!currentView || newViewName != currentView.name) { + _changeView(newViewName); + } + } + + + function _changeView(newViewName) { + ignoreWindowResize++; + + if (currentView) { + trigger('viewDestroy', currentView, currentView, currentView.element); + unselect(); + currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event + freezeContentHeight(); + currentView.element.remove(); + header.deactivateButton(currentView.name); + } + + header.activateButton(newViewName); + + currentView = new fcViews[newViewName]( + $("
") + .appendTo(content), + t // the calendar object + ); + + renderView(); + unfreezeContentHeight(); + + ignoreWindowResize--; + } + + + function renderView(inc) { + if ( + !currentView.start || // never rendered before + inc || date < currentView.start || date >= currentView.end // or new date range + ) { + if (elementVisible()) { + _renderView(inc); + } + } + } + + + function _renderView(inc) { // assumes elementVisible + ignoreWindowResize++; + + if (currentView.start) { // already been rendered? + trigger('viewDestroy', currentView, currentView, currentView.element); + unselect(); + clearEvents(); + } + + freezeContentHeight(); + currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else + setSize(); + unfreezeContentHeight(); + (currentView.afterRender || noop)(); + + updateTitle(); + updateTodayButton(); + + trigger('viewRender', currentView, currentView, currentView.element); + currentView.trigger('viewDisplay', _element); // deprecated + + ignoreWindowResize--; + + getAndRenderEvents(); + } + + + + /* Resizing + -----------------------------------------------------------------------------*/ + + + function updateSize() { + if (elementVisible()) { + unselect(); + clearEvents(); + calcSize(); + setSize(); + renderEvents(); + } + } + + + function calcSize() { // assumes elementVisible + if (options.contentHeight) { + suggestedViewHeight = options.contentHeight; + } + else if (options.height) { + suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } + + + function setSize() { // assumes elementVisible + + if (suggestedViewHeight === undefined) { + calcSize(); // for first time + // NOTE: we don't want to recalculate on every renderView because + // it could result in oscillating heights due to scrollbars. + } + + ignoreWindowResize++; + currentView.setHeight(suggestedViewHeight); + currentView.setWidth(content.width()); + ignoreWindowResize--; + + elementOuterWidth = element.outerWidth(); + } + + + function windowResize() { + if (!ignoreWindowResize) { + if (currentView.start) { // view has already been rendered + var uid = ++resizeUID; + setTimeout(function() { // add a delay + if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { + if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { + ignoreWindowResize++; // in case the windowResize callback changes the height + updateSize(); + currentView.trigger('windowResize', _element); + ignoreWindowResize--; + } + } + }, 200); + }else{ + // calendar must have been initialized in a 0x0 iframe that has just been resized + lateRender(); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view + + + function refetchEvents() { // can be called as an API method + clearEvents(); + fetchAndRenderEvents(); + } + + + function rerenderEvents(modifiedEventID) { // can be called as an API method + clearEvents(); + renderEvents(modifiedEventID); + } + + + function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack + if (elementVisible()) { + currentView.setEventData(events); // for View.js, TODO: unify with renderEvents + currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements + currentView.trigger('eventAfterAllRender'); + } + } + + + function clearEvents() { + currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event + currentView.clearEvents(); // actually remove the DOM elements + currentView.clearEventData(); // for View.js, TODO: unify with clearEvents + } + + + function getAndRenderEvents() { + if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.visStart, currentView.visEnd); + // ... will call reportEvents + // ... which will call renderEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange(eventID) { + rerenderEvents(eventID); + } + + + + /* Header Updating + -----------------------------------------------------------------------------*/ + + + function updateTitle() { + header.updateTitle(currentView.title); + } + + + function updateTodayButton() { + var today = new Date(); + if (today >= currentView.start && today < currentView.end) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end, allDay) { + currentView.select(start, end, allDay===undefined ? true : allDay); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + addYears(date, -1); + renderView(); + } + + + function nextYear() { + addYears(date, 1); + renderView(); + } + + + function today() { + date = new Date(); + renderView(); + } + + + function gotoDate(year, month, dateOfMonth) { + if (year instanceof Date) { + date = cloneDate(year); // provided 1 argument, a Date + }else{ + setYMD(date, year, month, dateOfMonth); + } + renderView(); + } + + + function incrementDate(years, months, days) { + if (years !== undefined) { + addYears(date, years); + } + if (months !== undefined) { + addMonths(date, months); + } + if (days !== undefined) { + addDays(date, days); + } + renderView(); + } + + + function getDate() { + return cloneDate(date); + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getView() { + return currentView; + } + + + function option(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + updateSize(); + } + } + + + function trigger(name, thisObj) { + if (options[name]) { + return options[name].apply( + thisObj || _element, + Array.prototype.slice.call(arguments, 2) + ); + } + } + + + + /* External Dragging + ------------------------------------------------------------------------*/ + + if (options.droppable) { + $(document) + .bind('dragstart', function(ev, ui) { + var _e = ev.target; + var e = $(_e); + if (!e.parents('.fc').length) { // not already inside a calendar + var accept = options.dropAccept; + if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { + _dragElement = _e; + currentView.dragStart(_dragElement, ev, ui); + } + } + }) + .bind('dragstop', function(ev, ui) { + if (_dragElement) { + currentView.dragStop(_dragElement, ev, ui); + _dragElement = null; + } + }); + } + + +} + +;; + +function Header(calendar, options) { + var t = this; + + + // exports + t.render = render; + t.destroy = destroy; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + + + // locals + var element = $([]); + var tm; + + + + function render() { + tm = options.theme ? 'ui' : 'fc'; + var sections = options.header; + if (sections) { + element = $("") + .append( + $("") + .append(renderSection('left')) + .append(renderSection('center')) + .append(renderSection('right')) + ); + return element; + } + } + + + function destroy() { + element.remove(); + } + + + function renderSection(position) { + var e = $(""; + + if (showWeekNumbers) { + html += + ""; + } + + for (col=0; col" + + htmlEscape(formatDate(date, colFormat)) + + ""; + } + + html += ""; + + return html; + } + + + function buildBodyHTML() { + var contentClass = tm + "-widget-content"; + var html = ''; + var row; + var col; + var date; + + html += ""; + + for (row=0; row" + + "
" + + htmlEscape(formatDate(date, weekNumberFormat)) + + "
" + + ""; + } + + for (col=0; col" + + "
"; + + if (showNumbers) { + html += "
" + date.getDate() + "
"; + } + + html += + "
" + + "
 
" + + "
" + + "
" + + ""; + + return html; + } + + + + /* Dimensions + -----------------------------------------------------------*/ + + + function setHeight(height) { + viewHeight = height; + + var bodyHeight = viewHeight - head.height(); + var rowHeight; + var rowHeightLast; + var cell; + + if (opt('weekMode') == 'variable') { + rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6)); + }else{ + rowHeight = Math.floor(bodyHeight / rowCnt); + rowHeightLast = bodyHeight - rowHeight * (rowCnt-1); + } + + bodyFirstCells.each(function(i, _cell) { + if (i < rowCnt) { + cell = $(_cell); + cell.find('> div').css( + 'min-height', + (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell) + ); + } + }); + + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + weekNumberWidth = 0; + if (showWeekNumbers) { + weekNumberWidth = head.find('th.fc-week-number').outerWidth(); + } + + colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt); + setOuterWidth(headCells.slice(0, -1), colWidth); + } + + + + /* Day clicking and binding + -----------------------------------------------------------*/ + + + function dayBind(days) { + days.click(dayClick) + .mousedown(daySelectionMousedown); + } + + + function dayClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + var date = parseISO8601($(this).data('date')); + trigger('dayClick', this, date, true, ev); + } + } + + + + /* Semi-transparent Overlay Helpers + ------------------------------------------------------*/ + // TODO: should be consolidated with AgendaView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i") + .appendTo(element); + + if (opt('allDaySlot')) { + + daySegmentContainer = + $("
") + .appendTo(slotLayer); + + s = + "
"); + var buttonStr = options.header[position]; + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + if (i > 0) { + e.append(""); + } + var prevButton; + $.each(this.split(','), function(j, buttonName) { + if (buttonName == 'title') { + e.append("

 

"); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + prevButton = null; + }else{ + var buttonClick; + if (calendar[buttonName]) { + buttonClick = calendar[buttonName]; // calendar method + } + else if (fcViews[buttonName]) { + buttonClick = function() { + button.removeClass(tm + '-state-hover'); // forget why + calendar.changeView(buttonName); + }; + } + if (buttonClick) { + var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here? + var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here? + var button = $( + "" + + (icon ? + "" + + "" + + "" : + text + ) + + "" + ) + .click(function() { + if (!button.hasClass(tm + '-state-disabled')) { + buttonClick(); + } + }) + .mousedown(function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); + } + ) + .appendTo(e); + disableTextSelection(button); + if (!prevButton) { + button.addClass(tm + '-corner-left'); + } + prevButton = button; + } + } + }); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + }); + } + return e; + } + + + function updateTitle(html) { + element.find('h2') + .html(html); + } + + + function activateButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .removeClass(tm + '-state-disabled'); + } + + +} + +;; + +fc.sourceNormalizers = []; +fc.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager(options, _sources) { + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.normalizeEvent = normalizeEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var cache = []; + + + for (var i=0; i<_sources.length; i++) { + _addEventSource(_sources[i]); + } + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || start < rangeStart || end > rangeEnd; + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i)), return null instead + return null; +} + + +function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false + // derived from http://delete.me.uk/2005/03/iso8601.html + // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html + var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/); + if (!m) { + return null; + } + var date = new Date(m[1], 0, 1); + if (ignoreTimezone || !m[13]) { + var check = new Date(m[1], 0, 1, 9, 0); + if (m[3]) { + date.setMonth(m[3] - 1); + check.setMonth(m[3] - 1); + } + if (m[5]) { + date.setDate(m[5]); + check.setDate(m[5]); + } + fixDate(date, check); + if (m[7]) { + date.setHours(m[7]); + } + if (m[8]) { + date.setMinutes(m[8]); + } + if (m[10]) { + date.setSeconds(m[10]); + } + if (m[12]) { + date.setMilliseconds(Number("0." + m[12]) * 1000); + } + fixDate(date, check); + }else{ + date.setUTCFullYear( + m[1], + m[3] ? m[3] - 1 : 0, + m[5] || 1 + ); + date.setUTCHours( + m[7] || 0, + m[8] || 0, + m[10] || 0, + m[12] ? Number("0." + m[12]) * 1000 : 0 + ); + if (m[14]) { + var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0); + offset *= m[15] == '-' ? 1 : -1; + date = new Date(+date + (offset * 60 * 1000)); + } + } + return date; +} + + +function parseTime(s) { // returns minutes since start of day + if (typeof s == 'number') { // an hour + return s * 60; + } + if (typeof s == 'object') { // a Date object + return s.getHours() * 60 + s.getMinutes(); + } + var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); + if (m) { + var h = parseInt(m[1], 10); + if (m[3]) { + h %= 12; + if (m[3].toLowerCase().charAt(0) == 'p') { + h += 12; + } + } + return h * 60 + (m[2] ? parseInt(m[2], 10) : 0); + } +} + + + +/* Date Formatting +-----------------------------------------------------------------------------*/ +// TODO: use same function formatDate(date, [date2], format, [options]) + + +function formatDate(date, format, options) { + return formatDates(date, null, format, options); +} + + +function formatDates(date1, date2, format, options) { + options = options || defaults; + var date = date1, + otherDate = date2, + i, len = format.length, c, + i2, formatter, + res = ''; + for (i=0; ii; i2--) { + if (formatter = dateFormatters[format.substring(i, i2)]) { + if (date) { + res += formatter(date, options); + } + i = i2 - 1; + break; + } + } + if (i2 == i) { + if (date) { + res += c; + } + } + } + } + return res; +}; + + +var dateFormatters = { + s : function(d) { return d.getSeconds() }, + ss : function(d) { return zeroPad(d.getSeconds()) }, + m : function(d) { return d.getMinutes() }, + mm : function(d) { return zeroPad(d.getMinutes()) }, + h : function(d) { return d.getHours() % 12 || 12 }, + hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, + H : function(d) { return d.getHours() }, + HH : function(d) { return zeroPad(d.getHours()) }, + d : function(d) { return d.getDate() }, + dd : function(d) { return zeroPad(d.getDate()) }, + ddd : function(d,o) { return o.dayNamesShort[d.getDay()] }, + dddd: function(d,o) { return o.dayNames[d.getDay()] }, + M : function(d) { return d.getMonth() + 1 }, + MM : function(d) { return zeroPad(d.getMonth() + 1) }, + MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] }, + MMMM: function(d,o) { return o.monthNames[d.getMonth()] }, + yy : function(d) { return (d.getFullYear()+'').substring(2) }, + yyyy: function(d) { return d.getFullYear() }, + t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, + tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, + T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, + TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, + u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, + S : function(d) { + var date = d.getDate(); + if (date > 10 && date < 20) { + return 'th'; + } + return ['st', 'nd', 'rd'][date%10-1] || 'th'; + }, + w : function(d, o) { // local + return o.weekNumberCalculation(d); + }, + W : function(d) { // ISO + return iso8601Week(d); + } +}; +fc.dateFormatters = dateFormatters; + + +/* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js) + * + * Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * `date` - the date to get the week for + * `number` - the number of the week within the year that contains this date + */ +function iso8601Week(date) { + var time; + var checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; +} + + +;; + +fc.applyAll = applyAll; + + +/* Event Date Math +-----------------------------------------------------------------------------*/ + + +function exclEndDay(event) { + if (event.end) { + return _exclEndDay(event.end, event.allDay); + }else{ + return addDays(cloneDate(event.start), 1); + } +} + + +function _exclEndDay(end, allDay) { + end = cloneDate(end); + return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); + // why don't we check for seconds/ms too? +} + + + +/* Event Element Binding +-----------------------------------------------------------------------------*/ + + +function lazySegBind(container, segs, bindHandlers) { + container.unbind('mouseover').mouseover(function(ev) { + var parent=ev.target, e, + i, seg; + while (parent != this) { + e = parent; + parent = parent.parentNode; + } + if ((i = e._fci) !== undefined) { + e._fci = undefined; + seg = segs[i]; + bindHandlers(seg.event, seg.element, seg); + $(ev.target).trigger(ev); + } + ev.stopPropagation(); + }); +} + + + +/* Element Dimensions +-----------------------------------------------------------------------------*/ + + +function setOuterWidth(element, width, includeMargins) { + for (var i=0, e; i=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['']; +} + + +function htmlEscape(s) { + return s.replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
'); +} + + +function disableTextSelection(element) { + element + .attr('unselectable', 'on') + .css('MozUserSelect', 'none') + .bind('selectstart.ui', function() { return false; }); +} + + +/* +function enableTextSelection(element) { + element + .attr('unselectable', 'off') + .css('MozUserSelect', '') + .unbind('selectstart.ui'); +} +*/ + + +function markFirstLast(e) { + e.children() + .removeClass('fc-first fc-last') + .filter(':first-child') + .addClass('fc-first') + .end() + .filter(':last-child') + .addClass('fc-last'); +} + + +function setDayID(cell, date) { + cell.each(function(i, _cell) { + _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]); + // TODO: make a way that doesn't rely on order of classes + }); +} + + +function getSkinCss(event, opt) { + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = opt('eventColor'); + var backgroundColor = + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + opt('eventBackgroundColor') || + optionColor; + var borderColor = + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + opt('eventBorderColor') || + optionColor; + var textColor = + event.textColor || + source.textColor || + opt('eventTextColor'); + var statements = []; + if (backgroundColor) { + statements.push('background-color:' + backgroundColor); + } + if (borderColor) { + statements.push('border-color:' + borderColor); + } + if (textColor) { + statements.push('color:' + textColor); + } + return statements.join(';'); +} + + +function applyAll(functions, thisObj, args) { + if ($.isFunction(functions)) { + functions = [ functions ]; + } + if (functions) { + var i; + var ret; + for (i=0; i") + .appendTo(element); + } + + + function buildTable() { + var html = buildTableHTML(); + + if (table) { + table.remove(); + } + table = $(html).appendTo(element); + + head = table.find('thead'); + headCells = head.find('.fc-day-header'); + body = table.find('tbody'); + bodyRows = body.find('tr'); + bodyCells = body.find('.fc-day'); + bodyFirstCells = bodyRows.find('td:first-child'); + + firstRowCellInners = bodyRows.eq(0).find('.fc-day > div'); + firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div'); + + markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's + markFirstLast(bodyRows); // marks first+last td's + bodyRows.eq(0).addClass('fc-first'); + bodyRows.filter(':last').addClass('fc-last'); + + bodyCells.each(function(i, _cell) { + var date = cellToDate( + Math.floor(i / colCnt), + i % colCnt + ); + trigger('dayRender', t, date, $(_cell)); + }); + + dayBind(bodyCells); + } + + + + /* HTML Building + -----------------------------------------------------------*/ + + + function buildTableHTML() { + var html = + "" + + buildHeadHTML() + + buildBodyHTML() + + "
"; + + return html; + } + + + function buildHeadHTML() { + var headerClass = tm + "-widget-header"; + var html = ''; + var col; + var date; + + html += "
" + + htmlEscape(weekNumberTitle) + + "
" + + "" + + "" + + "" + + "" + + "" + + "
" + opt('allDayText') + "" + + "
" + + "
 
"; + allDayTable = $(s).appendTo(slotLayer); + allDayRow = allDayTable.find('tr'); + + dayBind(allDayRow.find('td')); + + slotLayer.append( + "
" + + "
" + + "
" + ); + + }else{ + + daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() + + } + + slotScroller = + $("
") + .appendTo(slotLayer); + + slotContainer = + $("
") + .appendTo(slotScroller); + + slotSegmentContainer = + $("
") + .appendTo(slotContainer); + + s = + "" + + ""; + d = zeroDate(); + maxd = addMinutes(cloneDate(d), maxMinute); + addMinutes(d, minMinute); + slotCnt = 0; + for (i=0; d < maxd; i++) { + minutes = d.getMinutes(); + s += + "" + + "" + + "" + + ""; + addMinutes(d, opt('slotMinutes')); + slotCnt++; + } + s += + "" + + "
" + + ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + + "" + + "
 
" + + "
"; + slotTable = $(s).appendTo(slotContainer); + + slotBind(slotTable.find('td')); + } + + + + /* Build Day Table + -----------------------------------------------------------------------*/ + + + function buildDayTable() { + var html = buildDayTableHTML(); + + if (dayTable) { + dayTable.remove(); + } + dayTable = $(html).appendTo(element); + + dayHead = dayTable.find('thead'); + dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter + dayBody = dayTable.find('tbody'); + dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter + dayBodyCellInners = dayBodyCells.find('> div'); + dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div'); + + dayBodyFirstCell = dayBodyCells.eq(0); + dayBodyFirstCellStretcher = dayBodyCellInners.eq(0); + + markFirstLast(dayHead.add(dayHead.find('tr'))); + markFirstLast(dayBody.add(dayBody.find('tr'))); + + // TODO: now that we rebuild the cells every time, we should call dayRender + } + + + function buildDayTableHTML() { + var html = + "" + + buildDayTableHeadHTML() + + buildDayTableBodyHTML() + + "
"; + + return html; + } + + + function buildDayTableHeadHTML() { + var headerClass = tm + "-widget-header"; + var date; + var html = ''; + var weekText; + var col; + + html += + "" + + ""; + + if (showWeekNumbers) { + date = cellToDate(0, 0); + weekText = formatDate(date, weekNumberFormat); + if (rtl) { + weekText += weekNumberTitle; + } + else { + weekText = weekNumberTitle + weekText; + } + html += + "" + + htmlEscape(weekText) + + ""; + } + else { + html += " "; + } + + for (col=0; col" + + htmlEscape(formatDate(date, colFormat)) + + ""; + } + + html += + " " + + "" + + ""; + + return html; + } + + + function buildDayTableBodyHTML() { + var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called + var contentClass = tm + "-widget-content"; + var date; + var today = clearTime(new Date()); + var col; + var cellsHTML; + var cellHTML; + var classNames; + var html = ''; + + html += + "" + + "" + + " "; + + cellsHTML = ''; + + for (col=0; col" + + "
" + + "
" + + "
 
" + + "
" + + "
" + + ""; + + cellsHTML += cellHTML; + } + + html += cellsHTML; + html += + " " + + "" + + ""; + + return html; + } + + + // TODO: data-date on the cells + + + + /* Dimensions + -----------------------------------------------------------------------*/ + + + function setHeight(height) { + if (height === undefined) { + height = viewHeight; + } + viewHeight = height; + slotTopCache = {}; + + var headHeight = dayBody.position().top; + var allDayHeight = slotScroller.position().top; // including divider + var bodyHeight = Math.min( // total body height, including borders + height - headHeight, // when scrollbars + slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border + ); + + dayBodyFirstCellStretcher + .height(bodyHeight - vsides(dayBodyFirstCell)); + + slotLayer.css('top', headHeight); + + slotScroller.height(bodyHeight - allDayHeight - 1); + + // the stylesheet guarantees that the first row has no border. + // this allows .height() to work well cross-browser. + slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border + + snapRatio = opt('slotMinutes') / snapMinutes; + snapHeight = slotHeight / snapRatio; + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + var axisFirstCells = dayHead.find('th:first'); + if (allDayTable) { + axisFirstCells = axisFirstCells.add(allDayTable.find('th:first')); + } + axisFirstCells = axisFirstCells.add(slotTable.find('th:first')); + + axisWidth = 0; + setOuterWidth( + axisFirstCells + .width('') + .each(function(i, _cell) { + axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); + }), + axisWidth + ); + + var gutterCells = dayTable.find('.fc-agenda-gutter'); + if (allDayTable) { + gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter')); + } + + var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) + + gutterWidth = slotScroller.width() - slotTableWidth; + if (gutterWidth) { + setOuterWidth(gutterCells, gutterWidth); + gutterCells + .show() + .prev() + .removeClass('fc-last'); + }else{ + gutterCells + .hide() + .prev() + .addClass('fc-last'); + } + + colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); + setOuterWidth(dayHeadCells.slice(0, -1), colWidth); + } + + + + /* Scrolling + -----------------------------------------------------------------------*/ + + + function resetScroll() { + var d0 = zeroDate(); + var scrollDate = cloneDate(d0); + scrollDate.setHours(opt('firstHour')); + var top = timePosition(d0, scrollDate) + 1; // +1 for the border + function scroll() { + slotScroller.scrollTop(top); + } + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function afterRender() { // after the view has been freshly rendered and sized + resetScroll(); + } + + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(cells) { + cells.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(cells) { + cells.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth)); + var date = cellToDate(0, col); + var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data + if (rowMatch) { + var mins = parseInt(rowMatch[1]) * opt('slotMinutes'); + var hours = Math.floor(mins/60); + date.setHours(hours); + date.setMinutes(mins%60 + minMinute); + trigger('dayClick', dayBodyCells[col], date, false, ev); + }else{ + trigger('dayClick', dayBodyCells[col], date, true, ev); + } + } + } + + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + // TODO: should be consolidated with BasicView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i= 0) { + addMinutes(d, minMinute + slotIndex * snapMinutes); + } + return d; + } + + + // get the Y coordinate of the given time on the given day (both Date objects) + function timePosition(day, time) { // both date objects. day holds 00:00 of current day + day = cloneDate(day, true); + if (time < addMinutes(cloneDate(day), minMinute)) { + return 0; + } + if (time >= addMinutes(cloneDate(day), maxMinute)) { + return slotTable.height(); + } + var slotMinutes = opt('slotMinutes'), + minutes = time.getHours()*60 + time.getMinutes() - minMinute, + slotI = Math.floor(minutes / slotMinutes), + slotTop = slotTopCache[slotI]; + if (slotTop === undefined) { + slotTop = slotTopCache[slotI] = + slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop; + // .eq() is faster than ":eq()" selector + // [0].offsetTop is faster than .position().top (do we really need this optimization?) + // a better optimization would be to cache all these divs + } + return Math.max(0, Math.round( + slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) + )); + } + + + function getAllDayRow(index) { + return allDayRow; + } + + + function defaultEventEnd(event) { + var start = cloneDate(event.start); + if (event.allDay) { + return start; + } + return addMinutes(start, opt('defaultEventMinutes')); + } + + + + /* Selection + ---------------------------------------------------------------------------------*/ + + + function defaultSelectionEnd(startDate, allDay) { + if (allDay) { + return cloneDate(startDate); + } + return addMinutes(cloneDate(startDate), opt('slotMinutes')); + } + + + function renderSelection(startDate, endDate, allDay) { // only for all-day + if (allDay) { + if (opt('allDaySlot')) { + renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); + } + }else{ + renderSlotSelection(startDate, endDate); + } + } + + + function renderSlotSelection(startDate, endDate) { + var helperOption = opt('selectHelper'); + coordinateGrid.build(); + if (helperOption) { + var col = dateToCell(startDate).col; + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords + var top = timePosition(startDate, startDate); + var bottom = timePosition(startDate, endDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(slotContainer); + } + }else{ + rect.isStart = true; // conside rect a "seg" now + rect.isEnd = true; // + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: ['fc-select-helper'], + editable: false + }, + rect + )); + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + slotContainer.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var dates; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { + var d1 = realCellToDate(origCell); + var d2 = realCellToDate(cell); + dates = [ + d1, + addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes + d2, + addMinutes(cloneDate(d2), snapMinutes) + ].sort(dateCompare); + renderSlotSelection(dates[0], dates[3]); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + reportDayClick(dates[0], false, ev); + } + reportSelection(dates[0], dates[3], false, ev); + } + }); + } + } + + + function reportDayClick(date, allDay, ev) { + trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev); + } + + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + if (getIsCellAllDay(cell)) { + renderCellOverlay(cell.row, cell.col, cell.row, cell.col); + }else{ + var d1 = realCellToDate(cell); + var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); + renderSlotOverlay(d1, d2); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui); + } + } + + +} + +;; + +function AgendaEventRenderer() { + var t = this; + + + // exports + t.renderEvents = renderEvents; + t.clearEvents = clearEvents; + t.slotSegHtml = slotSegHtml; + + + // imports + DayEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var isEventDraggable = t.isEventDraggable; + var isEventResizable = t.isEventResizable; + var eventEnd = t.eventEnd; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var getMaxMinute = t.getMaxMinute; + var getMinMinute = t.getMinMinute; + var timePosition = t.timePosition; + var getIsCellAllDay = t.getIsCellAllDay; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var cellToDate = t.cellToDate; + var getColCnt = t.getColCnt; + var getColWidth = t.getColWidth; + var getSnapHeight = t.getSnapHeight; + var getSnapMinutes = t.getSnapMinutes; + var getSlotContainer = t.getSlotContainer; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventDrop = t.eventDrop; + var eventResize = t.eventResize; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var renderDayEvents = t.renderDayEvents; + var calendar = t.calendar; + var formatDate = calendar.formatDate; + var formatDates = calendar.formatDates; + + + // overrides + t.draggableDayEvent = draggableDayEvent; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i start && eventStart < end) { + if (eventStart < start) { + segStart = cloneDate(start); + isStart = false; + }else{ + segStart = eventStart; + isStart = true; + } + if (eventEnd > end) { + segEnd = cloneDate(end); + isEnd = false; + }else{ + segEnd = eventEnd; + isEnd = true; + } + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }); + } + } + return segs.sort(compareSlotSegs); + } + + + function slotEventEnd(event) { + if (event.end) { + return cloneDate(event.end); + }else{ + return addMinutes(cloneDate(event.start), opt('defaultEventMinutes')); + } + } + + + // renders events in the 'time slots' at the bottom + // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space + // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp) + + function renderSlotSegs(segs, modifiedEventId) { + + var i, segCnt=segs.length, seg, + event, + top, + bottom, + columnLeft, + columnRight, + columnWidth, + width, + left, + right, + html = '', + eventElements, + eventElement, + triggerRes, + titleElement, + height, + slotSegmentContainer = getSlotSegmentContainer(), + isRTL = opt('isRTL'); + + // calculate position/dimensions, create html + for (i=0; i" + + "
" + + "
" + + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + + "
" + + "
" + + htmlEscape(event.title || '') + + "
" + + "
" + + "
"; + if (seg.isEnd && isEventResizable(event)) { + html += + "
=
"; + } + html += + ""; + return html; + } + + + function bindSlotSeg(event, eventElement, seg) { + var timeElement = eventElement.find('div.fc-event-time'); + if (isEventDraggable(event)) { + draggableSlotEvent(event, eventElement, timeElement); + } + if (seg.isEnd && isEventResizable(event)) { + resizableSlotEvent(event, eventElement, timeElement); + } + eventElementHandlers(event, eventElement); + } + + + + /* Dragging + -----------------------------------------------------------------------------------*/ + + + // when event starts out FULL-DAY + // overrides DayEventRenderer's version because it needs to account for dragging elements + // to and from the slot area. + + function draggableDayEvent(event, eventElement, seg) { + var isStart = seg.isStart; + var origWidth; + var revert; + var allDay = true; + var dayDelta; + var hoverListener = getHoverListener(); + var colWidth = getColWidth(); + var snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + var minMinute = getMinMinute(); + eventElement.draggable({ + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + origWidth = eventElement.width(); + hoverListener.start(function(cell, origCell) { + clearOverlays(); + if (cell) { + revert = false; + var origDate = cellToDate(0, origCell.col); + var date = cellToDate(0, cell.col); + dayDelta = dayDiff(date, origDate); + if (!cell.row) { + // on full-days + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + resetElement(); + }else{ + // mouse is over bottom slots + if (isStart) { + if (allDay) { + // convert event to temporary slot-event + eventElement.width(colWidth - 10); // don't use entire width + setOuterHeight( + eventElement, + snapHeight * Math.round( + (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) / + snapMinutes + ) + ); + eventElement.draggable('option', 'grid', [colWidth, 1]); + allDay = false; + } + }else{ + revert = true; + } + } + revert = revert || (allDay && !dayDelta); + }else{ + resetElement(); + revert = true; + } + eventElement.draggable('option', 'revert', revert); + }, ev, 'drag'); + }, + stop: function(ev, ui) { + hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + if (revert) { + // hasn't moved or is out of bounds (draggable has already reverted) + resetElement(); + eventElement.css('filter', ''); // clear IE opacity side-effects + showEvents(event, eventElement); + }else{ + // changed! + var minuteDelta = 0; + if (!allDay) { + minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight) + * snapMinutes + + minMinute + - (event.start.getHours() * 60 + event.start.getMinutes()); + } + eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); + } + } + }); + function resetElement() { + if (!allDay) { + eventElement + .width(origWidth) + .height('') + .draggable('option', 'grid', null); + allDay = true; + } + } + } + + + // when event starts out IN TIMESLOTS + + function draggableSlotEvent(event, eventElement, timeElement) { + var coordinateGrid = t.getCoordinateGrid(); + var colCnt = getColCnt(); + var colWidth = getColWidth(); + var snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + + // states + var origPosition; // original position of the element, not the mouse + var origCell; + var isInBounds, prevIsInBounds; + var isAllDay, prevIsAllDay; + var colDelta, prevColDelta; + var dayDelta; // derived from colDelta + var minuteDelta, prevMinuteDelta; + + eventElement.draggable({ + scroll: false, + grid: [ colWidth, snapHeight ], + axis: colCnt==1 ? 'y' : false, + opacity: opt('dragOpacity'), + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + + coordinateGrid.build(); + + // initialize states + origPosition = eventElement.position(); + origCell = coordinateGrid.cell(ev.pageX, ev.pageY); + isInBounds = prevIsInBounds = true; + isAllDay = prevIsAllDay = getIsCellAllDay(origCell); + colDelta = prevColDelta = 0; + dayDelta = 0; + minuteDelta = prevMinuteDelta = 0; + + }, + drag: function(ev, ui) { + + // NOTE: this `cell` value is only useful for determining in-bounds and all-day. + // Bad for anything else due to the discrepancy between the mouse position and the + // element position while snapping. (problem revealed in PR #55) + // + // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event. + // We should overhaul the dragging system and stop relying on jQuery UI. + var cell = coordinateGrid.cell(ev.pageX, ev.pageY); + + // update states + isInBounds = !!cell; + if (isInBounds) { + isAllDay = getIsCellAllDay(cell); + + // calculate column delta + colDelta = Math.round((ui.position.left - origPosition.left) / colWidth); + if (colDelta != prevColDelta) { + // calculate the day delta based off of the original clicked column and the column delta + var origDate = cellToDate(0, origCell.col); + var col = origCell.col + colDelta; + col = Math.max(0, col); + col = Math.min(colCnt-1, col); + var date = cellToDate(0, col); + dayDelta = dayDiff(date, origDate); + } + + // calculate minute delta (only if over slots) + if (!isAllDay) { + minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes; + } + } + + // any state changes? + if ( + isInBounds != prevIsInBounds || + isAllDay != prevIsAllDay || + colDelta != prevColDelta || + minuteDelta != prevMinuteDelta + ) { + + updateUI(); + + // update previous states for next time + prevIsInBounds = isInBounds; + prevIsAllDay = isAllDay; + prevColDelta = colDelta; + prevMinuteDelta = minuteDelta; + } + + // if out-of-bounds, revert when done, and vice versa. + eventElement.draggable('option', 'revert', !isInBounds); + + }, + stop: function(ev, ui) { + + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + + if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed! + eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui); + } + else { // either no change or out-of-bounds (draggable has already reverted) + + // reset states for next time, and for updateUI() + isInBounds = true; + isAllDay = false; + colDelta = 0; + dayDelta = 0; + minuteDelta = 0; + + updateUI(); + eventElement.css('filter', ''); // clear IE opacity side-effects + + // sometimes fast drags make event revert to wrong position, so reset. + // also, if we dragged the element out of the area because of snapping, + // but the *mouse* is still in bounds, we need to reset the position. + eventElement.css(origPosition); + + showEvents(event, eventElement); + } + } + }); + + function updateUI() { + clearOverlays(); + if (isInBounds) { + if (isAllDay) { + timeElement.hide(); + eventElement.draggable('option', 'grid', null); // disable grid snapping + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + } + else { + updateTimeText(minuteDelta); + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping + } + } + } + + function updateTimeText(minuteDelta) { + var newStart = addMinutes(cloneDate(event.start), minuteDelta); + var newEnd; + if (event.end) { + newEnd = addMinutes(cloneDate(event.end), minuteDelta); + } + timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); + } + + } + + + + /* Resizing + --------------------------------------------------------------------------------------*/ + + + function resizableSlotEvent(event, eventElement, timeElement) { + var snapDelta, prevSnapDelta; + var snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + eventElement.resizable({ + handles: { + s: '.ui-resizable-handle' + }, + grid: snapHeight, + start: function(ev, ui) { + snapDelta = prevSnapDelta = 0; + hideEvents(event, eventElement); + trigger('eventResizeStart', this, event, ev, ui); + }, + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight); + if (snapDelta != prevSnapDelta) { + timeElement.text( + formatDates( + event.start, + (!snapDelta && !event.end) ? null : // no change, so don't display time range + addMinutes(eventEnd(event), snapMinutes*snapDelta), + opt('timeFormat') + ) + ); + prevSnapDelta = snapDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', this, event, ev, ui); + if (snapDelta) { + eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui); + }else{ + showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); + } + + +} + + + +/* Agenda Event Segment Utilities +-----------------------------------------------------------------------------*/ + + +// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new +// list in the order they should be placed into the DOM (an implicit z-index). +function placeSlotSegs(segs) { + var levels = buildSlotSegLevels(segs); + var level0 = levels[0]; + var i; + + computeForwardSlotSegs(levels); + + if (level0) { + + for (i=0; i seg2.start && seg1.start < seg2.end; +} + + +// A cmp function for determining which forward segment to rely on more when computing coordinates. +function compareForwardSlotSegs(seg1, seg2) { + // put higher-pressure first + return seg2.forwardPressure - seg1.forwardPressure || + // put segments that are closer to initial edge first (and favor ones with no coords yet) + (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || + // do normal sorting... + compareSlotSegs(seg1, seg2); +} + + +// A cmp function for determining which segment should be closer to the initial edge +// (the left edge on a left-to-right calendar). +function compareSlotSegs(seg1, seg2) { + return seg1.start - seg2.start || // earlier start time goes first + (seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first + (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title +} + + +;; + + +function View(element, calendar, viewName) { + var t = this; + + + // exports + t.element = element; + t.calendar = calendar; + t.name = viewName; + t.opt = opt; + t.trigger = trigger; + t.isEventDraggable = isEventDraggable; + t.isEventResizable = isEventResizable; + t.setEventData = setEventData; + t.clearEventData = clearEventData; + t.eventEnd = eventEnd; + t.reportEventElement = reportEventElement; + t.triggerEventDestroy = triggerEventDestroy; + t.eventElementHandlers = eventElementHandlers; + t.showEvents = showEvents; + t.hideEvents = hideEvents; + t.eventDrop = eventDrop; + t.eventResize = eventResize; + // t.title + // t.start, t.end + // t.visStart, t.visEnd + + + // imports + var defaultEventEnd = t.defaultEventEnd; + var normalizeEvent = calendar.normalizeEvent; // in EventManager + var reportEventChange = calendar.reportEventChange; + + + // locals + var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events) + var eventElementsByID = {}; // eventID mapped to array of jQuery elements + var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system + var options = calendar.options; + + + + function opt(name, viewNameOverride) { + var v = options[name]; + if ($.isPlainObject(v)) { + return smartProperty(v, viewNameOverride || viewName); + } + return v; + } + + + function trigger(name, thisObj) { + return calendar.trigger.apply( + calendar, + [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) + ); + } + + + + /* Event Editable Boolean Calculations + ------------------------------------------------------------------------------*/ + + + function isEventDraggable(event) { + var source = event.source || {}; + return firstDefined( + event.startEditable, + source.startEditable, + opt('eventStartEditable'), + event.editable, + source.editable, + opt('editable') + ) + && !opt('disableDragging'); // deprecated + } + + + function isEventResizable(event) { // but also need to make sure the seg.isEnd == true + var source = event.source || {}; + return firstDefined( + event.durationEditable, + source.durationEditable, + opt('eventDurationEditable'), + event.editable, + source.editable, + opt('editable') + ) + && !opt('disableResizing'); // deprecated + } + + + + /* Event Data + ------------------------------------------------------------------------------*/ + + + function setEventData(events) { // events are already normalized at this point + eventsByID = {}; + var i, len=events.length, event; + for (i=0; i