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 = '
';
+ $formquestion[] = array('type' => 'other', 'name' => 'task_progress', 'label' => '', 'value' => $tablehtml);
+ $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress');
+ } else {
+ $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');
+ }
+ }
+}
+
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 = '';
$formquestion[] = array('type' => 'other', 'name' => 'task_progress', 'label' => '', 'value' => $tablehtml);
$titleform = $langs->trans('MassActionUpdateSelectedTasksProgress');
} else {
- $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 = '';
- $formquestion[] = array('type' => 'other', 'name' => 'task_progress', 'label' => '', 'value' => $tablehtml);
- $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress');
- } else {
- $tablehtml = '';
- $tablehtml .= '| '.$langs->trans('Task').' | '.$langs->trans('DateHour').' | '.$langs->trans('MassActionKeepTaskDuration').' |
';
- 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 = '';
+ $formquestion[] = array('type' => 'other', 'name' => implode(',', $progressInputNames), 'label' => '', 'value' => $tablehtml);
+ $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress');
+ } else {
+ $dateInputNames = array();
+ $keepDurationNames = array();
+ $tablehtml = '';
+ $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 = '';
+ $formquestion[] = array('type' => 'other', 'name' => implode(',', $progressInputNames), 'label' => '', 'value' => $tablehtml);
+ $titleform = $langs->trans('MassActionUpdateSelectedTasksProgress');
+ } else {
+ $dateInputNames = array();
+ $keepDurationNames = array();
+ $tablehtml = '';
+ $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);