From abd3512133129918666490171629ccf9a7378a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20David?= Date: Tue, 16 Dec 2025 08:53:23 +0100 Subject: [PATCH] wip --- class/reedcrmdashboard.class.php | 291 +++++++++++++++++++++++++++++- core/modules/modReedCRM.class.php | 23 ++- css/stats.css | 284 +++++++++++++++++++++++++++++ js/modules/stats.js | 131 ++++++++++++++ js/reedcrm.min.js | 2 +- langs/en_US/reedcrm.lang | 18 ++ langs/fr_FR/reedcrm.lang | 13 ++ view/stats/stats.php | 232 ++++++++++++++++++++++++ 8 files changed, 987 insertions(+), 7 deletions(-) create mode 100644 css/stats.css create mode 100644 js/modules/stats.js create mode 100644 view/stats/stats.php diff --git a/class/reedcrmdashboard.class.php b/class/reedcrmdashboard.class.php index 3282f9d..9ece2c2 100644 --- a/class/reedcrmdashboard.class.php +++ b/class/reedcrmdashboard.class.php @@ -187,8 +187,6 @@ public function getProjectOpportunitiesList(): array } foreach ($projects as $project) { - $newThirdPartyUrl = DOL_URL_ROOT . '/societe/card.php?action=create&projectid=' . $project->id; - $arrayProjectOpportunitiesList[$project->id]['Ref']['value'] = $project->getNomUrl(1); $arrayProjectOpportunitiesList[$project->id]['Ref']['morecss'] = 'left'; $arrayProjectOpportunitiesList[$project->id]['Label']['value'] = $project->title; @@ -199,7 +197,7 @@ public function getProjectOpportunitiesList(): array $arrayProjectOpportunitiesList[$project->id]['FirstName']['value'] = $project->array_options['options_reedcrm_firstname'] ?? '-'; $arrayProjectOpportunitiesList[$project->id]['Phone']['value'] = $project->array_options['options_projectphone'] ?? '-'; $arrayProjectOpportunitiesList[$project->id]['Email']['value'] = $project->array_options['options_reedcrm_email'] ?? '-'; - $arrayProjectOpportunitiesList[$project->id]['ButtonActions']['value'] = ' '. $langs->trans('NewThirdparty') .''; + $arrayProjectOpportunitiesList[$project->id]['ButtonActions']['value'] = ''; } $array['data'] = $arrayProjectOpportunitiesList; @@ -288,4 +286,291 @@ public function getProductLastSellList(): array return $array; } + +/** + * Get sales funnel data (pyramide inversée) + * + * @param array $moreParams Optional parameters including date_start and date_end for filtering + * @return array $array Graph datas (label/color/type/title/data etc..) + * @throws Exception + */ + public function getSalesFunnel($dateStartTimestamp = null, $dateEndTimestamp = null): array + { + global $langs; + + require_once DOL_DOCUMENT_ROOT . '/projet/class/project.class.php'; + + // Graph Title parameters + $array['title'] = $langs->transnoentities('SalesFunnel'); + $array['name'] = 'SalesFunnel'; + $array['picto'] = 'project'; + + // Graph parameters - Type 'funnel_custom' pour un rendu HTML personnalisé + $array['width'] = '100%'; + $array['height'] = 400; + $array['type'] = 'funnel_custom'; + $array['dataset'] = 1; + + $arrayProjectOpportunitiesList = []; + $dateFilter = []; + + // Get date filters from request parameters or use provided defaults + require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php'; + + // If timestamps are not provided, try to get them from GET parameters + if ($dateStartTimestamp === null) { + $dateStartTimestamp = 0; + $dateStartDay = GETPOST('salesfunnel_date_startday', 'int'); + $dateStartMonth = GETPOST('salesfunnel_date_startmonth', 'int'); + $dateStartYear = GETPOST('salesfunnel_date_startyear', 'int'); + if ($dateStartDay > 0 && $dateStartMonth > 0 && $dateStartYear > 0) { + $dateStartTimestamp = dol_mktime(0, 0, 0, $dateStartMonth, $dateStartDay, $dateStartYear); + } + } + + if ($dateEndTimestamp === null) { + $dateEndTimestamp = 0; + $dateEndDay = GETPOST('salesfunnel_date_endday', 'int'); + $dateEndMonth = GETPOST('salesfunnel_date_endmonth', 'int'); + $dateEndYear = GETPOST('salesfunnel_date_endyear', 'int'); + if ($dateEndDay > 0 && $dateEndMonth > 0 && $dateEndYear > 0) { + $dateEndTimestamp = dol_mktime(23, 59, 59, $dateEndMonth, $dateEndDay, $dateEndYear); + } + } + + if ($dateStartTimestamp > 0 || $dateEndTimestamp > 0) { + global $db; // au cas où + + $customSql = ''; + + if ($dateStartTimestamp > 0) { + // t.datec est un champ DATETIME → on formate le timestamp en datetime SQL + $customSql .= " AND t.datec >= '" . $db->idate($dateStartTimestamp) . "'"; + } + + if ($dateEndTimestamp > 0) { + $customSql .= " AND t.datec <= '" . $db->idate($dateEndTimestamp) . "'"; + } + + if (!empty($customSql)) { + $dateFilter['customsql'] = ltrim($customSql, ' AND '); + } + } + +// $dateFilter = []; + $array['morehtmlright'] = $this->buildSalesFunnelFilters($dateStartTimestamp, $dateEndTimestamp); + + $projects = saturne_fetch_all_object_type('Project', 'DESC', 't.datec', 0, 0, $dateFilter, 'AND', true); + if (!is_array($projects)) { + $projects = []; + } + + $rawProjectOpportunity = 0; + $ponderatedProjectOpportunity = 0; + $signedProjectOpportunity = 0; + + if (is_array($projects) && !empty($projects)) { + foreach ($projects as $project) { + // Récupérer fk_opp_status (peut être 0, null ou une valeur > 0) + // Utiliser isset() et !== null pour différencier 0 de null + $oppStatus = isset($project->fk_opp_status) ? $project->fk_opp_status : (isset($project->opp_status) ? $project->opp_status : null); + + // Si fk_opp_status est null ou non défini, c'est une opportunité brute + if ($oppStatus === null || $oppStatus === '') { + $rawProjectOpportunity ++; + } else { + // fk_opp_status < 6 = opportunités pondérées + if ($oppStatus < 6) { + $ponderatedProjectOpportunity++; + } + // fk_opp_status == 6 = projets signés + elseif ($oppStatus == 6) { + $signedProjectOpportunity ++; + } + // fk_opp_status == 0 peut aussi être considéré comme brut + elseif ($oppStatus == 0) { + $rawProjectOpportunity ++; + } + } + } + } + + $funnelData = [ + ['label' => 'Opportunités de projet brut', 'value' => $rawProjectOpportunity, 'color' => '#2196F3'], + ['label' => 'Opportunités de projet pondérés', 'value' => $ponderatedProjectOpportunity, 'color' => '#4CAF50'], + ['label' => 'Projets signés', 'value' => $signedProjectOpportunity, 'color' => '#FF9800'], + ]; + + $labels = []; + $data = []; + $colors = []; + $maxValue = 0; + + foreach ($funnelData as $index => $stage) { + $labels[$index] = [ + 'label' => $langs->transnoentities($stage['label']), + 'color' => $stage['color'] + ]; + $data[$index] = $stage['value']; + $colors[$index] = $stage['color']; + if ($stage['value'] > $maxValue) { + $maxValue = $stage['value']; + } + } + + $array['labels'] = $labels; + $array['data'] = $data; + $array['colors'] = $colors; + $array['maxValue'] = $maxValue; + + $array['custom_html'] = $this->generateFunnelHTML($funnelData, $langs); + + return $array; + } + + /** + * Generate HTML for funnel chart (pyramide inversée) + * + * @param array $funnelData Data array with label, value, color + * @param Translate $langs Translation object + * @return string HTML code + */ + private function generateFunnelHTML(array $funnelData, Translate $langs): string + { + $maxValue = 0; + foreach ($funnelData as $stage) { + if ($stage['value'] > $maxValue) { + $maxValue = $stage['value']; + } + } + + if ($maxValue <= 0) { + $maxValue = 1; + } + + $countStages = count($funnelData); + $heightPerStage = 60; + $svgHeight = $countStages * $heightPerStage; + $svgWidth = 500; + + $html = '
'; + $html .= ''; + + $html .= ''; + for ($i = 0; $i < $countStages; $i++) { + $stage = $funnelData[$i]; + $fill = $stage['color']; + // Darken color by 15% directly + $color = ltrim($fill, '#'); + $rgb = [ + hexdec(substr($color, 0, 2)), + hexdec(substr($color, 2, 2)), + hexdec(substr($color, 4, 2)) + ]; + for ($j = 0; $j < 3; $j++) { + $rgb[$j] = max(0, min(255, round($rgb[$j] * 0.85))); + } + $fillDarker = '#' . str_pad(dechex($rgb[0]), 2, '0', STR_PAD_LEFT) + . str_pad(dechex($rgb[1]), 2, '0', STR_PAD_LEFT) + . str_pad(dechex($rgb[2]), 2, '0', STR_PAD_LEFT); + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= ''; + + for ($i = 0; $i < $countStages; $i++) { + $stage = $funnelData[$i]; + $topValue = $stage['value']; + $bottomValue = ($i < $countStages - 1) ? $funnelData[$i + 1]['value'] : $stage['value'] * 0.4; + + // S'assurer qu'il y a une largeur minimale même pour les valeurs à 0 + $minWidth = 50; // Largeur minimale en pixels + $topWidth = max($minWidth, ($topValue / $maxValue) * ($svgWidth - 100)); + $bottomWidth = max($minWidth * 0.7, ($bottomValue / $maxValue) * ($svgWidth - 100)); + + // S'assurer que le bas est toujours plus étroit que le haut + $bottomWidth = min($bottomWidth, $topWidth * 0.9); + + $topLeft = ($svgWidth - $topWidth) / 2; + $topRight = $topLeft + $topWidth; + + $bottomLeft = ($svgWidth - $bottomWidth) / 2; + $bottomRight = $bottomLeft + $bottomWidth; + + $yTop = $i * $heightPerStage; + $yBottom = $yTop + $heightPerStage; + + $points = [ + $topLeft . ',' . $yTop, + $topRight . ',' . $yTop, + $bottomRight . ',' . $yBottom, + $bottomLeft . ',' . $yBottom + ]; + + // Polygone du segment + $html .= ''; + + // Label et valeurs centrés + $labelX = $svgWidth / 2; + $labelY = $yTop + ($heightPerStage / 2); + + $labelText = $langs->transnoentities($stage['label']); + $valueText = number_format($stage['value'], 0, ',', ' '); + + $conversionRate = ($i > 0 && $funnelData[$i - 1]['value'] > 0) + ? round(($stage['value'] / $funnelData[$i - 1]['value']) * 100, 1) . '%' + : ''; + + // Utiliser une couleur de texte qui contraste bien avec le fond coloré + $textColor = '#FFFFFF'; + $html .= '' . dol_escape_htmltag($labelText) . ''; + $html .= '' . $valueText . ''; + if ($conversionRate) { + $html .= '' . $langs->transnoentities('ConversionRate') . ': ' . $conversionRate . ''; + } + } + + $html .= ''; + + $html .= '
'; + + return $html; + } + + + /** + * Render inline date selectors for the funnel graph + * + * @param int $dateStartTimestamp + * @param int $dateEndTimestamp + * @return string + */ + private function buildSalesFunnelFilters(int $dateStartTimestamp, int $dateEndTimestamp): string + { + global $form, $langs; + + if (!is_object($form)) { + $form = new Form($this->db); + } + + $html = '
'; + $html .= '
'; + $html .= ''; + $html .= $form->selectDate($dateStartTimestamp ?: '', 'salesfunnel_date_start', 1, 1, '', 'form', 1, 0, 0, '', '', '', 1); + $html .= '
'; + + $html .= '
'; + $html .= ''; + $html .= $form->selectDate($dateEndTimestamp ?: '', 'salesfunnel_date_end', 1, 1, '', 'form', 1, 0, 0, '', '', '', 1); + $html .= '
'; + + $html .= ''; + $html .= '
'; + + return $html; + } } diff --git a/core/modules/modReedCRM.class.php b/core/modules/modReedCRM.class.php index e430a5c..dd6db59 100644 --- a/core/modules/modReedCRM.class.php +++ b/core/modules/modReedCRM.class.php @@ -81,7 +81,7 @@ public function __construct($db) //$this->editor_squarred_logo = ''; // Must be image filename into the reedcrm/img directory followed with @reedcrm. Example: 'reedcrm.png@reedcrm' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated' or a version string like 'x.y.z' - $this->version = '21.0.0'; + $this->version = '22.0.0'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -122,6 +122,7 @@ public function __construct($db) // Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all') /* BEGIN MODULEBUILDER HOOKSCONTEXTS */ 'hooks' => [ + 'agenda', 'thirdpartycomm', 'projectcard', 'projectlist', @@ -147,7 +148,7 @@ public function __construct($db) ]; // Data directories to create when module is enabled - $this->dirs = ['/reedcrm/temp', '/reedcrm/import', '/reedcrm/import/project']; + $this->dirs = ['/reedcrm/temp']; // Config pages. Put here list of php page, stored into reedcrm/admin directory, to use to set up module $this->config_page_url = ['setup.php@reedcrm']; @@ -168,7 +169,7 @@ public function __construct($db) // Prerequisites $this->phpmin = [7, 4]; // Minimum version of PHP required by module // $this->phpmax = [8, 0]; // Maximum version of PHP required by module - $this->need_dolibarr_version = [21, 0]; // Minimum version of Dolibarr required by module + $this->need_dolibarr_version = [20, 0]; // Minimum version of Dolibarr required by module // $this->max_dolibarr_version = [21, 0]; // Maximum version of Dolibarr required by module $this->need_javascript_ajax = 0; @@ -491,6 +492,22 @@ public function __construct($db) 'user' => 0, ]; + $this->menu[$r++] = [ + 'fk_menu' => 'fk_mainmenu=reedcrm', + 'type' => 'left', + 'titre' => $langs->transnoentities('Statistics'), + 'prefix' => '', + 'mainmenu' => 'reedcrm', + 'leftmenu' => 'statistics', + 'url' => '/reedcrm/view/stats/stats.php', + 'langs' => 'reedcrm@reedcrm', + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled(\'reedcrm\')', + 'perms' => '$user->hasRight(\'reedcrm\', \'read\')', + 'target' => '', + 'user' => 0, + ]; + $this->menu[$r++] = [ 'fk_menu' => 'fk_mainmenu=reedcrm', 'type' => 'left', diff --git a/css/stats.css b/css/stats.css new file mode 100644 index 0000000..9f9983a --- /dev/null +++ b/css/stats.css @@ -0,0 +1,284 @@ +.reedcrm-stats-container { + width: 100%; + padding: 30px; + max-width: 1400px; + margin: 0 auto; +} + +.reedcrm-stats-header { + margin-bottom: 30px; + padding-bottom: 15px; + border-bottom: 1px solid #e0e0e0; +} + +.reedcrm-stats-header h1 { + margin: 0 0 5px 0; + font-size: 28px; + font-weight: 500; + color: #212529; +} + +.reedcrm-stats-header p { + margin: 0; + color: #6c757d; + font-size: 14px; + font-weight: 400; +} + +/* Section filtre minimaliste */ +.reedcrm-stats-filters-wrapper { + margin-bottom: 30px; + border: 1px solid #e0e0e0; + border-radius: 6px; + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.reedcrm-stats-filters-header { + padding: 12px 16px; + cursor: pointer; + user-select: none; + transition: background-color 0.2s ease; + border-bottom: 1px solid #f0f0f0; +} + +.reedcrm-stats-filters-header:hover { + background: #f8f9fa; +} + +.reedcrm-stats-filters-header h3 { + margin: 0; + font-size: 14px; + font-weight: 500; + color: #495057; + display: flex; + align-items: center; + gap: 10px; +} + +.reedcrm-stats-filters-header i { + transition: transform 0.3s ease; + font-size: 11px; + color: #6c757d; +} + +.reedcrm-stats-filters-header.collapsed i { + transform: rotate(-90deg); +} + +/* Contenu des filtres */ +.reedcrm-stats-filters { + padding: 16px; + border-top: 1px solid #f0f0f0; + display: block !important; +} + +.reedcrm-stats-filters.collapsed { + display: none !important; +} + +.reedcrm-stats-filters-content { + display: flex; + gap: 16px; + align-items: flex-end; + flex-wrap: wrap; +} + +.reedcrm-stats-filter-group { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 200px; + flex: 1; +} + +.reedcrm-stats-filter-group label { + font-weight: 500; + color: #6c757d; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Sélecteurs de date - style minimaliste */ +.reedcrm-stats-filter-group .date-selector-wrapper { + display: flex !important; + align-items: center !important; + gap: 4px !important; + flex-wrap: nowrap !important; + width: 100% !important; +} + +/* Force tous les selects et inputs sur une seule ligne */ +.reedcrm-stats-filter-group .date-selector-wrapper select, +.reedcrm-stats-filter-group .date-selector-wrapper input { + padding: 7px 10px !important; + border: 1px solid #ddd !important; + border-radius: 4px !important; + font-size: 13px !important; + background: white !important; + margin: 0 !important; + display: inline-block !important; + vertical-align: middle !important; + height: 34px !important; + box-sizing: border-box !important; + color: #495057 !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper select[name$="day"] { + width: 65px !important; + min-width: 65px !important; + max-width: 65px !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper select[name$="month"] { + width: 110px !important; + min-width: 110px !important; + max-width: 110px !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper select[name$="year"] { + width: 85px !important; + min-width: 85px !important; + max-width: 85px !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper input[type="text"] { + width: 110px !important; + min-width: 110px !important; + max-width: 110px !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper select:focus, +.reedcrm-stats-filter-group .date-selector-wrapper input:focus { + outline: none !important; + border-color: #007bff !important; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15) !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper button, +.reedcrm-stats-filter-group .date-selector-wrapper .dpInvisibleButtons { + background: #f5f5f5 !important; + border: 1px solid #ddd !important; + color: #495057 !important; + padding: 7px 9px !important; + border-radius: 4px !important; + cursor: pointer !important; + height: 34px !important; + min-width: 34px !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + margin: 0 !important; + vertical-align: middle !important; + transition: all 0.2s ease !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper button:hover, +.reedcrm-stats-filter-group .date-selector-wrapper .dpInvisibleButtons:hover { + background: #e9ecef !important; + border-color: #bbb !important; +} + +.reedcrm-stats-filter-group .date-selector-wrapper button img, +.reedcrm-stats-filter-group .date-selector-wrapper .dpInvisibleButtons img { + filter: brightness(0) invert(0.5) !important; + width: 14px !important; + height: 14px !important; +} + +/* Force les divs internes à être inline */ +.reedcrm-stats-filter-group .date-selector-wrapper .nowraponall, +.reedcrm-stats-filter-group .date-selector-wrapper .inline-block, +.reedcrm-stats-filter-group .date-selector-wrapper .divfordateinput { + display: inline-flex !important; + align-items: center !important; + gap: 5px !important; + flex-wrap: nowrap !important; + vertical-align: middle !important; + margin: 0 !important; +} + +/* Bouton de filtre */ +.reedcrm-stats-filter-btn { + background: #007bff; + color: white; + border: none; + padding: 7px 16px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + height: 34px; + white-space: nowrap; + width: auto; + min-width: 90px; + transition: background-color 0.2s ease; +} + +.reedcrm-stats-filter-btn:hover { + background: #0056b3; +} + +/* Graphique */ +.reedcrm-stats-graph-container { + background: #fff; + padding: 40px; + margin-bottom: 20px; + border: 1px solid #e0e0e0; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.reedcrm-stats-graph-title { + font-size: 18px; + font-weight: 500; + color: #212529; + margin-bottom: 30px; + text-align: center; +} + +.reedcrm-stats-no-data { + text-align: center; + padding: 60px 20px; + color: #6c757d; + font-size: 15px; +} + +.reedcrm-stats-funnel-container { + width: 100%; + max-width: 600px; + margin: 0 auto; +} + +/* Styles for funnel graph */ +.reedcrm-funnel-container { + width: 100%; + padding: 20px; +} + +.reedcrm-funnel-svg { + width: 100%; + height: auto; + display: block; +} + +.reedcrm-funnel-text { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +@media (max-width: 768px) { + .reedcrm-stats-filters-content { + flex-direction: column; + } + + .reedcrm-stats-filter-group { + width: 100%; + min-width: 100%; + } + + .reedcrm-stats-filter-btn { + width: 100%; + justify-content: center; + } +} diff --git a/js/modules/stats.js b/js/modules/stats.js new file mode 100644 index 0000000..4e16c2c --- /dev/null +++ b/js/modules/stats.js @@ -0,0 +1,131 @@ +/* Copyright (C) 2023-2025 EVARISK + * + * 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 js/stats.js + * \ingroup reedcrm + * \brief JavaScript file for statistics page + */ + +'use strict'; + +/** + * Init stats JS + * + * @since 1.0.0 + * @version 1.0.0 + * + * @type {Object} + */ +window.reedcrm.stats = {}; + +/** + * Stats init + * + * @since 1.0.0 + * @version 1.0.0 + * + * @returns {void} + */ +window.reedcrm.stats.init = function() { + window.reedcrm.stats.event(); +}; + +/** + * Stats event + * + * @since 1.0.0 + * @version 1.0.0 + * + * @returns {void} + */ +window.reedcrm.stats.event = function() { + // Collapse/expand filters + var hasCustomFilters = jQuery('#filters-header').data('has-filters') === 'true'; + + jQuery("#filters-header").click(function() { + jQuery(".reedcrm-stats-filters").toggle(); + jQuery(this).toggleClass("collapsed"); + }); + + if (hasCustomFilters !== 'true') { + jQuery(".reedcrm-stats-filters").hide(); + jQuery("#filters-header").addClass("collapsed"); + } + + // Sales funnel filter button (from buildSalesFunnelFilters) + jQuery(document).off("click", "#apply-salesfunnel-filter-btn").on("click", "#apply-salesfunnel-filter-btn", function () { + var dateStartDay = jQuery("select[name='salesfunnel_date_startday']").val() || ""; + var dateStartMonth = jQuery("select[name='salesfunnel_date_startmonth']").val() || ""; + var dateStartYear = jQuery("select[name='salesfunnel_date_startyear']").val() || ""; + var dateEndDay = jQuery("select[name='salesfunnel_date_endday']").val() || ""; + var dateEndMonth = jQuery("select[name='salesfunnel_date_endmonth']").val() || ""; + var dateEndYear = jQuery("select[name='salesfunnel_date_endyear']").val() || ""; + + var $form = jQuery("#dashBoardForm"); + if (!$form.length) { + $form = jQuery("#statsform").first(); + } + if (!$form.length) { + $form = jQuery("form.dashboard").first(); + } + + function setHidden($context, name, value) { + var $field = $context.find("input[name='" + name + "']"); + if (value === "" || value === null) { + $field.remove(); + return; + } + if (!$field.length) { + $field = jQuery("", { type: "hidden", name: name }); + $context.append($field); + } + $field.val(value); + } + + if ($form.length) { + setHidden($form, "salesfunnel_date_startday", dateStartDay); + setHidden($form, "salesfunnel_date_startmonth", dateStartMonth); + setHidden($form, "salesfunnel_date_startyear", dateStartYear); + setHidden($form, "salesfunnel_date_endday", dateEndDay); + setHidden($form, "salesfunnel_date_endmonth", dateEndMonth); + setHidden($form, "salesfunnel_date_endyear", dateEndYear); + setHidden($form, "apply_salesfunnel_filter", 1); + $form.submit(); + return; + } + + var params = new URLSearchParams(window.location.search); + params.delete("salesfunnel_date_startday"); + params.delete("salesfunnel_date_startmonth"); + params.delete("salesfunnel_date_startyear"); + params.delete("salesfunnel_date_endday"); + params.delete("salesfunnel_date_endmonth"); + params.delete("salesfunnel_date_endyear"); + if (dateStartDay && dateStartMonth && dateStartYear) { + params.set("salesfunnel_date_startday", dateStartDay); + params.set("salesfunnel_date_startmonth", dateStartMonth); + params.set("salesfunnel_date_startyear", dateStartYear); + } + if (dateEndDay && dateEndMonth && dateEndYear) { + params.set("salesfunnel_date_endday", dateEndDay); + params.set("salesfunnel_date_endmonth", dateEndMonth); + params.set("salesfunnel_date_endyear", dateEndYear); + } + window.location.href = window.location.pathname + "?" + params.toString(); + }); +}; + diff --git a/js/reedcrm.min.js b/js/reedcrm.min.js index 08f8000..9e7ed89 100644 --- a/js/reedcrm.min.js +++ b/js/reedcrm.min.js @@ -1 +1 @@ -window.reedcrm||(window.reedcrm={},window.reedcrm.scriptsLoaded=!1),window.reedcrm.scriptsLoaded||(window.reedcrm.init=function(){window.reedcrm.load_list_script()},window.reedcrm.load_list_script=function(){if(!window.reedcrm.scriptsLoaded){let e=void 0,n=void 0;for(e in window.reedcrm)for(n in window.reedcrm[e].init&&window.reedcrm[e].init(),window.reedcrm[e])window.reedcrm[e]&&window.reedcrm[e][n]&&window.reedcrm[e][n].init&&window.reedcrm[e][n].init();window.reedcrm.scriptsLoaded=!0}},window.reedcrm.refresh=function(){let e=void 0,n=void 0;for(e in window.reedcrm)for(n in window.reedcrm[e].refresh&&window.reedcrm[e].refresh(),window.reedcrm[e])window.reedcrm[e]&&window.reedcrm[e][n]&&window.reedcrm[e][n].refresh&&window.reedcrm[e][n].refresh()},$(document).ready(window.reedcrm.init)),window.reedcrm.address={},window.reedcrm.address.init=function(){window.reedcrm.address.event()},window.reedcrm.address.event=function(){$(document).on("click",'[name="favorite_address"]',window.reedcrm.address.toggleAddressFavorite)},window.reedcrm.address.toggleAddressFavorite=function(){var e=$(this).attr("value");let n=$(this);var i=window.saturne.toolbox.getToken();let o="?";document.URL.match(/\?/)&&(o="&"),$.ajax({url:document.URL+o+"action=toggle_favorite&contact_id="+e+"&token="+i,type:"POST",processData:!1,contentType:!1,success:function(){var e=$(".fas.fa-star");n.hasClass("far")&&(n.removeClass("far"),n.addClass("fas")),e.removeClass("fas").addClass("far")},error:function(){}})},window.reedcrm||(window.reedcrm={}),window.reedcrm.callnotifications={},window.reedcrm.callnotifications.init=function(){console.log("ReedCRM Call Notifications initialized"),window.reedcrm.callnotifications.config(),window.reedcrm.callnotifications.start()},window.reedcrm.callnotifications.config=function(){var e=document.getElementById("reedcrm-call-config");e?(window.reedcrm.callnotifications.frequency=parseInt(e.dataset.frequency)||60,window.reedcrm.callnotifications.autoOpen=parseInt(e.dataset.autoOpen)||0,window.reedcrm.callnotifications.openNewTab=parseInt(e.dataset.openNewTab)||1,window.reedcrm.callnotifications.checkUrl=e.dataset.checkUrl||"",window.reedcrm.callnotifications.trans={incomingCall:e.dataset.transIncomingCall||"Incoming Call",from:e.dataset.transFrom||"From",phone:e.dataset.transPhone||"Phone",email:e.dataset.transEmail||"Email",viewContact:e.dataset.transViewContact||"View Contact"}):(window.reedcrm.callnotifications.frequency=60,window.reedcrm.callnotifications.autoOpen=0,window.reedcrm.callnotifications.openNewTab=1,window.reedcrm.callnotifications.checkUrl="",window.reedcrm.callnotifications.trans={incomingCall:"Incoming Call",from:"From",phone:"Phone",email:"Email",viewContact:"View Contact"}),window.reedcrm.callnotifications.enabled=!0,window.reedcrm.callnotifications.interval=null},window.reedcrm.callnotifications.start=function(){window.reedcrm.callnotifications.checkUrl?setTimeout(function(){console.log("Starting ReedCRM call check with frequency: "+window.reedcrm.callnotifications.frequency+"s"),window.reedcrm.callnotifications.check(),window.reedcrm.callnotifications.interval=setInterval(window.reedcrm.callnotifications.check,1e3*window.reedcrm.callnotifications.frequency)},3e3):console.error("ReedCRM: No check URL configured for call notifications")},window.reedcrm.callnotifications.check=function(){window.reedcrm.callnotifications.enabled&&jQuery.ajax({url:window.reedcrm.callnotifications.checkUrl,type:"GET",dataType:"json",success:function(e){e&&0"+e.incomingCall+" : "+n.contact_name;n.caller&&(i+="
"+e.from+": "+n.caller),n.contact_phone&&(i+="
"+e.phone+": "+n.contact_phone),n.contact_email&&(i+="
"+e.email+": "+n.contact_email),i=i+'

",jQuery.jnotify(i,"success",!0,{sticky:!0,timeout:3e4}),window.reedcrm.callnotifications.autoOpen&&setTimeout(function(){var e=window.reedcrm.callnotifications.openNewTab?"_blank":"_self";window.open(n.url,e)},1e3)},window.reedcrm.callnotifications.enable=function(){window.reedcrm.callnotifications.enabled=!0,window.reedcrm.callnotifications.check(),window.reedcrm.callnotifications.interval=setInterval(window.reedcrm.callnotifications.check,1e3*window.reedcrm.callnotifications.frequency),console.log("ReedCRM call notifications enabled")},window.reedcrm.callnotifications.disable=function(){window.reedcrm.callnotifications.enabled=!1,window.reedcrm.callnotifications.interval&&clearInterval(window.reedcrm.callnotifications.interval),console.log("ReedCRM call notifications disabled")},jQuery(document).ready(function(){window.reedcrm.callnotifications.init()}),window.reedcrm.quickcreation={},window.reedcrm.quickcreation.latitude=null,window.reedcrm.quickcreation.longitude=null,window.reedcrm.quickcreation.init=function(){window.reedcrm.quickcreation.event()},window.reedcrm.quickcreation.event=function(){$(document).on("change","#upload-image",window.saturne.media.uploadImage),$(document).on("click",".image-validate",window.reedcrm.quickcreation.createImg),window.reedcrm.quickcreation.getCurrentPosition(),$(document).on("submit",".quickcreation-form",window.reedcrm.quickcreation.vibratePhone),$(document).on("input","#opp_percent",window.reedcrm.quickcreation.showOppPercentValue)},window.reedcrm.quickcreation.createImg=function(){var e=$(this).closest(".wpeo-modal").find("canvas")[0].toDataURL("image/jpeg"),n=window.saturne.toolbox.getToken(),i=window.saturne.toolbox.getQuerySeparator(document.URL),i=document.URL+i+"action=add_img&token="+n;$.ajax({url:i,type:"POST",processData:!1,contentType:"application/octet-stream",data:JSON.stringify({img:e}),success:function(e){$(".wpeo-modal").removeClass("modal-active"),$("#id-container .linked-medias-list").replaceWith($(e).find("#id-container .linked-medias-list"))},error:function(){}})},window.reedcrm.quickcreation.getCurrentPosition=function(){navigator.geolocation?navigator.geolocation.getCurrentPosition(function(e){window.reedcrm.quickcreation.latitude=e.coords.latitude,window.reedcrm.quickcreation.longitude=e.coords.longitude,$("#id-container #latitude").val(window.reedcrm.quickcreation.latitude),$("#id-container #longitude").val(window.reedcrm.quickcreation.longitude)},function(e){switch(e.code){case e.PERMISSION_DENIED:$("#id-container #geolocation-error").val("User denied the request for geolocation.");break;case e.POSITION_UNAVAILABLE:$("#id-container #geolocation-error").val("Location information is unavailable.");break;case e.TIMEOUT:$("#id-container #geolocation-error").val("The request to get user location timed out.");break;case e.UNKNOWN_ERROR:$("#id-container #geolocation-error").val("An unknown error occurred.")}}):$("#id-container #geolocation-error").val("Geolocation is not supported by this browser.")},window.reedcrm.quickcreation.vibratePhone=function(){"vibrate"in navigator&&navigator.vibrate([1e3,500,200,200,500,1e3]),window.saturne.loader.display($(".page-footer button"))},window.reedcrm.quickcreation.showOppPercentValue=function(){$(".opp_percent-value").text($("#opp_percent").val()+" %")},window.reedcrm.quickevent={},window.reedcrm.quickevent.init=function(){window.reedcrm.quickevent.event()},window.reedcrm.quickevent.event=function(){$(document).on("keyup","#label",window.reedcrm.quickevent.labelKeyUp)},window.reedcrm.quickevent.labelKeyUp=function(){$("#label").val().length>=.7*parseInt($("#label").attr("maxlength"))?$(".quickevent-label-warning-notice").removeClass("hidden"):$(".quickevent-label-warning-notice").addClass("hidden")}; \ No newline at end of file +window.reedcrm||(window.reedcrm={},window.reedcrm.scriptsLoaded=!1),window.reedcrm.scriptsLoaded||(window.reedcrm.init=function(){window.reedcrm.load_list_script()},window.reedcrm.load_list_script=function(){if(!window.reedcrm.scriptsLoaded){let e=void 0,n=void 0;for(e in window.reedcrm)for(n in window.reedcrm[e].init&&window.reedcrm[e].init(),window.reedcrm[e])window.reedcrm[e]&&window.reedcrm[e][n]&&window.reedcrm[e][n].init&&window.reedcrm[e][n].init();window.reedcrm.scriptsLoaded=!0}},window.reedcrm.refresh=function(){let e=void 0,n=void 0;for(e in window.reedcrm)for(n in window.reedcrm[e].refresh&&window.reedcrm[e].refresh(),window.reedcrm[e])window.reedcrm[e]&&window.reedcrm[e][n]&&window.reedcrm[e][n].refresh&&window.reedcrm[e][n].refresh()},$(document).ready(window.reedcrm.init)),window.reedcrm.address={},window.reedcrm.address.init=function(){window.reedcrm.address.event()},window.reedcrm.address.event=function(){$(document).on("click",'[name="favorite_address"]',window.reedcrm.address.toggleAddressFavorite)},window.reedcrm.address.toggleAddressFavorite=function(){var e=$(this).attr("value");let n=$(this);var t=window.saturne.toolbox.getToken();let i="?";document.URL.match(/\?/)&&(i="&"),$.ajax({url:document.URL+i+"action=toggle_favorite&contact_id="+e+"&token="+t,type:"POST",processData:!1,contentType:!1,success:function(){var e=$(".fas.fa-star");n.hasClass("far")&&(n.removeClass("far"),n.addClass("fas")),e.removeClass("fas").addClass("far")},error:function(){}})},window.reedcrm||(window.reedcrm={}),window.reedcrm.callnotifications={},window.reedcrm.callnotifications.init=function(){console.log("ReedCRM Call Notifications initialized"),window.reedcrm.callnotifications.config(),window.reedcrm.callnotifications.start()},window.reedcrm.callnotifications.config=function(){var e=document.getElementById("reedcrm-call-config");e?(window.reedcrm.callnotifications.frequency=parseInt(e.dataset.frequency)||60,window.reedcrm.callnotifications.autoOpen=parseInt(e.dataset.autoOpen)||0,window.reedcrm.callnotifications.openNewTab=parseInt(e.dataset.openNewTab)||1,window.reedcrm.callnotifications.checkUrl=e.dataset.checkUrl||"",window.reedcrm.callnotifications.trans={incomingCall:e.dataset.transIncomingCall||"Incoming Call",from:e.dataset.transFrom||"From",phone:e.dataset.transPhone||"Phone",email:e.dataset.transEmail||"Email",viewContact:e.dataset.transViewContact||"View Contact"}):(window.reedcrm.callnotifications.frequency=60,window.reedcrm.callnotifications.autoOpen=0,window.reedcrm.callnotifications.openNewTab=1,window.reedcrm.callnotifications.checkUrl="",window.reedcrm.callnotifications.trans={incomingCall:"Incoming Call",from:"From",phone:"Phone",email:"Email",viewContact:"View Contact"}),window.reedcrm.callnotifications.enabled=!0,window.reedcrm.callnotifications.interval=null},window.reedcrm.callnotifications.start=function(){window.reedcrm.callnotifications.checkUrl?setTimeout(function(){console.log("Starting ReedCRM call check with frequency: "+window.reedcrm.callnotifications.frequency+"s"),window.reedcrm.callnotifications.check(),window.reedcrm.callnotifications.interval=setInterval(window.reedcrm.callnotifications.check,1e3*window.reedcrm.callnotifications.frequency)},3e3):console.error("ReedCRM: No check URL configured for call notifications")},window.reedcrm.callnotifications.check=function(){window.reedcrm.callnotifications.enabled&&jQuery.ajax({url:window.reedcrm.callnotifications.checkUrl,type:"GET",dataType:"json",success:function(e){e&&0"+e.incomingCall+" : "+n.contact_name;n.caller&&(t+="
"+e.from+": "+n.caller),n.contact_phone&&(t+="
"+e.phone+": "+n.contact_phone),n.contact_email&&(t+="
"+e.email+": "+n.contact_email),t=t+'

",jQuery.jnotify(t,"success",!0,{sticky:!0,timeout:3e4}),window.reedcrm.callnotifications.autoOpen&&setTimeout(function(){var e=window.reedcrm.callnotifications.openNewTab?"_blank":"_self";window.open(n.url,e)},1e3)},window.reedcrm.callnotifications.enable=function(){window.reedcrm.callnotifications.enabled=!0,window.reedcrm.callnotifications.check(),window.reedcrm.callnotifications.interval=setInterval(window.reedcrm.callnotifications.check,1e3*window.reedcrm.callnotifications.frequency),console.log("ReedCRM call notifications enabled")},window.reedcrm.callnotifications.disable=function(){window.reedcrm.callnotifications.enabled=!1,window.reedcrm.callnotifications.interval&&clearInterval(window.reedcrm.callnotifications.interval),console.log("ReedCRM call notifications disabled")},jQuery(document).ready(function(){window.reedcrm.callnotifications.init()}),window.reedcrm.quickcreation={},window.reedcrm.quickcreation.latitude=null,window.reedcrm.quickcreation.longitude=null,window.reedcrm.quickcreation.init=function(){window.reedcrm.quickcreation.event()},window.reedcrm.quickcreation.event=function(){$(document).on("change","#upload-image",window.saturne.media.uploadImage),$(document).on("click",".image-validate",window.reedcrm.quickcreation.createImg),window.reedcrm.quickcreation.getCurrentPosition(),$(document).on("submit",".quickcreation-form",window.reedcrm.quickcreation.vibratePhone),$(document).on("input","#opp_percent",window.reedcrm.quickcreation.showOppPercentValue)},window.reedcrm.quickcreation.createImg=function(){var e=$(this).closest(".wpeo-modal").find("canvas")[0].toDataURL("image/jpeg"),n=window.saturne.toolbox.getToken(),t=window.saturne.toolbox.getQuerySeparator(document.URL),t=document.URL+t+"action=add_img&token="+n;$.ajax({url:t,type:"POST",processData:!1,contentType:"application/octet-stream",data:JSON.stringify({img:e}),success:function(e){$(".wpeo-modal").removeClass("modal-active"),$("#id-container .linked-medias-list").replaceWith($(e).find("#id-container .linked-medias-list"))},error:function(){}})},window.reedcrm.quickcreation.getCurrentPosition=function(){navigator.geolocation?navigator.geolocation.getCurrentPosition(function(e){window.reedcrm.quickcreation.latitude=e.coords.latitude,window.reedcrm.quickcreation.longitude=e.coords.longitude,$("#id-container #latitude").val(window.reedcrm.quickcreation.latitude),$("#id-container #longitude").val(window.reedcrm.quickcreation.longitude)},function(e){switch(e.code){case e.PERMISSION_DENIED:$("#id-container #geolocation-error").val("User denied the request for geolocation.");break;case e.POSITION_UNAVAILABLE:$("#id-container #geolocation-error").val("Location information is unavailable.");break;case e.TIMEOUT:$("#id-container #geolocation-error").val("The request to get user location timed out.");break;case e.UNKNOWN_ERROR:$("#id-container #geolocation-error").val("An unknown error occurred.")}}):$("#id-container #geolocation-error").val("Geolocation is not supported by this browser.")},window.reedcrm.quickcreation.vibratePhone=function(){"vibrate"in navigator&&navigator.vibrate([1e3,500,200,200,500,1e3]),window.saturne.loader.display($(".page-footer button"))},window.reedcrm.quickcreation.showOppPercentValue=function(){$(".opp_percent-value").text($("#opp_percent").val()+" %")},window.reedcrm.quickevent={},window.reedcrm.quickevent.init=function(){window.reedcrm.quickevent.event()},window.reedcrm.quickevent.event=function(){$(document).on("keyup","#label",window.reedcrm.quickevent.labelKeyUp)},window.reedcrm.quickevent.labelKeyUp=function(){$("#label").val().length>=.7*parseInt($("#label").attr("maxlength"))?$(".quickevent-label-warning-notice").removeClass("hidden"):$(".quickevent-label-warning-notice").addClass("hidden")},window.reedcrm.stats={},window.reedcrm.stats.init=function(){window.reedcrm.stats.event()},window.reedcrm.stats.event=function(){var e="true"===jQuery("#filters-header").data("has-filters");jQuery("#filters-header").click(function(){jQuery(".reedcrm-stats-filters").toggle(),jQuery(this).toggleClass("collapsed")}),"true"!==e&&(jQuery(".reedcrm-stats-filters").hide(),jQuery("#filters-header").addClass("collapsed")),jQuery(document).off("click","#apply-salesfunnel-filter-btn").on("click","#apply-salesfunnel-filter-btn",function(){var e=jQuery("select[name='salesfunnel_date_startday']").val()||"",n=jQuery("select[name='salesfunnel_date_startmonth']").val()||"",t=jQuery("select[name='salesfunnel_date_startyear']").val()||"",i=jQuery("select[name='salesfunnel_date_endday']").val()||"",o=jQuery("select[name='salesfunnel_date_endmonth']").val()||"",r=jQuery("select[name='salesfunnel_date_endyear']").val()||"",a=jQuery("#dashBoardForm");function c(e,n,t){var i=e.find("input[name='"+n+"']");""===t||null===t?i.remove():(i.length||(i=jQuery("",{type:"hidden",name:n}),e.append(i)),i.val(t))}(a=(a=a.length?a:jQuery("#statsform").first()).length?a:jQuery("form.dashboard").first()).length?(c(a,"salesfunnel_date_startday",e),c(a,"salesfunnel_date_startmonth",n),c(a,"salesfunnel_date_startyear",t),c(a,"salesfunnel_date_endday",i),c(a,"salesfunnel_date_endmonth",o),c(a,"salesfunnel_date_endyear",r),c(a,"apply_salesfunnel_filter",1),a.submit()):((a=new URLSearchParams(window.location.search)).delete("salesfunnel_date_startday"),a.delete("salesfunnel_date_startmonth"),a.delete("salesfunnel_date_startyear"),a.delete("salesfunnel_date_endday"),a.delete("salesfunnel_date_endmonth"),a.delete("salesfunnel_date_endyear"),e&&n&&t&&(a.set("salesfunnel_date_startday",e),a.set("salesfunnel_date_startmonth",n),a.set("salesfunnel_date_startyear",t)),i&&o&&r&&(a.set("salesfunnel_date_endday",i),a.set("salesfunnel_date_endmonth",o),a.set("salesfunnel_date_endyear",r)),window.location.href=window.location.pathname+"?"+a.toString())})}; \ No newline at end of file diff --git a/langs/en_US/reedcrm.lang b/langs/en_US/reedcrm.lang index 1458802..cab620e 100644 --- a/langs/en_US/reedcrm.lang +++ b/langs/en_US/reedcrm.lang @@ -69,3 +69,21 @@ NbLines = Number of lines # Keyyo Tab for Thirdparty KeyyoCalls = Keyyo Calls + +# Dashboard +ProjectOpportunitiesList = Last 5 project opportunities +OppPercent = Opportunity percentage +ButtonActions = Actions + +# Sales Funnel +SalesFunnel = Sales Funnel +LeadsGenerated = Leads Generated +Qualified = Qualified +Presentation = Presentation +ProposalSent = Proposal Sent +Negotiation = Negotiation +ClosedWon = Closed Won +ConversionRate = Conversion Rate + +# Statistics +Statistics = Statistics diff --git a/langs/fr_FR/reedcrm.lang b/langs/fr_FR/reedcrm.lang index 56edd23..af07a46 100644 --- a/langs/fr_FR/reedcrm.lang +++ b/langs/fr_FR/reedcrm.lang @@ -246,6 +246,19 @@ ProjectOpportunitiesList = Les 5 dernières opportunités de projet OppPercent = Pourcentage d'opportunité ButtonActions = Actions ContentToBeAdded = Contenu à ajouter + +# Sales Funnel - Entonnoir de vente +SalesFunnel = Entonnoir de vente + +# Statistics - Statistiques +Statistics = Statistiques +LeadsGenerated = Leads générés +Qualified = Qualifiés +Presentation = Présentation +ProposalSent = Proposition envoyée +Negotiation = Négociation +ClosedWon = Gagné +ConversionRate = Taux de conversion CommercialProposalsInProgress = Propositions commerciales en cours NoCommercialProposalsInProgress = Aucune proposition commerciale en cours AddEvent = Ajouter un événement diff --git a/view/stats/stats.php b/view/stats/stats.php new file mode 100644 index 0000000..3b138b3 --- /dev/null +++ b/view/stats/stats.php @@ -0,0 +1,232 @@ + + * + * 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 view/stats/stats.php + * \ingroup reedcrm + * \brief Statistics page with sales funnel graph + */ + +// Load ReedCRM environment +if (file_exists('../../reedcrm.main.inc.php')) { + require_once __DIR__ . '/../../reedcrm.main.inc.php'; +} elseif (file_exists('../../../reedcrm.main.inc.php')) { + require_once __DIR__ . '/../../../reedcrm.main.inc.php'; +} else { + die('Include of reedcrm main fails'); +} + +// Load Dolibarr libraries +require_once DOL_DOCUMENT_ROOT . '/core/lib/admin.lib.php'; +require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php'; +require_once DOL_DOCUMENT_ROOT . '/core/class/dolgraph.class.php'; +require_once DOL_DOCUMENT_ROOT . '/comm/propal/class/propal.class.php'; + +// Load ReedCRM libraries +require_once __DIR__ . '/../../class/reedcrmdashboard.class.php'; + +// Global variables definitions +global $conf, $db, $langs, $user; + +// Load translation files required by the page +saturne_load_langs(); + +// Get parameters +$action = GETPOST('action', 'aZ09'); + +// Initialize technical objects +$dashboard = new ReedcrmDashboard($db); +$form = new Form($db); + +// Security check - Protection if external user +$permissionToRead = $user->rights->reedcrm->read; +saturne_check_access($permissionToRead); + +// Get date filters from URL +$dateStartDay = GETPOST('salesfunnel_date_startday', 'int'); +$dateStartMonth = GETPOST('salesfunnel_date_startmonth', 'int'); +$dateStartYear = GETPOST('salesfunnel_date_startyear', 'int'); +$dateEndDay = GETPOST('salesfunnel_date_endday', 'int'); +$dateEndMonth = GETPOST('salesfunnel_date_endmonth', 'int'); +$dateEndYear = GETPOST('salesfunnel_date_endyear', 'int'); + +// Build timestamps from GET parameters for selectDate +// Par défaut : date début = il y a une semaine, date fin = aujourd'hui +$now = dol_now(); +$dateStartTimestamp = -1; +$dateEndTimestamp = -1; + +if ($dateStartDay > 0 && $dateStartMonth > 0 && $dateStartYear > 0) { + $dateStartTimestamp = dol_mktime(0, 0, 0, $dateStartMonth, $dateStartDay, $dateStartYear); +} else { + // Par défaut : il y a une semaine + $dateStartTimestamp = $now - (7 * 24 * 3600); +} + +if ($dateEndDay > 0 && $dateEndMonth > 0 && $dateEndYear > 0) { + $dateEndTimestamp = dol_mktime(23, 59, 59, $dateEndMonth, $dateEndDay, $dateEndYear); +} else { + // Par défaut : aujourd'hui + $dateEndTimestamp = $now; +} + +/* + * View + */ + +$title = $langs->transnoentities('Statistics'); +$helpUrl = 'FR:Module_ReedCRM'; + +saturne_header(0, '', $title, $helpUrl); + +// Load CSS +print ''; + +print '
'; + +print '
'; +print '

' . $langs->transnoentities('Statistics') . '

'; +print '

' . $langs->transnoentities('SalesFunnel') . '

'; +print '
'; + +// Filters section with collapse/expand +print '
'; +print '
'; +print '

' . $langs->trans('Filter') . '

'; +print '
'; + +print '
'; + +print '
'; + +print '
'; +print ''; +print '
'; +print $form->selectDate($dateStartTimestamp, 'salesfunnel_date_start', 0, 0, 1, 'form', 1, 0, 0, '', '', '', 1); +print '
'; +print '
'; + +print '
'; +print ''; +print '
'; +print $form->selectDate($dateEndTimestamp, 'salesfunnel_date_end', 0, 0, 0, 'form', 1, 0, 0, '', '', '', 1); +print '
'; +print '
'; + +print '
'; +print ''; +print ''; +print '
'; + +print '
'; +print '
'; +print '
'; + + +// Get sales funnel data with default dates +$funnelData = $dashboard->getSalesFunnel($dateStartTimestamp, $dateEndTimestamp); + +if (!empty($funnelData) && !empty($funnelData['custom_html'])) { + print '
'; + print '
' . $funnelData['title'] . '
'; + print '
'; + print $funnelData['custom_html']; + print '
'; + print '
'; +} else { + print '
'; + print '
' . (!empty($funnelData['title']) ? $funnelData['title'] : $langs->transnoentities('SalesFunnel')) . '
'; + print '
'; + print '
'; + print $langs->trans('NoData'); + print '
'; + print '
'; +} + +// --------------------------------------------------------------------- +// Graph - Statuts des propales ouvertes (brouillon / validée / signée) +// --------------------------------------------------------------------- + +print '
'; +print '
' . $langs->transnoentities('PropalsOpenStatus') . '
'; + +// Statuts ouverts : brouillon (0), validée (1), signée (2) +$openStatuses = array(Propal::STATUS_DRAFT, Propal::STATUS_VALIDATED, Propal::STATUS_SIGNED); + +$sql = "SELECT fk_statut, COUNT(*) as nb"; +$sql .= " FROM " . MAIN_DB_PREFIX . "propal"; +$sql .= " WHERE entity IN (" . getEntity('propal') . ")"; +$sql .= " AND fk_statut IN (" . implode(',', $openStatuses) . ")"; +$sql .= " GROUP BY fk_statut"; + +$resql = $db->query($sql); +$dataseries = array(); +$colorseries = array(); +$totalopen = 0; + +if ($resql) { + $propalstatic = new Propal($db); + include DOL_DOCUMENT_ROOT . '/theme/' . $conf->theme . '/theme_vars.inc.php'; + + while ($obj = $db->fetch_object($resql)) { + $label = $propalstatic->LibStatut((int) $obj->fk_statut, 1, 0, 1); + $dataseries[] = array($label, (int) $obj->nb); + $totalopen += (int) $obj->nb; + + if ((int) $obj->fk_statut === Propal::STATUS_DRAFT) { + $colorseries[$obj->fk_statut] = '-' . $badgeStatus0; + } elseif ((int) $obj->fk_statut === Propal::STATUS_VALIDATED) { + $colorseries[$obj->fk_statut] = $badgeStatus1; + } elseif ((int) $obj->fk_statut === Propal::STATUS_SIGNED) { + $colorseries[$obj->fk_statut] = $badgeStatus6; + } + } + $db->free($resql); +} + +if (!empty($dataseries) && $totalopen > 0) { + $dolgraph = new DolGraph(); + $dolgraph->SetData($dataseries); + if (!empty($colorseries)) { + $dolgraph->SetDataColor(array_values($colorseries)); + } + $dolgraph->setShowLegend(2); + $dolgraph->setShowPercent(1); + $dolgraph->SetType(array('pie')); + $dolgraph->setHeight('250'); + $dolgraph->draw('reedcrm-propals-open-status'); + + print '
'; + print $dolgraph->show(0); + print '
'; +} else { + print '
'; + print '
'; + print $langs->trans('NoData'); + print '
'; +} + +print '
'; +// --------------------------------------------------------------------- + +print '
'; + +// End of page +llxFooter(); +$db->close();