';
+
+ 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 '
';
+
+
+// Get sales funnel data with default dates
+$funnelData = $dashboard->getSalesFunnel($dateStartTimestamp, $dateEndTimestamp);
+
+if (!empty($funnelData) && !empty($funnelData['custom_html'])) {
+ print '