diff --git a/ChangeLog.md b/ChangeLog.md index 12f9af5..bd436ed 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,10 @@ # CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) +## 1.2.0 +- Ajoute le support des contrats « Cadre au forfait jour » avec sélecteurs Journée/Matin/Après-midi et conversion automatique des durées en base. / Adds support for "daily rate" contracts with Full day/Morning/Afternoon selectors and automatic duration conversion. +- Crée l'extrafield salarié « Contrat forfait jour » et stocke la sélection correspondante dans la colonne dédiée. / Creates the employee extrafield "Daily rate contract" and stores the related selection in the dedicated column. +- Complète les traductions « LastModification » et « TotalDays » pour harmoniser l'affichage du forfait jour. / Completes the "LastModification" and "TotalDays" translations to harmonise the daily rate display. + ## 1.1.1 - Mise à "plat" des permissions pour régler un problème d'affichage des PDF. / "Flattening" permissions to fix a PDF display issue. diff --git a/README.md b/README.md index 86fcf41..5e4e1cf 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ TimesheetWeek ajoute une gestion hebdomadaire des feuilles de temps fidèle à l - Statut « Scellée » pour verrouiller les feuilles approuvées et empêcher toute modification ultérieure, avec les permissions associées. - Redirection automatique vers la feuille existante en cas de tentative de doublon afin d'éviter les saisies multiples. - Suivi des compteurs hebdomadaires de zones et de paniers directement sur les feuilles et recalcul automatique à chaque enregistrement. +- Saisie dédiée pour les salariés en forfait jour grâce à des sélecteurs Journée/Matin/Après-midi convertissant automatiquement les heures. - Affichage des compteurs dans la liste hebdomadaire et ajout du libellé « Zone » sur chaque sélecteur quotidien pour clarifier la saisie. - Ligne de total en bas de la liste hebdomadaire pour additionner heures, zones, paniers et afficher la colonne de date de validation. - Création rapide d'une feuille d'heures via le raccourci « Ajouter » du menu supérieur. @@ -48,6 +49,7 @@ TimesheetWeek delivers weekly timesheet management that follows Dolibarr design - Statut « Scellée » (Sealed status) to lock approved timesheets together with the related permissions. - Automatic redirect to the existing timesheet when a duplicate creation is attempted. - Weekly counters for zones and meal allowances with automatic recomputation on each save. +- Dedicated input for daily rate employees with Full day/Morning/Afternoon selectors that automatically convert hours. - Counter display inside the weekly list plus a « Zone » caption on each daily selector for better input guidance. - Total row at the bottom of the weekly list to sum hours, zones, meals and expose the validation date column. - Quick creation shortcut available from the top-right « Add » menu. diff --git a/class/timesheetweekline.class.php b/class/timesheetweekline.class.php index 402fe94..0c53882 100644 --- a/class/timesheetweekline.class.php +++ b/class/timesheetweekline.class.php @@ -1,145 +1,169 @@ +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ /** - * \file timesheetweek/class/timesheetweekline.class.php - * \ingroup timesheetweek - * \brief TimesheetWeekLine class file - */ +* \file timesheetweek/class/timesheetweekline.class.php +* \ingroup timesheetweek +* \brief TimesheetWeekLine class file +*/ require_once DOL_DOCUMENT_ROOT.'/core/class/commonobjectline.class.php'; class TimesheetWeekLine extends CommonObjectLine { - public $element = 'timesheetweekline'; - public $table_element = 'timesheet_week_line'; - public $fk_element = 'fk_timesheet_week'; - public $parent_element = 'timesheetweek'; + public $element = 'timesheetweekline'; + public $table_element = 'timesheet_week_line'; + public $fk_element = 'fk_timesheet_week'; + public $parent_element = 'timesheetweek'; - // EN: Entity identifier bound to the line. - // FR: Identifiant d'entité lié à la ligne. - public $entity; - public $fk_timesheet_week; - public $fk_task; - public $day_date; - public $hours; - public $zone; - public $meal; + // EN: Entity identifier bound to the line. + // FR: Identifiant d'entité lié à la ligne. + public $entity; + public $fk_timesheet_week; + public $fk_task; + public $day_date; + public $hours; + // EN: Stores the selected daily rate value linked to forfait-jour entries. + // FR: Stocke la valeur de forfait-jour sélectionnée pour les saisies concernées. + public $daily_rate; + public $zone; + public $meal; - /** - * Fetches a timesheet line by its rowid. - * Récupère une ligne de feuille de temps via son rowid. - * - * @param int $id Row identifier / Identifiant de ligne - * @return int >0 success, <=0 error / >0 succès, <=0 erreur - */ - public function fetch($id) - { - if (empty($id)) { - return 0; - } + /** + * Fetches a timesheet line by its rowid. + * Récupère une ligne de feuille de temps via son rowid. + * + * @param int $id Row identifier / Identifiant de ligne + * @return int >0 success, <=0 error / >0 succès, <=0 erreur + */ + public function fetch($id) + { + if (empty($id)) { + return 0; + } - // Build the query for the line / Construit la requête pour la ligne - $sql = "SELECT rowid, entity, fk_timesheet_week, fk_task, day_date, hours, zone, meal"; - $sql .= " FROM ".MAIN_DB_PREFIX."timesheet_week_line"; - $sql .= " WHERE rowid=".(int) $id; - // EN: Respect module entity permissions during line fetch. - // FR: Respecte les permissions d'entité du module lors du chargement de la ligne. - $sql .= " AND entity IN (".getEntity('timesheetweek').")"; + // Build the query for the line / Construit la requête pour la ligne + $sql = "SELECT rowid, entity, fk_timesheet_week, fk_task, day_date, hours, daily_rate, zone, meal"; + $sql .= " FROM ".MAIN_DB_PREFIX."timesheet_week_line"; + $sql .= " WHERE rowid=".(int) $id; + // EN: Respect module entity permissions during line fetch. + // FR: Respecte les permissions d'entité du module lors du chargement de la ligne. + $sql .= " AND entity IN (".getEntity('timesheetweek').")"; - $resql = $this->db->query($sql); - if (!$resql) { - $this->error = $this->db->lasterror(); - return -1; - } + $resql = $this->db->query($sql); + if (!$resql) { + $this->error = $this->db->lasterror(); + return -1; + } - $obj = $this->db->fetch_object($resql); - $this->db->free($resql); - if (!$obj) { - return 0; - } + $obj = $this->db->fetch_object($resql); + $this->db->free($resql); + if (!$obj) { + return 0; + } - // Map database values to object / Mappe les valeurs de la base vers l'objet - $this->id = (int) $obj->rowid; - $this->rowid = (int) $obj->rowid; - $this->entity = (int) $obj->entity; - $this->fk_timesheet_week = (int) $obj->fk_timesheet_week; - $this->fk_task = (int) $obj->fk_task; - $this->day_date = $obj->day_date; - $this->hours = (float) $obj->hours; - $this->zone = (int) $obj->zone; - $this->meal = (int) $obj->meal; + // Map database values to object / Mappe les valeurs de la base vers l'objet + $this->id = (int) $obj->rowid; + $this->rowid = (int) $obj->rowid; + $this->entity = (int) $obj->entity; + $this->fk_timesheet_week = (int) $obj->fk_timesheet_week; + $this->fk_task = (int) $obj->fk_task; + $this->day_date = $obj->day_date; + $this->hours = (float) $obj->hours; + $this->daily_rate = (int) $obj->daily_rate; + $this->zone = (int) $obj->zone; + $this->meal = (int) $obj->meal; - return 1; - } + return 1; + } - /** - * Crée ou met à jour la ligne si elle existe déjà - */ - public function save($user) - { - // EN: Resolve the entity from the parent sheet when not provided. - // FR: Récupère l'entité depuis la feuille parente lorsqu'elle n'est pas fournie. - if (empty($this->entity) && !empty($this->fk_timesheet_week)) { - $sqlEntity = "SELECT entity FROM ".MAIN_DB_PREFIX."timesheet_week WHERE rowid=".(int) $this->fk_timesheet_week; - $sqlEntity .= " AND entity IN (".getEntity('timesheetweek').")"; - $resEntity = $this->db->query($sqlEntity); - if ($resEntity) { - $objEntity = $this->db->fetch_object($resEntity); - if ($objEntity) { - $this->entity = (int) $objEntity->entity; - } - } - } - if (empty($this->entity)) { - global $conf; - // EN: Fall back to the current context entity as a last resort. - // FR: Revient en dernier recours à l'entité du contexte courant. - $this->entity = isset($conf->entity) ? (int) $conf->entity : 1; - } +/** +* Saves the line or updates it when already stored. +* Crée ou met à jour la ligne si elle existe déjà. +*/ + public function save($user) + { + // EN: Resolve the entity from the parent sheet when not provided. + // FR: Récupère l'entité depuis la feuille parente lorsqu'elle n'est pas fournie. + if (empty($this->entity) && !empty($this->fk_timesheet_week)) { + $sqlEntity = "SELECT entity FROM ".MAIN_DB_PREFIX."timesheet_week WHERE rowid=".(int) $this->fk_timesheet_week; + $sqlEntity .= " AND entity IN (".getEntity('timesheetweek').")"; + $resEntity = $this->db->query($sqlEntity); + if ($resEntity) { + $objEntity = $this->db->fetch_object($resEntity); + if ($objEntity) { + $this->entity = (int) $objEntity->entity; + } + } + } + if (empty($this->entity)) { + global $conf; + // EN: Fall back to the current context entity as a last resort. + // FR: Revient en dernier recours à l'entité du contexte courant. + $this->entity = isset($conf->entity) ? (int) $conf->entity : 1; + } + + // EN: Normalize the stored daily rate to avoid null values reaching SQL. + // FR: Normalise la valeur de forfait-jour pour éviter d'envoyer des NULL en SQL. + if ($this->daily_rate === null) { + $this->daily_rate = 0; + } - // Vérifie si une ligne existe déjà pour cette tâche et ce jour - $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."timesheet_week_line"; - $sql .= " WHERE fk_timesheet_week = ".((int)$this->fk_timesheet_week); - $sql .= " AND fk_task = ".((int)$this->fk_task); - $sql .= " AND day_date = '".$this->db->escape($this->day_date)."'"; - // EN: Keep lookups limited to lines inside allowed entities. - // FR: Limite les recherches aux lignes situées dans les entités autorisées. - $sql .= " AND entity IN (".getEntity('timesheetweek').")"; +// EN: Check whether a line already exists for this task and day. +// FR: Vérifie si une ligne existe déjà pour cette tâche et ce jour. + $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."timesheet_week_line"; + $sql .= " WHERE fk_timesheet_week = ".((int)$this->fk_timesheet_week); + $sql .= " AND fk_task = ".((int)$this->fk_task); + $sql .= " AND day_date = '".$this->db->escape($this->day_date)."'"; + // EN: Keep lookups limited to lines inside allowed entities. + // FR: Limite les recherches aux lignes situées dans les entités autorisées. + $sql .= " AND entity IN (".getEntity('timesheetweek').")"; - $resql = $this->db->query($sql); - if ($resql && $this->db->num_rows($resql) > 0) { - // ---- UPDATE ---- - $obj = $this->db->fetch_object($resql); - $sqlu = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line SET "; - $sqlu .= " hours = ".((float)$this->hours).","; - $sqlu .= " zone = ".((int)$this->zone).","; - $sqlu .= " meal = ".((int)$this->meal); - $sqlu .= " WHERE rowid = ".((int)$obj->rowid); - // EN: Ensure updates stay within the permitted entity scope. - // FR: Assure que les mises à jour restent dans le périmètre d'entité autorisé. - $sqlu .= " AND entity IN (".getEntity('timesheetweek').")"; - return $this->db->query($sqlu) ? 1 : -1; - } - else { - // ---- INSERT ---- - // EN: Persist the entity alongside the usual line fields for consistency. - // FR: Enregistre l'entité avec les champs habituels de la ligne pour rester cohérent. - $sqli = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line("; - $sqli .= " entity, fk_timesheet_week, fk_task, day_date, hours, zone, meal)"; - $sqli .= " VALUES("; - $sqli .= (int)$this->entity.","; - $sqli .= (int)$this->fk_timesheet_week.","; - $sqli .= (int)$this->fk_task.","; - $sqli .= "'".$this->db->escape($this->day_date)."',"; - $sqli .= (float)$this->hours.","; - $sqli .= (int)$this->zone.","; - $sqli .= (int)$this->meal.")"; - return $this->db->query($sqli) ? 1 : -1; - } - } -} \ No newline at end of file + $resql = $this->db->query($sql); + if ($resql && $this->db->num_rows($resql) > 0) { + // ---- UPDATE ---- + $obj = $this->db->fetch_object($resql); + $sqlu = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line SET "; + $sqlu .= " hours = ".((float)$this->hours).","; + $sqlu .= " daily_rate = ".((int)$this->daily_rate).","; + $sqlu .= " zone = ".((int)$this->zone).","; + $sqlu .= " meal = ".((int)$this->meal); + $sqlu .= " WHERE rowid = ".((int)$obj->rowid); + // EN: Ensure updates stay within the permitted entity scope. + // FR: Assure que les mises à jour restent dans le périmètre d'entité autorisé. + $sqlu .= " AND entity IN (".getEntity('timesheetweek').")"; + return $this->db->query($sqlu) ? 1 : -1; + } + else { + // ---- INSERT ---- + // EN: Persist the entity alongside the usual line fields for consistency. + // FR: Enregistre l'entité avec les champs habituels de la ligne pour rester cohérent. + $sqli = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line("; + $sqli .= " entity, fk_timesheet_week, fk_task, day_date, hours, daily_rate, zone, meal)"; + $sqli .= " VALUES("; + $sqli .= (int)$this->entity.","; + $sqli .= (int)$this->fk_timesheet_week.","; + $sqli .= (int)$this->fk_task.","; + $sqli .= "'".$this->db->escape($this->day_date)."',"; + $sqli .= (float)$this->hours.","; + $sqli .= (int)$this->daily_rate.","; + $sqli .= (int)$this->zone.","; + $sqli .= (int)$this->meal.")"; + return $this->db->query($sqli) ? 1 : -1; + } + } +} diff --git a/core/modules/modTimesheetWeek.class.php b/core/modules/modTimesheetWeek.class.php index 982faa2..effc835 100644 --- a/core/modules/modTimesheetWeek.class.php +++ b/core/modules/modTimesheetWeek.class.php @@ -80,8 +80,8 @@ public function __construct($db) $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@timesheetweek' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '1.1.1'; // EN: "Flattening" permissions to fix a PDF display issue. - // FR: Mise à "plat" des permissions pour régler un problème d'affichage des PDF. + $this->version = '1.2.0'; // EN: Adds forfait-jour daily rate selectors with automatic hour conversion. + // FR: Ajoute les sélecteurs forfait jour Journée/Matin/Après-midi avec conversion automatique des heures. // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -740,14 +740,36 @@ public function init($options = '') } // Create extrafields during init - //include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; - //$extrafields = new ExtraFields($this->db); - //$result0=$extrafields->addExtraField('timesheetweek_separator1', "Separator 1", 'separator', 1, 0, 'thirdparty', 0, 0, '', array('options'=>array(1=>1)), 1, '', 1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")'); - //$result1=$extrafields->addExtraField('timesheetweek_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")'); - //$result2=$extrafields->addExtraField('timesheetweek_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")'); - //$result3=$extrafields->addExtraField('timesheetweek_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")'); - //$result4=$extrafields->addExtraField('timesheetweek_myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")'); - //$result5=$extrafields->addExtraField('timesheetweek_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")'); + dol_include_once('/core/class/extrafields.class.php'); + $extrafields = new ExtraFields($this->db); + $extrafields->fetch_name_optionals_label('user'); + if (empty($extrafields->attributes['user']['label']['lmdb_daily_rate'])) { + // EN: Register the daily rate toggle on employees when the module is activated. + // FR: Enregistre l'option de forfait jour sur les salariés lors de l'activation du module. + $extrafields->addExtraField('lmdb_daily_rate', 'TimesheetWeekDailyRateLabel', 'boolean', 100, '', 'user', 0, 0, '', '', 0, '', '0', '', '', '', 'timesheetweek@timesheetweek', 'isModEnabled("timesheetweek")', 0, 0); + } + + // EN: Ensure existing installations receive the daily_rate column for time entries. + // FR: Garantit que les installations existantes reçoivent la colonne daily_rate pour les lignes de temps. + $sqlCheckDailyRate = "SHOW COLUMNS FROM ".$this->db->prefix()."timesheet_week_line LIKE 'daily_rate'"; + $resqlCheckDailyRate = $this->db->query($sqlCheckDailyRate); + if (!$resqlCheckDailyRate) { + // EN: Abort activation when the structure verification query fails. + // FR: Interrompt l'activation si la requête de vérification de structure échoue. + $this->error = $this->db->lasterror(); + return -1; + } + $hasDailyRateColumn = (bool) $this->db->num_rows($resqlCheckDailyRate); + $this->db->free($resqlCheckDailyRate); + if (!$hasDailyRateColumn) { + $sqlAdd = "ALTER TABLE ".MAIN_DB_PREFIX."timesheet_week_line ADD COLUMN daily_rate INT NOT NULL DEFAULT 0"; + if (!$this->db->query($sqlAdd)) { + // EN: Stop activation when adding the column fails to keep database consistent. + // FR: Stoppe l'activation si l'ajout de la colonne échoue pour conserver une base cohérente. + $this->error = $this->db->lasterror(); + return -1; + } + } // Permissions $this->remove($options); diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index 45c9bb4..a1661dd 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -158,6 +158,13 @@ BadStatusForSeal = Only approved timesheets can be sealed. BadStatusForUnseal = Only sealed timesheets can be unsealed. TimesheetWeekAgendaCreated = Timesheet %s created. TimesheetWeekAgendaSubmitted = Timesheet %s submitted. +TimesheetWeekDailyRateLabel = Daily rate contract +TimesheetWeekDailyRateFullDay = Full day +TimesheetWeekDailyRateMorning = Morning +TimesheetWeekDailyRateAfternoon = Afternoon +LastModification = Last modification +TotalDays = Total days +TimesheetWeekTotalDays = Total days TimesheetWeekAgendaApproved = Timesheet %s approved. TimesheetWeekAgendaRefused = Timesheet %s refused. TimesheetWeekAgendaSealed = Timesheet %s sealed. diff --git a/langs/fr_FR/timesheetweek.lang b/langs/fr_FR/timesheetweek.lang index 2f153bb..b1a8ae9 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -226,6 +226,14 @@ TimesheetWeekTemplateRefuseBody = Bonjour __RECIPIENT_FULLNAME__,\n\nVotre feuil TimesheetWeekPDFModels = Modèles PDF TimesheetWeek +TimesheetWeekDailyRateLabel = Contrat forfait jour +TimesheetWeekDailyRateFullDay = Journée +TimesheetWeekDailyRateMorning = Matin +TimesheetWeekDailyRateAfternoon = Après-midi +LastModification = Dernière modification +TotalDays = Total jours +TimesheetWeekTotalDays = Total jours + ModelEnabled = Modèle %s activé ModelDisabled = Modèle %s désactivé diff --git a/sql/llx_timesheet_week.sql b/sql/llx_timesheet_week.sql index f724417..0a79837 100644 --- a/sql/llx_timesheet_week.sql +++ b/sql/llx_timesheet_week.sql @@ -11,14 +11,14 @@ CREATE TABLE IF NOT EXISTS llx_timesheet_week ( date_creation DATETIME DEFAULT CURRENT_TIMESTAMP, date_validation DATETIME DEFAULT NULL, fk_user_valid INT DEFAULT NULL, - total_hours DOUBLE(24,8) NOT NULL DEFAULT 0, - overtime_hours DOUBLE(24,8) NOT NULL DEFAULT 0, - zone1_count SMALLINT NOT NULL DEFAULT 0, - zone2_count SMALLINT NOT NULL DEFAULT 0, - zone3_count SMALLINT NOT NULL DEFAULT 0, - zone4_count SMALLINT NOT NULL DEFAULT 0, - zone5_count SMALLINT NOT NULL DEFAULT 0, - meal_count SMALLINT NOT NULL DEFAULT 0, + total_hours DOUBLE(24,8) NOT NULL DEFAULT 0, + overtime_hours DOUBLE(24,8) NOT NULL DEFAULT 0, + zone1_count SMALLINT NOT NULL DEFAULT 0, + zone2_count SMALLINT NOT NULL DEFAULT 0, + zone3_count SMALLINT NOT NULL DEFAULT 0, + zone4_count SMALLINT NOT NULL DEFAULT 0, + zone5_count SMALLINT NOT NULL DEFAULT 0, + meal_count SMALLINT NOT NULL DEFAULT 0, tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Unicité par entité @@ -44,10 +44,11 @@ CREATE TABLE IF NOT EXISTS llx_timesheet_week_line ( entity INT NOT NULL DEFAULT 1, fk_timesheet_week INT NOT NULL, fk_task INT NOT NULL, - day_date DATE NOT NULL, - hours DOUBLE(24,8) NOT NULL DEFAULT 0, - zone SMALLINT NOT NULL DEFAULT 0, - meal TINYINT NOT NULL DEFAULT 0, +day_date DATE NOT NULL, +hours DOUBLE(24,8) NOT NULL DEFAULT 0, +daily_rate INT NOT NULL DEFAULT 0, +zone SMALLINT NOT NULL DEFAULT 0, +meal TINYINT NOT NULL DEFAULT 0, tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Empêche les doublons de saisie pour la même tâche et le même jour dans une même feuille diff --git a/timesheetweek_card.php b/timesheetweek_card.php index 94e8bde..7eeeabe 100644 --- a/timesheetweek_card.php +++ b/timesheetweek_card.php @@ -1,15 +1,25 @@ +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ /** - * \file timesheetweek_card.php - * \ingroup timesheetweek - * \brief Page to create/edit/view a weekly timesheet - */ +* \file timesheetweek_card.php +* \ingroup timesheetweek +* \brief Page to create/edit/view a weekly timesheet +*/ // ---- Bootstrap Dolibarr env (robuste pour /custom) ---- $res = 0; @@ -23,6 +33,9 @@ require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; require_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php'; require_once DOL_DOCUMENT_ROOT.'/projet/class/task.class.php'; +// EN: Load price helpers to display day totals with Dolibarr formatting rules. +// FR: Charge les aides de prix pour afficher les totaux en jours avec les règles de formatage Dolibarr. +require_once DOL_DOCUMENT_ROOT.'/core/lib/price.lib.php'; dol_include_once('/timesheetweek/class/timesheetweek.class.php'); dol_include_once('/timesheetweek/lib/timesheetweek.lib.php'); // getWeekSelectorDolibarr(), formatHours(), ... @@ -39,13 +52,17 @@ $extrafields = new ExtraFields($db); $hookmanager->initHooks(array('timesheetweekcard','globalcard')); +// EN: Default daily rate flag to avoid undefined notices before data loading. +// FR: Définit le flag forfait jour par défaut pour éviter les notices avant chargement des données. +$isDailyRateEmployee = false; + // ---- Fetch (set $object if id) ---- include DOL_DOCUMENT_ROOT.'/core/actions_fetchobject.inc.php'; // ---- SHIM STATUTS (mappe vers les constantes de la classe, avec fallback) ---- function tw_status($name) { - static $map = null; - if ($map === null) { + static $map = null; + if ($map === null) { $approved = null; if (defined('TimesheetWeek::STATUS_APPROVED')) { $approved = TimesheetWeek::STATUS_APPROVED; @@ -54,27 +71,75 @@ function tw_status($name) { } else { $approved = 2; } - $map = array( - 'draft' => defined('TimesheetWeek::STATUS_DRAFT') ? TimesheetWeek::STATUS_DRAFT : 0, - 'submitted' => defined('TimesheetWeek::STATUS_SUBMITTED') ? TimesheetWeek::STATUS_SUBMITTED : 1, - 'approved' => $approved, // <— Approuvée - 'sealed' => defined('TimesheetWeek::STATUS_SEALED') ? TimesheetWeek::STATUS_SEALED : 8, - 'refused' => defined('TimesheetWeek::STATUS_REFUSED') ? TimesheetWeek::STATUS_REFUSED : 3, - ); - } - return $map[$name]; + $map = array( + 'draft' => defined('TimesheetWeek::STATUS_DRAFT') ? TimesheetWeek::STATUS_DRAFT : 0, + 'submitted' => defined('TimesheetWeek::STATUS_SUBMITTED') ? TimesheetWeek::STATUS_SUBMITTED : 1, + 'approved' => $approved, // <— Approuvée + 'sealed' => defined('TimesheetWeek::STATUS_SEALED') ? TimesheetWeek::STATUS_SEALED : 8, + 'refused' => defined('TimesheetWeek::STATUS_REFUSED') ? TimesheetWeek::STATUS_REFUSED : 3, + ); + } + return $map[$name]; } function tw_translate_error($errorKey, $langs) { - if (empty($errorKey)) { - return $langs->trans("Error"); - } - $msg = $langs->trans($errorKey); - if ($msg === $errorKey) { - $msg = $langs->trans("Error").' ('.dol_escape_htmltag($errorKey).')'; - } - return $msg; + if (empty($errorKey)) { + return $langs->trans("Error"); + } + $msg = $langs->trans($errorKey); + if ($msg === $errorKey) { + $msg = $langs->trans("Error").' ('.dol_escape_htmltag($errorKey).')'; + } + return $msg; +} + +/** + * EN: Format day totals by reusing Dolibarr price helpers to respect locale settings. + * FR: Formate les totaux en jours en réutilisant les aides de prix Dolibarr pour respecter les paramètres régionaux. + * + * @param float $value Day quantity to format / Quantité de jours à formater + * @param Translate $langs Translator instance / Instance de traduction + * @return string Formatted value / Valeur formatée + */ +function tw_format_days($value, Translate $langs) +{ + global $conf; + // EN: Normalize the numeric value to two decimals for predictable display. + // FR: Normalise la valeur numérique à deux décimales pour un affichage prévisible. + $normalized = price2num($value, '2'); + // EN: Use Dolibarr price formatter to apply thousand and decimal separators. + // FR: Utilise le formateur de prix Dolibarr pour appliquer les séparateurs de milliers et décimales. + return price($normalized, '', $langs, $conf, 1, 2); +} + +/** + * EN: Fetch the timesheet employee and detect the daily rate flag with caching. + * FR: Récupère le salarié de la feuille et détecte le forfait jour avec mise en cache. + * + * @param DoliDB $db Database handler / Gestionnaire de base de données + * @param int $userId Employee identifier / Identifiant du salarié + * @return array ['user' => ?User, 'is_daily_rate' => bool] + */ +function tw_get_employee_with_daily_rate(DoliDB $db, $userId) +{ + static $cache = array(); + $userId = (int) $userId; + if ($userId <= 0) { + return array('user' => null, 'is_daily_rate' => false); + } + if (isset($cache[$userId])) { + return $cache[$userId]; + } + $result = array('user' => null, 'is_daily_rate' => false); + $tmpUser = new User($db); + if ($tmpUser->fetch($userId) > 0) { + $tmpUser->fetch_optionals($tmpUser->id, $tmpUser->table_element); + $result['user'] = $tmpUser; + $result['is_daily_rate'] = !empty($tmpUser->array_options['options_lmdb_daily_rate']); + } + $cache[$userId] = $result; + return $result; } // ---- Permissions (nouveau modèle) ---- @@ -104,51 +169,51 @@ function tw_translate_error($errorKey, $langs) /** helpers permissions **/ function tw_can_validate_timesheet( - TimesheetWeek $o, - User $user, - $permValidate, - $permValidateOwn, - $permValidateChild, - $permValidateAll, - $permWrite = false, - $permWriteChild = false, - $permWriteAll = false + TimesheetWeek $o, + User $user, + $permValidate, + $permValidateOwn, + $permValidateChild, + $permValidateAll, + $permWrite = false, + $permWriteChild = false, + $permWriteAll = false ) { - $hasExplicitValidation = ($permValidate || $permValidateOwn || $permValidateChild || $permValidateAll); - - if (!empty($user->admin)) { - $permValidateAll = true; - $hasExplicitValidation = true; - } - - if (!$hasExplicitValidation) { - // Aucun droit de validation explicite : on retombe sur l'ancien comportement basé sur l'écriture - if ($permWriteAll) { - $permValidateAll = true; - } - if ($permWriteChild) { - $permValidateChild = true; - } - - if ($permWrite || $permWriteChild || $permWriteAll) { - // Autorise la validation lorsque l'utilisateur est désigné validateur - if ((int) $o->fk_user_valid === (int) $user->id) { - $permValidate = true; - } - - // Ancien comportement : les managers pouvaient valider via writeChild - if (!$permValidateChild && $permWriteChild) { - $permValidateChild = true; - } - } - } - - if ($permValidateAll) return true; - if ($permValidateChild && tw_is_manager_of($o->fk_user, $user)) return true; - if ($permValidateOwn && ((int) $user->id === (int) $o->fk_user)) return true; - if ($permValidate && ((int) $user->id === (int) $o->fk_user_valid)) return true; - - return false; + $hasExplicitValidation = ($permValidate || $permValidateOwn || $permValidateChild || $permValidateAll); + + if (!empty($user->admin)) { + $permValidateAll = true; + $hasExplicitValidation = true; + } + + if (!$hasExplicitValidation) { + // Aucun droit de validation explicite : on retombe sur l'ancien comportement basé sur l'écriture + if ($permWriteAll) { + $permValidateAll = true; + } + if ($permWriteChild) { + $permValidateChild = true; + } + + if ($permWrite || $permWriteChild || $permWriteAll) { + // Autorise la validation lorsque l'utilisateur est désigné validateur + if ((int) $o->fk_user_valid === (int) $user->id) { + $permValidate = true; + } + + // Ancien comportement : les managers pouvaient valider via writeChild + if (!$permValidateChild && $permWriteChild) { + $permValidateChild = true; + } + } + } + + if ($permValidateAll) return true; + if ($permValidateChild && tw_is_manager_of($o->fk_user, $user)) return true; + if ($permValidateOwn && ((int) $user->id === (int) $o->fk_user)) return true; + if ($permValidate && ((int) $user->id === (int) $o->fk_user_valid)) return true; + + return false; } // Sécurise l'objet si présent @@ -156,8 +221,8 @@ function tw_can_validate_timesheet( $canSendMail = false; if ($object->id > 0) { - $canSendMail = tw_can_act_on_user($object->fk_user, $permRead, $permReadChild, $permReadAll, $user) - || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); + $canSendMail = tw_can_act_on_user($object->fk_user, $permRead, $permReadChild, $permReadAll, $user) + || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); } // ----------------- Inline edits (crayons) ----------------- @@ -194,10 +259,10 @@ function tw_can_validate_timesheet( } if ($action === 'setweekyear' && $object->id > 0 && $object->status == tw_status('draft')) { - $weekyear = GETPOST('weekyear', 'alpha'); - if (!tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user)) accessforbidden(); - if (preg_match('/^(\d{4})-W(\d{2})$/', $weekyear, $m)) { - $object->year = (int) $m[1]; + $weekyear = GETPOST('weekyear', 'alpha'); + if (!tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user)) accessforbidden(); + if (preg_match('/^(\d{4})-W(\d{2})$/', $weekyear, $m)) { + $object->year = (int) $m[1]; $object->week = (int) $m[2]; $res = $object->update($user); if ($res > 0) setEventMessages($langs->trans("RecordModified"), null, 'mesgs'); @@ -205,75 +270,75 @@ function tw_can_validate_timesheet( } else { setEventMessages($langs->trans("InvalidWeekFormat"), null, 'errors'); } - $action = ''; + $action = ''; } // ----------------- Action: prepare send mail ----------------- if ($action === 'presend' && $id > 0) { - if ($object->id <= 0) { - $object->fetch($id); - } - - $canSendMail = tw_can_act_on_user($object->fk_user, $permRead, $permReadChild, $permReadAll, $user) - || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); - if (!$canSendMail) { - accessforbidden(); - } - - if (GETPOST('mode', 'aZ09') === 'init') { - $langs->load('mails'); - - $defaultRecipients = array(); - if ($object->fk_user > 0) { - $tmpMailUser = new User($db); - if ($tmpMailUser->fetch($object->fk_user) > 0 && !empty($tmpMailUser->email)) { - $defaultRecipients[] = $tmpMailUser->email; - } - } - if ($object->fk_user_valid > 0) { - $tmpMailValidator = new User($db); - if ($tmpMailValidator->fetch($object->fk_user_valid) > 0 && !empty($tmpMailValidator->email)) { - $defaultRecipients[] = $tmpMailValidator->email; - } - } - $defaultRecipients = array_unique($defaultRecipients); - - if (empty(GETPOST('sendto', 'none'))) { - $_POST['sendto'] = implode(', ', $defaultRecipients); - } - - $defaultFrom = !empty($user->email) ? $user->email : getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'); - if (empty(GETPOST('replyto', 'none')) && !empty($defaultFrom)) { - $_POST['replyto'] = $defaultFrom; - } - - if (empty(GETPOST('subject', 'restricthtml'))) { - $_POST['subject'] = $langs->trans('TimesheetWeekMailDefaultSubject', $object->ref); - } - - if (empty(GETPOST('message', 'restricthtml'))) { - $_POST['message'] = $langs->trans('TimesheetWeekMailDefaultBody', $object->ref, $object->week, $object->year); - } - } + if ($object->id <= 0) { + $object->fetch($id); + } + + $canSendMail = tw_can_act_on_user($object->fk_user, $permRead, $permReadChild, $permReadAll, $user) + || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); + if (!$canSendMail) { + accessforbidden(); + } + + if (GETPOST('mode', 'aZ09') === 'init') { + $langs->load('mails'); + + $defaultRecipients = array(); + if ($object->fk_user > 0) { + $tmpMailUser = new User($db); + if ($tmpMailUser->fetch($object->fk_user) > 0 && !empty($tmpMailUser->email)) { + $defaultRecipients[] = $tmpMailUser->email; + } + } + if ($object->fk_user_valid > 0) { + $tmpMailValidator = new User($db); + if ($tmpMailValidator->fetch($object->fk_user_valid) > 0 && !empty($tmpMailValidator->email)) { + $defaultRecipients[] = $tmpMailValidator->email; + } + } + $defaultRecipients = array_unique($defaultRecipients); + + if (empty(GETPOST('sendto', 'none'))) { + $_POST['sendto'] = implode(', ', $defaultRecipients); + } + + $defaultFrom = !empty($user->email) ? $user->email : getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'); + if (empty(GETPOST('replyto', 'none')) && !empty($defaultFrom)) { + $_POST['replyto'] = $defaultFrom; + } + + if (empty(GETPOST('subject', 'restricthtml'))) { + $_POST['subject'] = $langs->trans('TimesheetWeekMailDefaultSubject', $object->ref); + } + + if (empty(GETPOST('message', 'restricthtml'))) { + $_POST['message'] = $langs->trans('TimesheetWeekMailDefaultBody', $object->ref, $object->week, $object->year); + } + } } if ($action === 'send' && $id > 0 && !$canSendMail) { - accessforbidden(); + accessforbidden(); } if ($action === 'send' && $id > 0) { - if ($object->id <= 0) { - $object->fetch($id); - } - if (!is_array($object->context)) { - $object->context = array(); - } - $object->context['actioncode'] = 'TIMESHEETWEEK_SENTBYMAIL'; - $object->context['timesheetweek_card_action'] = 'send'; + if ($object->id <= 0) { + $object->fetch($id); + } + if (!is_array($object->context)) { + $object->context = array(); + } + $object->context['actioncode'] = 'TIMESHEETWEEK_SENTBYMAIL'; + $object->context['timesheetweek_card_action'] = 'send'; } if (in_array($action, array('presend', 'send'), true)) { - $langs->load('mails'); + $langs->load('mails'); } // ----------------- Action: Create (add) ----------------- @@ -354,32 +419,48 @@ function tw_can_validate_timesheet( header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); exit; } - if (!tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user)) { - accessforbidden(); - } +if (!tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user)) { + // EN: Stop the save gracefully without triggering a full access forbidden screen to remain user friendly. + // FR: Stoppe l'enregistrement en douceur sans déclencher un écran d'accès interdit pour rester convivial. + setEventMessages($langs->trans("ErrorForbidden"), null, 'errors'); + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; +} - $db->begin(); +// EN: Detect whether the employee relies on daily rate entries to adapt the save workflow. +// FR: Détecte si le salarié relève du forfait jour afin d'adapter le flux d'enregistrement. +$employeeInfoDaily = tw_get_employee_with_daily_rate($db, $object->fk_user); +$isDailyRateEmployee = $employeeInfoDaily['is_daily_rate']; - $map = array("Monday"=>0,"Tuesday"=>1,"Wednesday"=>2,"Thursday"=>3,"Friday"=>4,"Saturday"=>5,"Sunday"=>6); - $processed = 0; +$db->begin(); + +$map = array("Monday"=>0,"Tuesday"=>1,"Wednesday"=>2,"Thursday"=>3,"Friday"=>4,"Saturday"=>5,"Sunday"=>6); +$processed = 0; +$dailyRateHours = array(1 => 8.0, 2 => 4.0, 3 => 4.0); +$cellPattern = $isDailyRateEmployee ? '/^daily_(\d+)_(\w+)$/' : '/^hours_(\d+)_(\w+)$/' ; foreach ($_POST as $key => $val) { - if (preg_match('/^hours_(\d+)_(\w+)$/', $key, $m)) { - $taskid = (int) $m[1]; - $day = $m[2]; - $hoursStr = trim((string) $val); - - $h = 0.0; - if ($hoursStr !== '') { - if (strpos($hoursStr, ':') !== false) { - $tmp = explode(':', $hoursStr, 2); - $H = (int) ($tmp[0] ?? 0); - $M = (int) ($tmp[1] ?? 0); - $h = $H + ($M/60.0); - } else { - $h = (float) str_replace(',', '.', $hoursStr); - } - } +if (preg_match($cellPattern, $key, $m)) { +$taskid = (int) $m[1]; +$day = $m[2]; +$dailyRateValue = 0; +$h = 0.0; +if ($isDailyRateEmployee) { +$dailyRateValue = (int) $val; +$h = isset($dailyRateHours[$dailyRateValue]) ? (float) $dailyRateHours[$dailyRateValue] : 0.0; + } else { +$hoursStr = trim((string) $val); +if ($hoursStr !== '') { +if (strpos($hoursStr, ':') !== false) { +$tmp = explode(':', $hoursStr, 2); +$H = (int) ($tmp[0] ?? 0); +$M = (int) ($tmp[1] ?? 0); +$h = $H + ($M/60.0); + } else { +$h = (float) str_replace(',', '.', $hoursStr); +} +} +} $dto = new DateTime(); $dto->setISODate((int)$object->year, (int)$object->week); @@ -389,11 +470,11 @@ function tw_can_validate_timesheet( $zone = (int) GETPOST('zone_'.$day, 'int'); $meal = GETPOST('meal_'.$day) ? 1 : 0; - $sqlSel = "SELECT rowid FROM ".MAIN_DB_PREFIX."timesheet_week_line - WHERE fk_timesheet_week=".(int)$object->id." AND fk_task=".(int)$taskid." AND day_date='".$db->escape($datestr)."'"; - // EN: Secure the lookup by enforcing the entity restriction of the module. - // FR: Sécurise la recherche en appliquant la restriction d'entité du module. - $sqlSel .= " AND entity IN (".getEntity('timesheetweek').")"; + $sqlSel = "SELECT rowid FROM ".MAIN_DB_PREFIX."timesheet_week_line + WHERE fk_timesheet_week=".(int)$object->id." AND fk_task=".(int)$taskid." AND day_date='".$db->escape($datestr)."'"; + // EN: Secure the lookup by enforcing the entity restriction of the module. + // FR: Sécurise la recherche en appliquant la restriction d'entité du module. + $sqlSel .= " AND entity IN (".getEntity('timesheetweek').")"; $resSel = $db->query($sqlSel); $existingId = 0; if ($resSel && $db->num_rows($resSel) > 0) { @@ -403,12 +484,12 @@ function tw_can_validate_timesheet( if ($h > 0 && $taskid > 0) { if ($existingId > 0) { - $sqlUpd = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line - SET hours=".((float)$h).", zone=".(int)$zone.", meal=".(int)$meal." - WHERE rowid=".$existingId; - // EN: Guarantee that the update only affects rows from authorized entities. - // FR: Garantit que la mise à jour ne concerne que les lignes des entités autorisées. - $sqlUpd .= " AND entity IN (".getEntity('timesheetweek').")"; + $sqlUpd = "UPDATE ".MAIN_DB_PREFIX."timesheet_week_line + SET hours=".((float)$h).", daily_rate=".(int)$dailyRateValue.", zone=".(int)$zone.", meal=".(int)$meal." + WHERE rowid=".$existingId; + // EN: Guarantee that the update only affects rows from authorized entities. + // FR: Garantit que la mise à jour ne concerne que les lignes des entités autorisées. + $sqlUpd .= " AND entity IN (".getEntity('timesheetweek').")"; if (!$db->query($sqlUpd)) { $db->rollback(); setEventMessages($db->lasterror(), null, 'errors'); @@ -416,17 +497,18 @@ function tw_can_validate_timesheet( exit; } } else { - // EN: Store the line within the same entity as its parent timesheet to stay consistent. - // FR: Enregistre la ligne dans la même entité que sa feuille parente pour rester cohérent. - $sqlIns = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line (entity, fk_timesheet_week, fk_task, day_date, hours, zone, meal) VALUES (". - (int) $object->entity.", ". - (int)$object->id.", ". - (int)$taskid.", ". - "'".$db->escape($datestr)."', ". - ((float)$h).", ". - (int)$zone.", ". - (int)$meal. - ")"; + // EN: Store the line within the same entity as its parent timesheet to stay consistent. + // FR: Enregistre la ligne dans la même entité que sa feuille parente pour rester cohérent. + $sqlIns = "INSERT INTO ".MAIN_DB_PREFIX."timesheet_week_line (entity, fk_timesheet_week, fk_task, day_date, hours, daily_rate, zone, meal) VALUES (". + (int) $object->entity.", ". + (int) $object->id.", ". + (int) $taskid.", ". + "'".$db->escape($datestr)."', ". + ((float) $h).", ". + (int) $dailyRateValue.", ". + (int) $zone.", ". + (int) $meal.")"; + ")"; if (!$db->query($sqlIns)) { $db->rollback(); setEventMessages($db->lasterror(), null, 'errors'); @@ -437,29 +519,29 @@ function tw_can_validate_timesheet( $processed++; } else { if ($existingId > 0) { - // EN: Delete the line only if it belongs to an allowed entity scope. - // FR: Supprime la ligne uniquement si elle appartient à une entité autorisée. - $db->query("DELETE FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE rowid=".$existingId." AND entity IN (".getEntity('timesheetweek').")"); + // EN: Delete the line only if it belongs to an allowed entity scope. + // FR: Supprime la ligne uniquement si elle appartient à une entité autorisée. + $db->query("DELETE FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE rowid=".$existingId." AND entity IN (".getEntity('timesheetweek').")"); $processed++; } } } } - // EN: Reload the lines to work with the freshly stored data before aggregating. - // FR: Recharge les lignes pour travailler sur les données fraîchement enregistrées avant l'agrégation. - $linesReloaded = $object->fetchLines(); - if ($linesReloaded < 0) { - $db->rollback(); - setEventMessages($object->error ? $object->error : $db->lasterror(), $object->errors, 'errors'); - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; - } - - // EN: Recompute totals and counters so they follow the exact same flow as hours and overtime. - // FR: Recalcule les totaux et les compteurs afin qu'ils suivent exactement le même flux que les heures et les heures supplémentaires. - $object->computeTotals(); - $upd = $object->update($user); + // EN: Reload the lines to work with the freshly stored data before aggregating. + // FR: Recharge les lignes pour travailler sur les données fraîchement enregistrées avant l'agrégation. + $linesReloaded = $object->fetchLines(); + if ($linesReloaded < 0) { + $db->rollback(); + setEventMessages($object->error ? $object->error : $db->lasterror(), $object->errors, 'errors'); + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; + } + + // EN: Recompute totals and counters so they follow the exact same flow as hours and overtime. + // FR: Recalcule les totaux et les compteurs afin qu'ils suivent exactement le même flux que les heures et les heures supplémentaires. + $object->computeTotals(); + $upd = $object->update($user); if ($upd < 0) { $db->rollback(); setEventMessages($object->error, $object->errors, 'errors'); @@ -475,23 +557,23 @@ function tw_can_validate_timesheet( // ----------------- Action: Submit ----------------- if ($action === 'submit' && $id > 0) { - if ($object->id <= 0) $object->fetch($id); + if ($object->id <= 0) $object->fetch($id); - if (!in_array((int) $object->status, array((int) tw_status('draft'), (int) tw_status('refused')), true)) { - setEventMessages($langs->trans("ActionNotAllowedOnThisStatus"), null, 'warnings'); - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; - } + if (!in_array((int) $object->status, array((int) tw_status('draft'), (int) tw_status('refused')), true)) { + setEventMessages($langs->trans("ActionNotAllowedOnThisStatus"), null, 'warnings'); + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; + } if (!tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user)) { accessforbidden(); } - $totalHours = 0.0; - $sqlSum = "SELECT SUM(hours) as sh FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int)$object->id; - // EN: Keep the aggregation limited to the current entity scope for correctness. - // FR: Limite l'agrégation à l'entité courante pour garantir la cohérence. - $sqlSum .= " AND entity IN (".getEntity('timesheetweek').")"; - $resSum = $db->query($sqlSum); + $totalHours = 0.0; + $sqlSum = "SELECT SUM(hours) as sh FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int)$object->id; + // EN: Keep the aggregation limited to the current entity scope for correctness. + // FR: Limite l'agrégation à l'entité courante pour garantir la cohérence. + $sqlSum .= " AND entity IN (".getEntity('timesheetweek').")"; + $resSum = $db->query($sqlSum); if ($resSum) { $o = $db->fetch_object($resSum); $totalHours = (float) $o->sh; @@ -502,71 +584,71 @@ function tw_can_validate_timesheet( exit; } - if (!is_array($object->context)) { - $object->context = array(); - } - $object->context['actioncode'] = 'TIMESHEETWEEK_SUBMIT'; - $object->context['timesheetweek_card_action'] = 'submit'; - - $res = $object->submit($user); - if ($res > 0) { - setEventMessages($langs->trans("TimesheetSubmitted"), null, 'mesgs'); - } else { - $errmsg = tw_translate_error($object->error, $langs); - setEventMessages($errmsg, $object->errors, 'errors'); - } - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; + if (!is_array($object->context)) { + $object->context = array(); + } + $object->context['actioncode'] = 'TIMESHEETWEEK_SUBMIT'; + $object->context['timesheetweek_card_action'] = 'submit'; + + $res = $object->submit($user); + if ($res > 0) { + setEventMessages($langs->trans("TimesheetSubmitted"), null, 'mesgs'); + } else { + $errmsg = tw_translate_error($object->error, $langs); + setEventMessages($errmsg, $object->errors, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; } // ----------------- Action: Back to draft ----------------- if ($action === 'setdraft' && $id > 0) { - if ($object->id <= 0) $object->fetch($id); - - if ($object->status == tw_status('sealed')) { - // EN: Block direct draft revert on sealed sheets. - // FR : Empêche de repasser en brouillon une feuille scellée sans la desceller. - setEventMessages($langs->trans('CannotSetDraftWhenSealed'), null, 'warnings'); - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; - } - - if ($object->status == tw_status('draft')) { - setEventMessages($langs->trans("AlreadyDraft"), null, 'warnings'); - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; - } + if ($object->id <= 0) $object->fetch($id); + + if ($object->status == tw_status('sealed')) { + // EN: Block direct draft revert on sealed sheets. + // FR : Empêche de repasser en brouillon une feuille scellée sans la desceller. + setEventMessages($langs->trans('CannotSetDraftWhenSealed'), null, 'warnings'); + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; + } + + if ($object->status == tw_status('draft')) { + setEventMessages($langs->trans("AlreadyDraft"), null, 'warnings'); + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; + } $canEmployee = tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user); - $canValidator = tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); + $canValidator = tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); if (!$canEmployee && !$canValidator) accessforbidden(); - if (!is_array($object->context)) { - $object->context = array(); - } - $object->context['timesheetweek_card_action'] = 'setdraft'; - - $res = $object->revertToDraft($user); - if ($res > 0) { - setEventMessages($langs->trans("StatusSetToDraft"), null, 'mesgs'); - } else { - $errmsg = tw_translate_error($object->error, $langs); - setEventMessages($errmsg, $object->errors, 'errors'); - } - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; + if (!is_array($object->context)) { + $object->context = array(); + } + $object->context['timesheetweek_card_action'] = 'setdraft'; + + $res = $object->revertToDraft($user); + if ($res > 0) { + setEventMessages($langs->trans("StatusSetToDraft"), null, 'mesgs'); + } else { + $errmsg = tw_translate_error($object->error, $langs); + setEventMessages($errmsg, $object->errors, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; } // ----------------- Action: ASK APPROVE / REFUSE (confirm popups) ----------------- if ($action === 'ask_validate' && $id > 0) { if ($object->id <= 0) $object->fetch($id); if ($object->status != tw_status('submitted')) accessforbidden(); - if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) accessforbidden(); + if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) accessforbidden(); } if ($action === 'ask_refuse' && $id > 0) { if ($object->id <= 0) $object->fetch($id); if ($object->status != tw_status('submitted')) accessforbidden(); - if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) accessforbidden(); + if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) accessforbidden(); } // ----------------- Action: CONFIRM APPROVE (Approuver) ----------------- @@ -577,162 +659,162 @@ function tw_can_validate_timesheet( header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); exit; } - if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) { + if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) { accessforbidden(); } - if (!is_array($object->context)) { - $object->context = array(); - } - $object->context['actioncode'] = 'TIMESHEETWEEK_APPROVE'; - $object->context['timesheetweek_card_action'] = 'confirm_validate'; - - $res = $object->approve($user); - if ($res > 0) { - setEventMessages($langs->trans("TimesheetApproved"), null, 'mesgs'); - } else { - $errmsg = tw_translate_error($object->error, $langs); - setEventMessages($errmsg, $object->errors, 'errors'); - } - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; + if (!is_array($object->context)) { + $object->context = array(); + } + $object->context['actioncode'] = 'TIMESHEETWEEK_APPROVE'; + $object->context['timesheetweek_card_action'] = 'confirm_validate'; + + $res = $object->approve($user); + if ($res > 0) { + setEventMessages($langs->trans("TimesheetApproved"), null, 'mesgs'); + } else { + $errmsg = tw_translate_error($object->error, $langs); + setEventMessages($errmsg, $object->errors, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; } // ----------------- Action: CONFIRM REFUSE (Refuser) ----------------- if ($action === 'confirm_refuse' && $confirm === 'yes' && $id > 0) { - if ($object->id <= 0) $object->fetch($id); - if ($object->status != tw_status('submitted')) { - setEventMessages($langs->trans("ActionNotAllowedOnThisStatus"), null, 'warnings'); - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; - } - if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) { - accessforbidden(); - } - - if (!is_array($object->context)) { - $object->context = array(); - } - $object->context['actioncode'] = 'TIMESHEETWEEK_REFUSE'; - $object->context['timesheetweek_card_action'] = 'confirm_refuse'; - - $res = $object->refuse($user); - if ($res > 0) { - setEventMessages($langs->trans("TimesheetRefused"), null, 'mesgs'); - } else { - $errmsg = tw_translate_error($object->error, $langs); - setEventMessages($errmsg, $object->errors, 'errors'); - } - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; + if ($object->id <= 0) $object->fetch($id); + if ($object->status != tw_status('submitted')) { + setEventMessages($langs->trans("ActionNotAllowedOnThisStatus"), null, 'warnings'); + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; + } + if (!tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll)) { + accessforbidden(); + } + + if (!is_array($object->context)) { + $object->context = array(); + } + $object->context['actioncode'] = 'TIMESHEETWEEK_REFUSE'; + $object->context['timesheetweek_card_action'] = 'confirm_refuse'; + + $res = $object->refuse($user); + if ($res > 0) { + setEventMessages($langs->trans("TimesheetRefused"), null, 'mesgs'); + } else { + $errmsg = tw_translate_error($object->error, $langs); + setEventMessages($errmsg, $object->errors, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; } // ----------------- Action: SEAL ----------------- if ($action === 'seal' && $id > 0) { - if ($object->id <= 0) $object->fetch($id); - if (!$permSeal) accessforbidden(); - - if (!is_array($object->context)) { - $object->context = array(); - } - // EN: Flag the current action for triggers and logs. - // FR : Marque l'action courante pour les triggers et journaux. - $object->context['timesheetweek_card_action'] = 'seal'; - - $res = $object->seal($user); - if ($res > 0) { - setEventMessages($langs->trans('TimesheetSealed'), null, 'mesgs'); - } else { - $errmsg = tw_translate_error($object->error, $langs); - setEventMessages($errmsg, $object->errors, 'errors'); - } - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; + if ($object->id <= 0) $object->fetch($id); + if (!$permSeal) accessforbidden(); + + if (!is_array($object->context)) { + $object->context = array(); + } + // EN: Flag the current action for triggers and logs. + // FR : Marque l'action courante pour les triggers et journaux. + $object->context['timesheetweek_card_action'] = 'seal'; + + $res = $object->seal($user); + if ($res > 0) { + setEventMessages($langs->trans('TimesheetSealed'), null, 'mesgs'); + } else { + $errmsg = tw_translate_error($object->error, $langs); + setEventMessages($errmsg, $object->errors, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; } // ----------------- Action: UNSEAL ----------------- if ($action === 'unseal' && $id > 0) { - if ($object->id <= 0) $object->fetch($id); - if (!$permUnseal) accessforbidden(); - - if (!is_array($object->context)) { - $object->context = array(); - } - // EN: Flag the unseal action to inform future triggers. - // FR : Marque l'action de descellage pour informer les triggers. - $object->context['timesheetweek_card_action'] = 'unseal'; - - $res = $object->unseal($user); - if ($res > 0) { - setEventMessages($langs->trans('TimesheetUnsealed'), null, 'mesgs'); - } else { - $errmsg = tw_translate_error($object->error, $langs); - setEventMessages($errmsg, $object->errors, 'errors'); - } - header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); - exit; + if ($object->id <= 0) $object->fetch($id); + if (!$permUnseal) accessforbidden(); + + if (!is_array($object->context)) { + $object->context = array(); + } + // EN: Flag the unseal action to inform future triggers. + // FR : Marque l'action de descellage pour informer les triggers. + $object->context['timesheetweek_card_action'] = 'unseal'; + + $res = $object->unseal($user); + if ($res > 0) { + setEventMessages($langs->trans('TimesheetUnsealed'), null, 'mesgs'); + } else { + $errmsg = tw_translate_error($object->error, $langs); + setEventMessages($errmsg, $object->errors, 'errors'); + } + header("Location: ".$_SERVER["PHP_SELF"]."?id=".$object->id); + exit; } // ----------------- Action: Delete ----------------- if ($action === 'confirm_delete' && $confirm === 'yes' && $id > 0) { - if ($object->id <= 0) $object->fetch($id); - - // On autorise la suppression si l'utilisateur a les droits (own/child/all), - // ou s'il a des droits validate* (validateur), quelque soit le statut - $canDelete = tw_can_act_on_user($object->fk_user, $permDelete, $permDeleteChild, $permDeleteAll, $user) - || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); - - if (!$canDelete) accessforbidden(); - - $res = $object->delete($user); - if ($res > 0) { - setEventMessages($langs->trans("RecordDeleted"), null, 'mesgs'); - header("Location: ".dol_buildpath('/timesheetweek/timesheetweek_list.php',1)); - exit; - } else { - setEventMessages($object->error, $object->errors, 'errors'); - } + if ($object->id <= 0) $object->fetch($id); + + // On autorise la suppression si l'utilisateur a les droits (own/child/all), + // ou s'il a des droits validate* (validateur), quelque soit le statut + $canDelete = tw_can_act_on_user($object->fk_user, $permDelete, $permDeleteChild, $permDeleteAll, $user) + || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); + + if (!$canDelete) accessforbidden(); + + $res = $object->delete($user); + if ($res > 0) { + setEventMessages($langs->trans("RecordDeleted"), null, 'mesgs'); + header("Location: ".dol_buildpath('/timesheetweek/timesheetweek_list.php',1)); + exit; + } else { + setEventMessages($object->error, $object->errors, 'errors'); + } } if ($object->id > 0) { - $modelmail = 'timesheetweek'; - $defaulttopic = $langs->trans('TimesheetWeekMailDefaultSubject', $object->ref); - $defaultmessage = $langs->trans('TimesheetWeekMailDefaultBody', $object->ref, $object->week, $object->year); - $triggersendname = 'TIMESHEETWEEK_SENTBYMAIL'; - $trackid = 'timesheetweek'.$object->id; - $permissiontosend = $canSendMail; - $diroutput = isset($conf->timesheetweek->dir_output) ? $conf->timesheetweek->dir_output : (defined('DOL_DATA_ROOT') ? DOL_DATA_ROOT.'/timesheetweek' : ''); - - $moresubstit = array( - '__TIMESHEETWEEK_REF__' => $object->ref, - '__TIMESHEETWEEK_WEEK__' => $object->week, - '__TIMESHEETWEEK_YEAR__' => $object->year, - '__TIMESHEETWEEK_STATUS__' => $object->getLibStatut(0), - ); - - if ($object->fk_user > 0) { - $employee = new User($db); - if ($employee->fetch($object->fk_user) > 0) { - $moresubstit['__TIMESHEETWEEK_EMPLOYEE_FULLNAME__'] = $employee->getFullName($langs); - $moresubstit['__TIMESHEETWEEK_EMPLOYEE_EMAIL__'] = $employee->email; - } - } - if ($object->fk_user_valid > 0) { - $validator = new User($db); - if ($validator->fetch($object->fk_user_valid) > 0) { - $moresubstit['__TIMESHEETWEEK_VALIDATOR_FULLNAME__'] = $validator->getFullName($langs); - $moresubstit['__TIMESHEETWEEK_VALIDATOR_EMAIL__'] = $validator->email; - } - } - - $param = array( - 'sendcontext' => 'timesheetweek', - 'returnurl' => dol_buildpath('/timesheetweek/timesheetweek_card.php', 1).'?id='.$object->id, - 'models' => $modelmail, - 'trackid' => $trackid, - ); - - include DOL_DOCUMENT_ROOT.'/core/actions_sendmails.inc.php'; + $modelmail = 'timesheetweek'; + $defaulttopic = $langs->trans('TimesheetWeekMailDefaultSubject', $object->ref); + $defaultmessage = $langs->trans('TimesheetWeekMailDefaultBody', $object->ref, $object->week, $object->year); + $triggersendname = 'TIMESHEETWEEK_SENTBYMAIL'; + $trackid = 'timesheetweek'.$object->id; + $permissiontosend = $canSendMail; + $diroutput = isset($conf->timesheetweek->dir_output) ? $conf->timesheetweek->dir_output : (defined('DOL_DATA_ROOT') ? DOL_DATA_ROOT.'/timesheetweek' : ''); + + $moresubstit = array( + '__TIMESHEETWEEK_REF__' => $object->ref, + '__TIMESHEETWEEK_WEEK__' => $object->week, + '__TIMESHEETWEEK_YEAR__' => $object->year, + '__TIMESHEETWEEK_STATUS__' => $object->getLibStatut(0), + ); + + if ($object->fk_user > 0) { + $employee = new User($db); + if ($employee->fetch($object->fk_user) > 0) { + $moresubstit['__TIMESHEETWEEK_EMPLOYEE_FULLNAME__'] = $employee->getFullName($langs); + $moresubstit['__TIMESHEETWEEK_EMPLOYEE_EMAIL__'] = $employee->email; + } + } + if ($object->fk_user_valid > 0) { + $validator = new User($db); + if ($validator->fetch($object->fk_user_valid) > 0) { + $moresubstit['__TIMESHEETWEEK_VALIDATOR_FULLNAME__'] = $validator->getFullName($langs); + $moresubstit['__TIMESHEETWEEK_VALIDATOR_EMAIL__'] = $validator->email; + } + } + + $param = array( + 'sendcontext' => 'timesheetweek', + 'returnurl' => dol_buildpath('/timesheetweek/timesheetweek_card.php', 1).'?id='.$object->id, + 'models' => $modelmail, + 'trackid' => $trackid, + ); + + include DOL_DOCUMENT_ROOT.'/core/actions_sendmails.inc.php'; } // ----------------- View ----------------- @@ -807,7 +889,10 @@ function parseYearWeek(val) { var m=/^(\d{4})-W(\d{2})$/.exec(val||'');return m?{y:parseInt(m[1],10),w:parseInt(m[2],10)}:null; } function isoWeekStart(y,w){var s=new Date(Date.UTC(y,0,1+(w-1)*7));var d=s.getUTCDay();var st=new Date(s);if(d>=1&&d<=4)st.setUTCDate(s.getUTCDate()-(d-1));else st.setUTCDate(s.getUTCDate()+(d===0?1:(8-d)));return st;} - function fmt(d){var dd=String(d.getUTCDate()).padStart(2,'0');var mm=String(d.getUTCMonth()+1).padStart(2,'0');var yy=d.getUTCFullYear();return dd+'/'+mm+'/'+yy;} +// EN: Local helper to zero-pad day/month values for legacy browser compatibility. +// FR: Aide locale pour compléter les jours/mois avec un zéro et rester compatible avec les anciens navigateurs. +function pad2(v){return (v<10?'0':'')+v;} +function fmt(d){var dd=pad2(d.getUTCDate());var mm=pad2(d.getUTCMonth()+1);var yy=d.getUTCFullYear();return dd+'/'+mm+'/'+yy;} function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if(!p){$('#weekrange').text('');return;}var s=isoWeekStart(p.y,p.w);var e=new Date(s);e.setUTCDate(s.getUTCDate()+6);$('#weekrange').text('du '+fmt(s)+' au '+fmt(e));} $(function(){if($.fn.select2)$('#weekyear').select2({width:'resolve'});updateWeekRange();$('#weekyear').on('change',updateWeekRange);}); })(jQuery); @@ -827,45 +912,45 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( // Head + banner $head = timesheetweekPrepareHead($object); - print dol_get_fiche_head($head, 'card', $langs->trans("TimesheetWeek"), -1, 'bookcal'); - - $linkback = ''.$langs->trans("BackToList").''; - $morehtmlright = ''; - $morehtmlstatus = ''; - if (!empty($object->id)) { - $morehtmlstatus = $object->getLibStatut(5); - } - - $morehtmlref = ''; - if (!empty($conf->multicompany->enabled) && (int) $object->entity !== (int) $conf->entity) { - // EN: Fetch the entity label to display the native Multicompany badge below the reference. - // FR: Récupère le libellé de l'entité pour afficher le badge Multicompany natif sous la référence. - $entityName = ''; - $entityId = (int) $object->entity; - if ($entityId > 0) { - $sqlEntity = 'SELECT label FROM '.MAIN_DB_PREFIX."entity WHERE rowid = ".$entityId; - $resEntity = $db->query($sqlEntity); - if ($resEntity) { - $entityRow = $db->fetch_object($resEntity); - if ($entityRow) { - $entityName = trim((string) $entityRow->label); - } - $db->free($resEntity); - } - if ($entityName === '') { - // EN: Fall back to a generic label when the entity dictionary is empty. - // FR: Revient à un libellé générique lorsque le dictionnaire d'entités est vide. - $entityName = $langs->trans('Entity').' #'.$entityId; - } - $entityBadge = '
'.dol_escape_htmltag($entityName).'
'; - // EN: Inject the entity badge underneath the reference to mimic Dolibarr's native layout. - // FR: Insère le badge d'entité sous la référence pour reproduire la mise en page native de Dolibarr. - $morehtmlref .= '
'.$entityBadge; - } - } - - dol_banner_tab($object, 'ref', $linkback, 1, 'ref', 'ref', $morehtmlref, '', $morehtmlright, '', $morehtmlstatus); - print timesheetweekRenderStatusBadgeCleanup(); + print dol_get_fiche_head($head, 'card', $langs->trans("TimesheetWeek"), -1, 'bookcal'); + + $linkback = ''.$langs->trans("BackToList").''; + $morehtmlright = ''; + $morehtmlstatus = ''; + if (!empty($object->id)) { + $morehtmlstatus = $object->getLibStatut(5); + } + + $morehtmlref = ''; + if (!empty($conf->multicompany->enabled) && (int) $object->entity !== (int) $conf->entity) { + // EN: Fetch the entity label to display the native Multicompany badge below the reference. + // FR: Récupère le libellé de l'entité pour afficher le badge Multicompany natif sous la référence. + $entityName = ''; + $entityId = (int) $object->entity; + if ($entityId > 0) { + $sqlEntity = 'SELECT label FROM '.MAIN_DB_PREFIX."entity WHERE rowid = ".$entityId; + $resEntity = $db->query($sqlEntity); + if ($resEntity) { + $entityRow = $db->fetch_object($resEntity); + if ($entityRow) { + $entityName = trim((string) $entityRow->label); + } + $db->free($resEntity); + } + if ($entityName === '') { + // EN: Fall back to a generic label when the entity dictionary is empty. + // FR: Revient à un libellé générique lorsque le dictionnaire d'entités est vide. + $entityName = $langs->trans('Entity').' #'.$entityId; + } + $entityBadge = '
'.dol_escape_htmltag($entityName).'
'; + // EN: Inject the entity badge underneath the reference to mimic Dolibarr's native layout. + // FR: Insère le badge d'entité sous la référence pour reproduire la mise en page native de Dolibarr. + $morehtmlref .= '
'.$entityBadge; + } + } + + dol_banner_tab($object, 'ref', $linkback, 1, 'ref', 'ref', $morehtmlref, '', $morehtmlright, '', $morehtmlstatus); + print timesheetweekRenderStatusBadgeCleanup(); // Confirm modals if ($action === 'delete') { @@ -913,9 +998,9 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( echo '
'; echo ''; - // Employé - echo '
'.$langs->trans("Employee").''; - if ($action === 'editfk_user' && $canEditInline) { + // Employé + echo '
'.$langs->trans("Employee").''; + if ($action === 'editfk_user' && $canEditInline) { echo '
'; echo ''; echo ''; @@ -1005,33 +1090,54 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( echo '
'; echo '
'; - // Right block (Totaux en entête) - $uEmpDisp = new User($db); - $uEmpDisp->fetch($object->fk_user); - $contractedHoursDisp = (!empty($uEmpDisp->weeklyhours)?(float)$uEmpDisp->weeklyhours:35.0); - $th = (float) $object->total_hours; - $ot = (float) $object->overtime_hours; - if ($th <= 0) { - $sqlSum = "SELECT SUM(hours) as sh FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int)$object->id; - // EN: Respect entity boundaries when recomputing totals lazily. - // FR: Respecte les frontières d'entité lors du recalcul paresseux des totaux. - $sqlSum .= " AND entity IN (".getEntity('timesheetweek').")"; - $resSum = $db->query($sqlSum); - if ($resSum) { - $o = $db->fetch_object($resSum); - $th = (float) $o->sh; - $ot = max(0.0, $th - $contractedHoursDisp); - } + // EN: Load the employee once to reuse the daily rate flag across the header and grid. + // FR: Charge le salarié une seule fois pour réutiliser le flag forfait jour dans l'entête et la grille. + $employeeInfoDisplay = tw_get_employee_with_daily_rate($db, $object->fk_user); + $timesheetEmployee = $employeeInfoDisplay['user']; + $isDailyRateEmployee = $employeeInfoDisplay['is_daily_rate']; + +// Right block (Totaux en entête) + $contractedHoursDisp = 35.0; + if ($timesheetEmployee instanceof User) { + $contractedHoursDisp = !empty($timesheetEmployee->weeklyhours) ? (float) $timesheetEmployee->weeklyhours : 35.0; } - echo '
'; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo '
'.$langs->trans("DateCreation").''.dol_print_date($object->date_creation, 'dayhour').'
'.$langs->trans("LastModification").''.dol_print_date($object->tms, 'dayhour').'
'.$langs->trans("DateValidation").''.dol_print_date($object->date_validation, 'dayhour').'
'.$langs->trans("TotalHours").''.formatHours($th).'
'.$langs->trans("Overtime").' ('.formatHours($contractedHoursDisp).')'.formatHours($ot).'
'; - echo '
'; +$th = (float) $object->total_hours; +$ot = (float) $object->overtime_hours; +if ($th <= 0) { +$sqlSum = "SELECT SUM(hours) as sh FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int) $object->id; +// EN: Respect entity boundaries when recomputing totals lazily. +// FR: Respecte les frontières d'entité lors du recalcul paresseux des totaux. +$sqlSum .= " AND entity IN (".getEntity('timesheetweek').")"; +$resSum = $db->query($sqlSum); +if ($resSum) { +$o = $db->fetch_object($resSum); +$th = (float) $o->sh; +$ot = max(0.0, $th - $contractedHoursDisp); +} +} +$displayedTotal = $th; +$displayedTotalLabel = $langs->trans("TotalHours"); +$headerTotalClass = 'header-total-hours header-total-main'; +if ($isDailyRateEmployee) { + // EN: Convert stored hours into days for header display when the employee is forfait jour. + // FR: Convertit les heures enregistrées en jours pour l'affichage d'entête lorsque le salarié est au forfait jour. + $displayedTotal = ($th > 0 ? ($th / 8.0) : 0.0); + $displayedTotalLabel = $langs->trans("TotalDays"); + $headerTotalClass = 'header-total-days header-total-main'; +} +echo '
'; +echo ''; +echo ''; +echo ''; +echo ''; +if ($isDailyRateEmployee) { +echo ''; +} else { +echo ''; +echo ''; +} +echo '
'.$langs->trans("DateCreation").''.dol_print_date($object->date_creation, 'dayhour').'
'.$langs->trans("LastModification").''.dol_print_date($object->tms, 'dayhour').'
'.$langs->trans("DateValidation").''.dol_print_date($object->date_validation, 'dayhour').'
'.$displayedTotalLabel.''.tw_format_days($displayedTotal, $langs).'
'.$displayedTotalLabel.''.formatHours($displayedTotal).'
'.$langs->trans("Overtime").' ('.formatHours($contractedHoursDisp).')'.formatHours($ot).'
'; +echo '
'; echo ''; // fichecenter @@ -1049,30 +1155,34 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( echo '

'.$langs->trans("AssignedTasks").'

'; // 1) CHARGER LIGNES EXISTANTES - $hoursBy = array(); // [taskid][YYYY-mm-dd] = hours +$hoursBy = array(); // [taskid][YYYY-mm-dd] = hours +$dailyRateBy = array(); // [taskid][YYYY-mm-dd] = daily rate code $dayMeal = array('Monday'=>0,'Tuesday'=>0,'Wednesday'=>0,'Thursday'=>0,'Friday'=>0,'Saturday'=>0,'Sunday'=>0); $dayZone = array('Monday'=>null,'Tuesday'=>null,'Wednesday'=>null,'Thursday'=>null,'Friday'=>null,'Saturday'=>null,'Sunday'=>null); $taskIdsFromLines = array(); $linesCount = 0; - $sqlLines = "SELECT fk_task, day_date, hours, zone, meal - FROM ".MAIN_DB_PREFIX."timesheet_week_line - WHERE fk_timesheet_week=".(int)$object->id; - // EN: Limit the fetched lines to those belonging to authorized entities. - // FR: Limite les lignes récupérées à celles appartenant aux entités autorisées. - $sqlLines .= " AND entity IN (".getEntity('timesheetweek').")"; + $sqlLines = "SELECT fk_task, day_date, hours, daily_rate, zone, meal + FROM ".MAIN_DB_PREFIX."timesheet_week_line + WHERE fk_timesheet_week=".(int)$object->id; + // EN: Limit the fetched lines to those belonging to authorized entities. + // FR: Limite les lignes récupérées à celles appartenant aux entités autorisées. + $sqlLines .= " AND entity IN (".getEntity('timesheetweek').")"; $resLines = $db->query($sqlLines); if ($resLines) { while ($o = $db->fetch_object($resLines)) { $linesCount++; $fk_task = (int)$o->fk_task; $daydate = $o->day_date; - $hours = (float)$o->hours; - $zone = isset($o->zone) ? (int)$o->zone : null; - $meal = (int)$o->meal; +$hours = (float)$o->hours; +$dailyRate = isset($o->daily_rate) ? (int)$o->daily_rate : 0; +$zone = isset($o->zone) ? (int)$o->zone : null; +$meal = (int)$o->meal; - if (!isset($hoursBy[$fk_task])) $hoursBy[$fk_task] = array(); - $hoursBy[$fk_task][$daydate] = $hours; +if (!isset($hoursBy[$fk_task])) $hoursBy[$fk_task] = array(); +$hoursBy[$fk_task][$daydate] = $hours; +if (!isset($dailyRateBy[$fk_task])) $dailyRateBy[$fk_task] = array(); +$dailyRateBy[$fk_task][$daydate] = $dailyRate; $w = (int) date('N', strtotime($daydate)); $dayName = array(1=>'Monday',2=>'Tuesday',3=>'Wednesday',4=>'Thursday',5=>'Friday',6=>'Saturday',7=>'Sunday')[$w]; @@ -1085,13 +1195,13 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( } // 2) RÉCUPÉRER LES TÂCHES ASSIGNÉES - $tasks = $object->getAssignedTasks($object->fk_user); // id, label, project_id, project_ref, project_title, task_ref? - $tasksById = array(); - if (!empty($tasks)) { - foreach ($tasks as $t) { - $tasksById[(int)$t['task_id']] = $t; - } - } + $tasks = $object->getAssignedTasks($object->fk_user); // id, label, project_id, project_ref, project_title, task_ref? + $tasksById = array(); + if (!empty($tasks)) { + foreach ($tasks as $t) { + $tasksById[(int)$t['task_id']] = $t; + } + } // 3) COMPLÉTER AVEC TÂCHES PRÉSENTES DANS LES LIGNES MAIS PAS DANS LES ASSIGNATIONS if (!empty($taskIdsFromLines)) { @@ -1100,213 +1210,214 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( if (!isset($tasksById[$tid])) $missing[] = (int)$tid; } if (!empty($missing)) { - // EN: Bring in the same enriched task metadata for unassigned lines to keep filtering consistent. - // FR: Récupère les mêmes métadonnées enrichies pour les lignes non assignées afin d'harmoniser le filtrage. - $sqlMiss = "SELECT t.rowid as task_id, t.label as task_label, t.ref as task_ref, t.progress as task_progress, - t.fk_statut as task_status, t.dateo as task_date_start, t.datee as task_date_end, - p.rowid as project_id, p.ref as project_ref, p.title as project_title - FROM ".MAIN_DB_PREFIX."projet_task t - INNER JOIN ".MAIN_DB_PREFIX."projet p ON p.rowid = t.fk_projet - WHERE t.rowid IN (".implode(',', array_map('intval',$missing)).")"; - $resMiss = $db->query($sqlMiss); - if ($resMiss) { - while ($o = $db->fetch_object($resMiss)) { - $tasks[] = array( - 'task_id' => (int)$o->task_id, - 'task_label' => $o->task_label, - 'task_ref' => $o->task_ref, - 'task_progress' => ($o->task_progress !== null ? (float)$o->task_progress : null), - 'task_status' => ($o->task_status !== null ? (int)$o->task_status : null), - 'task_date_start' => ($o->task_date_start !== null ? (string)$o->task_date_start : null), - 'task_date_end' => ($o->task_date_end !== null ? (string)$o->task_date_end : null), - 'project_id' => (int)$o->project_id, - 'project_ref' => $o->project_ref, - 'project_title' => $o->project_title - ); - } - } - } - } - - $days = array("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"); - // EN: Map ISO day names to dedicated translation keys for full labels. - // FR: Associe les noms ISO aux clés de traduction dédiées pour les libellés complets. - $dayLabelKeys = array( - 'Monday' => 'TimesheetWeekDayMonday', - 'Tuesday' => 'TimesheetWeekDayTuesday', - 'Wednesday' => 'TimesheetWeekDayWednesday', - 'Thursday' => 'TimesheetWeekDayThursday', - 'Friday' => 'TimesheetWeekDayFriday', - 'Saturday' => 'TimesheetWeekDaySaturday', - 'Sunday' => 'TimesheetWeekDaySunday', - ); - $weekdates = array(); - $weekStartDate = null; - $weekEndDate = null; - // EN: Derive week boundaries defensively because the week metadata can be missing on drafts. - // FR: Calcule prudemment les bornes de semaine car les métadonnées peuvent manquer sur les brouillons. - if (!empty($object->year) && !empty($object->week)) { - $dto = new DateTime(); - $dto->setISODate((int)$object->year, (int)$object->week); - foreach ($days as $d) { - $weekdates[$d] = $dto->format('Y-m-d'); - $dto->modify('+1 day'); - } - $weekStartDate = isset($weekdates['Monday']) ? $weekdates['Monday'] : null; - $weekEndDate = isset($weekdates['Sunday']) ? $weekdates['Sunday'] : null; - } else { - foreach ($days as $d) { - $weekdates[$d] = null; - } - } - - if (!empty($tasks)) { - // EN: Define closed statuses dynamically to remain compatible across Dolibarr versions. - // FR: Définit dynamiquement les statuts clos pour rester compatible entre versions de Dolibarr. - $closedStatuses = array(); - if (defined('Task::STATUS_DONE')) $closedStatuses[] = Task::STATUS_DONE; - if (defined('Task::STATUS_CLOSED')) $closedStatuses[] = Task::STATUS_CLOSED; - if (defined('Task::STATUS_FINISHED')) $closedStatuses[] = Task::STATUS_FINISHED; - if (defined('Task::STATUS_CANCELLED')) $closedStatuses[] = Task::STATUS_CANCELLED; - if (defined('Task::STATUS_CANCELED')) $closedStatuses[] = Task::STATUS_CANCELED; - - $weekStartTs = ($weekStartDate ? strtotime($weekStartDate.' 00:00:00') : null); - $weekEndTs = ($weekEndDate ? strtotime($weekEndDate.' 23:59:59') : null); - - $filteredTasks = array(); - foreach ($tasks as $t) { - // EN: Skip tasks already completed or closed to declutter the weekly view. - // FR: Ignore les tâches déjà terminées ou clôturées pour épurer la vue hebdomadaire. - $progress = isset($t['task_progress']) ? $t['task_progress'] : null; - if ($progress !== null && (float)$progress >= 100) { - continue; - } - - $status = isset($t['task_status']) ? $t['task_status'] : null; - if ($status !== null) { - if (!empty($closedStatuses) && in_array((int)$status, $closedStatuses, true)) { - continue; - } - if (empty($closedStatuses) && (int)$status >= 3) { - continue; - } - } - - // EN: Extract scheduling information to hide tasks outside the sheet week. - // FR: Analyse les dates de planification pour masquer les tâches hors de la semaine de la feuille. - $startRaw = isset($t['task_date_start']) ? $t['task_date_start'] : null; - $endRaw = isset($t['task_date_end']) ? $t['task_date_end'] : null; - $startTs = null; - $endTs = null; - if (!empty($startRaw)) { - if (is_numeric($startRaw)) { - $startTs = (int)$startRaw; - } else { - $startTs = strtotime($startRaw); - if ($startTs === false) $startTs = null; - } - } - if (!empty($endRaw)) { - if (is_numeric($endRaw)) { - $endTs = (int)$endRaw; - } else { - $endTs = strtotime($endRaw); - if ($endTs === false) $endTs = null; - } - } - - // EN: Ignore tasks that start after the sheet week or end before it. - // FR: Ignore les tâches qui commencent après la semaine ou se terminent avant celle-ci. - if ($weekStartTs !== null && $weekEndTs !== null && $startTs !== null && $startTs > $weekEndTs) { - continue; - } - if ($weekStartTs !== null && $endTs !== null && $endTs < $weekStartTs) { - continue; - } - - $filteredTasks[] = $t; - } - $tasks = array_values($filteredTasks); - } - - // 4) AFFICHAGE - if (empty($tasks)) { - echo '
'.$langs->trans("NoTasksAssigned").'
'; - } else { - // Heures contractuelles - $userEmployee=new User($db); $userEmployee->fetch($object->fk_user); - $contractedHours = (!empty($userEmployee->weeklyhours)?(float)$userEmployee->weeklyhours:35.0); + // EN: Bring in the same enriched task metadata for unassigned lines to keep filtering consistent. + // FR: Récupère les mêmes métadonnées enrichies pour les lignes non assignées afin d'harmoniser le filtrage. + $sqlMiss = "SELECT t.rowid as task_id, t.label as task_label, t.ref as task_ref, t.progress as task_progress, + t.fk_statut as task_status, t.dateo as task_date_start, t.datee as task_date_end, + p.rowid as project_id, p.ref as project_ref, p.title as project_title + FROM ".MAIN_DB_PREFIX."projet_task t + INNER JOIN ".MAIN_DB_PREFIX."projet p ON p.rowid = t.fk_projet + WHERE t.rowid IN (".implode(',', array_map('intval',$missing)).")"; + $resMiss = $db->query($sqlMiss); + if ($resMiss) { + while ($o = $db->fetch_object($resMiss)) { + $tasks[] = array( + 'task_id' => (int)$o->task_id, + 'task_label' => $o->task_label, + 'task_ref' => $o->task_ref, + 'task_progress' => ($o->task_progress !== null ? (float)$o->task_progress : null), + 'task_status' => ($o->task_status !== null ? (int)$o->task_status : null), + 'task_date_start' => ($o->task_date_start !== null ? (string)$o->task_date_start : null), + 'task_date_end' => ($o->task_date_end !== null ? (string)$o->task_date_end : null), + 'project_id' => (int)$o->project_id, + 'project_ref' => $o->project_ref, + 'project_title' => $o->project_title + ); + } + } + } + } + + $days = array("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"); + // EN: Map ISO day names to dedicated translation keys for full labels. + // FR: Associe les noms ISO aux clés de traduction dédiées pour les libellés complets. + $dayLabelKeys = array( + 'Monday' => 'TimesheetWeekDayMonday', + 'Tuesday' => 'TimesheetWeekDayTuesday', + 'Wednesday' => 'TimesheetWeekDayWednesday', + 'Thursday' => 'TimesheetWeekDayThursday', + 'Friday' => 'TimesheetWeekDayFriday', + 'Saturday' => 'TimesheetWeekDaySaturday', + 'Sunday' => 'TimesheetWeekDaySunday', + ); + $weekdates = array(); + $weekStartDate = null; + $weekEndDate = null; + // EN: Derive week boundaries defensively because the week metadata can be missing on drafts. + // FR: Calcule prudemment les bornes de semaine car les métadonnées peuvent manquer sur les brouillons. + if (!empty($object->year) && !empty($object->week)) { + $dto = new DateTime(); + $dto->setISODate((int)$object->year, (int)$object->week); + foreach ($days as $d) { + $weekdates[$d] = $dto->format('Y-m-d'); + $dto->modify('+1 day'); + } + $weekStartDate = isset($weekdates['Monday']) ? $weekdates['Monday'] : null; + $weekEndDate = isset($weekdates['Sunday']) ? $weekdates['Sunday'] : null; + } else { + foreach ($days as $d) { + $weekdates[$d] = null; + } + } + + if (!empty($tasks)) { + // EN: Define closed statuses dynamically to remain compatible across Dolibarr versions. + // FR: Définit dynamiquement les statuts clos pour rester compatible entre versions de Dolibarr. + $closedStatuses = array(); + if (defined('Task::STATUS_DONE')) $closedStatuses[] = Task::STATUS_DONE; + if (defined('Task::STATUS_CLOSED')) $closedStatuses[] = Task::STATUS_CLOSED; + if (defined('Task::STATUS_FINISHED')) $closedStatuses[] = Task::STATUS_FINISHED; + if (defined('Task::STATUS_CANCELLED')) $closedStatuses[] = Task::STATUS_CANCELLED; + if (defined('Task::STATUS_CANCELED')) $closedStatuses[] = Task::STATUS_CANCELED; + + $weekStartTs = ($weekStartDate ? strtotime($weekStartDate.' 00:00:00') : null); + $weekEndTs = ($weekEndDate ? strtotime($weekEndDate.' 23:59:59') : null); + + $filteredTasks = array(); + foreach ($tasks as $t) { + // EN: Skip tasks already completed or closed to declutter the weekly view. + // FR: Ignore les tâches déjà terminées ou clôturées pour épurer la vue hebdomadaire. + $progress = isset($t['task_progress']) ? $t['task_progress'] : null; + if ($progress !== null && (float)$progress >= 100) { + continue; + } + + $status = isset($t['task_status']) ? $t['task_status'] : null; + if ($status !== null) { + if (!empty($closedStatuses) && in_array((int)$status, $closedStatuses, true)) { + continue; + } + if (empty($closedStatuses) && (int)$status >= 3) { + continue; + } + } + + // EN: Extract scheduling information to hide tasks outside the sheet week. + // FR: Analyse les dates de planification pour masquer les tâches hors de la semaine de la feuille. + $startRaw = isset($t['task_date_start']) ? $t['task_date_start'] : null; + $endRaw = isset($t['task_date_end']) ? $t['task_date_end'] : null; + $startTs = null; + $endTs = null; + if (!empty($startRaw)) { + if (is_numeric($startRaw)) { + $startTs = (int)$startRaw; + } else { + $startTs = strtotime($startRaw); + if ($startTs === false) $startTs = null; + } + } + if (!empty($endRaw)) { + if (is_numeric($endRaw)) { + $endTs = (int)$endRaw; + } else { + $endTs = strtotime($endRaw); + if ($endTs === false) $endTs = null; + } + } + + // EN: Ignore tasks that start after the sheet week or end before it. + // FR: Ignore les tâches qui commencent après la semaine ou se terminent avant celle-ci. + if ($weekStartTs !== null && $weekEndTs !== null && $startTs !== null && $startTs > $weekEndTs) { + continue; + } + if ($weekStartTs !== null && $endTs !== null && $endTs < $weekStartTs) { + continue; + } + + $filteredTasks[] = $t; + } + $tasks = array_values($filteredTasks); + } + + // 4) AFFICHAGE + if (empty($tasks)) { + echo '
'.$langs->trans("NoTasksAssigned").'
'; + } else { + // Heures contractuelles + $contractedHours = $contractedHoursDisp; // Inputs zone/panier bloqués si statut != brouillon $disabledAttr = ($object->status != tw_status('draft')) ? ' disabled' : ''; - echo '
'; - // EN: Scope the vertical and horizontal centering helper to the specific cells that need alignment (days/zones/baskets/hours/totals). - // FR: Limite l'aide de centrage vertical et horizontal aux cellules spécifiques nécessitant l'alignement (jours/zones/paniers/heures/totaux). - echo ''; - echo ''; - - // EN: Apply the vertical-centering helper on each day header to keep labels visually aligned. - // FR: Applique l'aide de centrage vertical sur chaque en-tête de jour pour conserver des libellés alignés visuellement. - // Header jours + echo '
'; + // EN: Scope the vertical and horizontal centering helper to the specific cells that need alignment (days/zones/baskets/hours/totals). + // FR: Limite l'aide de centrage vertical et horizontal aux cellules spécifiques nécessitant l'alignement (jours/zones/paniers/heures/totaux). + echo ''; + echo '
'; + + // EN: Apply the vertical-centering helper on each day header to keep labels visually aligned. + // FR: Applique l'aide de centrage vertical sur chaque en-tête de jour pour conserver des libellés alignés visuellement. + // Header jours echo ''; - echo ''; - foreach ($days as $d) { - // EN: Render day headers safely even if week dates are undefined. - // FR: Affiche les en-têtes de jours en sécurité même sans dates de semaine définies. - $labelDate = ''; - if (!empty($weekdates[$d])) { - $tmpTs = strtotime($weekdates[$d]); - if ($tmpTs !== false) { - $labelDate = dol_print_date($tmpTs, 'day'); - } - } - $dayLabelKey = isset($dayLabelKeys[$d]) ? $dayLabelKeys[$d] : $d; - // EN: Translate the full day name to avoid ambiguous abbreviations. - // FR: Traduit le nom complet du jour pour éviter les abréviations ambiguës. - $dayLabel = $langs->trans($dayLabelKey); - echo ''; - } - echo ''; + echo ''; + foreach ($days as $d) { + // EN: Render day headers safely even if week dates are undefined. + // FR: Affiche les en-têtes de jours en sécurité même sans dates de semaine définies. + $labelDate = ''; + if (!empty($weekdates[$d])) { + $tmpTs = strtotime($weekdates[$d]); + if ($tmpTs !== false) { + $labelDate = dol_print_date($tmpTs, 'day'); + } + } + $dayLabelKey = isset($dayLabelKeys[$d]) ? $dayLabelKeys[$d] : $d; + // EN: Translate the full day name to avoid ambiguous abbreviations. + // FR: Traduit le nom complet du jour pour éviter les abréviations ambiguës. + $dayLabel = $langs->trans($dayLabelKey); + echo ''; + } + echo ''; echo ''; - // EN: Add the vertical-centering helper on zone and meal cells so both controls stay centered whatever their height. - // FR: Ajoute l'aide de centrage vertical sur les cellules zone et repas afin que les deux contrôles restent centrés quelle que soit leur hauteur. - // Ligne zone + panier (préfills depuis lignes) - echo ''; - echo ''; - foreach ($days as $d) { - // EN: Attach the vertical-centering helper to keep both zone selector and meal checkbox aligned. - // FR: Attache l'aide de centrage vertical pour garder alignés le sélecteur de zone et la case repas. - echo ''; - } - echo ''; - echo ''; + // EN: Add the vertical-centering helper on zone and meal cells so both controls stay centered whatever their height. + // FR: Ajoute l'aide de centrage vertical sur les cellules zone et repas afin que les deux contrôles restent centrés quelle que soit leur hauteur. +// Ligne zone + panier (préfills depuis lignes) +if (!$isDailyRateEmployee) { +echo ''; +echo ''; +foreach ($days as $d) { +// EN: Attach the vertical-centering helper to keep both zone selector and meal checkbox aligned. +// FR: Attache l'aide de centrage vertical pour garder alignés le sélecteur de zone et la case repas. +echo ''; +} +echo ''; +echo ''; +} // Regrouper par projet $byproject = array(); @@ -1322,9 +1433,19 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( $byproject[$pid]['tasks'][] = $t; } - // Lignes - $grandInit = 0.0; - foreach ($byproject as $pid => $pdata) { +// Lignes +$grandInit = 0.0; +$dailyRateOptions = array(); +if ($isDailyRateEmployee) { +// EN: Prepare localized labels for each forfait-jour choice. +// FR: Prépare les libellés localisés pour chaque choix de forfait jour. +$dailyRateOptions = array( +1 => $langs->trans('TimesheetWeekDailyRateFullDay'), +2 => $langs->trans('TimesheetWeekDailyRateMorning'), +3 => $langs->trans('TimesheetWeekDailyRateAfternoon'), +); +} +foreach ($byproject as $pid => $pdata) { // Ligne projet echo ''; $colspan = 1 + count($days) + 1; @@ -1332,72 +1453,105 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( $proj = new Project($db); $proj->fetch($pid); if (empty($proj->ref)) { $proj->ref = $pdata['ref']; $proj->title = $pdata['title']; } - echo tw_get_project_nomurl($proj, 1); + echo tw_get_project_nomurl($proj, 1); echo ''; echo ''; // Tâches - foreach ($pdata['tasks'] as $task) { - echo ''; - echo ''; - - $rowTotal = 0.0; - foreach ($days as $d) { - // EN: Attach the vertical-centering helper to each time entry cell for consistent layouts. - // FR: Attache l'aide de centrage vertical à chaque cellule de temps pour des mises en page cohérentes. - $iname = 'hours_'.$task['task_id'].'_'.$d; - $val = ''; - $keydate = $weekdates[$d]; - if (isset($hoursBy[(int)$task['task_id']][$keydate])) { - $val = formatHours($hoursBy[(int)$task['task_id']][$keydate]); - $rowTotal += (float)$hoursBy[(int)$task['task_id']][$keydate]; - } - $readonly = ($object->status != tw_status('draft')) ? ' readonly' : ''; - echo ''; - } - $grandInit += $rowTotal; - // EN: Center task totals so they stay aligned with other centered figures. - // FR: Centre les totaux de tâche pour les garder alignés avec les autres valeurs centrées. - echo ''; - echo ''; - } - } - - $grand = ($object->total_hours > 0 ? (float)$object->total_hours : $grandInit); +foreach ($pdata['tasks'] as $task) { +echo ''; +echo ''; + +$rowTotal = 0.0; +foreach ($days as $d) { +// EN: Attach the vertical-centering helper to each time entry cell for consistent layouts. +// FR: Attache l'aide de centrage vertical à chaque cellule de temps pour des mises en page cohérentes. +$iname = 'hours_'.$task['task_id'].'_'.$d; +$rateName = 'daily_'.$task['task_id'].'_'.$d; +$val = ''; +$rateVal = 0; +$keydate = $weekdates[$d]; +if (isset($hoursBy[(int)$task['task_id']][$keydate])) { +$val = formatHours($hoursBy[(int)$task['task_id']][$keydate]); +$rowTotal += (float)$hoursBy[(int)$task['task_id']][$keydate]; +} +if (isset($dailyRateBy[(int)$task['task_id']][$keydate])) { +$rateVal = (int)$dailyRateBy[(int)$task['task_id']][$keydate]; +} +if ($isDailyRateEmployee) { +$disabledSelect = ($object->status != tw_status('draft')) ? ' disabled' : ''; +$selectHtml = ''; +echo ''; + } else { +$readonly = ($object->status != tw_status('draft')) ? ' readonly' : ''; +echo ''; +} +} +$grandInit += $rowTotal; +// EN: Center task totals so they stay aligned with other centered figures. +// FR: Centre les totaux de tâche pour les garder alignés avec les autres valeurs centrées. +if ($isDailyRateEmployee) { +echo ''; +} else { +echo ''; +} +echo ''; +} +} - echo ''; - // EN: Center overall totals and daily sums for consistent middle alignment. - // FR: Centre les totaux généraux et journaliers pour un alignement médian homogène. - echo ''; - foreach ($days as $d) echo ''; - echo ''; - echo ''; +$grand = ($object->total_hours > 0 ? (float) $object->total_hours : $grandInit); - // EN: Use the vertical-centering helper on totals to keep computed values aligned with their labels. - // FR: Utilise l'aide de centrage vertical sur les totaux pour conserver les valeurs calculées alignées avec leurs libellés. - echo ''; - // EN: Center meal counters to match the rest of the grid alignment. - // FR: Centre les compteurs de repas pour correspondre au reste de l'alignement de la grille. - echo ''; - $initMeals = array_sum($dayMeal); - echo ''; - echo ''; - echo ''; - - echo ''; - // EN: Center overtime summary cells so every footer row follows the same alignment pattern. - // FR: Centre les cellules du récapitulatif des heures supplémentaires pour harmoniser l'alignement de chaque ligne de pied. - echo ''; - $ot = ($object->overtime_hours > 0 ? (float)$object->overtime_hours : max(0.0, $grand - $contractedHours)); - //echo ''; - echo ''; - echo ''; - echo ''; +if ($isDailyRateEmployee) { +$grandDays = ($grand > 0 ? ($grand / 8.0) : 0.0); +echo ''; +// EN: Center overall totals expressed in days for forfait jour employees. +// FR: Centre les totaux globaux exprimés en jours pour les salariés au forfait jour. +echo ''; +foreach ($days as $d) { +echo ''; +} +echo ''; +echo ''; +} else { +echo ''; +// EN: Center overall totals and daily sums for consistent middle alignment. +// FR: Centre les totaux généraux et journaliers pour un alignement médian homogène. +echo ''; +foreach ($days as $d) { +echo ''; +} +echo ''; +echo ''; + +echo ''; +// EN: Center meal counters to match the rest of the grid alignment. +// FR: Centre les compteurs de repas pour correspondre au reste de l'alignement de la grille. +echo ''; +$initMeals = array_sum($dayMeal); +echo ''; +echo ''; +echo ''; + +echo ''; +// EN: Center overtime summary cells so every footer row follows the same alignment pattern. +// FR: Centre les cellules du récapitulatif des heures supplémentaires pour harmoniser l'alignement de chaque ligne de pied. +echo ''; +$ot = ($object->overtime_hours > 0 ? (float) $object->overtime_hours : max(0.0, $grand - $contractedHours)); +echo ''; +echo ''; +echo ''; +} echo '
'.$langs->trans("ProjectTaskColumn").''.$dayLabel; - if ($labelDate !== '') { - echo '
'.$labelDate.''; - } - echo '
'.$langs->trans("Total").''.$langs->trans("ProjectTaskColumn").''.$dayLabel; + if ($labelDate !== '') { + echo '
'.$labelDate.''; + } + echo '
'.$langs->trans("Total").'
'; - // EN: Prefix zone selector with its label to improve understanding. - // FR: Préfixe le sélecteur de zone avec son libellé pour améliorer la compréhension. - echo ''.$langs->trans("Zone").' '; - echo '
'; - $checked = $dayMeal[$d] ? ' checked' : ''; - echo ''; - echo '
'; +// EN: Prefix zone selector with its label to improve understanding. +// FR: Préfixe le sélecteur de zone avec son libellé pour améliorer la compréhension. +echo ''.$langs->trans("Zone").' '; +echo '
'; +$checked = $dayMeal[$d] ? ' checked' : ''; +echo ''; +echo '
'; - $tsk = new Task($db); - $tsk->fetch((int)$task['task_id']); - if (empty($tsk->label)) { $tsk->id = (int)$task['task_id']; $tsk->ref = $task['task_ref'] ?? ''; $tsk->label = $task['task_label']; } - echo tw_get_task_nomurl($tsk, 1); - echo ''.formatHours($rowTotal).'
'; +$tsk = new Task($db); +$tsk->fetch((int)$task['task_id']); +if (empty($tsk->label)) { $tsk->id = (int)$task['task_id']; $tsk->ref = $task['task_ref'] ?? ''; $tsk->label = $task['task_label']; } +echo tw_get_task_nomurl($tsk, 1); +echo ''.$selectHtml.''.tw_format_days(($rowTotal > 0 ? ($rowTotal / 8.0) : 0.0), $langs).''.formatHours($rowTotal).'
'.$langs->trans("Total").'00:00'.formatHours($grand).'
'.$langs->trans("Meals").''.$initMeals.'
'.$langs->trans("Overtime").' ('.formatHours($contractedHours).')'.formatHours($ot).'
'.$langs->trans("TimesheetWeekTotalDays").''.tw_format_days(0, $langs).''.tw_format_days($grandDays, $langs).'
'.$langs->trans("Total").'00:00'.formatHours($grand).'
'.$langs->trans("Meals").''.$initMeals.'
'.$langs->trans("Overtime").' ('.formatHours($contractedHours).')'.formatHours($ot).'
'; echo '
'; @@ -1412,165 +1566,214 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( echo ''; // JS totaux + mise à jour entête live - $jsGrid = << (function($){ - function parseHours(v){ - if(!v) return 0; - if(v.indexOf(":") === -1) return parseFloat(v)||0; - var p=v.split(":"); var h=parseInt(p[0],10)||0; var m=parseInt(p[1],10)||0; - return h + (m/60); - } - function formatHours(d){ - if(isNaN(d)) return "00:00"; - var h=Math.floor(d); var m=Math.round((d-h)*60); - if(m===60){ h++; m=0; } - return String(h).padStart(2,"0")+":"+String(m).padStart(2,"0"); +var isDailyRateMode = %s; +var dailyRateHoursMap = {1:8,2:4,3:4}; +var weeklyContract = %s; +function parseHours(v){ + if(!v) return 0; + if(v.indexOf(":") === -1) return parseFloat(v)||0; + var p=v.split(":"); var h=parseInt(p[0],10)||0; var m=parseInt(p[1],10)||0; + return h + (m/60); +} +function elementHours($el){ + if(isDailyRateMode && $el.is('select')){ + var code=parseInt($el.val(),10); + return dailyRateHoursMap[code] ? dailyRateHoursMap[code] : 0; } - function updateTotals(){ - var grand=0; - $(".task-total").text("00:00"); - $(".day-total").text("00:00"); - - $("table.noborder tr").each(function(){ - var rowT=0; - $(this).find("input.hourinput").each(function(){ - var v=parseHours($(this).val()); - if(v>0){ - rowT+=v; - var idx=$(this).closest("td").index(); - var daycell=$("tr.liste_total:first td").eq(idx); - var cur=parseHours(daycell.text()); - daycell.text(formatHours(cur+v)); - grand+=v; + return parseHours($el.val()); +} +function elementDays($el){ + // EN: Convert the hour contribution to days with a fixed 8h reference. + // FR: Convertit la contribution horaire en jours sur la base fixe de 8h. + return elementHours($el) / 8; +} +function formatHours(d){ + if(isNaN(d)) return "00:00"; + var h=Math.floor(d); var m=Math.round((d-h)*60); + if(m===60){ h++; m=0; } + // EN: Build HH:MM strings without padStart to work on legacy browsers. + // FR: Construit les chaînes HH:MM sans padStart pour fonctionner sur les anciens navigateurs. + var hh=(h<10?"0":"")+h; + var mm=(m<10?"0":"")+m; + return hh+":"+mm; +} +function formatDays(d){ + if(isNaN(d)) return "0.00"; + return (Math.round(d*100)/100).toFixed(2); +} +function updateTotals(){ + var totalRowSelector = isDailyRateMode ? ".row-total-days" : ".row-total-hours"; + var formatFn = isDailyRateMode ? formatDays : formatHours; + var elementFn = isDailyRateMode ? elementDays : elementHours; + var grand=0; + var dayTotals=[]; + + // EN: Reset per-task and per-day totals before recomputing the grid. + // FR: Réinitialise les totaux par tâche et par jour avant de recalculer la grille. + $(".task-total").text(formatFn(0)); + $(totalRowSelector+" .day-total").each(function(idx){ + dayTotals[idx]=0; + $(this).text(formatFn(0)); + }); + $(totalRowSelector+" .grand-total").text(formatFn(0)); + + $("table.noborder tr").each(function(){ + var rowT=0; + $(this).find("input.hourinput, select.daily-rate-select").each(function(){ + var v=elementFn($(this)); + if(v>0){ + rowT+=v; + // EN: Align the day counter with the footer cells by skipping the label column. + // FR: Aligne le compteur journalier sur les cellules du pied en ignorant la colonne du libellé. + var idx=$(this).closest("td").index()-1; + if(idx>=0 && typeof dayTotals[idx]!=="undefined"){ + dayTotals[idx]+=v; } - }); - if(rowT>0) $(this).find(".task-total").text(formatHours(rowT)); + grand+=v; + } }); + if(rowT>0) $(this).find(".task-total").text(formatFn(rowT)); + }); - $(".grand-total").text(formatHours(grand)); - - var meals = $(".mealbox:checked").length; - $(".meal-total").text(meals); + // EN: Reflect the new per-day totals after iterating over every input cell. + // FR: Répercute les nouveaux totaux journaliers après l'analyse de chaque cellule de saisie. + $(totalRowSelector+" .day-total").each(function(idx){ + $(this).text(formatFn(dayTotals[idx])); + }); - var weeklyContract = {$contractedHours}; - var ot = grand - weeklyContract; if (ot < 0) ot = 0; - $(".overtime-total").text(formatHours(ot)); + $(totalRowSelector+" .grand-total").text(formatFn(grand)); + +if(isDailyRateMode){ +$(".meal-total").text('0'); +} else { +var meals = $(".mealbox:checked").length; +$(".meal-total").text(meals); +var ot = grand - weeklyContract; if (ot < 0) ot = 0; +$(".overtime-total").text(formatFn(ot)); +if($(".header-overtime").length){ +$(".header-overtime").text(formatFn(ot)); +} +} - // met à jour l'entête - $(".header-total-hours").text(formatHours(grand)); - $(".header-overtime").text(formatHours(ot)); - } +// met à jour l'entête +$(".header-total-main").text(formatFn(grand)); +} $(function(){ updateTotals(); // au chargement - $(document).on("input change", "input.hourinput, input.mealbox", updateTotals); + $(document).on("input change", "input.hourinput, select.daily-rate-select, input.mealbox", updateTotals); }); })(jQuery); JS; + $jsGrid = sprintf($jsGrid, $isDailyRateEmployee ? 'true' : 'false', json_encode((float) price2num($contractedHours, '6'))); echo $jsGrid; } - // ---- Boutons d’action (barre) ---- - echo '
'; - - $token = newToken(); - - if ($object->status == tw_status('sealed')) { - // EN: In sealed state only show the unseal control for authorized users. - // FR : En statut scellé, n'afficher que l'action de descellage pour les utilisateurs autorisés. - if ($permUnseal) { - echo dolGetButtonAction('', $langs->trans('UnsealTimesheet'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=unseal&token='.$token); - } - } else { - if ($canSendMail) { - echo dolGetButtonAction('', $langs->trans('Sendbymail'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=presend&mode=init&token='.$token); - } - - // Soumettre : uniquement brouillon + au moins 1 ligne existante + droits - if ($object->status == tw_status('draft')) { - // Compter les lignes - $nbLines = 0; - // EN: Count lines only within authorized entities before enabling submission. - // FR: Compte les lignes uniquement dans les entités autorisées avant d'autoriser la soumission. - $rescnt = $db->query("SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int)$object->id." AND entity IN (".getEntity('timesheetweek').")"); - if ($rescnt) { $o=$db->fetch_object($rescnt); $nbLines=(int)$o->nb; } - if ($nbLines > 0 && tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user)) { - echo dolGetButtonAction('', $langs->trans("Submit"), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=submit&token='.$token); - } - } - - // Retour brouillon : si statut != brouillon (soumis / approuvé / refusé) pour salarié/or valideur - if ($object->status != tw_status('draft')) { - $canEmployee = tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user); - $canValidator = tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); - if ($canEmployee || $canValidator) { - echo dolGetButtonAction('', $langs->trans("SetToDraft"), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=setdraft&token='.$token); - } - } - - // Approuver / Refuser quand soumis (validateur/manager/all/own) - if ($object->status == tw_status('submitted')) { - $canValidator = tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); - if ($canValidator) { - echo dolGetButtonAction('', ($langs->trans("Approve")!='Approve'?$langs->trans("Approve"):'Approuver'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=ask_validate&token='.$token); - echo dolGetButtonAction('', $langs->trans("Refuse"), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=ask_refuse&token='.$token); - } - } - - // EN: Allow sealing once the sheet is approved and the user is authorized. - // FR : Autorise le scellement dès que la feuille est approuvée et que l'utilisateur est habilité. - if ($object->status == tw_status('approved') && $permSeal) { - echo dolGetButtonAction('', $langs->trans('SealTimesheet'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=seal&token='.$token); - } - - // Supprimer : brouillon OU soumis/approuvé/refusé si salarié (delete) ou validateur (validate*) ou all - $canDelete = tw_can_act_on_user($object->fk_user, $permDelete, $permDeleteChild, $permDeleteAll, $user) - || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); - if ($canDelete) { - echo dolGetButtonAction('', $langs->trans("Delete"), 'delete', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=delete&token='.$token); - } - } - - echo '
'; - - if ($action === 'presend') { - $formmail = new FormMail($db); - $formmail->showform = 1; - $formmail->withfrom = 1; - $formmail->fromtype = 'user'; - $formmail->fromid = $user->id; - $formmail->fromname = $user->getFullName($langs); - $formmail->frommail = !empty($user->email) ? $user->email : getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'); - $formmail->withreplyto = 1; - $formmail->replyto = GETPOST('replyto', 'none'); - $formmail->withto = 1; - $formmail->withtofree = 1; - $formmail->withtocc = 1; - $formmail->withbcc = 1; - $formmail->withtopic = 1; - $formmail->withfile = 2; - $formmail->withmaindocfile = 1; - $formmail->withdeliveryreceipt = 1; - $formmail->withtpl = 1; - $formmail->withsubstit = 1; - $formmail->withcancel = 1; - $formmail->modelmail = $modelmail; - $formmail->trackid = $trackid; - $formmail->subject = GETPOST('subject', 'restricthtml'); - $formmail->topic = $formmail->subject; - $formmail->content = GETPOST('message', 'restricthtml'); - $formmail->message = $formmail->content; - $formmail->sendto = GETPOST('sendto', 'none'); - $formmail->substit = $moresubstit; - $formmail->param = array_merge($param, array( - 'id' => $object->id, - 'action' => 'send', - )); - $formmail->langcode = $langs->defaultlang; - - include DOL_DOCUMENT_ROOT.'/core/tpl/card_presend.tpl.php'; - } + // ---- Boutons d’action (barre) ---- + echo '
'; + + $token = newToken(); + + if ($object->status == tw_status('sealed')) { + // EN: In sealed state only show the unseal control for authorized users. + // FR : En statut scellé, n'afficher que l'action de descellage pour les utilisateurs autorisés. + if ($permUnseal) { + echo dolGetButtonAction('', $langs->trans('UnsealTimesheet'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=unseal&token='.$token); + } + } else { + if ($canSendMail) { + echo dolGetButtonAction('', $langs->trans('Sendbymail'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=presend&mode=init&token='.$token); + } + + // Soumettre : uniquement brouillon + au moins 1 ligne existante + droits + if ($object->status == tw_status('draft')) { + // Compter les lignes + $nbLines = 0; + // EN: Count lines only within authorized entities before enabling submission. + // FR: Compte les lignes uniquement dans les entités autorisées avant d'autoriser la soumission. + $rescnt = $db->query("SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."timesheet_week_line WHERE fk_timesheet_week=".(int)$object->id." AND entity IN (".getEntity('timesheetweek').")"); + if ($rescnt) { $o=$db->fetch_object($rescnt); $nbLines=(int)$o->nb; } + if ($nbLines > 0 && tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user)) { + echo dolGetButtonAction('', $langs->trans("Submit"), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=submit&token='.$token); + } + } + + // Retour brouillon : si statut != brouillon (soumis / approuvé / refusé) pour salarié/or valideur + if ($object->status != tw_status('draft')) { + $canEmployee = tw_can_act_on_user($object->fk_user, $permWrite, $permWriteChild, $permWriteAll, $user); + $canValidator = tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); + if ($canEmployee || $canValidator) { + echo dolGetButtonAction('', $langs->trans("SetToDraft"), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=setdraft&token='.$token); + } + } + + // Approuver / Refuser quand soumis (validateur/manager/all/own) + if ($object->status == tw_status('submitted')) { + $canValidator = tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); + if ($canValidator) { + echo dolGetButtonAction('', ($langs->trans("Approve")!='Approve'?$langs->trans("Approve"):'Approuver'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=ask_validate&token='.$token); + echo dolGetButtonAction('', $langs->trans("Refuse"), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=ask_refuse&token='.$token); + } + } + + // EN: Allow sealing once the sheet is approved and the user is authorized. + // FR : Autorise le scellement dès que la feuille est approuvée et que l'utilisateur est habilité. + if ($object->status == tw_status('approved') && $permSeal) { + echo dolGetButtonAction('', $langs->trans('SealTimesheet'), 'default', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=seal&token='.$token); + } + + // Supprimer : brouillon OU soumis/approuvé/refusé si salarié (delete) ou validateur (validate*) ou all + $canDelete = tw_can_act_on_user($object->fk_user, $permDelete, $permDeleteChild, $permDeleteAll, $user) + || tw_can_validate_timesheet($object, $user, $permValidate, $permValidateOwn, $permValidateChild, $permValidateAll, $permWrite, $permWriteChild, $permWriteAll); + if ($canDelete) { + echo dolGetButtonAction('', $langs->trans("Delete"), 'delete', $_SERVER["PHP_SELF"].'?id='.$object->id.'&action=delete&token='.$token); + } + } + + echo '
'; + + if ($action === 'presend') { + $formmail = new FormMail($db); + $formmail->showform = 1; + $formmail->withfrom = 1; + $formmail->fromtype = 'user'; + $formmail->fromid = $user->id; + $formmail->fromname = $user->getFullName($langs); + $formmail->frommail = !empty($user->email) ? $user->email : getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'); + $formmail->withreplyto = 1; + $formmail->replyto = GETPOST('replyto', 'none'); + $formmail->withto = 1; + $formmail->withtofree = 1; + $formmail->withtocc = 1; + $formmail->withbcc = 1; + $formmail->withtopic = 1; + $formmail->withfile = 2; + $formmail->withmaindocfile = 1; + $formmail->withdeliveryreceipt = 1; + $formmail->withtpl = 1; + $formmail->withsubstit = 1; + $formmail->withcancel = 1; + $formmail->modelmail = $modelmail; + $formmail->trackid = $trackid; + $formmail->subject = GETPOST('subject', 'restricthtml'); + $formmail->topic = $formmail->subject; + $formmail->content = GETPOST('message', 'restricthtml'); + $formmail->message = $formmail->content; + $formmail->sendto = GETPOST('sendto', 'none'); + $formmail->substit = $moresubstit; + $formmail->param = array_merge($param, array( + 'id' => $object->id, + 'action' => 'send', + )); + $formmail->langcode = $langs->defaultlang; + + include DOL_DOCUMENT_ROOT.'/core/tpl/card_presend.tpl.php'; + } }