diff --git a/ChangeLog.md b/ChangeLog.md index 0a99cea6a6322..46cbd73bb2044 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -## Version 0.1 [2020-12-25] +- NEW : Split dialog for kanban card of type User story or Scrum task *28/07/2022* - 1.1.0 + +## Version 1.0 [2020-12-25] - FIX : Suppression des champs qté réalisée et quantité prévue à la création d'un sprint *21/07/2022* - 1.0.9 - FIX : Update visuel de la liste US planifiée *21/07/2022* - 1.0.8 diff --git a/admin/setup.php b/admin/setup.php index 87ddb45b809f4..2aa7688cfe4a8 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -95,7 +95,8 @@ $formSetup = new FormSetup($db); - +/* + * DEMO // Hôte $item = $formSetup->newItem('NO_PARAM_JUST_TEXT'); $item->fieldOverride = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://') . $_SERVER['HTTP_HOST']; @@ -127,6 +128,11 @@ $formSetup->newItem('Title')->setAsTitle(); + + + + + // Setup conf MYMODULE_MYPARAM8 $item = $formSetup->newItem('MYMODULE_MYPARAM8'); $TField = array( @@ -157,6 +163,24 @@ //$item->fieldOverride = false; // set this var to override field output will override $fieldInputOverride and $fieldOutputOverride too //$item->fieldInputOverride = false; // set this var to override field input //$item->fieldOutputOverride = false; // set this var to override field output +*/ + + +$item = $formSetup->newItem('SP_MAX_SCRUM_TASK_STEP_QTY'); +$item->fieldAttr = array( + 'type' => 'number', + 'step' => '0.01', + 'min' => 0 +); + +$item = $formSetup->newItem('SP_MAX_SCRUM_TASK_MAX_QTY'); +$item->fieldAttr = array( + 'type' => 'number', + 'step' => getDolGlobalString('SP_MAX_SCRUM_TASK_STEP_QTY'), + 'min' => 0 +); + +$formSetup->htmlAfterOutputForm.=''; $setupnotempty =+ count($formSetup->items); diff --git a/class/scrumcard.class.php b/class/scrumcard.class.php index fa4d517d3ec61..cb5952843b6d9 100644 --- a/class/scrumcard.class.php +++ b/class/scrumcard.class.php @@ -948,6 +948,7 @@ public function getNextNumRef() public function getScrumKanBanItemObjectFormatted(){ global $user; + // TODO : voir si $object peut être factorisé avec getScrumKanBanItemObjectStd mais attention il doit être compatible avec l'objet js des items de kanban $object = new stdClass(); $object->id = 'scrumcard-' . $this->id; // kanban dom id @@ -1098,6 +1099,56 @@ public function getScrumKanBanItemObjectFormatted(){ } + /** + * get this object formatted for ajax ans json + * @return stdClass + */ + public function getScrumKanBanItemObjectStd(){ + + + $object = new stdClass(); + $object->objectId = $this->id; + $object->type = 'scrum-card';// le type dans le kanban tel que getScrumKanBanItemObjectFormatted le fait + $object->id = 'scrumcard-' . $this->id; // kanban dom id + $object->label = $this->label; + $object->element = $this->element; + $object->cardUrl = dol_buildpath('/scrumproject/scrumcard_card.php',1).'?id='.$this->id; + $object->title = ''; + $object->status = intval($this->status); + $object->statusLabel = $this->LibStatut(intval($this->status), 1); + $object->contactUsersAffected = $this->liste_contact(-1,'internal',1); + + /** + * Traitement de l'élément attaché + */ + + $object->targetelementid = $this->fk_element; + $object->targetelement = $this->element_type; + + $res = $this->fetchElementObject(); + if($res){ + $object->elementObject = false; + if(is_callable(array($this->elementObject, 'getScrumKanBanItemObjectStd'))){ + $object->elementObject = $this->elementObject->getScrumKanBanItemObjectStd($this, $object); + } + + // Si gestion de l'object sans getScrumKanBanItemObjectStd : **typiquement les objects Dolibarr** + if(!$object->elementObject){ + $object->elementObject = new stdClass(); + $object->elementObject->contactUsersAffected = $this->elementObject->liste_contact(-1,'internal', 1); + + if($this->elementObject->element == 'project_task'){ + $object->type = 'project-task'; + }else{ + $object->type = 'scrum-card-linked'; + } + } + } + + return $object; + } + + /** * Return HTML string to put an input field into a page @@ -1392,4 +1443,20 @@ public static function getInternalContactIdFromCode($code, $object, &$error = '' } } + /** + * Permet de spliter la carte en 2 + * @param double $qty la quantité de la nouvelle carte + * @param string $newCardLabel le libelle de la nouvelle carte + * @return void + */ + public function splitCard($qty, $newCardLabel, User $user){ + $this->fetchElementObject(); + if(is_callable(array($this->elementObject, 'splitCard'))){ + return $this->elementObject->splitCard( $qty, $newCardLabel, $this, $user); + }else{ + $this->error = 'splitCard not supported'; + return false; + } + } + } diff --git a/class/scrumtask.class.php b/class/scrumtask.class.php index bd678a659de2d..23ceec858024a 100644 --- a/class/scrumtask.class.php +++ b/class/scrumtask.class.php @@ -1250,6 +1250,40 @@ public function getProjectTaskTimeLinkId($fk_project_task_time){ return false; } + + /** + * get this object formatted for ajax ans json + * @return stdClass + */ + public function getScrumKanBanItemObjectStd(){ + + + $object = new stdClass(); + $object->objectId = $this->id; + $object->ref= $this->ref; + $object->type = 'scrum-user-story-task';// le type dans le kanban tel que getScrumKanBanItemObjectFormatted le fait + $object->label = $this->label; + $object->element = $this->element; + $object->cardUrl = dol_buildpath('/scrumproject/scrumtask_card.php',1).'?id='.$this->id; + $object->status = intval($this->status); + $object->statusLabel = $this->LibStatut(intval($this->status), 1); + $object->contactUsersAffected = $this->liste_contact(-1,'internal',1); + + $object->fk_scrum_user_story_sprint = $this->fk_scrum_user_story_sprint; + $object->fk_scrum_user_story_sprint = $this->fk_scrum_user_story_sprint; + $object->qty_planned = doubleval($this->qty_planned); + $object->qty_consumed = doubleval($this->qty_consumed); + + $object->qty_remain_for_split = 0; + if($this->qty_planned - $this->qty_consumed > 0){ + $object->qty_remain_for_split = $this->qty_planned - $this->qty_consumed; + } + + return $object; + } + + + /** * @param $msg * @return void @@ -1270,4 +1304,86 @@ public function setErrorMsg($msg){ $this->errors[] = $msg; } } + + + /** + * Permet de spliter la carte en 2 + * @param double $qty la quantité de la nouvelle carte + * @param string $newCardLabel le libelle de la nouvelle carte + * @param ScrumCard $scrumCard + * @return bool + */ + public function splitCard($qty, $newCardLabel, ScrumCard $scrumCard, User $user ){ + + if(!class_exists('ScrumTask')){ + require_once __DIR__ . '/scrumtask.class.php'; + } + if(!class_exists('ScrumCard')){ + require_once __DIR__ . '/scrumcard.class.php'; + } + + $qty = doubleval($qty); + + $this->calcTimeSpent(); + + // Vérification de la liaison entre ScrumCard et ScrumTask + if($scrumCard->element_type != $this->element || $scrumCard->fk_element != $this->id ){ + $this->error = 'Error : scrum card not linked'; + $this->errors[] = $this->error; + return false; + } + + + // Vérification du temps restant + if($qty > $this->qty_planned - $this->qty_consumed ){ + $this->error = 'Too much quantity'; + $this->errors[] = $this->error; + return false; + } + + // Ajout de la nouvelle ScrumTask + $newScrumTask = new ScrumTask($this->db); + $newScrumTask->fk_scrum_user_story_sprint = $this->fk_scrum_user_story_sprint; + $newScrumTask->description = $this->description; + + $newScrumTask->qty_planned = $qty; + $newScrumTask->label = $newCardLabel; + if(empty($newCardLabel) || is_array($newCardLabel)){ $newScrumTask->label = $this->label;} + + $resCreate = $newScrumTask->create($user); + if($resCreate<0){ + $this->error = $newScrumTask->error; + $this->errors = array_merge($this->errors, $newScrumTask->errors); + return false; + } + + // MISE A JOUR DE LA SCRUM TASK QUE L'ON SPLIT + $this->qty_planned-= $qty; + $resUpdate = $this->update($user); + if($resUpdate<1){ + $this->error = 'Error updating ScrumTask : '.$this->error; + $this->errors = array_merge($this->errors, $this->errors); + return false; + } + +// Bloc deja effectué par $newScrumTask->create +// // AJOUT DE LA CARD LIÉE +// $newScrumCard = new ScrumCard($this->db); +// if(empty($newCardLabel)){ +// $newScrumCard->label = $this->label; +// } +// $newScrumCard->label = $newScrumTask->label; +// $newScrumCard->fk_element = $newScrumTask->id; +// $newScrumCard->element_type = $newScrumTask->element; +// $newScrumCard->fk_scrum_kanbanlist = $scrumCard->fk_scrum_kanbanlist; +// $newScrumCard->fk_rank = $scrumCard->fk_rank; +// $res = $newScrumCard->create($user); +// if($res<=0){ +// $this->error = 'Error creating ScrumCard : '.$newScrumCard->error; +// $this->errors = array_merge($this->errors, $newScrumCard->errors); +// return false; +// } + + return true; + } } diff --git a/class/scrumuserstorysprint.class.php b/class/scrumuserstorysprint.class.php index 7621f1cb11ff3..e5b0e9472d13e 100644 --- a/class/scrumuserstorysprint.class.php +++ b/class/scrumuserstorysprint.class.php @@ -146,6 +146,11 @@ class ScrumUserStorySprint extends CommonObject public $model_pdf; // END MODULEBUILDER PROPERTIES + /** + * valeur dynamique non stocké en base , recupérée par $this->calcTimeTaskPlanned() + * @var $qty_task_planned + */ + public $qty_task_planned; // If this object has a subtable with lines @@ -945,6 +950,121 @@ public function info($id) } } + + + /** + * get this object formatted for ajax ans json + * @return stdClass + */ + public function getScrumKanBanItemObjectStd(){ + + $object = new stdClass(); + $object->objectId = $this->id; + $object->ref= $this->ref; + $object->type = 'scrum-user-story';// le type dans le kanban tel que getScrumKanBanItemObjectFormatted le fait + $object->label = $this->label; + $object->element = $this->element; + $object->cardUrl = dol_buildpath('/scrumproject/scrumuserstorysprint_card.php',1).'?id='.$this->id; + + $object->status = intval($this->status); + $object->statusLabel = $this->LibStatut(intval($this->status), 1); + $object->contactUsersAffected = $this->liste_contact(-1,'internal',1); + + $object->fk_scrum_user_story_sprint = $this->fk_scrum_user_story_sprint; + $object->fk_scrum_user_story_sprint= $this->fk_scrum_user_story_sprint; + $object->qty_planned = doubleval($this->qty_planned); + $object->qty_consumed = doubleval($this->qty_consumed); + + $this->calcTimeTaskPlanned(); + $object->qty_task_planned = doubleval($this->qty_task_planned); + + $object->qty_remain_for_split = 0; + $qtyConsumeBase = max($this->qty_task_planned, $this->qty_consumed); + if($this->qty_planned - $qtyConsumeBase > 0){ + $object->qty_remain_for_split = $this->qty_planned - $qtyConsumeBase; + } + + + return $object; + } + + + + /** + * Permet de spliter l'us carte en scrum task + * @param double $qty la quantité de la nouvelle carte + * @param string $newCardLabel le libelle de la nouvelle carte + * @param ScrumCard $scrumCard + * @return bool + */ + public function splitCard($qty, $newCardLabel, ScrumCard $scrumCard, User $user ){ + + $qty = doubleval($qty); + + if(!class_exists('ScrumTask')){ + require_once __DIR__ . '/scrumtask.class.php'; + } + if(!class_exists('ScrumCard')){ + require_once __DIR__ . '/scrumcard.class.php'; + } + + $this->calcTimeTaskPlanned(); + + // Vérification de la liaison entre ScrumCard et ScrumTask + if($scrumCard->element_type != $this->element || $scrumCard->fk_element != $this->id ){ + $this->error = 'Error : scrum card not linked'; + $this->errors[] = $this->error; + return false; + } + + + // Vérification du temps restant + if($qty > $this->qty_planned - $this->qty_task_planned ){ + $this->error = 'Too much quantity'; + $this->errors[] = $this->error; + return false; + } + + // Ajout de la nouvelle ScrumTask + $newScrumTask = new ScrumTask($this->db); + $newScrumTask->fk_scrum_user_story_sprint = $this->id; + $newScrumTask->description = $this->description; + + $newScrumTask->qty_planned = $qty; + $newScrumTask->label = $newCardLabel; + if(empty($newCardLabel) || is_array($newCardLabel)){ $newScrumTask->label = $this->label;} + + $resCreate = $newScrumTask->create($user); + if($resCreate<0){ + $this->error = $newScrumTask->error; + $this->errors = array_merge($this->errors, $newScrumTask->errors); + return false; + } + + // MISE A JOUR DE LA SCRUM TASK QUE L'ON SPLIT + $this->qty_task_planned-= $qty; + +// Bloc deja effectué par $newScrumTask->create +// // AJOUT DE LA CARD LIÉE +// $newScrumCard = new ScrumCard($this->db); +// if(empty($newCardLabel)){ +// $newScrumCard->label = $this->label; +// } +// $newScrumCard->label = $newScrumTask->label; +// $newScrumCard->fk_element = $newScrumTask->id; +// $newScrumCard->element_type = $newScrumTask->element; +// $newScrumCard->fk_scrum_kanbanlist = $scrumCard->fk_scrum_kanbanlist; +// $newScrumCard->fk_rank = $scrumCard->fk_rank; +// $res = $newScrumCard->create($user); +// if($res<=0){ +// $this->error = 'Error creating ScrumCard : '.$newScrumCard->error; +// $this->errors = array_merge($this->errors, $newScrumCard->errors); +// return false; +// } + + return true; + } + /** * Initialise object with example values * Id must be 0 if object instance is a specimen @@ -1188,6 +1308,24 @@ public function calcTimeSpent(){ return 0; } + /** + * + * @return int + */ + public function calcTimeTaskPlanned(){ + + $sql = /** @lang MySQL */ "SELECT SUM(qty_planned) sumTaskPlanned FROM ".MAIN_DB_PREFIX."scrumproject_scrumtask " + ." WHERE fk_scrum_user_story_sprint = ".intval($this->id); + + $obj = $this->db->getRow($sql); + if($obj){ + $this->qty_task_planned = doubleval($obj->sumTaskPlanned); + return $this->qty_task_planned; + } + + return 0; + } + /** * @param User $user * @param $notrigger diff --git a/core/modules/modScrumProject.class.php b/core/modules/modScrumProject.class.php index de8ba43bd9a05..bfd8659acd80a 100644 --- a/core/modules/modScrumProject.class.php +++ b/core/modules/modScrumProject.class.php @@ -64,7 +64,7 @@ public function __construct($db) $this->editor_name = 'ATM Consulting'; $this->editor_url = 'www.atm-consulting.fr'; // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z' - $this->version = '1.0.9'; + $this->version = '1.1.0'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; diff --git a/css/kanban.css b/css/kanban.css index 478b3279bd010..f3932d4001ba6 100644 --- a/css/kanban.css +++ b/css/kanban.css @@ -96,7 +96,153 @@ dialog.kanban-dialog[open]{ } +/* Dialog content */ +.dialog-form-head{ + font-size: 0.8em; +} + +.dialog-form-head-item:not(:first-child){ + margin-left: 30px; +} + +.dialog-form-head-item.split-qty-remain{ + font-weight: bold; +} + +.dialog-form-body{ + margin-top: 10px; +} + + +.dialog-form-control{ + margin-top: var(--dialog-padding); +} + +.curent-split-item-line{ + +} +.new-split-item-line{ + border-top: 1px solid rgba(0,0,0,0.1); +} + +.dialog-form-icon-btn{ + + --bg-gradian-start: #ffffff; + --bg-gradian-end: #e6e6e6; + --border-color: #aaa; + --colortext: #444; + + margin-bottom: 3px; + margin-top: 3px; + margin-left: 5px; + margin-right: 5px; + display: inline-block; + padding: 3px 8px; + text-align: center; + cursor: pointer; + text-decoration: none !important; + background-color: var(--bg-gradian-end); + background-image: -moz-linear-gradient(top,var(--bg-gradian-start), var(--bg-gradian-end)); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(var(--bg-gradian-start)), to(var(--bg-gradian-end))); + background-image: -webkit-linear-gradient(top, var(--bg-gradian-start), var(--bg-gradian-end)); + background-image: -o-linear-gradient(top, var(--bg-gradian-start), var(--bg-gradian-end)); + background-image: linear-gradient(to bottom, var(--bg-gradian-start), var(--bg-gradian-end)); + background-repeat: repeat-x; + border: 1px solid var(--border-color); + -webkit-border-radius: 2px; + border-radius: 1px; + font-weight: bold; + text-transform: uppercase; + + + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + /* + Introduced in Internet Explorer 10. + See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/ + */ + -ms-user-select: none; + user-select: none; +} + +.dialog-form-icon-btn:active{ + --bg-gradian-start: #e6e6e6; + --bg-gradian-end: #ffffff; +} + +html[data-theme-color-scheme="dark"] .dialog-form-icon-btn{ + --bg-gradian-start: rgb(44, 55, 70); + --bg-gradian-end: var(--bg-gradian-start); + --colortext: var(--general-text-color); + --border-color: var(--bg-gradian-start); +} +html[data-theme-color-scheme="dark"] .dialog-form-icon-btn:active{ + --bg-gradian-start: rgb(26, 35, 42); +} + + +/* Line separation */ +hr.dialog-separator { + + --border-color : rgba(0,0,0,0.2); + + border: none; + overflow: visible; + box-sizing: content-box; + order: 0; + height: 1px; + width: 10%; + position: relative; + margin: 10px auto; + + background: var(--border-color); +} +hr.dialog-separator:before { + content: ""; + width: 4px; + height: 4px; + background: #fff; + display: inline-block; + border: 1px solid var(--border-color); + position: absolute; + top: -3px;/* un petit fallback au cas ou la virgule en css passe pas*/ + top: -2.5px; + left: 50%; + margin: 0 0 0 -3px; + transform: rotate(45deg); + -ms-transform: rotate(45deg); + /* IE 9 */ + -webkit-transform: rotate(45deg); + /* Opera, Chrome, and Safari */ +} + +.dialog-form-head-number{ + min-width: 35px; + display: inline-block; +} + +.dialog-form-item{ + display: inline-block; +} +.dialog-form-item [readonly]{ + --inputbackgroundcolor: #efefef; +} + + +html[data-theme-color-scheme="dark"] .dialog-form-item [readonly]{ + --inputbackgroundcolor: #535c6a; + --colortext: #8e96a5; +} + +.split-qty-planned{ + width: 90px; +} + +.split-item-label{ + width: 600px; +} html[data-theme-color-scheme="dark"] dialog.kanban-dialog{ @@ -138,7 +284,6 @@ button.kanban-btn { -webkit-user-select: none; touch-action: manipulation; } - button.kanban-btn[data-btn-role="accept"]{ font-weight: bold; --button-background-color : var(--butactionbg); @@ -152,6 +297,12 @@ button.kanban-btn[data-btn-role="accept"]:hover{ filter: hue-rotate(5deg); } +button.kanban-btn[disabled]{ + opacity: 0.3; + cursor: not-allowed; +} + + button.kanban-btn[data-btn-role="cancel"]{ --button-background-color : rgba(51, 51, 51, 0); --button-color : #595959; @@ -909,6 +1060,11 @@ html[data-theme-color-scheme="dark"]{ color: var(--general-text-color); } +html[data-theme-color-scheme="dark"] input{ + --inputbackgroundcolor: #4c5666; + --colortext:#ffffff; +} + html[data-theme-color-scheme="dark"] a:link, html[data-theme-color-scheme="dark"] a:visited{ color: #1c8ee7; } diff --git a/interface-kanban.php b/interface-kanban.php index 67719f4d28464..a5d5d284b950a 100644 --- a/interface-kanban.php +++ b/interface-kanban.php @@ -67,6 +67,12 @@ elseif ($action === 'getAllItemToList') { _actionAddItemToList($jsonResponse); } +elseif ($action === 'getScrumCardData') { + _actionGetScrumCardData($jsonResponse); +} +elseif ($action === 'splitScrumCard') { + _actionSplitScrumCard($jsonResponse); +} elseif ($action === 'dropItemToList') { _actionDropItemToList($jsonResponse); } @@ -372,12 +378,41 @@ function _actionAddItemToList($jsonResponse){ } } + +/** + * @param JsonResponse $jsonResponse + * @return bool|void + */ +function _actionGetScrumCardData($jsonResponse){ + global $db; + + $data = GETPOST("data", "array"); + + // check kanban list data + if(empty($data['id'])){ + $jsonResponse->msg = 'Need scrumcard Id'; + return false; + } + + $scrumCard = new ScrumCard($db); + $res = $scrumCard->fetch($data['id']); + if($res <= 0){ + $jsonResponse->msg = 'Scrumcard fetch error'; + return false; + } + + $jsonResponse->result = 1; + $jsonResponse->data = $scrumCard->getScrumKanBanItemObjectStd(); + return true; +} + + /** * @param JsonResponse $jsonResponse * @return bool|void */ function _actionDropItemToList($jsonResponse){ - global $user, $langs, $db; + global $langs, $db; $data = GETPOST("data", "array"); @@ -641,6 +676,67 @@ function _checkObjectByElement($elementType, $id, $jsonResponse){ } +/** + * @param JsonResponse $jsonResponse + * @return bool|CommonObject + */ +function _actionSplitScrumCard($jsonResponse){ + global $langs, $db, $user; + + + $data = GETPOST("data", "array"); + + if(empty($data['id'])){ + $jsonResponse->msg = 'Need card Id'; + return false; + } + + $scrumCard = new ScrumCard($db); + if($scrumCard->fetch($data['id']) <= 0){ + $jsonResponse->msg = $langs->trans('RequireValidExistingElement'); + return false; + } + + $errors = 0; + + if(empty($data['form']) || empty($data['form']['new-item-qty-planned'])){ + $jsonResponse->msg = $langs->trans('RequireValidSplitData'); + return false; + } + + if(!is_array($data['form']['new-item-qty-planned'])){ + $data['form']['new-item-qty-planned'] = array( + $data['form']['new-item-qty-planned'] + ); + } + + if(!is_array($data['form']['new-item-label'])){ + $data['form']['new-item-label'] = array( + $data['form']['new-item-label'] + ); + } + + foreach ($data['form']['new-item-qty-planned'] as $key => $qty){ + $newCardLabel = ''; + if(!empty($data['form']['new-item-label'][$key])){ + $newCardLabel = $data['form']['new-item-label'][$key]; + } + + if(is_array($newCardLabel)){ $newCardLabel = '';} + + $res = $scrumCard->splitCard($qty, $newCardLabel, $user); + if($res<=0){ + $jsonResponse->msg = $scrumCard->errorsToString(); + $errors++; + } + } + + + + return $errors==0; +} + + /** * @param JsonResponse $jsonResponse * @param int|bool $userId diff --git a/js/dialog.js b/js/dialog.js index ba2a3ff0bfb56..4652f1f1c5f71 100644 --- a/js/dialog.js +++ b/js/dialog.js @@ -16,6 +16,9 @@ class Dialog { title: '', dialogClass: '', content: '', + onClose : function (){ return true; }, + onOpen : function (){ return true; }, + onAccept : function (){ return true; }, template: '
' + '
\n' + '
\n' + @@ -30,6 +33,13 @@ class Dialog { this.init() } + /** + * @param $functionName + * @returns {boolean} + */ + isCallableFunction($functionName) { + return window[$functionName] instanceof Function; + } init() { @@ -45,8 +55,26 @@ class Dialog { this.dialog.querySelector('header').textContent = this.settings.title; this.buttons.accept = this.dialog.querySelector('[data-btn-role="accept"]'); + this.buttons.accept.addEventListener("click", (e)=>{ + e.preventDefault(); // Cancel the native event + e.stopPropagation();// Don't bubble/capture the event any further + if(this.settings.onAccept(this)){ + this.toggle(); + } + }); + + + + this.buttons.cancel = this.dialog.querySelector('[data-btn-role="cancel"]'); // $(this.dialog.querySelector('header')).dragsDialog(); //TODO : faut-il le mettre ou pas ? // rend les boites de dialogue draggable + this.buttons.cancel.addEventListener("click", (e)=>{ + e.preventDefault(); // Cancel the native event + e.stopPropagation();// Don't bubble/capture the event any further + this.toggle(); + }); + + this.dialog.querySelector('.body').insertAdjacentHTML('afterbegin', this.settings.content); @@ -79,16 +107,18 @@ class Dialog { open(settings = {}) { const dialog = Object.assign({}, this.settings, settings) - this.toggle() } toggle() { if(this.dialog.hasAttribute('open')){ - this.dialog.close(); + if(this.settings.onClose(this)){ + this.dialog.close(); + } } else{ this.dialog.showModal(); + this.settings.onOpen(this); } } diff --git a/js/scrumkanban.js b/js/scrumkanban.js index d318eb3555b06..68d173a74c10d 100644 --- a/js/scrumkanban.js +++ b/js/scrumkanban.js @@ -34,7 +34,9 @@ scrumKanban = {}; interface_liveupdate_url: '../interface-liveupdate.php', srumprojectModuleFolderUrl: '../', fk_kanban : false, - token: false // to set at init + token: false, // to set at init + maxScrumTaskStepQty: 0, + maxScrumTaskMaxQty: 0 }; @@ -52,7 +54,8 @@ scrumKanban = {}; Copy:"Copier", Delete:"Supprimer", CardClone:"Cloner", - CardSplit:"Séparer", + CardSplit:"Découper en plusieurs cartes", + CardUsSplit:"Découper en tâches", AssignMe:"M'assigner à la tâche", UnAssignMe:"Me désengager de la tâche", PressEscapeToAvoid:"Appuyer sur la touche ECHAP pour annuler", @@ -60,7 +63,20 @@ scrumKanban = {}; SplitUsInTask:"Séparer l'user story en tâches scrum", SplitCard:"Découper la carte", CloneCard:"Cloner la carte", - DeleteCardDialogTitle:"Supprimer cette carte ?" + DeleteCardDialogTitle:"Supprimer cette carte ?", + + QtyPlanned : 'Quantités planifiés', + QtyConsumed : 'Quantités consommées', + QtyRemain : 'Quantités restantes', + QtyRemainToSplit : 'Quantités découpables', + AddScrumTaskLine : 'Ajouter une tâche scrum', + SplitScrumTask : 'Découper la tâche scrum', + NotSplittable : 'N\'est pas découpable', + + RemoveLine : 'Supprimer la ligne', + AddLine : 'Ajouter une ligne', + QtyScrumTaskAlreadySplited: 'Quantités découpées en tâche(s) scrum ' + }; @@ -704,7 +720,7 @@ scrumKanban = {}; content: o.menuIcons.deleteIcon + o.langs.Delete, events: { click: function (e) { - o.deleteBoardDialog(boardId); + o.dialogDeleteBoard(boardId); } // mouseover: () => console.log("Copy Button Mouseover") // You can use any event listener from here @@ -740,72 +756,102 @@ scrumKanban = {}; } let menuDropDownId = $(el).attr('id'); + let dataType = $(el).attr('data-type'); - let menuItems = [ - { - content: '' + o.langs.AssignMe, - events: { - click: function (e) { - let sendData = { - 'fk_kanban': o.config.fk_kanban, - 'card-id': el.getAttribute('data-objectid') - }; - o.callKanbanInterface('assignMeToCard', sendData, function(response){ - if(response.result > 0) { - // recupérer les bonnes infos - o.jkanban.replaceElement(el, response.data); - } - }); - } + let menuItems = []; + + // Assign me to card + menuItems.push({ + content: '' + o.langs.AssignMe, + events: { + click: function (e) { + let sendData = { + 'fk_kanban': o.config.fk_kanban, + 'card-id': el.getAttribute('data-objectid') + }; + + o.callKanbanInterface('assignMeToCard', sendData, function(response){ + if(response.result > 0) { + // recupérer les bonnes infos + o.jkanban.replaceElement(el, response.data); + } + }); } - }, - { - content: '' + o.langs.UnAssignMe, - events: { - click: function (e) { - let sendData = { - 'fk_kanban': o.config.fk_kanban, - 'card-id': el.getAttribute('data-objectid') - }; + } + }); - o.callKanbanInterface('removeMeFromCard', sendData, function(response){ - if(response.result > 0) { - // recupérer les bonnes infos - o.jkanban.replaceElement(el, response.data); - } - }); - } + // Un assign me to card + menuItems.push({ + content: '' + o.langs.UnAssignMe, + events: { + click: function (e) { + let sendData = { + 'fk_kanban': o.config.fk_kanban, + 'card-id': el.getAttribute('data-objectid') + }; + + o.callKanbanInterface('removeMeFromCard', sendData, function(response){ + if(response.result > 0) { + // recupérer les bonnes infos + o.jkanban.replaceElement(el, response.data); + } + }); } - }, - { + } + }); + + + // Clone card menu : il n'est pas possible de cloner une US ou une tache : dans c'est cas là il faut spliter le temps + if(dataType != undefined && dataType != 'scrum-user-story' && dataType != 'scrum-user-story-task') + { + menuItems.push({ content: o.menuIcons.copyIcon + o.langs.CardClone, events: { click: function (e) { - o.cloneCardDialog(e); + o.dialogCloneCard(e); } } - }, - { - content: '' + o.langs.CardSplit, + }); + } + + // Split US card Dialog + if(dataType != undefined && dataType == 'scrum-user-story') { + menuItems.push({ + content: '' + o.langs.CardUsSplit, events: { click: function (e) { - o.splitCardDialog(el); + o.dialogSplitCard(el); } } - }, - { - content: o.menuIcons.deleteIcon + o.langs.Delete, + }); + } + + // Split US TASK card Dialog + if(dataType != undefined && dataType == 'scrum-user-story-task') { + menuItems.push({ + content: '' + o.langs.CardSplit, events: { click: function (e) { - o.deleteCardDialog(el.getAttribute('data-eid')); + o.dialogSplitCard(el); } - // mouseover: () => console.log("Copy Button Mouseover") - // You can use any event listener from here - }, - divider: "top" // top, bottom, top-bottom - } - ]; + } + }); + } + + menuItems.push({ + content: o.menuIcons.deleteIcon + o.langs.Delete, + events: { + click: function (e) { + o.deleteCardDialog(el.getAttribute('data-eid')); + } + // mouseover: () => console.log("Copy Button Mouseover") + // You can use any event listener from here + }, + divider: "top" // top, bottom, top-bottom + }); + + let tclick = new ContextMenu({ target: '#' + menuDropDownId, @@ -1071,30 +1117,387 @@ scrumKanban = {}; } } - o.splitCardDialog = function(el){ + /** + * + * @param {HTMLElement} el + */ + o.dialogSplitCard = function(el){ + + const type = el.getAttribute('data-type'); + const objectId = el.getAttribute('data-objectid'); - // TODO detect type of element before + // init quantity vars + let qtyPlannedCurentItem; // quantité planifiée sur la carte d'origine + let qtyRemain; // Quantité restante disponible sur la quantité planifiée, ex j'ai planifié 10H mais j'ai saisi 4heures de temps passé sur la tâche, j'ai donc consommé 4H sur les 10H il reste donc 6H - const content = '

Work in progress

'; + let lineItemCounter = 0; - const splitDialog = new Dialog({ - title: o.langs.SplitCard, - content: content + /** + * @returns {boolean} true if split is good or false if not + * also toggle disable buttons + */ + const checkSplitDialogBTN = function (){ + + let $addLineBtn = $('#add-split-line'); + if($addLineBtn.length > 0){ + if(qtyRemain == 0){ + $addLineBtn.css('visibility','hidden'); + }else{ + $addLineBtn.css('visibility',''); + } + } + + //ne pas permettre l'ajout si l'us n'est pas entièrement splitée + if(type == 'scrum-user-story'){ + let $acceptBtn = $('[data-btn-role="accept"]'); + if($acceptBtn.length > 0){ + if(qtyRemain > 0){ + $acceptBtn.prop('disabled', true); + return false; + }else{ + $acceptBtn.prop('disabled', false); + } + } + } + + return true; + } + + /** + * @param {object} tplVars + * @returns {string} + */ + const getSplitItemTpl = function(tplVars){ + + tplVars = Object.assign( + { + label: '', + qty_planned_min: '', + qty_planned_max: '', + qty_planned: '', + data_lastValue: 0, + maxScrumTaskStepQty: o.config.maxScrumTaskStepQty + }, + tplVars + ) + + let content = ''; + content+= '
'; + content+= '
'; + content+= ' '; + content+= '
'; + content+= '
'; + content+= ' '; + content+= '
'; + content+= '
'; + content+= ' '; + content+= '
'; + content+= '
'; + + return content; + } + + /** + * Ajoute une ligne avec prise en compte des données en cours + * @param {object} tplVars + */ + const addSplitLine = function(qty_planned_to_add = 0){ + + qty_planned_to_add = parseFloat(qty_planned_to_add); + + lineItemCounter++; + + + let maxQtyPlannedForLine = parseFloat(qtyRemain); + if(parseFloat(o.config.maxScrumTaskMaxQty) > 0 && maxQtyPlannedForLine > parseFloat(o.config.maxScrumTaskMaxQty)){ + maxQtyPlannedForLine = parseFloat(o.config.maxScrumTaskMaxQty); + } + + // todo mettre à jour les données d'entrées + let newLine = getSplitItemTpl({ + label: $('[name="curent-item-label"]').val(), + qty_planned_min: 0, + qty_planned_max: maxQtyPlannedForLine, + qty_planned: qty_planned_to_add, + data_lastValue: 0, + }); + + let newLineAppended = $(newLine).appendTo('#split-line-form-container'); + newItemQuantityPlannedChange(newLineAppended.find('[name="new-item-qty-planned"]')); + checkSplitDialogBTN(); + } + + /** + * Met à jour les quantité + * @param newPlannedQtyMvt + */ + const updateSplitQty = function(newPlannedQtyMvt = 0){ + newPlannedQtyMvt = parseFloat(newPlannedQtyMvt); + + if(qtyRemain - newPlannedQtyMvt < 0){ + newPlannedQtyMvt = qtyRemain; + } + + qtyRemain = Math.round((qtyRemain - newPlannedQtyMvt) * 100) / 100; + + if(type == 'scrum-user-story'){ + // cas particulier des us + $('#split-qty-task-planned').html(qtyRemain); + $('#split-qty-task-planned').html(qtyPlannedCurentItem-qtyRemain); + } + else{ + qtyPlannedCurentItem = Math.round((qtyPlannedCurentItem - newPlannedQtyMvt) * 100) / 100; + $('#curent-item-qty-planned').val(qtyPlannedCurentItem); + } + + $('#split-qty-remain').html(qtyRemain); + + // mise à jour du max sur les inputs + $('.new-item-qty-planned').each(function( index ) { + + let maxQtyPlannedForLine = parseFloat(qtyRemain); + if(parseFloat(o.config.maxScrumTaskMaxQty) > 0 && maxQtyPlannedForLine > parseFloat(o.config.maxScrumTaskMaxQty)){ + maxQtyPlannedForLine = parseFloat(o.config.maxScrumTaskMaxQty); + } + + if(maxQtyPlannedForLine < parseFloat($(this).attr('max'))){ + maxQtyPlannedForLine = parseFloat($(this).attr('max')); + } + + $(this).attr('max', maxQtyPlannedForLine); + }) + + checkSplitDialogBTN(); + + return newPlannedQtyMvt; // retourne la valeur appliquée + } + + + // Ajout au click sur button plus d'une ligne + $(document).off('click', '#add-split-line'); // suppression du handler existant + $(document).on('click', '#add-split-line', function (){ + addSplitLine(0); + }); + + // Fermeture au click sur le message de fermeture + $(document).off('click', '.btn-remove-split-line-card'); // suppression du handler existant + $(document).on('click', '.btn-remove-split-line-card', function() { + let newLineQty = $(this).closest('.new-split-item-line').find('.split-qty-planned').val(); + updateSplitQty(-parseFloat(newLineQty));// re-alloue les quantités + $(this).closest('.new-split-item-line').remove(); + }); + + // Update des calcules + $(document).off('change', '.new-item-qty-planned'); // suppression du handler existant + $(document).on('change', '.new-item-qty-planned', function() { + newItemQuantityPlannedChange($(this)); + }); + + /** + * + * @param {jQuery} $el + */ + function newItemQuantityPlannedChange($el) { + let newLineQty = parseFloat($el.val()); + let oldLineQty = parseFloat($el.attr('data-lastvalue')); + newLineQty = oldLineQty + updateSplitQty(newLineQty-oldLineQty); + newLineQty = Math.round((newLineQty) * 100) / 100; + $el.val(newLineQty); // force la valeur saisie avec la valeur de retour de updateSplitQty + $el.attr('data-lastvalue', newLineQty); + } + + o.callKanbanInterface('getScrumCardData', {'id': objectId}, function(response){ + if(response.result > 0) { + // recupérer les info de la card + let content = ''; + let canSplit = false; + + // Géneration du formulaire + if(response.data.elementObject != undefined){ + + // mise à jour des quantités de départ + qtyPlannedCurentItem = parseFloat(response.data.elementObject.qty_planned); + qtyRemain = parseFloat(response.data.elementObject.qty_remain_for_split); + + + content+= '
'; + + content+= '' + o.langs.QtyPlanned + ' : ' + response.data.elementObject.qty_planned + ''; + if(type == 'scrum-user-story'){ + content+= '' + o.langs.QtyScrumTaskAlreadySplited + ' : '; + content+= '' + response.data.elementObject.qty_task_planned + ''; + content+= ''; + } + content+= '' + o.langs.QtyConsumed + ' : ' + response.data.elementObject.qty_consumed + ''; + content+= '' + o.langs.QtyRemainToSplit + ' : ' + response.data.elementObject.qty_remain_for_split + ''; + + content+= '
'; + + + + content+='
'; + + canSplit = response.data.elementObject.qty_remain_for_split > 0; + if(canSplit){ + + let label = response.data.label; + if(response.data.elementObject.label != undefined && response.data.elementObject.label.length){ + label = response.data.elementObject.label; + } + + content+= '
'; + content+= '
'; + let qtyPlannedvalueDisplayed = response.data.elementObject.qty_planned; + // if(type == 'scrum-user-story'){ qtyPlannedvalueDisplayed = ''; } + content+= ' '; + content+= '
'; + + let labelDislayed = label; + // if(type == 'scrum-user-story'){ labelDislayed = ''; } + content+= '
'; + content+= ' '; + content+= '
'; + + + content+= '
'; + content+= ' '; + if(type == 'scrum-user-story'){ + content+= ' '; + }else{ + content+= ' '; + } + content+= ' '; + content+= '
'; + + content+= '
'; + + + // + // let maxQtyPlannedForLine = parseFloat(response.data.elementObject.qty_remain_for_split); + // if(parseFloat(o.config.maxScrumTaskMaxQty) > 0 && maxQtyPlannedForLine > parseFloat(o.config.maxScrumTaskMaxQty)){ + // maxQtyPlannedForLine = parseFloat(o.config.maxScrumTaskMaxQty); + // } + // content+= getSplitItemTpl({ + // label : label, + // qty_planned_min: 0, + // qty_planned_max: maxQtyPlannedForLine, + // qty_planned: 0, + // }); + + + }else{ + content+='' + o.langs.NotSplittable + ''; + } + + content+='
'; + } + else{ + content+='
Error data elementObject
'; + } + + const splitDialog = new Dialog({ + title: o.langs.SplitCard, + content: content, + onAccept: function(){ + + if(canSplit && checkSplitDialogBTN()){ + // récupération des données de formulaire en Html5 + let sendData = { + 'id': objectId, + 'form': o.serializeFormJson($(splitDialog.dialog).find('form')) + }; + + o.callKanbanInterface('splitScrumCard', sendData, function(){ + o.refreshAllBoards(); + }); + + return true; + } + + return false; + } + ,onOpen: function(){ + + if(type == 'scrum-user-story' && parseFloat(o.config.maxScrumTaskMaxQty) > 0){ + let qtyRemainToSplit = parseFloat(qtyRemain); + while(qtyRemainToSplit >= parseFloat(o.config.maxScrumTaskMaxQty)){ + addSplitLine(o.config.maxScrumTaskMaxQty); + qtyRemainToSplit-=parseFloat(o.config.maxScrumTaskMaxQty); + } + + if(qtyRemainToSplit>0){ + addSplitLine(qtyRemainToSplit); + } + } + + checkSplitDialogBTN(); + } + }); + + // utilisation par les promesses arrété pour l'instant + // splitDialog.waitForUser().then((userValidate) => { + // if(canSplit && userValidate){ + // // récupération des données de formulaire en Html5 + // let sendData = { + // 'id': objectId, + // 'form': o.serializeFormJson($(splitDialog.dialog).find('form')) + // }; + // + // o.callKanbanInterface('splitScrumCard', sendData, function(){ + // o.refreshAllBoards(); + // }); + // + // }else{ + // // user cancel + // } + // }); + } }); } - o.cloneCardDialog = function(el){ + /** + * A function to serialize an html form to JSON + * @param {JQuery} $el + * @returns {{}} + */ + o.serializeFormJson = function($el) { + var obj = {}; + var a = $el.serializeArray(); + + $.each(a, function() { + if (obj[this.name]) { + if (!obj[this.name].push) { + obj[this.name] = [obj[this.name]]; + } + obj[this.name].push(this.value || ''); + } else { + obj[this.name] = this.value || ''; + } + }); + return obj; + }; + + o.dialogCloneCard = function(el){ // TODO detect type of element before // User story and scrum task can not be cloned const content = '

Work in progress

'; - const splitDialog = new Dialog({ + const cloneDialog = new Dialog({ title: o.langs.CloneCard, content: content }); + + + cloneDialog.waitForUser().then((userValidate) => { + if(userValidate){ + + }else{ + // user cancel + } + }); } @@ -1106,13 +1509,13 @@ scrumKanban = {}; let content = '

Work in progress to delete ' + eid + '

' + '

Pour l\'instant la suppression se fait sans distinction, les cards spéciales ne sont pas prisent en compte : US, US-Tâches etc...

' - const splitDialog = new Dialog({ + const delDialog = new Dialog({ title: o.langs.DeleteCardDialogTitle, dialogClass: '--danger', content: content }); - splitDialog.waitForUser().then((userValidate) => { + delDialog.waitForUser().then((userValidate) => { if(userValidate){ o.delItem(eid); }else{ @@ -1121,7 +1524,7 @@ scrumKanban = {}; }); } - o.deleteBoardDialog = function(boardId){ + o.dialogDeleteBoard = function(boardId){ // TODO detect type of element before // User story and scrum task can not be deleted ? @@ -1144,4 +1547,9 @@ scrumKanban = {}; } }); } + + o.htmlEntities = function(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + })(scrumKanban); diff --git a/langs/fr_FR/scrumproject.lang b/langs/fr_FR/scrumproject.lang index c8599d246374a..78653290e8b75 100644 --- a/langs/fr_FR/scrumproject.lang +++ b/langs/fr_FR/scrumproject.lang @@ -32,11 +32,14 @@ ScrumProjectSetupPage = Page de configuration du module Projet Scrum ScrumSprintExtraFields = Attributs supplémentaires sprints ScrumKanbanExtraFields = Attributs supplémentaires Kanban ScrumUserStoryExtraFields = Attributs supplémentaires User story -ScrumUserStorySprintExtraFields = Attributs supplémentaires User story plannifiés +ScrumUserStorySprintExtraFields = Attributs supplémentaires User story planifiés ScrumCardExtraFields = Attributs supplémentaires cartes NumberingModules = Modèles de numérotation pour %s ScrumCardStage = Projet Scrum - Étapes des cartes +SP_MAX_SCRUM_TASK_STEP_QTY = Granularité des quantités pour les tâches scrum et les User stories +SP_MAX_SCRUM_TASK_MAX_QTY = Quantités maximal pouvant être affecter à une tâche scrum + # # MENU # diff --git a/scrumkanban_view.php b/scrumkanban_view.php index db4bf68f8eae0..44099017b4601 100644 --- a/scrumkanban_view.php +++ b/scrumkanban_view.php @@ -160,14 +160,16 @@ top_htmlhead($head, $object->ref . ' - ' . $object->label, 0, 0, $arrayofjs, $arrayofcss); $confToJs = array( - 'MAIN_MAX_DECIMALS_TOT' => $conf->global->MAIN_MAX_DECIMALS_TOT, - 'MAIN_MAX_DECIMALS_UNIT' => $conf->global->MAIN_MAX_DECIMALS_UNIT, + 'MAIN_MAX_DECIMALS_TOT' => getDolGlobalInt('MAIN_MAX_DECIMALS_TOT'), + 'MAIN_MAX_DECIMALS_UNIT' => getDolGlobalInt('MAIN_MAX_DECIMALS_UNIT'), 'interface_kanban_url' => dol_buildpath('scrumproject/interface-kanban.php',1), 'interface_liveupdate_url' => dol_buildpath('scrumproject/interface-liveupdate.php',1), 'js_url' => dol_buildpath('scrumproject/js/scrumkanban.js',1), 'srumprojectModuleFolderUrl'=> dol_buildpath('scrumproject/',1), 'fk_kanban' => $object->id, - 'token' => newToken() + 'token' => newToken(), + 'maxScrumTaskStepQty' => getDolGlobalString('SP_MAX_SCRUM_TASK_STEP_QTY', 0), + 'maxScrumTaskMaxQty' => getDolGlobalString('SP_MAX_SCRUM_TASK_MAX_QTY', 0) ); $jsLangs = array(