diff --git a/ChangeLog.md b/ChangeLog.md index 8cbd24b..36eb6bf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,9 @@ # CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) +## 1.6.0 (20/06/2026) +- Ajoute un rappel hebdomadaire automatique par email configurable (activation, jour, heure, modèle) et une tâche planifiée dédiée avec bouton de test administrateur. + / Adds a configurable automatic weekly email reminder (enablement, day, time, template) plus a dedicated scheduled task and admin test button. + ## 1.5.0 (01/12/2025) - Ajoute une action de masse pour générer un PDF unique fusionnant les feuilles sélectionnées en utilisant le modèle par défaut. / Adds a mass action to generate a single merged PDF for selected timesheets using the default template. diff --git a/README.md b/README.md index 3273f32..364e614 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ TimesheetWeek ajoute une gestion hebdomadaire des feuilles de temps fidèle à l - 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. +- Rappel hebdomadaire automatique par email configurable (activation, jour, heure, modèle) avec tâche planifiée dédiée et bouton d'envoi de test administrateur. - 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. @@ -51,6 +52,7 @@ TimesheetWeek delivers weekly timesheet management that follows Dolibarr design - 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. +- Configurable automatic weekly email reminder (enablement, weekday, time, template) with a dedicated scheduled task and admin test send button. - 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/admin/setup.php b/admin/setup.php index 1394de5..7d19608 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -22,7 +22,6 @@ */ // EN: Load Dolibarr environment with fallback paths. -// FR: Charge l'environnement Dolibarr en testant les chemins possibles. $res = 0; if (!$res && file_exists(__DIR__.'/../main.inc.php')) { $res = require_once __DIR__.'/../main.inc.php'; @@ -40,34 +39,37 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php'; // EN: Load document helper functions required for model toggles. -// FR: Charge les fonctions d'aide aux documents nécessaires aux commutateurs de modèles. require_once DOL_DOCUMENT_ROOT.'/core/lib/doc.lib.php'; dol_include_once('/timesheetweek/lib/timesheetweek.lib.php'); dol_include_once('/timesheetweek/class/timesheetweek.class.php'); +dol_include_once('/timesheetweek/class/timesheetweek_reminder.class.php'); // EN: Load translation files required for the configuration page. -// FR: Charge les fichiers de traduction nécessaires à la page de configuration. $langs->loadLangs(array('admin', 'other', 'timesheetweek@timesheetweek')); // EN: Only administrators can access the setup. -// FR: Seuls les administrateurs peuvent accéder à la configuration. if (empty($user->admin)) { accessforbidden(); } // EN: Read HTTP parameters once so we can re-use them further down. -// FR: Lit les paramètres HTTP une seule fois pour les réutiliser ensuite. $action = GETPOST('action', 'aZ09'); $value = GETPOST('value', 'alphanohtml'); $token = GETPOST('token', 'alphanohtml'); // EN: Capture additional parameters used to reproduce Dolibarr's document model toggles. -// FR: Capture les paramètres additionnels utilisés par les bascules de modèles de document Dolibarr. $docLabel = GETPOST('label', 'alphanohtml'); $scanDir = GETPOST('scan_dir', 'alpha'); +$reminderAction = GETPOST('reminder_action', 'aZ09'); + +if (is_readable(DOL_DOCUMENT_ROOT.'/core/class/cemailtemplates.class.php')) { + dol_include_once('/core/class/cemailtemplates.class.php'); +} elseif (is_readable(DOL_DOCUMENT_ROOT.'/core/class/emailtemplates.class.php')) { + dol_include_once('/core/class/emailtemplates.class.php'); +} // EN: Helper to enable a PDF model in the database. -// FR: Aide pour activer un modèle PDF dans la base. function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') { global $db, $conf; @@ -77,7 +79,6 @@ function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') } // EN: Check if the model already exists for the current entity to avoid duplicates. - // FR: Vérifie si le modèle existe déjà pour l'entité courante afin d'éviter les doublons. $sql = 'SELECT rowid FROM '.MAIN_DB_PREFIX."document_model WHERE nom='".$db->escape($model)."' AND type='timesheetweek' AND entity=".((int) $conf->entity); $resql = $db->query($sql); if (!$resql) { @@ -91,13 +92,11 @@ function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') $fields = array(); // EN: Refresh label when provided to keep UI messages in sync. - // FR: Met à jour le libellé fourni pour garder l'interface cohérente. if ($label !== '') { $fields[] = "libelle='".$db->escape($label)."'"; } // EN: Refresh directory hint when provided to ease future scans. - // FR: Met à jour le chemin fourni pour faciliter les scans ultérieurs. if ($scandir !== '') { $fields[] = "description='".$db->escape($scandir)."'"; } @@ -122,7 +121,6 @@ function timesheetweekEnableDocumentModel($model, $label = '', $scandir = '') // EN: Helper to disable a PDF model from the database. -// FR: Aide pour désactiver un modèle PDF dans la base. function timesheetweekDisableDocumentModel($model) { if (empty($model)) { @@ -134,7 +132,6 @@ function timesheetweekDisableDocumentModel($model) } // EN: Build the list of numbering modules available for the module. -// FR: Construit la liste des modules de numérotation disponibles pour le module. function timesheetweekListNumberingModules(array $directories, Translate $langs, TimesheetWeek $sample, $selected) { global $db; @@ -143,7 +140,6 @@ function timesheetweekListNumberingModules(array $directories, Translate $langs, foreach ($directories as $reldir) { // EN: Resolve the directory that holds the numbering module classes. - // FR: Résout le répertoire qui contient les classes de numérotation. $dir = dol_buildpath(rtrim($reldir, '/').'/timesheetweek/core/modules/timesheetweek/'); if (!is_dir($dir)) { continue; @@ -210,7 +206,6 @@ function timesheetweekListNumberingModules(array $directories, Translate $langs, } // EN: Build the list of available document models for the module. -// FR: Construit la liste des modèles de documents disponibles pour le module. function timesheetweekListDocumentModels(array $directories, Translate $langs, array $enabled, $default) { global $db; @@ -219,7 +214,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a foreach ($directories as $reldir) { // EN: Resolve the directory that stores the document model definitions. - // FR: Résout le répertoire qui contient les définitions de modèles de document. $dir = dol_buildpath(rtrim($reldir, '/').'/timesheetweek/core/modules/timesheetweek/doc/'); if (!is_dir($dir)) { continue; @@ -275,8 +269,15 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Verify CSRF token when the request changes the configuration. -// FR: Vérifie le jeton CSRF lorsque la requête modifie la configuration. if (in_array($action, array('setmodule', 'setdoc', 'setdocmodel', 'delmodel', 'setquarterday'), true)) { + if (function_exists('dol_verify_token')) { + if (empty($token) || dol_verify_token($token) <= 0) { + accessforbidden(); + } + } +} + +if (in_array($reminderAction, array('savereminder', 'testreminder'), true)) { if (function_exists('dol_verify_token')) { if (empty($token) || dol_verify_token($token) <= 0) { accessforbidden(); @@ -285,7 +286,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Persist the chosen numbering module. -// FR: Enregistre le module de numérotation choisi. if ($action === 'setmodule' && !empty($value)) { $result = dolibarr_set_const($db, 'TIMESHEETWEEK_ADDON', $value, 'chaine', 0, '', $conf->entity); if ($result > 0) { @@ -296,7 +296,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Set the default PDF model while ensuring the model is enabled. -// FR: Définit le modèle PDF par défaut tout en s'assurant qu'il est activé. if ($action === 'setdoc' && !empty($value)) { $res = timesheetweekEnableDocumentModel($value, $docLabel, $scanDir); if ($res > 0) { @@ -310,7 +309,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Activate a PDF model without making it the default. -// FR: Active un modèle PDF sans le définir comme défaut. if ($action === 'setdocmodel' && !empty($value)) { $res = timesheetweekEnableDocumentModel($value, $docLabel, $scanDir); if ($res > 0) { @@ -321,7 +319,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Disable a PDF model and remove the default flag if needed. -// FR: Désactive un modèle PDF et supprime le statut par défaut si nécessaire. if ($action === 'delmodel' && !empty($value)) { $res = timesheetweekDisableDocumentModel($value); if ($res > 0) { @@ -335,12 +332,11 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Enable or disable the quarter-day selector for daily rate contracts. -// FR: Active ou désactive le sélecteur quart de jour pour les contrats au forfait jour. if ($action === 'setquarterday') { - $targetValue = (int) GETPOST('value', 'int'); - if ($targetValue !== 0) { - $targetValue = 1; - } + $targetValue = (int) GETPOST('value', 'int'); + if ($targetValue !== 0) { + $targetValue = 1; + } $res = dolibarr_set_const($db, 'TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', $targetValue, 'chaine', 0, '', $conf->entity); if ($res > 0) { setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); @@ -349,19 +345,70 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } } +if ($reminderAction === 'savereminder') { +$reminderEnabledValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_ENABLED', 'int'); +$reminderWeekdayValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_WEEKDAY', 'int'); +$reminderHourValue = trim(GETPOST('TIMESHEETWEEK_REMINDER_HOUR', 'alphanohtml')); + $reminderTemplateValue = (int) GETPOST('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', 'int'); + + $error = 0; + + if ($reminderWeekdayValue < 1 || $reminderWeekdayValue > 7) { + setEventMessages($langs->trans('TimesheetWeekReminderWeekdayInvalid'), null, 'errors'); + $error++; + } + + if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $reminderHourValue)) { + setEventMessages($langs->trans('TimesheetWeekReminderHourInvalid'), null, 'errors'); + $error++; + } + + if (!$error) { + $results = array(); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_ENABLED', ($reminderEnabledValue ? 1 : 0), 'chaine', 0, '', $conf->entity); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_WEEKDAY', $reminderWeekdayValue, 'chaine', 0, '', $conf->entity); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_HOUR', $reminderHourValue, 'chaine', 0, '', $conf->entity); + $results[] = dolibarr_set_const($db, 'TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', $reminderTemplateValue, 'chaine', 0, '', $conf->entity); + + $hasError = false; + foreach ($results as $resultValue) { + if ($resultValue <= 0) { + $hasError = true; + break; + } + } + + if ($hasError) { + setEventMessages($langs->trans('Error'), null, 'errors'); + } else { + setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); +} +} +} + + if ($reminderAction === 'testreminder') { + $resultTest = TimesheetweekReminder::sendTest($db, $user); + if ($resultTest > 0) { + setEventMessages($langs->trans('TimesheetWeekReminderTestSuccess', $resultTest), null, 'mesgs'); + } else { + setEventMessages($langs->trans('TimesheetWeekReminderTestError'), null, 'errors'); + } +} + // EN: Read the selected options so we can highlight them in the UI. -// FR: Lit les options sélectionnées pour les mettre en avant dans l'interface. $selectedNumbering = getDolGlobalString('TIMESHEETWEEK_ADDON', 'mod_timesheetweek_standard'); $defaultPdf = getDolGlobalString('TIMESHEETWEEK_ADDON_PDF', 'standard_timesheetweek'); $useQuarterDaySelector = getDolGlobalInt('TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', 0); +$reminderEnabled = getDolGlobalInt('TIMESHEETWEEK_REMINDER_ENABLED', 0); +$reminderWeekday = getDolGlobalInt('TIMESHEETWEEK_REMINDER_WEEKDAY', 1); +$reminderHour = getDolGlobalString('TIMESHEETWEEK_REMINDER_HOUR', '18:00'); +$reminderTemplateId = getDolGlobalInt('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', 0); $directories = array_merge(array('/'), (array) $conf->modules_parts['models']); // EN: Prepare a lightweight object to test numbering module activation. -// FR: Prépare un objet léger pour tester l'activation des modules de numérotation. $sampleTimesheet = new TimesheetWeek($db); // EN: Fetch the enabled document models from the database. -// FR: Récupère les modèles de documents activés depuis la base. $enabledModels = array(); $sql = 'SELECT nom FROM '.MAIN_DB_PREFIX."document_model WHERE type='timesheetweek' AND entity IN (0, ".((int) $conf->entity).')'; $resql = $db->query($sql); @@ -373,10 +420,29 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a } // EN: Build the metadata arrays used by the HTML rendering below. -// FR: Construit les tableaux de métadonnées utilisés par l'affichage HTML ci-dessous. $numberingModules = timesheetweekListNumberingModules($directories, $langs, $sampleTimesheet, $selectedNumbering); $documentModels = timesheetweekListDocumentModels($directories, $langs, $enabledModels, $defaultPdf); $pageToken = function_exists('newToken') ? newToken() : ''; +$form = new Form($db); + +$emailTemplates = array(); +$emailTemplateClass = ''; +if (class_exists('CEmailTemplates')) { + $emailTemplateClass = 'CEmailTemplates'; +} elseif (class_exists('EmailTemplates')) { + $emailTemplateClass = 'EmailTemplates'; +} + +if (!empty($emailTemplateClass)) { + $emailTemplateObject = new $emailTemplateClass($db); + if (method_exists($emailTemplateObject, 'fetchAll')) { + $filters = array('entity' => $conf->entity); + $templatesResult = $emailTemplateObject->fetchAll('', '', 0, 0, $filters); + if (is_array($templatesResult)) { + $emailTemplates = $templatesResult; + } + } +} $title = $langs->trans('ModuleSetup', 'Timesheetweek'); $helpurl = ''; @@ -397,7 +463,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; // EN: Display the numbering modules with switch-based activation instead of radios. -// FR: Affiche les modules de numérotation avec des commutateurs plutôt qu'avec des radios. print '
'.$langs->trans('TimesheetWeekNumberingHelp').'
'; print '
'; @@ -433,7 +498,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print ''.(!empty($moduleInfo['example']) ? dol_escape_htmltag($moduleInfo['example']) : ' ').''; // EN: Render the activation toggle that selects the numbering model with CSRF protection. - // FR: Affiche le commutateur d’activation qui sélectionne le modèle de numérotation avec protection CSRF. print ''; if ($moduleInfo['active']) { print img_picto($langs->trans('Enabled'), 'switch_on'); @@ -455,7 +519,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; // EN: Display the helper switches dedicated to the daily-rate contract workflows. -// FR: Affiche les commutateurs dédiés aux workflows des contrats au forfait jour. print load_fiche_titre($langs->trans('TimesheetWeekDailyRateOptions'), '', 'setup'); print '
'.$langs->trans('TimesheetWeekDailyRateOptionsHelp').'
'; @@ -468,7 +531,6 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print ''; // EN: Render the switch dedicated to the quarter-day declaration helper. -// FR: Affiche le commutateur dédié à l'aide de déclaration des quarts de jour. print ''; print ''.$langs->trans('TimesheetWeekQuarterDayForDailyContract').''; print ''.$langs->trans('TimesheetWeekQuarterDayForDailyContractHelp').''; @@ -488,6 +550,95 @@ function timesheetweekListDocumentModels(array $directories, Translate $langs, a print '
'; +print load_fiche_titre($langs->trans('TimesheetWeekReminderSectionTitle'), '', 'email'); +print '
'.$langs->trans('TimesheetWeekReminderSectionHelp').'
'; + +print '
'; +print ''; + +print '
'; +print ''; +print ''; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; +print ''; + +$weekdayOptions = array( +1 => $langs->trans('Monday'), +2 => $langs->trans('Tuesday'), +3 => $langs->trans('Wednesday'), +4 => $langs->trans('Thursday'), +5 => $langs->trans('Friday'), +6 => $langs->trans('Saturday'), +7 => $langs->trans('Sunday'), +); + +print ''; +print ''; +print ''; +print ''; +print ''; + +print ''; +print ''; +print ''; +print ''; +print ''; + +$templateOptions = array(0 => $langs->trans('None')); +foreach ($emailTemplates as $templateItem) { + $templateId = 0; + if (!empty($templateItem->id)) { + $templateId = (int) $templateItem->id; + } elseif (!empty($templateItem->rowid)) { + $templateId = (int) $templateItem->rowid; + } + + if (empty($templateId)) { + continue; + } + + $templateLabel = ''; + if (!empty($templateItem->label)) { + $templateLabel = $templateItem->label; + } elseif (!empty($templateItem->ref)) { + $templateLabel = $templateItem->ref; + } elseif (!empty($templateItem->topic)) { + $templateLabel = $templateItem->topic; + } else { + $templateLabel = '#'.$templateId; + } + + $templateOptions[$templateId] = $templateLabel; +} + +print ''; +print ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans('Name').''.$langs->trans('Description').''.$langs->trans('Value').'
'.$langs->trans('TimesheetWeekReminderEnabled').''.$langs->trans('TimesheetWeekReminderEnabledHelp').''; +print ''; +print '
'.$langs->trans('TimesheetWeekReminderWeekday').''.$langs->trans('TimesheetWeekReminderWeekdayHelp').''.$form->selectarray('TIMESHEETWEEK_REMINDER_WEEKDAY', $weekdayOptions, $reminderWeekday, 0, 0, 0, '', 0, 0, 0, '', '', 1).'
'.$langs->trans('TimesheetWeekReminderHour').''.$langs->trans('TimesheetWeekReminderHourHelp').'
'.$langs->trans('TimesheetWeekReminderEmailTemplate').''.$langs->trans('TimesheetWeekReminderEmailTemplateHelp').''.$form->selectarray('TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', $templateOptions, $reminderTemplateId, 0, 0, 0, '', 0, 0, 0, '', '', 1).'
'; +print '
'; + +print '
'; +print ''; +print ' '; +print ''; +print '
'; +print '
'; + +print '
'; + print load_fiche_titre($langs->trans('TimesheetWeekPDFModels'), '', 'pdf'); print '
'.$langs->trans('TimesheetWeekPDFModelsHelp').'
'; diff --git a/class/timesheetweek_reminder.class.php b/class/timesheetweek_reminder.class.php new file mode 100644 index 0000000..763dea0 --- /dev/null +++ b/class/timesheetweek_reminder.class.php @@ -0,0 +1,251 @@ + + * + * 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 . + */ + +if (!defined('NOREQUIREUSER')) { + define('NOREQUIREUSER', 1); +} +if (!defined('NOREQUIREDB')) { + define('NOREQUIREDB', 1); +} +if (!defined('NOREQUIRESOC')) { + define('NOREQUIRESOC', 1); +} +if (!defined('NOREQUIRETRAN')) { + define('NOREQUIRETRAN', 1); +} +if (!defined('NOREQUIRESUBPERMS')) { + define('NOREQUIRESUBPERMS', 1); +} +if (!defined('NOREQUIREMENU')) { + define('NOREQUIREMENU', 1); +} + +require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/class/CMailFile.class.php'; +require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; +dol_include_once('/core/class/emailtemplates.class.php'); + +dol_include_once('/timesheetweek/class/timesheetweek.class.php'); + +/** + * EN: Cron helper used to send weekly reminders. + * FR: Assistant cron utilisé pour envoyer les rappels hebdomadaires. + */ +class TimesheetweekReminder +{ + /** + * EN: Run cron job to send weekly reminder emails. + * FR: Exécuter la tâche planifiée pour envoyer les rappels hebdomadaires par email. + * + * @param DoliDB $db Database handler + * @param int $limit Optional limit for recipients + * @param int $forcerun Force execution (1) or use normal scheduling (0) + * @param array $targetUserIds Limit execution to specific user ids when provided + * @return int <0 if KO, >=0 if OK (number of emails sent) + */ + public static function run($db, $limit = 0, $forcerun = 0, array $targetUserIds = array()) + { + global $conf, $langs; + + $langs->loadLangs(array('timesheetweek@timesheetweek')); + + dol_syslog(__METHOD__, LOG_DEBUG); + + $reminderEnabledConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_ENABLED', $conf->entity); + $reminderEnabled = !empty($reminderEnabledConst) ? (int) $reminderEnabledConst : 0; + if (empty($reminderEnabled) && empty($forcerun)) { + dol_syslog('TimesheetweekReminder: reminder disabled', LOG_INFO); + return 0; + } + + $reminderWeekdayConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_WEEKDAY', $conf->entity); + $reminderWeekday = !empty($reminderWeekdayConst) ? (int) $reminderWeekdayConst : 1; + if ($reminderWeekday < 1 || $reminderWeekday > 7) { + dol_syslog($langs->trans('TimesheetWeekReminderWeekdayInvalid'), LOG_ERR); + return -1; + } + + $reminderHourConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_HOUR', $conf->entity); + $reminderHour = !empty($reminderHourConst) ? $reminderHourConst : '18:00'; + if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $reminderHour)) { + dol_syslog($langs->trans('TimesheetWeekReminderHourInvalid'), LOG_ERR); + return -1; + } + + $templateIdConst = dolibarr_get_const($db, 'TIMESHEETWEEK_REMINDER_EMAIL_TEMPLATE', $conf->entity); + $templateId = !empty($templateIdConst) ? (int) $templateIdConst : 0; + if (empty($templateId)) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_WARNING); + return 0; + } + + $timezoneCode = !empty($conf->timezone) ? $conf->timezone : 'UTC'; + $now = dol_now(); + $nowArray = dol_getdate($now, true, $timezoneCode); + $currentWeekday = (int) $nowArray['wday']; + $currentWeekdayIso = ($currentWeekday === 0) ? 7 : $currentWeekday; + $currentMinutes = ((int) $nowArray['hours'] * 60) + (int) $nowArray['minutes']; + + list($targetHour, $targetMinute) = explode(':', $reminderHour); + $targetMinutes = ((int) $targetHour * 60) + (int) $targetMinute; + $windowMinutes = 60; + $lowerBound = max(0, $targetMinutes - $windowMinutes); + $upperBound = min(1440, $targetMinutes + $windowMinutes); + + if (empty($forcerun)) { + if ($currentWeekdayIso !== $reminderWeekday) { + dol_syslog('TimesheetweekReminder: not the configured day, skipping execution', LOG_DEBUG); + return 0; + } + if ($currentMinutes < $lowerBound || $currentMinutes > $upperBound) { + dol_syslog('TimesheetweekReminder: outside configured time window, skipping execution', LOG_DEBUG); + return 0; + } + } + + $emailTemplateClass = ''; + if (class_exists('CEmailTemplates')) { + $emailTemplateClass = 'CEmailTemplates'; + } elseif (class_exists('EmailTemplates')) { + $emailTemplateClass = 'EmailTemplates'; + } + + if (empty($emailTemplateClass)) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_ERR); + return -1; + } + + $emailTemplate = new $emailTemplateClass($db); + $templateFetch = $emailTemplate->fetch($templateId); + if ($templateFetch <= 0) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_WARNING); + return 0; + } + + $subject = !empty($emailTemplate->topic) ? $emailTemplate->topic : $emailTemplate->label; + $body = !empty($emailTemplate->content) ? $emailTemplate->content : ''; + + if (empty($subject) || empty($body)) { + dol_syslog($langs->trans('TimesheetWeekReminderTemplateMissing'), LOG_WARNING); + return 0; + } + + $from = getDolGlobalString('MAIN_MAIL_EMAIL_FROM', ''); + if (empty($from)) { + $from = getDolGlobalString('MAIN_INFO_SOCIETE_MAIL', ''); + } + + $substitutions = getCommonSubstitutionArray($langs, 0, null, null, null); + complete_substitutions_array($substitutions, $langs, null); + + $eligibleRights = array( + 45000301, // read own + 45000302, // read child + 45000303, // read all + 45000304, // write own + 45000305, // write child + 45000306, // write all + 45000310, // validate generic + 45000311, // validate own + 45000312, // validate child + 45000313, // validate all + 45000314, // seal + 45000315, // unseal + ); + + $entityFilter = getEntity('user'); + $sql = 'SELECT DISTINCT u.rowid, u.lastname, u.firstname, u.email'; + $sql .= ' FROM '.MAIN_DB_PREFIX."user AS u"; + $sql .= ' INNER JOIN '.MAIN_DB_PREFIX."user_rights AS ur ON ur.fk_user = u.rowid AND ur.entity IN (".$entityFilter.')'; + $sql .= " WHERE u.statut = 1 AND u.email IS NOT NULL AND u.email <> ''"; + $sql .= ' AND u.entity IN ('.$entityFilter.')'; + $sql .= ' AND ur.fk_id IN ('.implode(',', array_map('intval', $eligibleRights)).')'; + if (!empty($targetUserIds)) { + $sql .= ' AND u.rowid IN ('.implode(',', array_map('intval', $targetUserIds)).')'; + } + $sql .= ' ORDER BY u.rowid ASC'; + if ($limit > 0) { + $sql .= $db->plimit((int) $limit); + } + + $resql = $db->query($sql); + if (!$resql) { + dol_syslog($db->lasterror(), LOG_ERR); + return -1; + } + + $emailsSent = 0; + $errors = 0; + + while ($obj = $db->fetch_object($resql)) { + $recipient = trim($obj->email); + if (empty($recipient)) { + continue; + } + + $user = new User($db); + $fetchUser = $user->fetch($obj->rowid); + if ($fetchUser < 0) { + dol_syslog($user->error, LOG_ERR); + $errors++; + continue; + } + + $userSubstitutions = $substitutions; + $userSubstitutions['__USER_FIRSTNAME__'] = $user->firstname; + $userSubstitutions['__USER_LASTNAME__'] = $user->lastname; + $userSubstitutions['__USER_FULLNAME__'] = dolGetFirstLastname($user->firstname, $user->lastname); + $userSubstitutions['__USER_EMAIL__'] = $recipient; + complete_substitutions_array($userSubstitutions, $langs, null, $user); + + $preparedSubject = make_substitutions($subject, $userSubstitutions); + $preparedBody = make_substitutions($body, $userSubstitutions); + + $mail = new CMailFile($preparedSubject, $recipient, $from, $preparedBody, array(), array(), array(), '', '', 0, 0, '', '', '', 'utf-8'); + $resultSend = $mail->sendfile(); + if ($resultSend) { + $emailsSent++; + dol_syslog($langs->trans('TimesheetWeekReminderSendSuccess', $recipient), LOG_INFO); + } else { + dol_syslog($langs->trans('TimesheetWeekReminderSendFailed', $recipient), LOG_ERR); + $errors++; + } + } + + $db->free($resql); + + if ($errors > 0) { + return -$errors; + } + + return $emailsSent; + } + + /** + * Send a reminder test email to the current user using the configured template. + * + * @param DoliDB $db Database handler + * @param User $user Current user + * @return int <0 if KO, >=0 if OK (number of emails sent) + */ + public static function sendTest($db, User $user) + { + return self::run($db, 1, 1, array((int) $user->id)); + } +} \ No newline at end of file diff --git a/core/modules/modTimesheetWeek.class.php b/core/modules/modTimesheetWeek.class.php index c11dd0c..16c79b1 100644 --- a/core/modules/modTimesheetWeek.class.php +++ b/core/modules/modTimesheetWeek.class.php @@ -309,20 +309,20 @@ public function __construct($db) // unit_frequency must be 60 for minute, 3600 for hour, 86400 for day, 604800 for week /* BEGIN MODULEBUILDER CRON */ $this->cronjobs = array( - // 0 => array( - // 'label' => 'MyJob label', - // 'jobtype' => 'method', - // 'class' => '/timesheetweek/class/timesheetweek.class.php', - // 'objectname' => 'TimesheetWeek', - // 'method' => 'doScheduledJob', - // 'parameters' => '', - // 'comment' => 'Comment', - // 'frequency' => 2, - // 'unitfrequency' => 3600, - // 'status' => 0, - // 'test' => 'isModEnabled("timesheetweek")', - // 'priority' => 50, - // ), + 0 => array( + 'label' => 'TimesheetWeekReminderCronLabel', + 'jobtype' => 'method', + 'class' => '/timesheetweek/class/timesheetweek_reminder.class.php', + 'objectname' => 'TimesheetweekReminder', + 'method' => 'run', + 'parameters' => '', + 'comment' => 'TimesheetWeekReminderCronComment', + 'frequency' => 1, + 'unitfrequency' => 86400, + 'status' => 0, + 'test' => 'isModEnabled("timesheetweek")', + 'priority' => 50, + ), ); /* END MODULEBUILDER CRON */ // Example: $this->cronjobs=array( diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index abe3a41..d43a10f 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -27,6 +27,26 @@ TimesheetWeekDailyRateOptions = Daily-rate options TimesheetWeekDailyRateOptionsHelp = Configure the helpers available for daily-rate contracts. TimesheetWeekQuarterDayForDailyContract = Quarter-day selector for daily-rate contracts TimesheetWeekQuarterDayForDailyContractHelp = Allow daily-rate employees to declare quarter days instead of half-days only. +TimesheetWeekReminderSectionTitle = Weekly email reminder +TimesheetWeekReminderSectionHelp = Configure the automatic reminder asking users to fill, validate or send their weekly timesheets. +TimesheetWeekReminderEnabled = Enable automatic reminder +TimesheetWeekReminderEnabledHelp = Turn the weekly reminder on or off. +TimesheetWeekReminderWeekday = Reminder day of week +TimesheetWeekReminderWeekdayHelp = Select which day of the week the reminder should be sent. +TimesheetWeekReminderHour = Reminder time (HH:MM) +TimesheetWeekReminderHourHelp = Time of day when the reminder should be sent. +TimesheetWeekReminderHourInvalid = Please enter a valid time formatted as HH:MM. +TimesheetWeekReminderWeekdayInvalid = Please choose a valid day of the week for the reminder. +TimesheetWeekReminderEmailTemplate = Email template for reminder +TimesheetWeekReminderEmailTemplateHelp = Choose the Dolibarr email template used to send the reminder. +TimesheetWeekReminderCronLabel = Weekly timesheet reminder +TimesheetWeekReminderCronComment = Send weekly reminder emails prompting users to complete their timesheets. +TimesheetWeekReminderTemplateMissing = Reminder email template is not configured. +TimesheetWeekReminderSendFailed = Failed to send the reminder email to %s. +TimesheetWeekReminderSendSuccess = Reminder email successfully sent to %s. +TimesheetWeekReminderSendTest = Send test email +TimesheetWeekReminderTestSuccess = Test reminder email sent (%s) +TimesheetWeekReminderTestError = Unable to send the test reminder email. NewSection=New section TIMESHEETWEEK_MYPARAM1 = My param 1 TIMESHEETWEEK_MYPARAM1Tooltip = My param 1 tooltip diff --git a/langs/fr_FR/timesheetweek.lang b/langs/fr_FR/timesheetweek.lang index e0b0a9b..047ce7b 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -27,6 +27,26 @@ TimesheetWeekDailyRateOptions = Options forfait jour TimesheetWeekDailyRateOptionsHelp = Configure les assistants disponibles pour les contrats au forfait jour. TimesheetWeekQuarterDayForDailyContract = Sélecteur quart de jour pour forfait jour TimesheetWeekQuarterDayForDailyContractHelp = Autorise les salariés au forfait jour à déclarer des quarts de jour au lieu des seules demi-journées. +TimesheetWeekReminderSectionTitle = Rappel hebdomadaire par email +TimesheetWeekReminderSectionHelp = Configure le rappel automatique invitant les utilisateurs à saisir, valider ou envoyer leurs feuilles hebdomadaires. +TimesheetWeekReminderEnabled = Activer le rappel automatique +TimesheetWeekReminderEnabledHelp = Active ou désactive l'envoi hebdomadaire du rappel. +TimesheetWeekReminderWeekday = Jour d'envoi du rappel +TimesheetWeekReminderWeekdayHelp = Choisissez le jour de la semaine pour envoyer le rappel. +TimesheetWeekReminderHour = Heure d'envoi (HH:MM) +TimesheetWeekReminderHourHelp = Heure d'envoi du rappel, au format 24 h. +TimesheetWeekReminderHourInvalid = Merci de saisir une heure valide au format HH:MM. +TimesheetWeekReminderWeekdayInvalid = Merci de choisir un jour de semaine valide pour le rappel. +TimesheetWeekReminderEmailTemplate = Modèle d'email pour le rappel +TimesheetWeekReminderEmailTemplateHelp = Sélectionnez le modèle d'email Dolibarr utilisé pour envoyer le rappel. +TimesheetWeekReminderCronLabel = Rappel hebdomadaire des feuilles d'heures +TimesheetWeekReminderCronComment = Envoie un rappel hebdomadaire par email pour inviter les utilisateurs à compléter leurs feuilles d'heures. +TimesheetWeekReminderTemplateMissing = Le modèle d'email de rappel n'est pas configuré. +TimesheetWeekReminderSendFailed = Échec de l'envoi du rappel à %s. +TimesheetWeekReminderSendSuccess = Rappel envoyé avec succès à %s. +TimesheetWeekReminderSendTest = Envoyer un mail de test +TimesheetWeekReminderTestSuccess = Mail de test envoyé (%s) +TimesheetWeekReminderTestError = Échec de l'envoi du mail de test. NewSection=Nouvelle section TIMESHEETWEEK_MYPARAM1 = Mon paramètre 1 TIMESHEETWEEK_MYPARAM1Tooltip = Info-bulle de mon paramètre 1