diff --git a/ChangeLog.md b/ChangeLog.md index fd6f372..4c79d5c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,9 @@ # CHANGELOG MODULE TIMESHEETWEEK FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) +## 1.4.0 +- Ajoute un interrupteur d'administration pour activer le sélecteur quart de jour des salariés au forfait jour. / Adds an admin switch to enable the quarter-day selector for daily-rate employees. +- Affiche les durées forfait jour (0.25, 0.5 et 1 jour) dans les PDF standard et de synthèse lorsque le quart de jour est actif. / Displays the daily-rate durations (0.25, 0.5 and 1 day) inside the standard and summary PDFs whenever quarter-day mode is enabled. + ## 1.3.0 - Active la génération de PDF depuis la fiche hebdomadaire avec le widget Documents Dolibarr et respecte les modèles configurés dans la page de setup. / Enables PDF generation from the weekly sheet using Dolibarr's Documents widget and honours the templates configured in the setup page. - Introduit le modèle PDF « standard_timesheetweek » basé sur la synthèse partagée afin de produire les fichiers dans le répertoire documentaire Dolibarr. / Introduces the "standard_timesheetweek" PDF model powered by the shared summary engine so files land into Dolibarr's document directory. diff --git a/admin/setup.php b/admin/setup.php index 3624524..1394de5 100644 --- a/admin/setup.php +++ b/admin/setup.php @@ -276,12 +276,12 @@ 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'), true)) { - if (function_exists('dol_verify_token')) { - if (empty($token) || dol_verify_token($token) <= 0) { - accessforbidden(); - } +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(); } + } } // EN: Persist the chosen numbering module. @@ -323,21 +323,37 @@ 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) { - if ($value === getDolGlobalString('TIMESHEETWEEK_ADDON_PDF')) { - dolibarr_del_const($db, 'TIMESHEETWEEK_ADDON_PDF', $conf->entity); - } - setEventMessages($langs->trans('ModelDisabled', $value), null, 'mesgs'); - } else { - setEventMessages($langs->trans('Error'), null, 'errors'); + $res = timesheetweekDisableDocumentModel($value); + if ($res > 0) { + if ($value === getDolGlobalString('TIMESHEETWEEK_ADDON_PDF')) { + dolibarr_del_const($db, 'TIMESHEETWEEK_ADDON_PDF', $conf->entity); } + setEventMessages($langs->trans('ModelDisabled', $value), null, 'mesgs'); + } else { + setEventMessages($langs->trans('Error'), null, 'errors'); + } +} + +// 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; + } + $res = dolibarr_set_const($db, 'TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', $targetValue, 'chaine', 0, '', $conf->entity); + if ($res > 0) { + setEventMessages($langs->trans('SetupSaved'), null, 'mesgs'); + } else { + setEventMessages($langs->trans('Error'), 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); $directories = array_merge(array('/'), (array) $conf->modules_parts['models']); // EN: Prepare a lightweight object to test numbering module activation. @@ -438,6 +454,40 @@ 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').'
'; + +print '
'; +print ''; +print ''; +print ''; +print ''; +print ''; +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 ''; +print ''; +print ''; +print ''; + +print '
'.$langs->trans('Name').''.$langs->trans('Description').''.$langs->trans('Status').'
'.$langs->trans('TimesheetWeekQuarterDayForDailyContract').''.$langs->trans('TimesheetWeekQuarterDayForDailyContractHelp').''; + if (!empty($useQuarterDaySelector)) { + $url = $_SERVER['PHP_SELF'].'?action=setquarterday&value=0&token='.$pageToken; + print ''.img_picto($langs->trans('Disable'), 'switch_on').''; + } else { + $url = $_SERVER['PHP_SELF'].'?action=setquarterday&value=1&token='.$pageToken; + print ''.img_picto($langs->trans('Activate'), 'switch_off').''; + } + print '
'; +print '
'; + +print '
'; + print load_fiche_titre($langs->trans('TimesheetWeekPDFModels'), '', 'pdf'); print '
'.$langs->trans('TimesheetWeekPDFModelsHelp').'
'; diff --git a/core/modules/modTimesheetWeek.class.php b/core/modules/modTimesheetWeek.class.php index a92ef83..4ab82ea 100644 --- a/core/modules/modTimesheetWeek.class.php +++ b/core/modules/modTimesheetWeek.class.php @@ -112,7 +112,7 @@ public function __construct($db) } // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '1.3.0'; // EN: Enables PDF generation with document model management like native Dolibarr cards. + $this->version = '1.4.0'; // EN: Enables PDF generation with document model management like native Dolibarr cards. // FR: Active la génération PDF avec gestion des modèles de documents comme sur les fiches Dolibarr natives. // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; diff --git a/core/modules/timesheetweek/doc/pdf_standard_timesheetweek.modules.php b/core/modules/timesheetweek/doc/pdf_standard_timesheetweek.modules.php index 3cf53ac..746e43e 100644 --- a/core/modules/timesheetweek/doc/pdf_standard_timesheetweek.modules.php +++ b/core/modules/timesheetweek/doc/pdf_standard_timesheetweek.modules.php @@ -276,6 +276,10 @@ public function write_file($object, $outputlangs, $srctemplatepath = '', $hidede return -1; } + // EN: Remember whether the quarter-day selector is enabled to adapt PDF hints and mappings. + // FR: Retient si le sélecteur quart de jour est actif afin d'adapter les aides et correspondances PDF. + $useQuarterDayDailyContract = (bool) getDolGlobalInt('TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', 0); + // EN: Collect employee details to reproduce the on-screen grid behaviour. // FR: Récupère les informations salarié pour reproduire le comportement de la grille à l'écran. $timesheetEmployee = null; @@ -503,10 +507,19 @@ public function write_file($object, $outputlangs, $srctemplatepath = '', $hidede $dailyRateOptions = array(); if ($isDailyRateEmployee) { $dailyRateOptions = array( - 1 => $outputlangs->trans('TimesheetWeekDailyRateFullDay'), - 2 => $outputlangs->trans('TimesheetWeekDailyRateMorning'), - 3 => $outputlangs->trans('TimesheetWeekDailyRateAfternoon') + 1 => $outputlangs->trans('TimesheetWeekDailyRateFullDay'), + 2 => $outputlangs->trans('TimesheetWeekDailyRateMorning'), + 3 => $outputlangs->trans('TimesheetWeekDailyRateAfternoon') ); + if ($useQuarterDayDailyContract) { + // EN: Append the duration hints to each option and expose the quarter-day entry when enabled. + // FR: Ajoute les repères de durée à chaque option et expose l'entrée quart de jour lorsqu'elle est activée. + $dailyRateOptions[1] .= ' ('.$outputlangs->trans('TimesheetWeekDailyRateOneDay').')'; + $halfDayLabel = $outputlangs->trans('TimesheetWeekDailyRateHalfDay'); + $dailyRateOptions[2] = $halfDayLabel; + $dailyRateOptions[3] = $halfDayLabel; + $dailyRateOptions[4] = $outputlangs->trans('TimesheetWeekDailyRateQuarterDay'); + } } $dailyRateHoursMap = $this->getDailyRateHoursMap(); $dayTotalsHours = array(); @@ -514,10 +527,25 @@ public function write_file($object, $outputlangs, $srctemplatepath = '', $hidede $dayTotalsHours[$dayName] = 0.0; } $grandHours = 0.0; - + + // EN: Prepare the legend describing available daily-rate durations when quarter-day support is active. + // FR: Prépare la légende décrivant les durées forfait jour disponibles lorsque le quart de jour est actif. + $dailyRateLegendText = ''; + if ($isDailyRateEmployee && $useQuarterDayDailyContract) { + $dailyRateLegendText = $outputlangs->trans( + 'TimesheetWeekDailyRateDurationsLegend', + $outputlangs->trans('TimesheetWeekDailyRateQuarterDay'), + $outputlangs->trans('TimesheetWeekDailyRateHalfDay'), + $outputlangs->trans('TimesheetWeekDailyRateOneDay') + ); + } + // EN: Build the HTML table mirroring the editable grid layout. // FR: Construit le tableau HTML reflétant la grille éditable. $htmlGrid = ''; + if ($dailyRateLegendText !== '') { + $htmlGrid .= '

'.tw_pdf_format_cell_html($dailyRateLegendText).'

'; + } if (empty($tasksByProject)) { $htmlGrid .= '

'.tw_pdf_format_cell_html($outputlangs->trans('NoTasksAssigned')).'

'; } else { @@ -848,10 +876,16 @@ protected function formatDays($value, $langs) */ protected function getDailyRateHoursMap() { - return array( + $map = array( 1 => 8.0, 2 => 4.0, 3 => 4.0 ); + if (getDolGlobalInt('TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', 0)) { + // EN: Register the quarter-day equivalence when the selector is enabled in configuration. + // FR: Enregistre l'équivalence du quart de jour lorsque le sélecteur est activé en configuration. + $map[4] = 2.0; + } + return $map; } } diff --git a/langs/en_US/timesheetweek.lang b/langs/en_US/timesheetweek.lang index 9afbe82..1762c7b 100644 --- a/langs/en_US/timesheetweek.lang +++ b/langs/en_US/timesheetweek.lang @@ -23,6 +23,10 @@ TimesheetWeekPDFModelsEmpty = No PDF template is available. PDFStandardTimesheetWeekDescription = Standard PDF model for weekly timesheets TimesheetWeekPdfReferenceLabel = Timesheet reference: %s TimesheetWeekPdfApprovedDetails = Approved on %s by %s +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. NewSection=New section TIMESHEETWEEK_MYPARAM1 = My param 1 TIMESHEETWEEK_MYPARAM1Tooltip = My param 1 tooltip @@ -173,6 +177,10 @@ TimesheetWeekDailyRateLabel = Daily rate contract TimesheetWeekDailyRateFullDay = Full day TimesheetWeekDailyRateMorning = Morning TimesheetWeekDailyRateAfternoon = Afternoon +TimesheetWeekDailyRateOneDay = 1 day +TimesheetWeekDailyRateHalfDay = 0.5 day +TimesheetWeekDailyRateQuarterDay = 0.25 day +TimesheetWeekDailyRateDurationsLegend = Available daily-rate durations: %1$s / %2$s / %3$s LastModification = Last modification TotalDays = Total days TimesheetWeekTotalDays = Total days diff --git a/langs/fr_FR/timesheetweek.lang b/langs/fr_FR/timesheetweek.lang index acbcb95..c9bd0f9 100644 --- a/langs/fr_FR/timesheetweek.lang +++ b/langs/fr_FR/timesheetweek.lang @@ -23,6 +23,10 @@ TimesheetWeekPDFModelsEmpty = Aucun modèle PDF disponible. PDFStandardTimesheetWeekDescription = Modèle PDF standard pour les feuilles hebdomadaires TimesheetWeekPdfReferenceLabel = Référence de la feuille : %s TimesheetWeekPdfApprovedDetails = Approuvée le %s par %s +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. NewSection=Nouvelle section TIMESHEETWEEK_MYPARAM1 = Mon paramètre 1 TIMESHEETWEEK_MYPARAM1Tooltip = Info-bulle de mon paramètre 1 @@ -241,6 +245,10 @@ TimesheetWeekDailyRateLabel = Contrat forfait jour TimesheetWeekDailyRateFullDay = Journée TimesheetWeekDailyRateMorning = Matin TimesheetWeekDailyRateAfternoon = Après-midi +TimesheetWeekDailyRateOneDay = 1 jour +TimesheetWeekDailyRateHalfDay = 0.5 jour +TimesheetWeekDailyRateQuarterDay = 0.25 jour +TimesheetWeekDailyRateDurationsLegend = Durées forfait jour disponibles : %1$s / %2$s / %3$s LastModification = Dernière modification TotalDays = Total jours TimesheetWeekTotalDays = Total jours diff --git a/lib/timesheetweek_pdf.lib.php b/lib/timesheetweek_pdf.lib.php index 825b4fd..5ab7196 100644 --- a/lib/timesheetweek_pdf.lib.php +++ b/lib/timesheetweek_pdf.lib.php @@ -984,6 +984,20 @@ function tw_generate_summary_pdf($db, $conf, $langs, User $user, array $timeshee }); $sortedUsers = array_values($dataset); + // EN: Detect whether at least one daily-rate employee is included to display duration hints later on. + // FR: Détecte si au moins un salarié au forfait jour est inclus afin d'afficher les repères de durée ensuite. + $hasDailyRateEmployee = false; + foreach ($sortedUsers as $sortedUserSummary) { + if (!empty($sortedUserSummary['is_daily_rate'])) { + $hasDailyRateEmployee = true; + break; + } + } + + // EN: Remember if the quarter-day selector is enabled to conditionally show the duration legend in PDFs. + // FR: Mémorise si le sélecteur quart de jour est actif pour afficher conditionnellement la légende de durée dans les PDF. + $useQuarterDayDailyContract = (bool) getDolGlobalInt('TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT', 0); + // EN: Track the lowest and highest ISO weeks among the selected records for filename generation. // FR: Suit les semaines ISO minimale et maximale parmi les enregistrements sélectionnés pour le nom du fichier. $earliestWeek = null; @@ -1133,6 +1147,27 @@ function tw_generate_summary_pdf($db, $conf, $langs, User $user, array $timeshee $usableWidth = $pdf->getPageWidth() - $margeGauche - $margeDroite; + // EN: Describe the legend reminding daily-rate durations whenever the quarter-day selector is active. + // FR: Décrit la légende rappelant les durées forfait jour lorsque le sélecteur quart de jour est actif. + $dailyRateLegendText = ''; + if ($useQuarterDayDailyContract && $hasDailyRateEmployee) { + $dailyRateLegendText = $langs->trans( + 'TimesheetWeekDailyRateDurationsLegend', + $langs->trans('TimesheetWeekDailyRateQuarterDay'), + $langs->trans('TimesheetWeekDailyRateHalfDay'), + $langs->trans('TimesheetWeekDailyRateOneDay') + ); + } + if ($dailyRateLegendText !== '') { + // EN: Print the legend in italics to highlight the duration equivalences without overpowering the tables. + // FR: Affiche la légende en italique pour mettre en avant les équivalences de durée sans dominer les tableaux. + $pdf->SetFont('', 'I', max($defaultFontSize - 1, 6)); + $pdf->SetX($margeGauche); + $pdf->MultiCell($usableWidth, 0, tw_pdf_normalize_string($dailyRateLegendText), 0, 'L', false); + $pdf->Ln(2); + $pdf->SetFont('', '', $defaultFontSize); + } + // EN: Describe the standard hour-based layout used for classic employees. // FR: Décrit la mise en page standard en heures utilisée pour les salariés classiques. $hoursColumnConfig = array( diff --git a/timesheetweek_card.php b/timesheetweek_card.php index 33acaac..6941e30 100644 --- a/timesheetweek_card.php +++ b/timesheetweek_card.php @@ -65,6 +65,9 @@ // 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. +// EN: Default the quarter-day flag and daily rate usage before evaluating the employee profile. +// FR: Définit par défaut le drapeau quart de jour et l'utilisation du forfait avant d'évaluer le profil du salarié. +$useQuarterDayDailyContract = !empty($conf->global->TIMESHEETWEEK_QUARTERDAYFORDAILYCONTRACT); $isDailyRateEmployee = false; // ---- Fetch (set $object if id) ---- @@ -199,6 +202,28 @@ function tw_get_enabled_pdf_models(DoliDB $db) return $models; } +/** + * EN: Return the hour equivalents for each daily rate code (adds quarter-day when enabled). + * FR: Retourne les équivalences en heures pour chaque code forfait (ajoute le quart de jour si activé). + * + * @param bool $useQuarterDayDailyContract Flag for quarter-day support / Drapeau d'activation du quart de jour + * @return array Hour mapping by code / Correspondance heures par code + */ +function tw_get_daily_rate_hours_map($useQuarterDayDailyContract) +{ + $map = array( + 1 => 8.0, + 2 => 4.0, + 3 => 4.0, + ); + if ($useQuarterDayDailyContract) { + // EN: Expose the quarter-day option with its 2-hour contribution when the feature is enabled. + // FR: Expose l'option quart de jour avec sa contribution de 2 heures lorsque la fonctionnalité est activée. + $map[4] = 2.0; + } + return $map; +} + // ---- Permissions (nouveau modèle) ---- $permRead = $user->hasRight('timesheetweek','read'); $permReadChild = $user->hasRight('timesheetweek','readChild'); @@ -497,7 +522,9 @@ function tw_can_validate_timesheet( $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); +// EN: Mirror the front-end quarter-day mapping when converting select choices into stored hours. +// FR: Reflète le mapping quart de jour du front-end lors de la conversion des sélections en heures stockées. +$dailyRateHours = tw_get_daily_rate_hours_map($useQuarterDayDailyContract); $cellPattern = $isDailyRateEmployee ? '/^daily_(\d+)_(\w+)$/' : '/^hours_(\d+)_(\w+)$/' ; foreach ($_POST as $key => $val) { @@ -1342,6 +1369,9 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( // 1) CHARGER LIGNES EXISTANTES $hoursBy = array(); // [taskid][YYYY-mm-dd] = hours $dailyRateBy = array(); // [taskid][YYYY-mm-dd] = daily rate code +// EN: Track legacy afternoon code usage to keep it available in quarter-day mode. +// FR: Suit l'utilisation de l'ancien code après-midi pour le conserver en mode quart de jour. +$hasLegacyHalfDayDailyRate = false; $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(); @@ -1368,6 +1398,11 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( $hoursBy[$fk_task][$daydate] = $hours; if (!isset($dailyRateBy[$fk_task])) $dailyRateBy[$fk_task] = array(); $dailyRateBy[$fk_task][$daydate] = $dailyRate; +if ($dailyRate === 3) { +// EN: Remember the legacy afternoon code to keep it selectable when quarter-day mode is active. +// FR: Retient l'ancien code après-midi pour le rendre sélectionnable lorsque le mode quart de jour est actif. +$hasLegacyHalfDayDailyRate = true; +} $w = (int) date('N', strtotime($daydate)); $dayName = array(1=>'Monday',2=>'Tuesday',3=>'Wednesday',4=>'Thursday',5=>'Friday',6=>'Saturday',7=>'Sunday')[$w]; @@ -1622,13 +1657,28 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( $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'), -); + if ($useQuarterDayDailyContract) { + // EN: Provide fractional-day labels when the quarter-day constant is enabled. + // FR: Fournit les libellés fractionnaires lorsque la constante quart de jour est activée. + $dailyRateOptions = array( + 4 => $langs->trans('TimesheetWeekDailyRateQuarterDay'), + 2 => $langs->trans('TimesheetWeekDailyRateHalfDay'), + 1 => $langs->trans('TimesheetWeekDailyRateOneDay'), + ); + if ($hasLegacyHalfDayDailyRate && empty($dailyRateOptions[3])) { + // EN: Keep the historical afternoon code selectable to display past data consistently. + // FR: Maintient le code historique de l'après-midi pour afficher les données passées de manière cohérente. + $dailyRateOptions[3] = $langs->trans('TimesheetWeekDailyRateHalfDay'); + } + } else { + // 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 @@ -1757,7 +1807,7 @@ function updateWeekRange(){var v=$('#weekyear').val();var p=parseYearWeek(v);if( JS; - $jsGrid = sprintf($jsGrid, $isDailyRateEmployee ? 'true' : 'false', json_encode((float) price2num($contractedHours, '6'))); + // EN: Reuse the PHP hour map so JavaScript mirrors the backend conversions (quarter-day included). + // FR: Réutilise la correspondance horaire PHP pour que JavaScript reflète les conversions (quart de jour inclus). + $jsDailyRateHoursMap = tw_get_daily_rate_hours_map($useQuarterDayDailyContract); + $jsGrid = sprintf( + $jsGrid, + $isDailyRateEmployee ? 'true' : 'false', + json_encode($jsDailyRateHoursMap), + json_encode((float) price2num($contractedHours, '6')) + ); echo $jsGrid; }