From 5f204e3a32cf9b4c3f07ecb0279ec92fae5d6758 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:41:33 +0100 Subject: [PATCH 01/27] refactor(project): move task pre-massaction dialogs to massactions_pre template --- htdocs/core/tpl/massactions_pre.tpl.php | 54 ++++++++++ htdocs/langs/de_DE/projects.lang | 12 +++ htdocs/langs/en_US/projects.lang | 12 +++ htdocs/langs/es_ES/projects.lang | 12 +++ htdocs/langs/fr_FR/projects.lang | 12 +++ htdocs/langs/it_IT/projects.lang | 12 +++ htdocs/projet/class/task.class.php | 41 ++++++++ htdocs/projet/tasks/list.php | 129 +++++++++++++++++++++++- 8 files changed, 282 insertions(+), 2 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 2e96ab1d3912e..bdd801e3f485c 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -94,6 +94,60 @@ print $form->formconfirm($_SERVER['PHP_SELF'] . '?id=' . $object->id . $selected, $langs->trans('ConfirmMassClone'), '', 'clonetasks', $formquestion, '', 1, 300, 590); } +if (in_array($massaction, array('preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'), true) && is_object($objecttmp) && $objecttmp->element == 'project_task') { + if (!$user->hasRight('projet', 'creer')) { + setEventMessages($langs->trans('ErrorNotEnoughPermissions'), null, 'errors'); + } else { + $tasksById = $objecttmp->getAuthorizedTasksForMassAction($user, $toselect); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + $massactionTaskMap = array( + 'preupdate_selected_tasks_progress' => 'update_selected_tasks_progress', + 'preupdate_selected_tasks_start_date' => 'update_selected_tasks_start_date', + 'preupdate_selected_tasks_deadline' => 'update_selected_tasks_deadline' + ); + $finalAction = $massactionTaskMap[$massaction]; + $formquestion = array(); + $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); + + if ($finalAction == 'update_selected_tasks_progress') { + $tablehtml = ''; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $inputname = 'task_progress_'.$taskId; + $tablehtml .= ''; + $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('Progress').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).' %
'; + $formquestion[] = array('type' => 'other', 'name' => 'task_progress', 'label' => '', 'value' => $tablehtml); + $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress'); + } else { + $tablehtml = ''; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $datetimeName = 'task_datetime_'.$taskId; + $keepdurationname = 'keep_duration_'.$taskId; + if ($finalAction == 'update_selected_tasks_start_date') { + $currenttimestamp = (!empty($taskDb->dateo) ? (int) $db->jdate($taskDb->dateo) : dol_now()); + } else { + $currenttimestamp = (!empty($taskDb->datee) ? (int) $db->jdate($taskDb->datee) : dol_now()); + } + $datetimevalue = dol_print_date($currenttimestamp, '%Y-%m-%dT%H:%M'); + $tablehtml .= ''; + $tablehtml .= ''; + $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('DateHour').''.$langs->trans('MassActionKeepTaskDuration').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).'
'; + $formquestion[] = array('type' => 'other', 'name' => 'task_datetime', 'label' => '', 'value' => $tablehtml); + $titleform = ($finalAction == 'update_selected_tasks_start_date' ? $langs->trans('MassActionUpdateSelectedTasksStartDate') : $langs->trans('MassActionUpdateSelectedTasksDeadline')); + } + + print $form->formconfirm($_SERVER['PHP_SELF'], $titleform, $langs->trans('MassActionTaskPopupDescription'), $finalAction, $formquestion, '', 1, 500, 800, 0, 'Validate', 'Cancel'); + } + } +} + if ($massaction == 'preaffecttag' && isModEnabled('category')) { require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; $categ = new Categorie($db); diff --git a/htdocs/langs/de_DE/projects.lang b/htdocs/langs/de_DE/projects.lang index 368aa1ae78697..a930fbdcfd72a 100644 --- a/htdocs/langs/de_DE/projects.lang +++ b/htdocs/langs/de_DE/projects.lang @@ -337,3 +337,15 @@ TaskHourlyRateUpdated=Stundensatz für Aufgabe aktualisiert UpdateWithLastHourlyRate=Alle aufgezeichneten Zeitaufwende jedes Benutzer mit dem letzten Stundensatz des jeweiligen Benutzers aktualisieren UpdateUndefinedWithLastHourlyRate=Aktualisieren der Zeitaufwende, für die kein Stundensatz festgelegt ist, mit dem letzten Stundensatz des Benutzers. EnterUsersHourlyRateFirst=Zunächst einen Stundensatz für den/die Benutzer eingeben... +MassActionCloturerTachesProjet=Ausgewählte Projektaufgaben schließen +MassActionModifierAvancementTachesProjet=Fortschritt der ausgewählten Projektaufgaben ändern +MassActionModifierDateDebutTachesProjet=Startdatum der ausgewählten Projektaufgaben ändern +MassActionModifierEcheanceTachesProjet=Fälligkeitsdatum der ausgewählten Projektaufgaben ändern +MassActionTaskKeepDuration=Dauer beibehalten +MassActionTaskPopupDescription=Massenaktion für ausgewählte Aufgaben bestätigen. +MassActionTaskClosed=%s Aufgabe(n) geschlossen. +MassActionTaskProgressUpdated=Fortschritt für %s Aufgabe(n) aktualisiert. +MassActionTaskStartDateUpdated=Startdatum für %s Aufgabe(n) aktualisiert. +MassActionTaskDeadlineUpdated=Fälligkeitsdatum für %s Aufgabe(n) aktualisiert. +MassActionTaskInvalidProgressValue=Ungültiger Fortschrittswert für Aufgaben-ID %s. +MassActionTaskInvalidDateValue=Ungültiger Datumswert für Aufgaben-ID %s. diff --git a/htdocs/langs/en_US/projects.lang b/htdocs/langs/en_US/projects.lang index 221a9b8ea6b2a..0387482332e43 100644 --- a/htdocs/langs/en_US/projects.lang +++ b/htdocs/langs/en_US/projects.lang @@ -337,3 +337,15 @@ TaskHourlyRateUpdated=Task's hourly rate updated UpdateWithLastHourlyRate=Update all recorded time spent of each user with the last hourly rate of the user UpdateUndefinedWithLastHourlyRate=Update the time spent that has no hourly rate defined with the last hourly rate of the user EnterUsersHourlyRateFirst=First, enter the hourly rate for the user(s)... +MassActionCloseSelectedTasks=Close selected project tasks +MassActionUpdateSelectedTasksProgress=Change progress of selected project tasks +MassActionUpdateSelectedTasksStartDate=Change start date of selected project tasks +MassActionUpdateSelectedTasksDeadline=Change deadline of selected project tasks +MassActionKeepTaskDuration=Keep duration +MassActionTaskPopupDescription=Confirm the mass action on selected tasks. +MassActionSelectedTasksClosed=%s task(s) closed. +MassActionSelectedTasksProgressUpdated=%s task(s) progress updated. +MassActionSelectedTasksStartDateUpdated=%s task(s) start date updated. +MassActionSelectedTasksDeadlineUpdated=%s task(s) deadline updated. +MassActionInvalidTaskProgressValue=Invalid progress value for task ID %s. +MassActionInvalidTaskDateValue=Invalid date value for task ID %s. diff --git a/htdocs/langs/es_ES/projects.lang b/htdocs/langs/es_ES/projects.lang index 39d64fbd8e3f3..28309114ca1cc 100644 --- a/htdocs/langs/es_ES/projects.lang +++ b/htdocs/langs/es_ES/projects.lang @@ -337,3 +337,15 @@ TaskHourlyRateUpdated=Tarifa por hora de la tarea actualizada UpdateWithLastHourlyRate=Actualizar todo el tiempo registrado empleado por cada usuario con la última tarifa por hora del usuario UpdateUndefinedWithLastHourlyRate=Actualizar el tiempo empleado que no tiene tarifa horaria definida con la última tarifa horaria del usuario EnterUsersHourlyRateFirst=Primero, ingrese la tarifa por hora para el/los usuario(s)... +MassActionCloturerTachesProjet=Cerrar las tareas de proyecto seleccionadas +MassActionModifierAvancementTachesProjet=Modificar el avance de las tareas de proyecto seleccionadas +MassActionModifierDateDebutTachesProjet=Modificar la fecha de inicio de las tareas de proyecto seleccionadas +MassActionModifierEcheanceTachesProjet=Modificar la fecha límite de las tareas de proyecto seleccionadas +MassActionTaskKeepDuration=Mantener duración +MassActionTaskPopupDescription=Confirmar la acción masiva sobre las tareas seleccionadas. +MassActionTaskClosed=%s tarea(s) cerrada(s). +MassActionTaskProgressUpdated=Avance actualizado para %s tarea(s). +MassActionTaskStartDateUpdated=Fecha de inicio actualizada para %s tarea(s). +MassActionTaskDeadlineUpdated=Fecha límite actualizada para %s tarea(s). +MassActionTaskInvalidProgressValue=Valor de avance no válido para la tarea ID %s. +MassActionTaskInvalidDateValue=Valor de fecha no válido para la tarea ID %s. diff --git a/htdocs/langs/fr_FR/projects.lang b/htdocs/langs/fr_FR/projects.lang index 68ffbad5ed73e..66caf2eec4488 100644 --- a/htdocs/langs/fr_FR/projects.lang +++ b/htdocs/langs/fr_FR/projects.lang @@ -337,3 +337,15 @@ TaskHourlyRateUpdated=Mise à jour horaire de tâche et de taux UpdateWithLastHourlyRate=Mettre à jour toutes les durées enregistrées pour chaque utilisateur avec la dernière durée horaire de taux du utilisateur UpdateUndefinedWithLastHourlyRate=Mettez à jour le temps passé pour lequel aucune valeur horaire taux n'est définie avec la dernière valeur horaire taux de utilisateur. EnterUsersHourlyRateFirst=Tout d'abord, saisissez le code horaire taux pour le(s) utilisateur... +MassActionCloturerTachesProjet=Clôturer les tâches projet sélectionnées +MassActionModifierAvancementTachesProjet=Modifier l'avancement des tâches projet sélectionnées +MassActionModifierDateDebutTachesProjet=Modifier la date de début des tâches projet sélectionnées +MassActionModifierEcheanceTachesProjet=Modifier l'échéance des tâches projet sélectionnées +MassActionTaskKeepDuration=Conserver la durée +MassActionTaskPopupDescription=Confirmer l'action de masse sur les tâches sélectionnées. +MassActionTaskClosed=%s tâche(s) clôturée(s). +MassActionTaskProgressUpdated=Avancement mis à jour pour %s tâche(s). +MassActionTaskStartDateUpdated=Date de début mise à jour pour %s tâche(s). +MassActionTaskDeadlineUpdated=Échéance mise à jour pour %s tâche(s). +MassActionTaskInvalidProgressValue=Valeur d'avancement invalide pour la tâche ID %s. +MassActionTaskInvalidDateValue=Valeur de date invalide pour la tâche ID %s. diff --git a/htdocs/langs/it_IT/projects.lang b/htdocs/langs/it_IT/projects.lang index 956f34470c668..2e9d8677cead9 100644 --- a/htdocs/langs/it_IT/projects.lang +++ b/htdocs/langs/it_IT/projects.lang @@ -337,3 +337,15 @@ TaskHourlyRateUpdated=Task's hourly rate updated UpdateWithLastHourlyRate=Update all recorded time spent of each user with the last hourly rate of the user UpdateUndefinedWithLastHourlyRate=Update the time spent that has no hourly rate defined with the last hourly rate of the user EnterUsersHourlyRateFirst=First, enter the hourly rate for the user(s)... +MassActionCloturerTachesProjet=Chiudere le attività progetto selezionate +MassActionModifierAvancementTachesProjet=Modificare l'avanzamento delle attività progetto selezionate +MassActionModifierDateDebutTachesProjet=Modificare la data di inizio delle attività progetto selezionate +MassActionModifierEcheanceTachesProjet=Modificare la scadenza delle attività progetto selezionate +MassActionTaskKeepDuration=Mantieni durata +MassActionTaskPopupDescription=Conferma l'azione di massa sulle attività selezionate. +MassActionTaskClosed=%s attività chiusa/e. +MassActionTaskProgressUpdated=Avanzamento aggiornato per %s attività. +MassActionTaskStartDateUpdated=Data di inizio aggiornata per %s attività. +MassActionTaskDeadlineUpdated=Scadenza aggiornata per %s attività. +MassActionTaskInvalidProgressValue=Valore avanzamento non valido per attività ID %s. +MassActionTaskInvalidDateValue=Valore data non valido per attività ID %s. diff --git a/htdocs/projet/class/task.class.php b/htdocs/projet/class/task.class.php index 79c4ba0c3c0de..3c0bee1d39a19 100644 --- a/htdocs/projet/class/task.class.php +++ b/htdocs/projet/class/task.class.php @@ -620,6 +620,47 @@ public function fetch($id, $ref = '', $loadparentdata = 0) } } + /** + * Return authorized tasks for selected ids. + * + * @param User $user Current user + * @param array $toselect List of selected task ids + * @return array Tasks indexed by task id + */ + public function getAuthorizedTasksForMassAction($user, $toselect) + { + $toselect = array_unique(array_filter(array_map('intval', (array) $toselect))); + if (empty($toselect)) { + return array(); + } + + $projectstatic = new Project($this->db); + $projectlistfilter = ''; + if (!$user->hasRight('projet', 'all', 'lire')) { + $projectsListId = $projectstatic->getProjectsAuthorizedForUser($user, 0, 1, 0); + $projectlistfilter = " AND p.rowid IN (".$this->db->sanitize($projectsListId ? $projectsListId : '0').")"; + } + + $sql = "SELECT t.rowid, t.ref, t.label, t.dateo, t.datee, t.progress, t.fk_statut, t.fk_projet"; + $sql .= " FROM ".MAIN_DB_PREFIX."projet_task AS t"; + $sql .= " INNER JOIN ".MAIN_DB_PREFIX."projet AS p ON p.rowid = t.fk_projet"; + $sql .= " WHERE t.rowid IN (".implode(',', $toselect).")"; + $sql .= " AND p.entity IN (".getEntity('project').")"; + $sql .= $projectlistfilter; + + $resql = $this->db->query($sql); + if (!$resql) { + return array(); + } + + $tasksById = array(); + while ($obj = $this->db->fetch_object($resql)) { + $tasksById[(int) $obj->rowid] = $obj; + } + + return $tasksById; + } + /** * Update database diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index 7d3b21a12a433..35a22f7a6c1db 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -198,6 +198,17 @@ $permissiontoread = $user->hasRight('projet', 'lire'); $permissiontocreate = $user->hasRight('projet', 'creer'); $permissiontodelete = $user->hasRight('projet', 'supprimer'); +$massactionTaskMap = array( + 'preupdate_selected_tasks_progress' => 'update_selected_tasks_progress', + 'preupdate_selected_tasks_start_date' => 'update_selected_tasks_start_date', + 'preupdate_selected_tasks_deadline' => 'update_selected_tasks_deadline' +); +$massactionTaskList = array( + 'close_selected_tasks', + 'update_selected_tasks_progress', + 'update_selected_tasks_start_date', + 'update_selected_tasks_deadline' +); if (!$permissiontoread) { accessforbidden(); @@ -212,7 +223,7 @@ $action = 'list'; $massaction = ''; } -if (!GETPOST('confirmmassaction', 'alpha') && $massaction != 'presend' && $massaction != 'confirm_presend') { +if (!GETPOST('confirmmassaction', 'alpha') && !in_array($massaction, array('presend', 'confirm_presend'), true) && !isset($massactionTaskMap[$massaction]) && !in_array($massaction, $massactionTaskList, true)) { $massaction = ''; } @@ -270,6 +281,114 @@ $objectlabel = 'Tasks'; $uploaddir = $conf->project->dir_output.'/tasks'; include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; + + $effectiveMassAction = ''; + if (in_array($action, $massactionTaskList, true) && $confirm == 'yes') { + $effectiveMassAction = $action; + } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { + $effectiveMassAction = $massaction; + } + if ($permissiontocreate && !empty($effectiveMassAction)) { + $toselectpost = GETPOST('toselect', 'array:int'); + if (empty($toselectpost)) { + $toselectcsv = GETPOST('toselect', 'alphanohtml'); + if (!empty($toselectcsv)) { + $toselectpost = array_map('intval', explode(',', $toselectcsv)); + } + } + $tasksById = $object->getAuthorizedTasksForMassAction($user, $toselectpost); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + $error = 0; + $done = 0; + $db->begin(); + foreach ($tasksById as $taskId => $taskDb) { + $task = new Task($db); + if ($task->fetch($taskId) <= 0) { + $error++; + $task->error = empty($task->error) ? $langs->trans('ErrorRecordNotFound') : $task->error; + $task->errors[] = $task->error; + continue; + } + + if ($effectiveMassAction == 'close_selected_tasks') { + $task->progress = 100; + $task->status = Task::STATUS_CLOSED; + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + $progressraw = GETPOST('task_progress_'.$taskId, 'alphanohtml'); + if ($progressraw === '' || !is_numeric($progressraw)) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskProgressValue', $taskId); + continue; + } + $task->progress = max(0, min(100, (int) $progressraw)); + $task->status = ($task->progress >= 100 ? Task::STATUS_CLOSED : Task::STATUS_ONGOING); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date' || $effectiveMassAction == 'update_selected_tasks_deadline') { + $taskdatetime = GETPOST('task_datetime_'.$taskId, 'alphanohtml'); + $tasktimestamp = 0; + if (!empty($taskdatetime)) { + $tasktimestamp = dol_stringtotime(str_replace('T', ' ', $taskdatetime), 1); + } + if (empty($tasktimestamp) || $tasktimestamp < 0) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskDateValue', $taskId); + continue; + } + $keepduration = GETPOSTINT('keep_duration_'.$taskId); + $oldstart = (!empty($task->date_start) ? (int) $task->date_start : 0); + $oldend = (!empty($task->date_end) ? (int) $task->date_end : 0); + $durationseconds = ($oldstart > 0 && $oldend > 0 ? ($oldend - $oldstart) : null); + + if ($effectiveMassAction == 'update_selected_tasks_start_date') { + $task->date_start = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_end = $tasktimestamp + $durationseconds; + } + } else { + $task->date_end = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_start = $tasktimestamp - $durationseconds; + } + } + } + + if ($task->update($user) <= 0) { + $error++; + if (!empty($task->errors)) { + setEventMessages('', $task->errors, 'errors'); + } else { + setEventMessages($task->error, null, 'errors'); + } + } else { + $done++; + } + } + + if ($error) { + $db->rollback(); + } else { + $db->commit(); + } + + if ($done > 0 && !$error) { + if ($effectiveMassAction == 'close_selected_tasks') { + setEventMessages($langs->trans('MassActionSelectedTasksClosed', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + setEventMessages($langs->trans('MassActionSelectedTasksProgressUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date') { + setEventMessages($langs->trans('MassActionSelectedTasksStartDateUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_deadline') { + setEventMessages($langs->trans('MassActionSelectedTasksDeadlineUpdated', $done), null, 'mesgs'); + } + } elseif (!$error) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } + } + + $action = 'list'; + $massaction = ''; + } } // already done at line 85 @@ -734,7 +853,13 @@ if (!empty($permissiontodelete)) { $arrayofmassactions['predelete'] = img_picto('', 'delete', 'class="pictofixedwidth"').$langs->trans("Delete"); } -if (GETPOSTINT('nomassaction') || in_array($massaction, array('presend', 'predelete'))) { +if ($permissiontocreate) { + $arrayofmassactions['close_selected_tasks'] = img_picto('', 'tick', 'class="pictofixedwidth"').$langs->trans("MassActionCloseSelectedTasks"); + $arrayofmassactions['preupdate_selected_tasks_progress'] = img_picto('', 'projecttask', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksProgress"); + $arrayofmassactions['preupdate_selected_tasks_start_date'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksStartDate"); + $arrayofmassactions['preupdate_selected_tasks_deadline'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksDeadline"); +} +if (GETPOSTINT('nomassaction') || in_array($massaction, array('presend', 'predelete', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'))) { $arrayofmassactions = array(); } $massactionbutton = $form->selectMassAction('', $arrayofmassactions); From 4f8d3280d6b2d27197544be6ddb52fd2dfdff621 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:43:33 +0100 Subject: [PATCH 02/27] chore(i18n): remove added task massaction keys from non-en locales --- htdocs/langs/de_DE/projects.lang | 12 ------------ htdocs/langs/es_ES/projects.lang | 12 ------------ htdocs/langs/fr_FR/projects.lang | 12 ------------ htdocs/langs/it_IT/projects.lang | 12 ------------ 4 files changed, 48 deletions(-) diff --git a/htdocs/langs/de_DE/projects.lang b/htdocs/langs/de_DE/projects.lang index a930fbdcfd72a..368aa1ae78697 100644 --- a/htdocs/langs/de_DE/projects.lang +++ b/htdocs/langs/de_DE/projects.lang @@ -337,15 +337,3 @@ TaskHourlyRateUpdated=Stundensatz für Aufgabe aktualisiert UpdateWithLastHourlyRate=Alle aufgezeichneten Zeitaufwende jedes Benutzer mit dem letzten Stundensatz des jeweiligen Benutzers aktualisieren UpdateUndefinedWithLastHourlyRate=Aktualisieren der Zeitaufwende, für die kein Stundensatz festgelegt ist, mit dem letzten Stundensatz des Benutzers. EnterUsersHourlyRateFirst=Zunächst einen Stundensatz für den/die Benutzer eingeben... -MassActionCloturerTachesProjet=Ausgewählte Projektaufgaben schließen -MassActionModifierAvancementTachesProjet=Fortschritt der ausgewählten Projektaufgaben ändern -MassActionModifierDateDebutTachesProjet=Startdatum der ausgewählten Projektaufgaben ändern -MassActionModifierEcheanceTachesProjet=Fälligkeitsdatum der ausgewählten Projektaufgaben ändern -MassActionTaskKeepDuration=Dauer beibehalten -MassActionTaskPopupDescription=Massenaktion für ausgewählte Aufgaben bestätigen. -MassActionTaskClosed=%s Aufgabe(n) geschlossen. -MassActionTaskProgressUpdated=Fortschritt für %s Aufgabe(n) aktualisiert. -MassActionTaskStartDateUpdated=Startdatum für %s Aufgabe(n) aktualisiert. -MassActionTaskDeadlineUpdated=Fälligkeitsdatum für %s Aufgabe(n) aktualisiert. -MassActionTaskInvalidProgressValue=Ungültiger Fortschrittswert für Aufgaben-ID %s. -MassActionTaskInvalidDateValue=Ungültiger Datumswert für Aufgaben-ID %s. diff --git a/htdocs/langs/es_ES/projects.lang b/htdocs/langs/es_ES/projects.lang index 28309114ca1cc..39d64fbd8e3f3 100644 --- a/htdocs/langs/es_ES/projects.lang +++ b/htdocs/langs/es_ES/projects.lang @@ -337,15 +337,3 @@ TaskHourlyRateUpdated=Tarifa por hora de la tarea actualizada UpdateWithLastHourlyRate=Actualizar todo el tiempo registrado empleado por cada usuario con la última tarifa por hora del usuario UpdateUndefinedWithLastHourlyRate=Actualizar el tiempo empleado que no tiene tarifa horaria definida con la última tarifa horaria del usuario EnterUsersHourlyRateFirst=Primero, ingrese la tarifa por hora para el/los usuario(s)... -MassActionCloturerTachesProjet=Cerrar las tareas de proyecto seleccionadas -MassActionModifierAvancementTachesProjet=Modificar el avance de las tareas de proyecto seleccionadas -MassActionModifierDateDebutTachesProjet=Modificar la fecha de inicio de las tareas de proyecto seleccionadas -MassActionModifierEcheanceTachesProjet=Modificar la fecha límite de las tareas de proyecto seleccionadas -MassActionTaskKeepDuration=Mantener duración -MassActionTaskPopupDescription=Confirmar la acción masiva sobre las tareas seleccionadas. -MassActionTaskClosed=%s tarea(s) cerrada(s). -MassActionTaskProgressUpdated=Avance actualizado para %s tarea(s). -MassActionTaskStartDateUpdated=Fecha de inicio actualizada para %s tarea(s). -MassActionTaskDeadlineUpdated=Fecha límite actualizada para %s tarea(s). -MassActionTaskInvalidProgressValue=Valor de avance no válido para la tarea ID %s. -MassActionTaskInvalidDateValue=Valor de fecha no válido para la tarea ID %s. diff --git a/htdocs/langs/fr_FR/projects.lang b/htdocs/langs/fr_FR/projects.lang index 66caf2eec4488..68ffbad5ed73e 100644 --- a/htdocs/langs/fr_FR/projects.lang +++ b/htdocs/langs/fr_FR/projects.lang @@ -337,15 +337,3 @@ TaskHourlyRateUpdated=Mise à jour horaire de tâche et de taux UpdateWithLastHourlyRate=Mettre à jour toutes les durées enregistrées pour chaque utilisateur avec la dernière durée horaire de taux du utilisateur UpdateUndefinedWithLastHourlyRate=Mettez à jour le temps passé pour lequel aucune valeur horaire taux n'est définie avec la dernière valeur horaire taux de utilisateur. EnterUsersHourlyRateFirst=Tout d'abord, saisissez le code horaire taux pour le(s) utilisateur... -MassActionCloturerTachesProjet=Clôturer les tâches projet sélectionnées -MassActionModifierAvancementTachesProjet=Modifier l'avancement des tâches projet sélectionnées -MassActionModifierDateDebutTachesProjet=Modifier la date de début des tâches projet sélectionnées -MassActionModifierEcheanceTachesProjet=Modifier l'échéance des tâches projet sélectionnées -MassActionTaskKeepDuration=Conserver la durée -MassActionTaskPopupDescription=Confirmer l'action de masse sur les tâches sélectionnées. -MassActionTaskClosed=%s tâche(s) clôturée(s). -MassActionTaskProgressUpdated=Avancement mis à jour pour %s tâche(s). -MassActionTaskStartDateUpdated=Date de début mise à jour pour %s tâche(s). -MassActionTaskDeadlineUpdated=Échéance mise à jour pour %s tâche(s). -MassActionTaskInvalidProgressValue=Valeur d'avancement invalide pour la tâche ID %s. -MassActionTaskInvalidDateValue=Valeur de date invalide pour la tâche ID %s. diff --git a/htdocs/langs/it_IT/projects.lang b/htdocs/langs/it_IT/projects.lang index 2e9d8677cead9..956f34470c668 100644 --- a/htdocs/langs/it_IT/projects.lang +++ b/htdocs/langs/it_IT/projects.lang @@ -337,15 +337,3 @@ TaskHourlyRateUpdated=Task's hourly rate updated UpdateWithLastHourlyRate=Update all recorded time spent of each user with the last hourly rate of the user UpdateUndefinedWithLastHourlyRate=Update the time spent that has no hourly rate defined with the last hourly rate of the user EnterUsersHourlyRateFirst=First, enter the hourly rate for the user(s)... -MassActionCloturerTachesProjet=Chiudere le attività progetto selezionate -MassActionModifierAvancementTachesProjet=Modificare l'avanzamento delle attività progetto selezionate -MassActionModifierDateDebutTachesProjet=Modificare la data di inizio delle attività progetto selezionate -MassActionModifierEcheanceTachesProjet=Modificare la scadenza delle attività progetto selezionate -MassActionTaskKeepDuration=Mantieni durata -MassActionTaskPopupDescription=Conferma l'azione di massa sulle attività selezionate. -MassActionTaskClosed=%s attività chiusa/e. -MassActionTaskProgressUpdated=Avanzamento aggiornato per %s attività. -MassActionTaskStartDateUpdated=Data di inizio aggiornata per %s attività. -MassActionTaskDeadlineUpdated=Scadenza aggiornata per %s attività. -MassActionTaskInvalidProgressValue=Valore avanzamento non valido per attività ID %s. -MassActionTaskInvalidDateValue=Valore data non valido per attività ID %s. From 069e1e28e1a952c5be9d5b040e32440024945572 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:48:49 +0100 Subject: [PATCH 03/27] refactor(project): simplify task massaction routing logic --- htdocs/core/tpl/massactions_pre.tpl.php | 13 +++++++------ htdocs/projet/tasks/list.php | 15 ++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index bdd801e3f485c..430ed311373ab 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -102,12 +102,13 @@ if (empty($tasksById)) { setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); } else { - $massactionTaskMap = array( - 'preupdate_selected_tasks_progress' => 'update_selected_tasks_progress', - 'preupdate_selected_tasks_start_date' => 'update_selected_tasks_start_date', - 'preupdate_selected_tasks_deadline' => 'update_selected_tasks_deadline' - ); - $finalAction = $massactionTaskMap[$massaction]; + if ($massaction == 'preupdate_selected_tasks_progress') { + $finalAction = 'update_selected_tasks_progress'; + } elseif ($massaction == 'preupdate_selected_tasks_start_date') { + $finalAction = 'update_selected_tasks_start_date'; + } else { + $finalAction = 'update_selected_tasks_deadline'; + } $formquestion = array(); $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index 35a22f7a6c1db..2d0de39adf45d 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -198,17 +198,6 @@ $permissiontoread = $user->hasRight('projet', 'lire'); $permissiontocreate = $user->hasRight('projet', 'creer'); $permissiontodelete = $user->hasRight('projet', 'supprimer'); -$massactionTaskMap = array( - 'preupdate_selected_tasks_progress' => 'update_selected_tasks_progress', - 'preupdate_selected_tasks_start_date' => 'update_selected_tasks_start_date', - 'preupdate_selected_tasks_deadline' => 'update_selected_tasks_deadline' -); -$massactionTaskList = array( - 'close_selected_tasks', - 'update_selected_tasks_progress', - 'update_selected_tasks_start_date', - 'update_selected_tasks_deadline' -); if (!$permissiontoread) { accessforbidden(); @@ -223,7 +212,7 @@ $action = 'list'; $massaction = ''; } -if (!GETPOST('confirmmassaction', 'alpha') && !in_array($massaction, array('presend', 'confirm_presend'), true) && !isset($massactionTaskMap[$massaction]) && !in_array($massaction, $massactionTaskList, true)) { +if (!GETPOST('confirmmassaction', 'alpha') && !in_array($massaction, array('presend', 'confirm_presend', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline', 'close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true)) { $massaction = ''; } @@ -283,7 +272,7 @@ include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; $effectiveMassAction = ''; - if (in_array($action, $massactionTaskList, true) && $confirm == 'yes') { + if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { $effectiveMassAction = $action; } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { $effectiveMassAction = $massaction; From 6804fdaf55f0a7232ded8bb85be68860a2ec4c4e Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:59:22 +0100 Subject: [PATCH 04/27] ui(project): auto-size task massaction modal and center in viewport --- htdocs/core/tpl/massactions_pre.tpl.php | 40 +++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 430ed311373ab..1f3a30118008d 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -109,22 +109,25 @@ } else { $finalAction = 'update_selected_tasks_deadline'; } + $rowCount = count($tasksById) + 1; + $modalBodyHeight = min(620, max(220, 56 + ($rowCount * 34))); + $modalHeight = min(760, $modalBodyHeight + 170); $formquestion = array(); $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); if ($finalAction == 'update_selected_tasks_progress') { - $tablehtml = ''; + $tablehtml = '
'; $tablehtml .= ''; foreach ($tasksById as $taskId => $taskDb) { $inputname = 'task_progress_'.$taskId; $tablehtml .= ''; $tablehtml .= ''; } - $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('Progress').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).' %
'; + $tablehtml .= ''; $formquestion[] = array('type' => 'other', 'name' => 'task_progress', 'label' => '', 'value' => $tablehtml); $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress'); } else { - $tablehtml = ''; + $tablehtml = '
'; $tablehtml .= ''; foreach ($tasksById as $taskId => $taskDb) { $datetimeName = 'task_datetime_'.$taskId; @@ -139,12 +142,39 @@ $tablehtml .= ''; $tablehtml .= ''; } - $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('DateHour').''.$langs->trans('MassActionKeepTaskDuration').'
'; + $tablehtml .= ''; $formquestion[] = array('type' => 'other', 'name' => 'task_datetime', 'label' => '', 'value' => $tablehtml); $titleform = ($finalAction == 'update_selected_tasks_start_date' ? $langs->trans('MassActionUpdateSelectedTasksStartDate') : $langs->trans('MassActionUpdateSelectedTasksDeadline')); } - print $form->formconfirm($_SERVER['PHP_SELF'], $titleform, $langs->trans('MassActionTaskPopupDescription'), $finalAction, $formquestion, '', 1, 500, 800, 0, 'Validate', 'Cancel'); + print $form->formconfirm($_SERVER['PHP_SELF'], $titleform, $langs->trans('MassActionTaskPopupDescription'), $finalAction, $formquestion, '', 1, $modalHeight, 800, 0, 'Validate', 'Cancel'); + print ''; } } } From ff9f5ae88ad8a580b1e229b246fcbb3f71ef1cc1 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:05:28 +0100 Subject: [PATCH 05/27] fix(project): ensure task massaction final action is posted and processed --- htdocs/core/tpl/massactions_pre.tpl.php | 9 +++++---- htdocs/projet/tasks/list.php | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 1f3a30118008d..8a4ca585c70e4 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -110,10 +110,11 @@ $finalAction = 'update_selected_tasks_deadline'; } $rowCount = count($tasksById) + 1; - $modalBodyHeight = min(620, max(220, 56 + ($rowCount * 34))); - $modalHeight = min(760, $modalBodyHeight + 170); - $formquestion = array(); - $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); + $modalBodyHeight = min(620, max(220, 56 + ($rowCount * 34))); + $modalHeight = min(760, $modalBodyHeight + 170); + $formquestion = array(); + $formquestion[] = array('type' => 'hidden', 'name' => 'massaction', 'value' => $finalAction); + $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); if ($finalAction == 'update_selected_tasks_progress') { $tablehtml = '
'; diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index 2d0de39adf45d..2adbe9a7f2fa5 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -274,6 +274,8 @@ $effectiveMassAction = ''; if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { $effectiveMassAction = $action; + } elseif (in_array($massaction, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massaction; } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { $effectiveMassAction = $massaction; } From 802c103d7d64cfcae56bc82b0f77f959ca2183ed Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:13:33 +0100 Subject: [PATCH 06/27] fix(project): post final task massaction key and add debug tracing --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- htdocs/projet/tasks/list.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 8a4ca585c70e4..b21b90ccfc74b 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -113,7 +113,7 @@ $modalBodyHeight = min(620, max(220, 56 + ($rowCount * 34))); $modalHeight = min(760, $modalBodyHeight + 170); $formquestion = array(); - $formquestion[] = array('type' => 'hidden', 'name' => 'massaction', 'value' => $finalAction); + $formquestion[] = array('type' => 'hidden', 'name' => 'massactiontaskfinal', 'value' => $finalAction); $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); if ($finalAction == 'update_selected_tasks_progress') { diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index 2adbe9a7f2fa5..ba1d0545f7f21 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -272,8 +272,12 @@ include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; $effectiveMassAction = ''; + $massactiontaskfinal = GETPOST('massactiontaskfinal', 'aZ09'); + dol_syslog(__FILE__." massaction='".$massaction."' action='".$action."' confirm='".$confirm."' massactiontaskfinal='".$massactiontaskfinal."'", LOG_DEBUG); if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { $effectiveMassAction = $action; + } elseif (in_array($massactiontaskfinal, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massactiontaskfinal; } elseif (in_array($massaction, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { $effectiveMassAction = $massaction; } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { @@ -288,6 +292,7 @@ } } $tasksById = $object->getAuthorizedTasksForMassAction($user, $toselectpost); + dol_syslog(__FILE__." effectiveMassAction='".$effectiveMassAction."' selected=".count($toselectpost)." authorized=".count($tasksById), LOG_DEBUG); if (empty($tasksById)) { setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); } else { From 753af7e449a03eeac6a67b42d83e12e8e33c6c6e Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:21:11 +0100 Subject: [PATCH 07/27] fix(project): add warning-level logs for task massaction flow --- htdocs/core/tpl/massactions_pre.tpl.php | 2 ++ htdocs/projet/tasks/list.php | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index b21b90ccfc74b..f1321ae4537ff 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -95,10 +95,12 @@ } if (in_array($massaction, array('preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'), true) && is_object($objecttmp) && $objecttmp->element == 'project_task') { + dol_syslog(__FILE__." render pre-massaction modal for massaction='".$massaction."' selected=".count((array) $toselect), LOG_WARNING); if (!$user->hasRight('projet', 'creer')) { setEventMessages($langs->trans('ErrorNotEnoughPermissions'), null, 'errors'); } else { $tasksById = $objecttmp->getAuthorizedTasksForMassAction($user, $toselect); + dol_syslog(__FILE__." pre-massaction authorized tasks=".count($tasksById), LOG_WARNING); if (empty($tasksById)) { setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); } else { diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index ba1d0545f7f21..fd59765e64f08 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -273,7 +273,7 @@ $effectiveMassAction = ''; $massactiontaskfinal = GETPOST('massactiontaskfinal', 'aZ09'); - dol_syslog(__FILE__." massaction='".$massaction."' action='".$action."' confirm='".$confirm."' massactiontaskfinal='".$massactiontaskfinal."'", LOG_DEBUG); + dol_syslog(__FILE__." massaction='".$massaction."' action='".$action."' confirm='".$confirm."' massactiontaskfinal='".$massactiontaskfinal."'", LOG_WARNING); if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { $effectiveMassAction = $action; } elseif (in_array($massactiontaskfinal, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { @@ -292,7 +292,7 @@ } } $tasksById = $object->getAuthorizedTasksForMassAction($user, $toselectpost); - dol_syslog(__FILE__." effectiveMassAction='".$effectiveMassAction."' selected=".count($toselectpost)." authorized=".count($tasksById), LOG_DEBUG); + dol_syslog(__FILE__." effectiveMassAction='".$effectiveMassAction."' selected=".count($toselectpost)." authorized=".count($tasksById), LOG_WARNING); if (empty($tasksById)) { setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); } else { @@ -303,6 +303,7 @@ $task = new Task($db); if ($task->fetch($taskId) <= 0) { $error++; + dol_syslog(__FILE__." fetch failed for taskId=".$taskId, LOG_WARNING); $task->error = empty($task->error) ? $langs->trans('ErrorRecordNotFound') : $task->error; $task->errors[] = $task->error; continue; @@ -351,6 +352,7 @@ if ($task->update($user) <= 0) { $error++; + dol_syslog(__FILE__." update failed for taskId=".$taskId." error=".$task->error, LOG_WARNING); if (!empty($task->errors)) { setEventMessages('', $task->errors, 'errors'); } else { @@ -358,6 +360,7 @@ } } else { $done++; + dol_syslog(__FILE__." update success for taskId=".$taskId." action=".$effectiveMassAction, LOG_WARNING); } } @@ -384,6 +387,8 @@ $action = 'list'; $massaction = ''; + } else { + dol_syslog(__FILE__." no effective mass action resolved (massaction='".$massaction."', action='".$action."', confirm='".$confirm."', massactiontaskfinal='".$massactiontaskfinal."')", LOG_WARNING); } } From 31db070dd1a8e2238c88873623796ab20dd288db Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:26:47 +0100 Subject: [PATCH 08/27] fix(project): add detailed massaction runtime traces --- htdocs/projet/tasks/list.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index fd59765e64f08..e8b0bd3fd7207 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -298,6 +298,7 @@ } else { $error = 0; $done = 0; + dol_syslog(__FILE__." start update loop action='".$effectiveMassAction."' ids=".implode(',', array_keys($tasksById)), LOG_WARNING); $db->begin(); foreach ($tasksById as $taskId => $taskDb) { $task = new Task($db); @@ -314,6 +315,7 @@ $task->status = Task::STATUS_CLOSED; } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { $progressraw = GETPOST('task_progress_'.$taskId, 'alphanohtml'); + dol_syslog(__FILE__." progress payload taskId=".$taskId." value='".$progressraw."'", LOG_WARNING); if ($progressraw === '' || !is_numeric($progressraw)) { $error++; $task->errors[] = $langs->trans('MassActionInvalidTaskProgressValue', $taskId); @@ -323,6 +325,7 @@ $task->status = ($task->progress >= 100 ? Task::STATUS_CLOSED : Task::STATUS_ONGOING); } elseif ($effectiveMassAction == 'update_selected_tasks_start_date' || $effectiveMassAction == 'update_selected_tasks_deadline') { $taskdatetime = GETPOST('task_datetime_'.$taskId, 'alphanohtml'); + dol_syslog(__FILE__." datetime payload taskId=".$taskId." value='".$taskdatetime."' keep_duration=".GETPOSTINT('keep_duration_'.$taskId), LOG_WARNING); $tasktimestamp = 0; if (!empty($taskdatetime)) { $tasktimestamp = dol_stringtotime(str_replace('T', ' ', $taskdatetime), 1); @@ -366,8 +369,10 @@ if ($error) { $db->rollback(); + dol_syslog(__FILE__." rollback action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); } else { $db->commit(); + dol_syslog(__FILE__." commit action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); } if ($done > 0 && !$error) { @@ -380,8 +385,10 @@ } elseif ($effectiveMassAction == 'update_selected_tasks_deadline') { setEventMessages($langs->trans('MassActionSelectedTasksDeadlineUpdated', $done), null, 'mesgs'); } + dol_syslog(__FILE__." success message sent action='".$effectiveMassAction."' done=".$done, LOG_WARNING); } elseif (!$error) { setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + dol_syslog(__FILE__." warning message sent action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); } } From df5ea6b4fa45e7357d2a49e4767ac380b85fd6d1 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:31:35 +0100 Subject: [PATCH 09/27] fix(project): submit per-task modal fields in ajax formconfirm --- htdocs/core/tpl/massactions_pre.tpl.php | 56 ++++++++++++++----------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index f1321ae4537ff..6a2a48e536ab0 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -118,37 +118,43 @@ $formquestion[] = array('type' => 'hidden', 'name' => 'massactiontaskfinal', 'value' => $finalAction); $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); - if ($finalAction == 'update_selected_tasks_progress') { - $tablehtml = '
'; - $tablehtml .= ''; - foreach ($tasksById as $taskId => $taskDb) { - $inputname = 'task_progress_'.$taskId; - $tablehtml .= ''; - $tablehtml .= ''; - } - $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('Progress').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).' %
'; - $formquestion[] = array('type' => 'other', 'name' => 'task_progress', 'label' => '', 'value' => $tablehtml); - $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress'); - } else { - $tablehtml = '
'; - $tablehtml .= ''; - foreach ($tasksById as $taskId => $taskDb) { - $datetimeName = 'task_datetime_'.$taskId; - $keepdurationname = 'keep_duration_'.$taskId; - if ($finalAction == 'update_selected_tasks_start_date') { - $currenttimestamp = (!empty($taskDb->dateo) ? (int) $db->jdate($taskDb->dateo) : dol_now()); - } else { - $currenttimestamp = (!empty($taskDb->datee) ? (int) $db->jdate($taskDb->datee) : dol_now()); + if ($finalAction == 'update_selected_tasks_progress') { + $progressInputNames = array(); + $tablehtml = '
'.$langs->trans('Task').''.$langs->trans('DateHour').''.$langs->trans('MassActionKeepTaskDuration').'
'; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $inputname = 'task_progress_'.$taskId; + $progressInputNames[] = $inputname; + $tablehtml .= ''; + $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('Progress').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).' %
'; + $formquestion[] = array('type' => 'other', 'name' => implode(',', $progressInputNames), 'label' => '', 'value' => $tablehtml); + $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress'); + } else { + $dateInputNames = array(); + $keepDurationNames = array(); + $tablehtml = '
'; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $datetimeName = 'task_datetime_'.$taskId; + $keepdurationname = 'keep_duration_'.$taskId; + $dateInputNames[] = $datetimeName; + $keepDurationNames[] = $keepdurationname; + if ($finalAction == 'update_selected_tasks_start_date') { + $currenttimestamp = (!empty($taskDb->dateo) ? (int) $db->jdate($taskDb->dateo) : dol_now()); + } else { + $currenttimestamp = (!empty($taskDb->datee) ? (int) $db->jdate($taskDb->datee) : dol_now()); } $datetimevalue = dol_print_date($currenttimestamp, '%Y-%m-%dT%H:%M'); $tablehtml .= ''; $tablehtml .= ''; $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('DateHour').''.$langs->trans('MassActionKeepTaskDuration').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).'
'; + $formquestion[] = array('type' => 'other', 'name' => implode(',', array_merge($dateInputNames, $keepDurationNames)), 'label' => '', 'value' => $tablehtml); + $titleform = ($finalAction == 'update_selected_tasks_start_date' ? $langs->trans('MassActionUpdateSelectedTasksStartDate') : $langs->trans('MassActionUpdateSelectedTasksDeadline')); } - $tablehtml .= ''; - $formquestion[] = array('type' => 'other', 'name' => 'task_datetime', 'label' => '', 'value' => $tablehtml); - $titleform = ($finalAction == 'update_selected_tasks_start_date' ? $langs->trans('MassActionUpdateSelectedTasksStartDate') : $langs->trans('MassActionUpdateSelectedTasksDeadline')); - } print $form->formconfirm($_SERVER['PHP_SELF'], $titleform, $langs->trans('MassActionTaskPopupDescription'), $finalAction, $formquestion, '', 1, $modalHeight, 800, 0, 'Validate', 'Cancel'); print ''; diff --git a/htdocs/langs/en_US/projects.lang b/htdocs/langs/en_US/projects.lang index 0387482332e43..4994030bfc714 100644 --- a/htdocs/langs/en_US/projects.lang +++ b/htdocs/langs/en_US/projects.lang @@ -349,3 +349,4 @@ MassActionSelectedTasksStartDateUpdated=%s task(s) start date updated. MassActionSelectedTasksDeadlineUpdated=%s task(s) deadline updated. MassActionInvalidTaskProgressValue=Invalid progress value for task ID %s. MassActionInvalidTaskDateValue=Invalid date value for task ID %s. +MassActionApplyDateToTasks=Update tasks below From d60e1ae33aef0135d190f75cfbd8beefe570ddd5 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:58:57 +0100 Subject: [PATCH 13/27] ui(project): add 5px buffer to task massaction modal body height --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index f1b8b6f89ab21..e3e7c4119d61e 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -186,7 +186,7 @@ function adjustProjectTaskMassactionModal() { var targetModalHeight = parseInt(jqWrapper.attr("data-modal-height"), 10) || 520; var maxModalHeight = Math.max(220, jQuery(window).height() - 100); var finalModalHeight = Math.min(targetModalHeight, maxModalHeight); - var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight, finalModalHeight - 150)); + var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight + 5, finalModalHeight - 150)); jqDialogContent.css("max-height", finalBodyHeight + "px"); jqDialogContent.css("overflow-y", "auto"); jqDialogContent.dialog("option", "height", finalModalHeight); From abd617a2734b8e6b5b45e835421088dbc2973cbd Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:06:57 +0100 Subject: [PATCH 14/27] ui(project): increase modal body extra height buffer to 10px --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index e3e7c4119d61e..442f6381cbd32 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -186,7 +186,7 @@ function adjustProjectTaskMassactionModal() { var targetModalHeight = parseInt(jqWrapper.attr("data-modal-height"), 10) || 520; var maxModalHeight = Math.max(220, jQuery(window).height() - 100); var finalModalHeight = Math.min(targetModalHeight, maxModalHeight); - var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight + 5, finalModalHeight - 150)); + var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight + 10, finalModalHeight - 150)); jqDialogContent.css("max-height", finalBodyHeight + "px"); jqDialogContent.css("overflow-y", "auto"); jqDialogContent.dialog("option", "height", finalModalHeight); From ef25b6fbe0e49bb34b4b22b125ed8ed0b07758ae Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:13:28 +0100 Subject: [PATCH 15/27] ui(project): compute modal body height from task rows and fixed header rows --- htdocs/core/tpl/massactions_pre.tpl.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 442f6381cbd32..9864a946eb49f 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -111,8 +111,14 @@ } else { $finalAction = 'update_selected_tasks_deadline'; } - $rowCount = count($tasksById) + 1; - $modalBodyHeight = min(620, max(220, 56 + ($rowCount * 34))); + $rowCount = count($tasksById); + $isDateAction = ($finalAction != 'update_selected_tasks_progress'); + $taskRowsHeight = $rowCount * 34; + $tableHeaderHeight = 34; + $updateTasksRowHeight = ($isDateAction ? 36 : 0); + $confirmQuestionRowHeight = 34; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight; + $modalBodyHeight = min(700, max(220, $computedBodyHeight)); $modalHeight = min(760, $modalBodyHeight + 170); $formquestion = array(); $currentProjectId = GETPOSTINT('id'); @@ -186,7 +192,7 @@ function adjustProjectTaskMassactionModal() { var targetModalHeight = parseInt(jqWrapper.attr("data-modal-height"), 10) || 520; var maxModalHeight = Math.max(220, jQuery(window).height() - 100); var finalModalHeight = Math.min(targetModalHeight, maxModalHeight); - var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight + 10, finalModalHeight - 150)); + var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight, finalModalHeight - 150)); jqDialogContent.css("max-height", finalBodyHeight + "px"); jqDialogContent.css("overflow-y", "auto"); jqDialogContent.dialog("option", "height", finalModalHeight); From 03c2e99538990bf1de457c48a508e909b9cb0797 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:18:32 +0100 Subject: [PATCH 16/27] ui(project): add 15px extra body height to task massaction modal --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 9864a946eb49f..8467925e46705 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -117,7 +117,7 @@ $tableHeaderHeight = 34; $updateTasksRowHeight = ($isDateAction ? 36 : 0); $confirmQuestionRowHeight = 34; - $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 15; $modalBodyHeight = min(700, max(220, $computedBodyHeight)); $modalHeight = min(760, $modalBodyHeight + 170); $formquestion = array(); From 55600f9cf0d3a4d79b42d25da82e906448ea25b6 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:23:20 +0100 Subject: [PATCH 17/27] ui(project): increase modal body extra offset to +25px --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 8467925e46705..397c510cc8f4e 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -117,7 +117,7 @@ $tableHeaderHeight = 34; $updateTasksRowHeight = ($isDateAction ? 36 : 0); $confirmQuestionRowHeight = 34; - $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 15; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 25; $modalBodyHeight = min(700, max(220, $computedBodyHeight)); $modalHeight = min(760, $modalBodyHeight + 170); $formquestion = array(); From 8a7ab6469cd777a4dd77d6723ce1d180db791083 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:27:31 +0100 Subject: [PATCH 18/27] ui(project): increase precomputed modal height budget before display --- htdocs/core/tpl/massactions_pre.tpl.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 397c510cc8f4e..9da497fa81150 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -119,7 +119,7 @@ $confirmQuestionRowHeight = 34; $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 25; $modalBodyHeight = min(700, max(220, $computedBodyHeight)); - $modalHeight = min(760, $modalBodyHeight + 170); + $modalHeight = min(900, $modalBodyHeight + 220); $formquestion = array(); $currentProjectId = GETPOSTINT('id'); if ($currentProjectId > 0) { @@ -192,7 +192,7 @@ function adjustProjectTaskMassactionModal() { var targetModalHeight = parseInt(jqWrapper.attr("data-modal-height"), 10) || 520; var maxModalHeight = Math.max(220, jQuery(window).height() - 100); var finalModalHeight = Math.min(targetModalHeight, maxModalHeight); - var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight, finalModalHeight - 150)); + var finalBodyHeight = Math.max(120, Math.min(targetBodyHeight, finalModalHeight - 120)); jqDialogContent.css("max-height", finalBodyHeight + "px"); jqDialogContent.css("overflow-y", "auto"); jqDialogContent.dialog("option", "height", finalModalHeight); From 20daed9e97cfa20a7271bfc49a48eb54b4a7b3da Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:32:40 +0100 Subject: [PATCH 19/27] ui(project): reduce modal body extra offset by 10px --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 9da497fa81150..1f64607817de6 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -117,7 +117,7 @@ $tableHeaderHeight = 34; $updateTasksRowHeight = ($isDateAction ? 36 : 0); $confirmQuestionRowHeight = 34; - $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 25; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 15; $modalBodyHeight = min(700, max(220, $computedBodyHeight)); $modalHeight = min(900, $modalBodyHeight + 220); $formquestion = array(); From 24e368e71adc601807e491a09ad8ea0e297b762c Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:35:13 +0100 Subject: [PATCH 20/27] ui(project): reduce modal body extra offset by 5px --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 1f64607817de6..4d7e64e1c5139 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -117,7 +117,7 @@ $tableHeaderHeight = 34; $updateTasksRowHeight = ($isDateAction ? 36 : 0); $confirmQuestionRowHeight = 34; - $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 15; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 10; $modalBodyHeight = min(700, max(220, $computedBodyHeight)); $modalHeight = min(900, $modalBodyHeight + 220); $formquestion = array(); From 933df9082e68f37b2045f3464d44b0e89358b527 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:39:55 +0100 Subject: [PATCH 21/27] ui(project): reduce modal body extra offset to +5px --- htdocs/core/tpl/massactions_pre.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 4d7e64e1c5139..2113146a05506 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -117,7 +117,7 @@ $tableHeaderHeight = 34; $updateTasksRowHeight = ($isDateAction ? 36 : 0); $confirmQuestionRowHeight = 34; - $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 10; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 5; $modalBodyHeight = min(700, max(220, $computedBodyHeight)); $modalHeight = min(900, $modalBodyHeight + 220); $formquestion = array(); From 06db76f25a389515254ea05e725a24a2192ec52d Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:50:41 +0100 Subject: [PATCH 22/27] Update massactions_pre.tpl.php --- htdocs/core/tpl/massactions_pre.tpl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 2113146a05506..60061ad7494f0 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -6,6 +6,7 @@ * Copyright (C) 2024-2025 MDW * Copyright (C) 2024 Frédéric France * Copyright (C) 2024 Ferran Marcet + * Copyright (C) 2026 Pierre Ardoin * * 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 From 328699e33bd327a1e1166feae0a8a1b71e5f3ce5 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:51:17 +0100 Subject: [PATCH 23/27] Update task.class.php --- htdocs/projet/class/task.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/projet/class/task.class.php b/htdocs/projet/class/task.class.php index 3c0bee1d39a19..544ff62c982f2 100644 --- a/htdocs/projet/class/task.class.php +++ b/htdocs/projet/class/task.class.php @@ -8,6 +8,7 @@ * Copyright (C) 2023 Gauthier VERDOL * Copyright (C) 2024-2025 MDW * Copyright (C) 2024 Vincent de Grandpré + * Copyright (C) 2026 Pierre Ardoin * * 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 From 4682845dba1bd1d9b0bd0b3ea87dfa6ba818b4a7 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:51:41 +0100 Subject: [PATCH 24/27] Update list.php --- htdocs/projet/tasks/list.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index e8b0bd3fd7207..3d0325d4dc94b 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -7,6 +7,7 @@ * Copyright (C) 2023 Gauthier VERDOL * Copyright (C) 2024-2025 MDW * Copyright (C) 2024-2025 Frédéric France + * Copyright (C) 2026 Pierre Ardoin * * 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 From f8e61c20fd6847ea2a4a417dca4ecc7f179c0119 Mon Sep 17 00:00:00 2001 From: Laurent Destailleur Date: Wed, 25 Mar 2026 22:51:43 +0100 Subject: [PATCH 25/27] Doc --- htdocs/conf/conf.php.example | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/htdocs/conf/conf.php.example b/htdocs/conf/conf.php.example index 926bb2a137bb9..8253ddcc55a8c 100644 --- a/htdocs/conf/conf.php.example +++ b/htdocs/conf/conf.php.example @@ -417,9 +417,26 @@ $dolibarr_cron_allow_cli='0'; // 2 will allow custom PHP inside website features even if your PHP setup does not protect you against dangerous system calls (this may be dangerous if you don't have a RCE protection like SELnux or Apparmor). // Default value: '0' // Examples: '0', '1' or '2' - +// // $dolibarr_website_allow_custom_php='0'; +// dolibarr_allow_localurl_for_webhooks +// ==================================== +// Allow webhooks to use a local url +// Default value: '0' +// Examples: '1' +// +// $dolibarr_allow_localurl_for_webhooks = '0'; + +// dolibarr_allow_unsecured_select_in_extrafields_filter +// ===================================================== +// Allow the use of subrequests inside USF IN filters +// Default value: '0' +// Examples: '1' +// +// $dolibarr_allow_unsecured_select_in_extrafields_filter = '0'; + + // php_session_save_handler // ======================== From b6375f9f8baff2d645b234fa35a71fb86f842f26 Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:52:07 +0100 Subject: [PATCH 26/27] Update tasks.php --- htdocs/projet/tasks.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/projet/tasks.php b/htdocs/projet/tasks.php index fe96dd6e70f0b..fee901afedd25 100644 --- a/htdocs/projet/tasks.php +++ b/htdocs/projet/tasks.php @@ -4,6 +4,7 @@ * Copyright (C) 2005-2017 Regis Houssin * Copyright (C) 2024-2025 MDW * Copyright (C) 2024-2025 Frédéric France + * Copyright (C) 2026 Pierre Ardoin * * 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 From f53e4d3547e3dad12d6542080b4276a7af7d7a6b Mon Sep 17 00:00:00 2001 From: Pierre Ardoin <32256817+mapiolca@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:03:01 +0100 Subject: [PATCH 27/27] fix(project tasks): align massaction labels and indentation --- htdocs/core/tpl/massactions_pre.tpl.php | 121 +++++++++++++++++++++ htdocs/langs/en_US/projects.lang | 13 +++ htdocs/projet/class/task.class.php | 41 +++++++ htdocs/projet/tasks.php | 122 ++++++++++++++++++++- htdocs/projet/tasks/list.php | 137 +++++++++++++++++++++++- 5 files changed, 430 insertions(+), 4 deletions(-) diff --git a/htdocs/core/tpl/massactions_pre.tpl.php b/htdocs/core/tpl/massactions_pre.tpl.php index 2e96ab1d3912e..dd43b79ed8fbf 100644 --- a/htdocs/core/tpl/massactions_pre.tpl.php +++ b/htdocs/core/tpl/massactions_pre.tpl.php @@ -94,6 +94,127 @@ print $form->formconfirm($_SERVER['PHP_SELF'] . '?id=' . $object->id . $selected, $langs->trans('ConfirmMassClone'), '', 'clonetasks', $formquestion, '', 1, 300, 590); } +if (in_array($massaction, array('preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'), true) && is_object($objecttmp) && $objecttmp->element == 'project_task') { + dol_syslog(__FILE__." render pre-massaction modal for massaction='".$massaction."' selected=".count((array) $toselect), LOG_WARNING); + if (!$user->hasRight('projet', 'creer')) { + setEventMessages($langs->trans('NotEnoughPermissions'), null, 'errors'); + } else { + $tasksById = $objecttmp->getAuthorizedTasksForMassAction($user, $toselect); + dol_syslog(__FILE__." pre-massaction authorized tasks=".count($tasksById), LOG_WARNING); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + if ($massaction == 'preupdate_selected_tasks_progress') { + $finalAction = 'update_selected_tasks_progress'; + } elseif ($massaction == 'preupdate_selected_tasks_start_date') { + $finalAction = 'update_selected_tasks_start_date'; + } else { + $finalAction = 'update_selected_tasks_deadline'; + } + $rowCount = count($tasksById); + $isDateAction = ($finalAction != 'update_selected_tasks_progress'); + $taskRowsHeight = $rowCount * 34; + $tableHeaderHeight = 34; + $updateTasksRowHeight = ($isDateAction ? 36 : 0); + $confirmQuestionRowHeight = 34; + $computedBodyHeight = $taskRowsHeight + $tableHeaderHeight + $updateTasksRowHeight + $confirmQuestionRowHeight + 5; + $modalBodyHeight = min(700, max(220, $computedBodyHeight)); + $modalHeight = min(900, $modalBodyHeight + 220); + $formquestion = array(); + $currentProjectId = GETPOSTINT('id'); + if ($currentProjectId > 0) { + $formquestion[] = array('type' => 'hidden', 'name' => 'id', 'value' => $currentProjectId); + } + $formquestion[] = array('type' => 'hidden', 'name' => 'massactiontaskfinal', 'value' => $finalAction); + $formquestion[] = array('type' => 'hidden', 'name' => 'toselect', 'value' => implode(',', array_keys($tasksById))); + + if ($finalAction == 'update_selected_tasks_progress') { + $progressInputNames = array(); + $tablehtml = '
'; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $inputname = 'task_progress_'.$taskId; + $progressInputNames[] = $inputname; + $tablehtml .= ''; + $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('Progress').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).' %
'; + $formquestion[] = array('type' => 'other', 'name' => implode(',', $progressInputNames), 'label' => '', 'value' => $tablehtml); + $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress'); + } else { + $dateInputNames = array(); + $keepDurationNames = array(); + $tablehtml = '
'; + $tablehtml .= '
'; + $tablehtml .= ''.$langs->trans('MassActionApplyDateToTasks').''; + $tablehtml .= ''; + $tablehtml .= ''; + $tablehtml .= '
'; + $tablehtml .= ''; + $tablehtml .= ''; + foreach ($tasksById as $taskId => $taskDb) { + $datetimeName = 'task_datetime_'.$taskId; + $keepdurationname = 'keep_duration_'.$taskId; + $dateInputNames[] = $datetimeName; + $keepDurationNames[] = $keepdurationname; + if ($finalAction == 'update_selected_tasks_start_date') { + $currenttimestamp = (!empty($taskDb->dateo) ? (int) $db->jdate($taskDb->dateo) : dol_now()); + } else { + $currenttimestamp = (!empty($taskDb->datee) ? (int) $db->jdate($taskDb->datee) : dol_now()); + } + $datetimevalue = dol_print_date($currenttimestamp, '%Y-%m-%dT%H:%M'); + $tablehtml .= ''; + $tablehtml .= ''; + $tablehtml .= ''; + } + $tablehtml .= '
'.$langs->trans('Task').''.$langs->trans('DateHour').''.$langs->trans('Duration').'
'.dol_escape_htmltag(!empty($taskDb->label) ? $taskDb->label : $taskDb->ref).'
'; + $formquestion[] = array('type' => 'other', 'name' => implode(',', array_merge($dateInputNames, $keepDurationNames)), 'label' => '', 'value' => $tablehtml); + $titleform = ($finalAction == 'update_selected_tasks_start_date' ? $langs->trans('MassActionUpdateSelectedTasksStartDate') : $langs->trans('MassActionUpdateSelectedTasksDeadline')); + } + + $pageforconfirm = $_SERVER['PHP_SELF']; + if ($currentProjectId > 0) { + $pageforconfirm .= (strpos($pageforconfirm, '?') === false ? '?' : '&').'id='.$currentProjectId; + } + print $form->formconfirm($pageforconfirm, $titleform, $langs->trans('MassActionTaskPopupDescription'), $finalAction, $formquestion, '', 1, $modalHeight, 800, 0, 'Validate', 'Cancel'); + print ''; + } + } +} + if ($massaction == 'preaffecttag' && isModEnabled('category')) { require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; $categ = new Categorie($db); diff --git a/htdocs/langs/en_US/projects.lang b/htdocs/langs/en_US/projects.lang index 221a9b8ea6b2a..4994030bfc714 100644 --- a/htdocs/langs/en_US/projects.lang +++ b/htdocs/langs/en_US/projects.lang @@ -337,3 +337,16 @@ TaskHourlyRateUpdated=Task's hourly rate updated UpdateWithLastHourlyRate=Update all recorded time spent of each user with the last hourly rate of the user UpdateUndefinedWithLastHourlyRate=Update the time spent that has no hourly rate defined with the last hourly rate of the user EnterUsersHourlyRateFirst=First, enter the hourly rate for the user(s)... +MassActionCloseSelectedTasks=Close selected project tasks +MassActionUpdateSelectedTasksProgress=Change progress of selected project tasks +MassActionUpdateSelectedTasksStartDate=Change start date of selected project tasks +MassActionUpdateSelectedTasksDeadline=Change deadline of selected project tasks +MassActionKeepTaskDuration=Keep duration +MassActionTaskPopupDescription=Confirm the mass action on selected tasks. +MassActionSelectedTasksClosed=%s task(s) closed. +MassActionSelectedTasksProgressUpdated=%s task(s) progress updated. +MassActionSelectedTasksStartDateUpdated=%s task(s) start date updated. +MassActionSelectedTasksDeadlineUpdated=%s task(s) deadline updated. +MassActionInvalidTaskProgressValue=Invalid progress value for task ID %s. +MassActionInvalidTaskDateValue=Invalid date value for task ID %s. +MassActionApplyDateToTasks=Update tasks below diff --git a/htdocs/projet/class/task.class.php b/htdocs/projet/class/task.class.php index 79c4ba0c3c0de..3c0bee1d39a19 100644 --- a/htdocs/projet/class/task.class.php +++ b/htdocs/projet/class/task.class.php @@ -620,6 +620,47 @@ public function fetch($id, $ref = '', $loadparentdata = 0) } } + /** + * Return authorized tasks for selected ids. + * + * @param User $user Current user + * @param array $toselect List of selected task ids + * @return array Tasks indexed by task id + */ + public function getAuthorizedTasksForMassAction($user, $toselect) + { + $toselect = array_unique(array_filter(array_map('intval', (array) $toselect))); + if (empty($toselect)) { + return array(); + } + + $projectstatic = new Project($this->db); + $projectlistfilter = ''; + if (!$user->hasRight('projet', 'all', 'lire')) { + $projectsListId = $projectstatic->getProjectsAuthorizedForUser($user, 0, 1, 0); + $projectlistfilter = " AND p.rowid IN (".$this->db->sanitize($projectsListId ? $projectsListId : '0').")"; + } + + $sql = "SELECT t.rowid, t.ref, t.label, t.dateo, t.datee, t.progress, t.fk_statut, t.fk_projet"; + $sql .= " FROM ".MAIN_DB_PREFIX."projet_task AS t"; + $sql .= " INNER JOIN ".MAIN_DB_PREFIX."projet AS p ON p.rowid = t.fk_projet"; + $sql .= " WHERE t.rowid IN (".implode(',', $toselect).")"; + $sql .= " AND p.entity IN (".getEntity('project').")"; + $sql .= $projectlistfilter; + + $resql = $this->db->query($sql); + if (!$resql) { + return array(); + } + + $tasksById = array(); + while ($obj = $this->db->fetch_object($resql)) { + $tasksById[(int) $obj->rowid] = $obj; + } + + return $tasksById; + } + /** * Update database diff --git a/htdocs/projet/tasks.php b/htdocs/projet/tasks.php index 546331f1580c9..fe96dd6e70f0b 100644 --- a/htdocs/projet/tasks.php +++ b/htdocs/projet/tasks.php @@ -216,7 +216,7 @@ $action = 'list'; $massaction = ''; } -if (!GETPOST('confirmmassaction', 'alpha') && $massaction != 'presend' && $massaction != 'confirm_presend') { +if (!GETPOST('confirmmassaction', 'alpha') && !in_array($massaction, array('presend', 'confirm_presend', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline', 'close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true)) { $massaction = ''; } @@ -272,9 +272,123 @@ $objectclass = 'Task'; $objectlabel = 'Tasks'; $permissiontoread = $user->hasRight('projet', 'lire'); + $permissiontocreate = $user->hasRight('projet', 'creer'); $permissiontodelete = $user->hasRight('projet', 'supprimer'); $uploaddir = $conf->project->dir_output.'/tasks'; include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; + + $effectiveMassAction = ''; + $massactiontaskfinal = GETPOST('massactiontaskfinal', 'aZ09'); + dol_syslog(__FILE__." massaction='".$massaction."' action='".$action."' confirm='".$confirm."' massactiontaskfinal='".$massactiontaskfinal."'", LOG_WARNING); + if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $action; + } elseif (in_array($massactiontaskfinal, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massactiontaskfinal; + } elseif (in_array($massaction, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massaction; + } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { + $effectiveMassAction = $massaction; + } + if ($permissiontocreate && !empty($effectiveMassAction)) { + $toselectpost = GETPOST('toselect', 'array:int'); + if (empty($toselectpost)) { + $toselectcsv = GETPOST('toselect', 'alphanohtml'); + if (!empty($toselectcsv)) { + $toselectpost = array_map('intval', explode(',', $toselectcsv)); + } + } + $tasksById = $taskstatic->getAuthorizedTasksForMassAction($user, $toselectpost); + dol_syslog(__FILE__." effectiveMassAction='".$effectiveMassAction."' selected=".count($toselectpost)." authorized=".count($tasksById), LOG_WARNING); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + $error = 0; + $done = 0; + $db->begin(); + foreach ($tasksById as $taskId => $taskDb) { + $task = new Task($db); + if ($task->fetch($taskId) <= 0) { + $error++; + continue; + } + + if ($effectiveMassAction == 'close_selected_tasks') { + $task->progress = 100; + $task->status = Task::STATUS_CLOSED; + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + $progressraw = GETPOST('task_progress_'.$taskId, 'alphanohtml'); + if ($progressraw === '' || !is_numeric($progressraw)) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskProgressValue', $taskId); + continue; + } + $task->progress = max(0, min(100, (int) $progressraw)); + $task->status = ($task->progress >= 100 ? Task::STATUS_CLOSED : Task::STATUS_ONGOING); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date' || $effectiveMassAction == 'update_selected_tasks_deadline') { + $taskdatetime = GETPOST('task_datetime_'.$taskId, 'alphanohtml'); + $tasktimestamp = 0; + if (!empty($taskdatetime)) { + $tasktimestamp = dol_stringtotime(str_replace('T', ' ', $taskdatetime), 1); + } + if (empty($tasktimestamp) || $tasktimestamp < 0) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskDateValue', $taskId); + continue; + } + $keepduration = GETPOSTINT('keep_duration_'.$taskId); + $oldstart = (!empty($task->date_start) ? (int) $task->date_start : 0); + $oldend = (!empty($task->date_end) ? (int) $task->date_end : 0); + $durationseconds = ($oldstart > 0 && $oldend > 0 ? ($oldend - $oldstart) : null); + + if ($effectiveMassAction == 'update_selected_tasks_start_date') { + $task->date_start = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_end = $tasktimestamp + $durationseconds; + } + } else { + $task->date_end = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_start = $tasktimestamp - $durationseconds; + } + } + } + + if ($task->update($user) <= 0) { + $error++; + if (!empty($task->errors)) { + setEventMessages('', $task->errors, 'errors'); + } else { + setEventMessages($task->error, null, 'errors'); + } + } else { + $done++; + } + } + + if ($error) { + $db->rollback(); + } else { + $db->commit(); + } + + if ($done > 0 && !$error) { + if ($effectiveMassAction == 'close_selected_tasks') { + setEventMessages($langs->trans('MassActionSelectedTasksClosed', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + setEventMessages($langs->trans('MassActionSelectedTasksProgressUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date') { + setEventMessages($langs->trans('MassActionSelectedTasksStartDateUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_deadline') { + setEventMessages($langs->trans('MassActionSelectedTasksDeadlineUpdated', $done), null, 'mesgs'); + } + } elseif (!$error) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } + } + + $action = 'list'; + $massaction = ''; + } } $morewherefilterarray = array(); @@ -607,11 +721,15 @@ $arrayofmassactions = array(); if ($user->hasRight('projet', 'creer')) { $arrayofmassactions['preclonetasks'] = img_picto('', 'clone', 'class="pictofixedwidth"').$langs->trans("Clone"); + $arrayofmassactions['close_selected_tasks'] = img_picto('', 'tick', 'class="pictofixedwidth"').$langs->trans("MassActionCloseSelectedTasks"); + $arrayofmassactions['preupdate_selected_tasks_progress'] = img_picto('', 'projecttask', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksProgress"); + $arrayofmassactions['preupdate_selected_tasks_start_date'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksStartDate"); + $arrayofmassactions['preupdate_selected_tasks_deadline'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksDeadline"); } if ($permissiontodelete) { $arrayofmassactions['predelete'] = img_picto('', 'delete', 'class="pictofixedwidth"').$langs->trans("Delete"); } - if (in_array($massaction, array('presend', 'predelete'))) { + if (in_array($massaction, array('presend', 'predelete', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'), true)) { $arrayofmassactions = array(); } $massactionbutton = $form->selectMassAction('', $arrayofmassactions); diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index 7d3b21a12a433..e8b0bd3fd7207 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -212,7 +212,7 @@ $action = 'list'; $massaction = ''; } -if (!GETPOST('confirmmassaction', 'alpha') && $massaction != 'presend' && $massaction != 'confirm_presend') { +if (!GETPOST('confirmmassaction', 'alpha') && !in_array($massaction, array('presend', 'confirm_presend', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline', 'close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true)) { $massaction = ''; } @@ -270,6 +270,133 @@ $objectlabel = 'Tasks'; $uploaddir = $conf->project->dir_output.'/tasks'; include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php'; + + $effectiveMassAction = ''; + $massactiontaskfinal = GETPOST('massactiontaskfinal', 'aZ09'); + dol_syslog(__FILE__." massaction='".$massaction."' action='".$action."' confirm='".$confirm."' massactiontaskfinal='".$massactiontaskfinal."'", LOG_WARNING); + if (in_array($action, array('close_selected_tasks', 'update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $action; + } elseif (in_array($massactiontaskfinal, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massactiontaskfinal; + } elseif (in_array($massaction, array('update_selected_tasks_progress', 'update_selected_tasks_start_date', 'update_selected_tasks_deadline'), true) && $confirm == 'yes') { + $effectiveMassAction = $massaction; + } elseif ($massaction == 'close_selected_tasks' && GETPOST('confirmmassaction', 'alpha')) { + $effectiveMassAction = $massaction; + } + if ($permissiontocreate && !empty($effectiveMassAction)) { + $toselectpost = GETPOST('toselect', 'array:int'); + if (empty($toselectpost)) { + $toselectcsv = GETPOST('toselect', 'alphanohtml'); + if (!empty($toselectcsv)) { + $toselectpost = array_map('intval', explode(',', $toselectcsv)); + } + } + $tasksById = $object->getAuthorizedTasksForMassAction($user, $toselectpost); + dol_syslog(__FILE__." effectiveMassAction='".$effectiveMassAction."' selected=".count($toselectpost)." authorized=".count($tasksById), LOG_WARNING); + if (empty($tasksById)) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + } else { + $error = 0; + $done = 0; + dol_syslog(__FILE__." start update loop action='".$effectiveMassAction."' ids=".implode(',', array_keys($tasksById)), LOG_WARNING); + $db->begin(); + foreach ($tasksById as $taskId => $taskDb) { + $task = new Task($db); + if ($task->fetch($taskId) <= 0) { + $error++; + dol_syslog(__FILE__." fetch failed for taskId=".$taskId, LOG_WARNING); + $task->error = empty($task->error) ? $langs->trans('ErrorRecordNotFound') : $task->error; + $task->errors[] = $task->error; + continue; + } + + if ($effectiveMassAction == 'close_selected_tasks') { + $task->progress = 100; + $task->status = Task::STATUS_CLOSED; + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + $progressraw = GETPOST('task_progress_'.$taskId, 'alphanohtml'); + dol_syslog(__FILE__." progress payload taskId=".$taskId." value='".$progressraw."'", LOG_WARNING); + if ($progressraw === '' || !is_numeric($progressraw)) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskProgressValue', $taskId); + continue; + } + $task->progress = max(0, min(100, (int) $progressraw)); + $task->status = ($task->progress >= 100 ? Task::STATUS_CLOSED : Task::STATUS_ONGOING); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date' || $effectiveMassAction == 'update_selected_tasks_deadline') { + $taskdatetime = GETPOST('task_datetime_'.$taskId, 'alphanohtml'); + dol_syslog(__FILE__." datetime payload taskId=".$taskId." value='".$taskdatetime."' keep_duration=".GETPOSTINT('keep_duration_'.$taskId), LOG_WARNING); + $tasktimestamp = 0; + if (!empty($taskdatetime)) { + $tasktimestamp = dol_stringtotime(str_replace('T', ' ', $taskdatetime), 1); + } + if (empty($tasktimestamp) || $tasktimestamp < 0) { + $error++; + $task->errors[] = $langs->trans('MassActionInvalidTaskDateValue', $taskId); + continue; + } + $keepduration = GETPOSTINT('keep_duration_'.$taskId); + $oldstart = (!empty($task->date_start) ? (int) $task->date_start : 0); + $oldend = (!empty($task->date_end) ? (int) $task->date_end : 0); + $durationseconds = ($oldstart > 0 && $oldend > 0 ? ($oldend - $oldstart) : null); + + if ($effectiveMassAction == 'update_selected_tasks_start_date') { + $task->date_start = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_end = $tasktimestamp + $durationseconds; + } + } else { + $task->date_end = $tasktimestamp; + if ($keepduration && $durationseconds !== null) { + $task->date_start = $tasktimestamp - $durationseconds; + } + } + } + + if ($task->update($user) <= 0) { + $error++; + dol_syslog(__FILE__." update failed for taskId=".$taskId." error=".$task->error, LOG_WARNING); + if (!empty($task->errors)) { + setEventMessages('', $task->errors, 'errors'); + } else { + setEventMessages($task->error, null, 'errors'); + } + } else { + $done++; + dol_syslog(__FILE__." update success for taskId=".$taskId." action=".$effectiveMassAction, LOG_WARNING); + } + } + + if ($error) { + $db->rollback(); + dol_syslog(__FILE__." rollback action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); + } else { + $db->commit(); + dol_syslog(__FILE__." commit action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); + } + + if ($done > 0 && !$error) { + if ($effectiveMassAction == 'close_selected_tasks') { + setEventMessages($langs->trans('MassActionSelectedTasksClosed', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_progress') { + setEventMessages($langs->trans('MassActionSelectedTasksProgressUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_start_date') { + setEventMessages($langs->trans('MassActionSelectedTasksStartDateUpdated', $done), null, 'mesgs'); + } elseif ($effectiveMassAction == 'update_selected_tasks_deadline') { + setEventMessages($langs->trans('MassActionSelectedTasksDeadlineUpdated', $done), null, 'mesgs'); + } + dol_syslog(__FILE__." success message sent action='".$effectiveMassAction."' done=".$done, LOG_WARNING); + } elseif (!$error) { + setEventMessages($langs->trans('NoRecordSelected'), null, 'warnings'); + dol_syslog(__FILE__." warning message sent action='".$effectiveMassAction."' done=".$done." error=".$error, LOG_WARNING); + } + } + + $action = 'list'; + $massaction = ''; + } else { + dol_syslog(__FILE__." no effective mass action resolved (massaction='".$massaction."', action='".$action."', confirm='".$confirm."', massactiontaskfinal='".$massactiontaskfinal."')", LOG_WARNING); + } } // already done at line 85 @@ -734,7 +861,13 @@ if (!empty($permissiontodelete)) { $arrayofmassactions['predelete'] = img_picto('', 'delete', 'class="pictofixedwidth"').$langs->trans("Delete"); } -if (GETPOSTINT('nomassaction') || in_array($massaction, array('presend', 'predelete'))) { +if ($permissiontocreate) { + $arrayofmassactions['close_selected_tasks'] = img_picto('', 'tick', 'class="pictofixedwidth"').$langs->trans("MassActionCloseSelectedTasks"); + $arrayofmassactions['preupdate_selected_tasks_progress'] = img_picto('', 'projecttask', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksProgress"); + $arrayofmassactions['preupdate_selected_tasks_start_date'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksStartDate"); + $arrayofmassactions['preupdate_selected_tasks_deadline'] = img_picto('', 'calendar', 'class="pictofixedwidth"').$langs->trans("MassActionUpdateSelectedTasksDeadline"); +} +if (GETPOSTINT('nomassaction') || in_array($massaction, array('presend', 'predelete', 'preupdate_selected_tasks_progress', 'preupdate_selected_tasks_start_date', 'preupdate_selected_tasks_deadline'))) { $arrayofmassactions = array(); } $massactionbutton = $form->selectMassAction('', $arrayofmassactions);