Skip to content

Commit ad56a28

Browse files
committed
Improve clients and contacts reports
1 parent d8f5161 commit ad56a28

5 files changed

Lines changed: 248 additions & 38 deletions

File tree

Controller/ReportClients.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ class ReportClients extends Controller
4141
/** @var array */
4242
public $charts = [];
4343

44+
/** @var int */
45+
public $customersWithDebt;
46+
4447
/** @var array todas las empresas a listar en el formulario [idEmpresa => nombre empresa] */
4548
public $companies = [];
4649

@@ -59,6 +62,9 @@ class ReportClients extends Controller
5962
/** @var int */
6063
public $inactiveCustomers;
6164

65+
/** @var int */
66+
public $newCustomers30Days;
67+
6268
/** @var int */
6369
public $totalCustomers;
6470

@@ -93,6 +99,8 @@ public function privateCore(&$response, $user, $permissions)
9399
$this->loadActiveCustomers();
94100
$this->loadInactiveCustomers();
95101
$this->loadActiveCustomersYear();
102+
$this->loadNewCustomers30Days();
103+
$this->loadCustomersWithDebt();
96104
$this->loadCustomersByCountry();
97105
$this->loadCustomersByProvince();
98106
$this->loadCustomersByGroup();
@@ -192,8 +200,8 @@ protected function loadCompanies(): void
192200
// empresa seleccionada
193201
$this->idempresa = $this->request()->queryOrInput('idempresa', null);
194202
if (null === $this->idempresa) {
195-
// seleccionar por defecto si no hay nada
196-
$this->idempresa = (int)Tools::settings('default', 'idempresa');
203+
// seleccionar todas por defecto si no hay nada
204+
$this->idempresa = 'all';
197205
} elseif ($this->idempresa === 'all') {
198206
// seleccionar todas
199207
$this->idempresa = 'all';
@@ -276,6 +284,17 @@ protected function loadInvoicesByProvince(): void
276284
$this->charts['invoicesByProvince'] = $report;
277285
}
278286

287+
protected function loadNewCustomers30Days(): void
288+
{
289+
$sql = "SELECT COUNT(*) as total FROM clientes WHERE fechaalta >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)";
290+
291+
if ($this->idempresa !== 'all') {
292+
$sql .= " AND codcliente IN (SELECT codcliente FROM facturascli WHERE idempresa = " . (int)$this->idempresa . ")";
293+
}
294+
295+
$this->newCustomers30Days = (int)$this->dataBase->select($sql)[0]['total'];
296+
}
297+
279298
protected function loadNewCustomersByMonth(): void
280299
{
281300
$report = new Report();
@@ -340,6 +359,17 @@ protected function loadTopDebtors(): void
340359
$this->charts['topDebtors'] = $report;
341360
}
342361

362+
protected function loadCustomersWithDebt(): void
363+
{
364+
$sql = "SELECT COUNT(DISTINCT codcliente) as total FROM facturascli WHERE pagada = " . $this->dataBase->var2str(false);
365+
366+
if ($this->idempresa !== 'all') {
367+
$sql .= " AND idempresa = " . $this->dataBase->var2str((int)$this->idempresa);
368+
}
369+
370+
$this->customersWithDebt = (int)$this->dataBase->select($sql)[0]['total'];
371+
}
372+
343373
protected function getDebtorsSql(int $limit = 0): string
344374
{
345375
$sql = "SELECT "

Controller/ReportContacts.php

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
<?php
2+
/**
3+
* This file is part of Informes plugin for FacturaScripts
4+
* Copyright (C) 2026 Carlos Garcia Gomez <carlos@facturascripts.com>
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Lesser General Public License as
8+
* published by the Free Software Foundation, either version 3 of the
9+
* License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
219

320
namespace FacturaScripts\Plugins\Informes\Controller;
421

522
use FacturaScripts\Core\Base\Controller;
623
use FacturaScripts\Core\Plugins;
724
use FacturaScripts\Core\Tools;
25+
use FacturaScripts\Dinamic\Model\Pais;
826
use FacturaScripts\Plugins\Informes\Model\Report;
927
use FacturaScripts\Plugins\Informes\Lib\Informes\ContactsReport;
1028

@@ -15,6 +33,8 @@ class ReportContacts extends Controller
1533
public $charts = [];
1634
public $totals = [];
1735
public $countries = [];
36+
public $companyCountry = '';
37+
public $companyCountryCode = '';
1838

1939
// extra data for view
2040
public $sources_periods = [];
@@ -45,6 +65,7 @@ public function privateCore(&$response, $user, $permissions)
4565
// charts
4666
$this->loadNewContactsByMonth();
4767
$this->loadNewContactsByYear();
68+
$this->loadContactsByProvince();
4869

4970
if ($this->isEnabledCRM) {
5071
$this->loadSourcesChart();
@@ -61,7 +82,14 @@ public function privateCore(&$response, $user, $permissions)
6182

6283
protected function loadData(): void
6384
{
64-
// nothing special for now
85+
$this->companyCountryCode = Tools::settings('default', 'codpais', 'ESP');
86+
87+
$country = new Pais();
88+
if ($country->load($this->companyCountryCode)) {
89+
$this->companyCountry = $country->nombre;
90+
} else {
91+
$this->companyCountry = $this->companyCountryCode;
92+
}
6593
}
6694

6795
protected function loadNewContactsByMonth(): void
@@ -94,17 +122,32 @@ protected function loadNewContactsByYear(): void
94122
$this->charts['newByYears'] = $report;
95123
}
96124

125+
protected function loadContactsByProvince(): void
126+
{
127+
$report = new Report();
128+
$report->type = Report::TYPE_TREE_MAP;
129+
$report->table = 'contactos c';
130+
$report->xcolumn = "COALESCE(NULLIF(c.provincia, ''), '" . Tools::trans('no-data') . "')";
131+
$report->ycolumn = 'c.idcontacto';
132+
$report->yoperation = 'COUNT';
133+
134+
Report::activateAdvancedReport(true);
135+
$report->addCustomFilter('c.codpais', '=', $this->companyCountryCode);
136+
137+
$this->charts['contactsByProvince'] = $report;
138+
}
139+
97140
protected function loadSourcesChart(): void
98141
{
99142
$report = new Report();
100143
$report->type = Report::TYPE_DOUGHNUT;
101-
$report->table = 'crm_fuentes2 f';
102-
$report->xcolumn = "COALESCE(f.nombre, '" . Tools::trans('no-data') . "')";
144+
$report->table = 'contactos c';
145+
$report->xcolumn = "COALESCE(NULLIF(f.nombre, ''), '" . Tools::trans('no-data') . "')";
103146
$report->ycolumn = 'c.idcontacto';
104147
$report->yoperation = 'COUNT';
105148

106149
Report::activateAdvancedReport(true);
107-
$report->addCustomJoin('LEFT JOIN contactos c ON c.idfuente = f.id');
150+
$report->addCustomJoin('LEFT JOIN crm_fuentes2 f ON c.idfuente = f.id');
108151

109152
$this->charts['sources'] = $report;
110153
}
@@ -113,13 +156,14 @@ protected function loadInterestsChart(): void
113156
{
114157
$report = new Report();
115158
$report->type = Report::TYPE_DOUGHNUT;
116-
$report->table = 'crm_intereses i';
117-
$report->xcolumn = "COALESCE(i.nombre, '" . Tools::trans('no-data') . "')";
118-
$report->ycolumn = 'ic.id';
159+
$report->table = 'contactos c';
160+
$report->xcolumn = "COALESCE(NULLIF(i.nombre, ''), '" . Tools::trans('no-data') . "')";
161+
$report->ycolumn = 'c.idcontacto';
119162
$report->yoperation = 'COUNT';
120163

121164
Report::activateAdvancedReport(true);
122-
$report->addCustomJoin('LEFT JOIN crm_intereses_contactos ic ON ic.idinteres = i.id');
165+
$report->addCustomJoin('LEFT JOIN crm_intereses_contactos ic ON ic.idcontacto = c.idcontacto');
166+
$report->addCustomJoin('LEFT JOIN crm_intereses i ON ic.idinteres = i.id');
123167

124168
$this->charts['interests'] = $report;
125169
}

Lib/Informes/ContactsReport.php

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
<?php
2+
/**
3+
* This file is part of Informes plugin for FacturaScripts
4+
* Copyright (C) 2026 Carlos Garcia Gomez <carlos@facturascripts.com>
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Lesser General Public License as
8+
* published by the Free Software Foundation, either version 3 of the
9+
* License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
219
namespace FacturaScripts\Plugins\Informes\Lib\Informes;
320

421
use FacturaScripts\Core\Base\DataBase;
@@ -25,6 +42,26 @@ public static function summary(): array
2542
$out['verified_perc'] = $out['total'] > 0 ? round(($out['verified'] * 100) / $out['total'], 2) : 0;
2643
$out['marketing'] = (int)$db->select("SELECT COUNT(*) as total FROM contactos WHERE admitemarketing = " . $db->var2str(true) . ";")[0]['total'];
2744
$out['marketing_perc'] = $out['total'] > 0 ? round(($out['marketing'] * 100) / $out['total'], 2) : 0;
45+
$out['new_30_days'] = (int)$db->select("SELECT COUNT(*) as total FROM contactos WHERE fechaalta >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);")[0]['total'];
46+
$out['without_source'] = 0;
47+
if ($db->tableExists('crm_fuentes2')) {
48+
$out['without_source'] = (int)$db->select("SELECT COUNT(*) as total
49+
FROM contactos c
50+
LEFT JOIN crm_fuentes2 f ON c.idfuente = f.id
51+
WHERE f.id IS NULL OR NULLIF(f.nombre, '') IS NULL;")[0]['total'];
52+
}
53+
$out['without_source_perc'] = $out['total'] > 0 ? round(($out['without_source'] * 100) / $out['total'], 2) : 0;
54+
$out['without_interests'] = 0;
55+
if ($db->tableExists('crm_intereses_contactos')) {
56+
$out['without_interests'] = (int)$db->select("SELECT COUNT(*) as total
57+
FROM contactos c
58+
WHERE NOT EXISTS (
59+
SELECT 1
60+
FROM crm_intereses_contactos ic
61+
WHERE ic.idcontacto = c.idcontacto
62+
);")[0]['total'];
63+
}
64+
$out['without_interests_perc'] = $out['total'] > 0 ? round(($out['without_interests'] * 100) / $out['total'], 2) : 0;
2865

2966
return $out;
3067
}
@@ -89,10 +126,14 @@ public static function sourcesAnalysis(): array
89126
$result = [];
90127

91128
foreach ($periods as $days) {
92-
$sql = "SELECT f.id, f.nombre, COUNT(c.idcontacto) AS total
93-
FROM crm_fuentes2 f
94-
LEFT JOIN contactos c ON c.idfuente = f.id AND c.fechaalta >= DATE_SUB(CURDATE(), INTERVAL " . (int)$days . " DAY)
95-
GROUP BY f.id, f.nombre ORDER BY total DESC;";
129+
$sql = "SELECT COALESCE(f.id, 0) AS id,
130+
COALESCE(NULLIF(f.nombre, ''), " . $db->var2str(Tools::trans('no-data')) . ") AS nombre,
131+
COUNT(c.idcontacto) AS total
132+
FROM contactos c
133+
LEFT JOIN crm_fuentes2 f ON c.idfuente = f.id
134+
WHERE c.fechaalta >= DATE_SUB(CURDATE(), INTERVAL " . (int)$days . " DAY)
135+
GROUP BY COALESCE(f.id, 0), COALESCE(NULLIF(f.nombre, ''), " . $db->var2str(Tools::trans('no-data')) . ")
136+
ORDER BY total DESC;";
96137
$result[$days] = $db->select($sql);
97138
}
98139

@@ -110,10 +151,15 @@ public static function interestsAnalysis(): array
110151
$result = [];
111152

112153
foreach ($periods as $days) {
113-
$sql = "SELECT i.id, i.nombre, COUNT(ic.id) AS total
114-
FROM crm_intereses i
115-
LEFT JOIN crm_intereses_contactos ic ON ic.idinteres = i.id AND ic.fecha >= DATE_SUB(CURDATE(), INTERVAL " . (int)$days . " DAY)
116-
GROUP BY i.id, i.nombre ORDER BY total DESC;";
154+
$sql = "SELECT COALESCE(i.id, 0) AS id,
155+
COALESCE(NULLIF(i.nombre, ''), " . $db->var2str(Tools::trans('no-data')) . ") AS nombre,
156+
COUNT(c.idcontacto) AS total
157+
FROM contactos c
158+
LEFT JOIN crm_intereses_contactos ic ON ic.idcontacto = c.idcontacto
159+
LEFT JOIN crm_intereses i ON ic.idinteres = i.id
160+
WHERE c.fechaalta >= DATE_SUB(CURDATE(), INTERVAL " . (int)$days . " DAY)
161+
GROUP BY COALESCE(i.id, 0), COALESCE(NULLIF(i.nombre, ''), " . $db->var2str(Tools::trans('no-data')) . ")
162+
ORDER BY total DESC;";
117163
$result[$days] = $db->select($sql);
118164
}
119165

View/ReportClients.html.twig

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Author Abderrahim Darghal Belkacemi
44
#}
55

6-
{% extends "Master/MenuTemplate.html.twig" %}
6+
{% extends "Master/MenuBgTemplate.html.twig" %}
77
{% import "Macro/Forms.html.twig" as forms %}
88

99
{% block body %}
@@ -32,7 +32,7 @@
3232

3333
{# 1. KPIs #}
3434
<div class="row">
35-
<div class="col-xl-3 col-md-6 mb-4">
35+
<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
3636
<div class="card border-left-primary shadow h-100 py-2">
3737
<div class="card-body">
3838
<div class="row no-gutters align-items-center">
@@ -47,7 +47,7 @@
4747
</div>
4848
</div>
4949
</div>
50-
<div class="col-xl-3 col-md-6 mb-4">
50+
<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
5151
<div class="card border-left-success shadow h-100 py-2">
5252
<div class="card-body">
5353
<div class="row no-gutters align-items-center">
@@ -62,7 +62,22 @@
6262
</div>
6363
</div>
6464
</div>
65-
<div class="col-xl-3 col-md-6 mb-4">
65+
<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
66+
<div class="card border-left-info shadow h-100 py-2">
67+
<div class="card-body">
68+
<div class="row no-gutters align-items-center">
69+
<div class="col mr-2">
70+
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">{{ trans('active-customers-year') }}</div>
71+
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ fsc.activeCustomersYear }}</div>
72+
</div>
73+
<div class="col-auto">
74+
<i class="fas fa-file-invoice-dollar fa-2x text-gray-300"></i>
75+
</div>
76+
</div>
77+
</div>
78+
</div>
79+
</div>
80+
<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
6681
<div class="card border-left-secondary shadow h-100 py-2">
6782
<div class="card-body">
6883
<div class="row no-gutters align-items-center">
@@ -77,16 +92,31 @@
7792
</div>
7893
</div>
7994
</div>
80-
<div class="col-xl-3 col-md-6 mb-4">
81-
<div class="card border-left-info shadow h-100 py-2">
95+
<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
96+
<div class="card border-left-warning shadow h-100 py-2">
8297
<div class="card-body">
8398
<div class="row no-gutters align-items-center">
8499
<div class="col mr-2">
85-
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">{{ trans('active-customers-year') }}</div>
86-
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ fsc.activeCustomersYear }}</div>
100+
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Nuevos 30 días</div>
101+
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ fsc.newCustomers30Days }}</div>
87102
</div>
88103
<div class="col-auto">
89-
<i class="fas fa-file-invoice-dollar fa-2x text-gray-300"></i>
104+
<i class="fas fa-user-plus fa-2x text-gray-300"></i>
105+
</div>
106+
</div>
107+
</div>
108+
</div>
109+
</div>
110+
<div class="col-xl-2 col-md-4 col-sm-6 mb-4">
111+
<div class="card border-left-danger shadow h-100 py-2">
112+
<div class="card-body">
113+
<div class="row no-gutters align-items-center">
114+
<div class="col mr-2">
115+
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Con deuda</div>
116+
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ fsc.customersWithDebt }}</div>
117+
</div>
118+
<div class="col-auto">
119+
<i class="fas fa-triangle-exclamation fa-2x text-gray-300"></i>
90120
</div>
91121
</div>
92122
</div>

0 commit comments

Comments
 (0)