Add Account statistics

This commit is contained in:
Timothée Jaussoin 2023-10-04 08:24:47 +00:00
parent e516ae788c
commit 7feb7fd184
25 changed files with 864 additions and 366 deletions

View file

@ -23,17 +23,20 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Carbon\Carbon;
use Awobaz\Compoships\Compoships;
use App\ApiKey;
use App\Password;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
class Account extends Authenticatable
{
use HasFactory;
use Compoships;
protected $with = ['passwords', 'admin', 'alias', 'activationExpiration', 'emailChangeCode', 'types', 'actions'];
protected $hidden = ['alias', 'expire_time', 'confirmation_key', 'provisioning_token', 'pivot'];
@ -180,6 +183,26 @@ class Account extends Authenticatable
return $this->belongsToMany(AccountType::class);
}
public function statisticsFromCalls()
{
return $this->hasMany(StatisticsCall::class, ['from_username', 'from_domain'], ['username', 'domain']);
}
public function statisticsToCalls()
{
return $this->hasMany(StatisticsCall::class, ['to_username', 'to_domain'], ['username', 'domain']);
}
public function statisticsFromMessages()
{
return $this->hasMany(StatisticsMessage::class, ['from_username', 'from_domain'], ['username', 'domain']);
}
public function statisticsToMessageDevices()
{
return $this->hasMany(StatisticsMessageDevice::class, ['to_username', 'to_domain'], ['username', 'domain']);
}
/**
* Attributes
*/

View file

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Account;
use App\Http\Controllers\Controller;
use App\Libraries\StatisticsGraphFactory;
use Illuminate\Http\Request;
class AccountStatisticsController extends Controller
{
public function edit(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
return redirect()->route('admin.account.statistics.show', [
'account' => $account,
'from' => $request->get('from'),
'to' => $request->get('to'),
'by' => $request->get('by'),
]);
}
public function show(Request $request, int $accountId)
{
$request->validate([
'from' => 'date_format:Y-m-d|before:to',
'to' => 'date_format:Y-m-d|after:from',
'by' => 'in:day,week,month,year',
]);
$account = Account::findOrFail($accountId);
$messagesFromGraph = view('parts.graph', [
'jsonConfig' => json_encode((new StatisticsGraphFactory($request, 'messages', fromUsername: $account->username, fromDomain: $account->domain))->getConfig()),
'request' => $request
])->render();
$messagesToGraph = view('parts.graph', [
'jsonConfig' => json_encode((new StatisticsGraphFactory($request, 'messages', toUsername: $account->username, toDomain: $account->domain))->getConfig()),
'request' => $request
])->render();
$callsFromGraph = view('parts.graph', [
'jsonConfig' => json_encode((new StatisticsGraphFactory($request, 'calls', fromUsername: $account->username, fromDomain: $account->domain))->getConfig()),
'request' => $request
])->render();
$callsToGraph = view('parts.graph', [
'jsonConfig' => json_encode((new StatisticsGraphFactory($request, 'calls', toUsername: $account->username, toDomain: $account->domain))->getConfig()),
'request' => $request
])->render();
return view('admin.account.statistics.show', [
'account' => $account,
'messagesFromGraph' => $messagesFromGraph,
'messagesToGraph' => $messagesToGraph,
'callsFromGraph' => $callsFromGraph,
'callsToGraph' => $callsToGraph,
]);
}
}

View file

@ -3,16 +3,13 @@
namespace App\Http\Controllers\Admin;
use App\Account;
use App\ContactsList;
use App\StatisticsMessage;
use App\StatisticsCall;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Libraries\StatisticsGraphFactory;
use Carbon\Carbon;
use Carbon\CarbonInterval;
use Carbon\CarbonPeriod;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
class StatisticsController extends Controller
{
@ -27,12 +24,19 @@ class StatisticsController extends Controller
{
return redirect()->route('admin.statistics.show', [
'from' => $request->get('from'),
'type' => $request->get('type'),
'to' => $request->get('to'),
'by' => $request->get('by'),
'type' => $request->get('type'),
'domain' => $request->get('domain'),
'contacts_list' => $request->get('contacts_list'),
]);
}
/*public function search(Request $request)
{
return redirect()->route('admin.statistics.search', $request->except('_token', 'query'));
}*/
public function show(Request $request, string $type = 'messages')
{
$request->validate([
@ -41,186 +45,36 @@ class StatisticsController extends Controller
'by' => 'in:day,week,month,year',
]);
$dateColumn = 'created_at';
$label = 'Label';
switch ($type) {
case 'messages':
$dateColumn = 'sent_at';
$label = 'Messages';
$data = StatisticsMessage::orderBy($dateColumn, 'asc');
break;
case 'calls':
$dateColumn = 'initiated_at';
$label = 'Calls';
$data = StatisticsCall::orderBy($dateColumn, 'asc');
break;
case 'accounts':
$label = 'Accounts';
$data = Account::orderBy($dateColumn, 'asc');
break;
}
$data = $data->groupBy('moment')
->orderBy('moment', 'desc')
->setEagerLoads([]);
if ($request->get('to')) {
$data = $data->where($dateColumn, '<=', $request->get('to'));
}
$by = $request->get('by', 'day');
switch ($by) {
case 'day':
$data = $data->where($dateColumn, '>=', $request->get('from', Carbon::now()->subDay()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m-%d %H') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
case 'week':
$data = $data->where($dateColumn, '>=', $request->get('from', Carbon::now()->subWeek()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
case 'month':
$data = $data->where($dateColumn, '>=', $request->get('from', Carbon::now()->subMonth()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
case 'year':
$data = $data->where($dateColumn, '>=', $request->get('from', Carbon::now()->subYear()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
}
$data = $data->each->setAppends([])->pluck('count', 'moment');
$data = $this->compileStatistics(
$by,
$request->get('from'),
$request->get('to'),
$data
);
$graph = new StatisticsGraphFactory($request, $type, fromDomain: $request->get('domain'));
$config = $graph->getConfig();
$domains = collect();
if ($request->get('export', false)) {
$file = fopen('php://output', 'w');
$callback = function () use ($data, $file) {
foreach ($data as $key => $value) {
fputcsv($file, [$key, $value]);
}
fclose($file);
};
return response()->stream($callback, 200, [
"Content-type" => "text/csv",
"Content-Disposition" => "attachment; filename=export.csv",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
]);
return $graph->export();
}
$config = [
'type' => 'bar',
'data' => [
'labels' => $data->keys()->toArray(),
'datasets' => [[
'label' => $label,
'borderColor' => 'rgba(108, 122, 135, 1)',
'backgroundColor' => 'rgba(108, 122, 135, 1)',
'data' => $data->values()->toArray(),
'order' => 1
]]
],
'options' => [
'maintainAspectRatio' => false,
'spanGaps' => true,
'legend' => [
'position' => 'right'
],
'scales' => [
'y' => [
'stacked' => true,
'title' => [
'display' => true,
'text' => $label
]
],
'x' => [
'stacked' => true,
]
],
'interaction' => [
'mode' => 'nearest',
'axis' => 'x',
'intersect' => false
],
]
];
if (config('app.admins_manage_multi_domains')) {
switch ($type) {
case 'messages':
$domains = StatisticsMessage::groupBy('from_domain')->pluck('from_domain');
break;
case 'calls':
$domains = StatisticsCall::groupBy('from_domain')->pluck('from_domain');
break;
case 'accounts':
$domains = Account::groupBy('domain')->pluck('domain');
break;
}
}
return view('admin.statistics.show', [
'domains' => $domains,
'contacts_lists' => ContactsList::all()->pluck('title', 'id'),
'jsonConfig' => json_encode($config),
'by' => $by,
'type' => $type,
'request' => $request
]);
}
private static function compileStatistics(string $by, $from, $to, $data): Collection
{
$stats = [];
switch ($by) {
case 'day':
$period = collect(CarbonInterval::hour()->toPeriod(
$from ?? Carbon::now()->subDay()->format('Y-m-d H:i:s'),
$to ?? Carbon::now()->format('Y-m-d H:i:s')
))->map->format('Y-m-d H');
break;
case 'week':
$period = collect(CarbonPeriod::create(
$from ?? Carbon::now()->subWeek(),
$to ?? Carbon::now()
))->map->format('Y-m-d');
break;
case 'month':
$period = collect(
CarbonPeriod::create(
$from ?? Carbon::now()->subMonth(),
$to ?? Carbon::now()
)
)->map->format('Y-m-d');
break;
case 'year':
$period = collect(
CarbonPeriod::create(
$from ?? Carbon::now()->subYear(),
$to ?? Carbon::now()
)
)->map->format('Y-m');
break;
}
foreach ($period as $moment) {
$stats[$moment] = $data[$moment] ?? 0;
}
return collect($stats);
}
}

View file

@ -0,0 +1,282 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Libraries;
use App\Account;
use App\ContactsList;
use App\StatisticsCall;
use App\StatisticsMessage;
use Carbon\Carbon;
use Carbon\CarbonInterval;
use Carbon\CarbonPeriod;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class StatisticsGraphFactory
{
private $data = null;
public function __construct(
private Request $request,
private string $type = 'messages',
public ?string $fromUsername = null,
public ?string $fromDomain = null,
public ?string $toUsername = null,
public ?string $toDomain = null
) {
}
public function getConfig()
{
$dateColumn = 'created_at';
$label = 'Label';
switch ($this->type) {
case 'messages':
$dateColumn = 'sent_at';
$label = 'Messages';
$this->data = StatisticsMessage::orderBy($dateColumn, 'asc');
if (!config('app.admins_manage_multi_domains')) {
$this->data->where('from_domain', config('app.sip_domain'));
} elseif ($this->fromDomain) {
$this->data->where('from_domain', $this->fromDomain)->orderBy('from_domain');
if ($this->fromUsername) {
$this->data->where('from_username', $this->fromUsername);
}
} elseif ($this->toDomain && $this->toUsername) {
$this->data->whereIn('id', function ($query) {
$query->select('message_id')
->from('statistics_message_devices')
->where('to_username', $this->toUsername)
->where('to_domain', $this->toDomain);
});
}
break;
case 'calls':
$dateColumn = 'initiated_at';
$label = 'Calls';
$this->data = StatisticsCall::orderBy($dateColumn, 'asc');
if (!config('app.admins_manage_multi_domains')) {
$this->data->where('from_domain', config('app.sip_domain'));
} elseif ($this->fromDomain) {
$this->data->where('from_domain', $this->fromDomain)->orderBy('from_domain');
if ($this->fromUsername) {
$this->data->where('from_username', $this->fromUsername);
}
} elseif ($this->toDomain) {
$this->data->where('to_domain', $this->toDomain)->orderBy('to_domain');
if ($this->toUsername) {
$this->data->where('to_username', $this->toUsername);
}
}
break;
case 'accounts':
$label = 'Accounts';
$this->data = Account::orderBy($dateColumn, 'asc');
if (!config('app.admins_manage_multi_domains')) {
$this->data->where('domain', config('app.sip_domain'));
} elseif ($this->fromDomain) {
$this->data->where('domain', $this->fromDomain)->orderBy('domain');
if ($this->fromUsername) {
$this->data->where('username', $this->fromUsername);
}
}
if ($this->request->has('contacts_list')) {
$this->data->whereIn('id', function ($query) {
$query->select('contact_id')
->from('contacts_list_contact')
->where('contacts_list_id', $this->request->get('contacts_list'));
});
}
break;
}
$this->data = $this->data->groupBy('moment')
->orderBy('moment', 'desc')
->setEagerLoads([]);
if ($this->request->get('to')) {
$this->data = $this->data->where($dateColumn, '<=', $this->request->get('to'));
}
$by = $this->request->get('by', 'day');
switch ($by) {
case 'day':
$this->data = $this->data->where($dateColumn, '>=', $this->request->get('from', Carbon::now()->subDay()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m-%d %H') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
case 'week':
$this->data = $this->data->where($dateColumn, '>=', $this->request->get('from', Carbon::now()->subWeek()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
case 'month':
$this->data = $this->data->where($dateColumn, '>=', $this->request->get('from', Carbon::now()->subMonth()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
case 'year':
$this->data = $this->data->where($dateColumn, '>=', $this->request->get('from', Carbon::now()->subYear()->format('Y-m-d H:i:s')))
->get([
DB::raw("date_format(" . $dateColumn . ",'%Y-%m') as moment"),
DB::raw('COUNT(*) as "count"')
]);
break;
}
$this->data = $this->data->each->setAppends([])->pluck('count', 'moment');
$this->data = $this->compileStatistics(
$by,
$this->request->get('from'),
$this->request->get('to'),
$this->data
);
return [
'type' => 'bar',
'data' => [
'labels' => $this->data->keys()->toArray(),
'datasets' => [[
'label' => $label,
'borderColor' => 'rgba(108, 122, 135, 1)',
'backgroundColor' => 'rgba(108, 122, 135, 1)',
'data' => $this->data->values()->toArray(),
'order' => 1
]]
],
'options' => [
'maintainAspectRatio' => false,
'spanGaps' => true,
'legend' => [
'position' => 'right'
],
'scales' => [
'y' => [
'stacked' => true,
'title' => [
'display' => true,
'text' => $label
]
],
'x' => [
'stacked' => true,
]
],
'interaction' => [
'mode' => 'nearest',
'axis' => 'x',
'intersect' => false
],
]
];
}
public function export()
{
$file = fopen('php://output', 'w');
if ($this->data == null) {
$this->getConfig();
}
$callback = function () use ($file) {
foreach ($this->data as $key => $value) {
fputcsv($file, [$key, $value]);
}
fclose($file);
};
return response()->stream($callback, 200, [
"Content-type" => "text/csv",
"Content-Disposition" => "attachment; filename=export.csv",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
]);
}
private function compileStatistics(string $by, $from, $to, $data): Collection
{
$stats = [];
switch ($by) {
case 'day':
$period = collect(CarbonInterval::hour()->toPeriod(
$from ?? Carbon::now()->subDay()->format('Y-m-d H:i:s'),
$to ?? Carbon::now()->format('Y-m-d H:i:s')
))->map->format('Y-m-d H');
break;
case 'week':
$period = collect(CarbonPeriod::create(
$from ?? Carbon::now()->subWeek(),
$to ?? Carbon::now()
))->map->format('Y-m-d');
break;
case 'month':
$period = collect(
CarbonPeriod::create(
$from ?? Carbon::now()->subMonth(),
$to ?? Carbon::now()
)
)->map->format('Y-m-d');
break;
case 'year':
$period = collect(
CarbonPeriod::create(
$from ?? Carbon::now()->subYear(),
$to ?? Carbon::now()
)
)->map->format('Y-m');
break;
}
foreach ($period as $moment) {
$stats[$moment] = $data[$moment] ?? 0;
}
return collect($stats);
}
}

View file

@ -22,11 +22,24 @@ namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Awobaz\Compoships\Compoships;
class StatisticsCall extends Model
{
use HasFactory;
use Compoships;
public $incrementing = false;
protected $casts = ['initiated_at' => 'datetime', 'ended_at' => 'datetime'];
protected $keyType = 'string';
public function accountFrom()
{
return $this->belongsTo(Account::class, ['username', 'domain'], ['to_username', 'to_domain']);
}
public function accountTo()
{
return $this->belongsTo(Account::class, ['username', 'domain'], ['to_username', 'to_domain']);
}
}

View file

@ -20,13 +20,21 @@
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Awobaz\Compoships\Compoships;
use Awobaz\Compoships\Database\Eloquent\Model;
class StatisticsMessage extends Model
{
use HasFactory;
use Compoships;
public $incrementing = false;
protected $casts = ['sent_at' => 'datetime'];
protected $keyType = 'string';
public function accountFrom()
{
return $this->belongsTo(Account::class, ['username', 'domain'], ['to_username', 'to_domain']);
}
}

View file

@ -20,7 +20,8 @@
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Awobaz\Compoships\Database\Eloquent\Model;
class StatisticsMessageDevice extends Model
{
@ -33,4 +34,9 @@ class StatisticsMessageDevice extends Model
{
return $this->hasOne(StatisticsMessage::class, 'id', 'message_id');
}
public function accountTo()
{
return $this->belongsTo(Account::class, ['username', 'domain'], ['to_username', 'to_domain']);
}
}

View file

@ -10,6 +10,7 @@
"require": {
"php": ">=8.0.2",
"anhskohbo/no-captcha": "^3.5",
"awobaz/compoships": "^2.2",
"doctrine/dbal": "^3.6.6",
"endroid/qr-code": "^4.8.2",
"fakerphp/faker": "^1.23",

236
flexiapi/composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1c0a59db2fada52615ad6f31e0599291",
"content-hash": "b6a450b1e7adf4c259121350856f83a6",
"packages": [
{
"name": "anhskohbo/no-captcha",
@ -70,6 +70,68 @@
},
"time": "2023-02-15T16:07:08+00:00"
},
{
"name": "awobaz/compoships",
"version": "2.2.3",
"source": {
"type": "git",
"url": "https://github.com/topclaudy/compoships.git",
"reference": "404901e2ebd6794f70d2710a56edd4b0c500ce1f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/topclaudy/compoships/zipball/404901e2ebd6794f70d2710a56edd4b0c500ce1f",
"reference": "404901e2ebd6794f70d2710a56edd4b0c500ce1f",
"shasum": ""
},
"require": {
"fakerphp/faker": "^1.18",
"illuminate/database": ">=5.6 <11.0"
},
"require-dev": {
"ext-sqlite3": "*",
"phpunit/phpunit": "^6.0|^8.0|^9.0"
},
"suggest": {
"awobaz/blade-active": "Blade directives for the Laravel 'Active' package",
"awobaz/eloquent-auto-append": "Automatically append accessors to model serialization",
"awobaz/eloquent-mutators": "Reusable mutators (getters/setters) for Laravel 5's Eloquent",
"awobaz/syntactic": "Syntactic sugar for named and indexed parameters call."
},
"type": "library",
"autoload": {
"psr-4": {
"Awobaz\\Compoships\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Claudin J. Daniel",
"email": "cdaniel@awobaz.com"
}
],
"description": "Laravel relationships with support for composite/multiple keys",
"keywords": [
"laravel",
"laravel composite keys",
"laravel relationships"
],
"support": {
"issues": "https://github.com/topclaudy/compoships/issues",
"source": "https://github.com/topclaudy/compoships/tree/2.2.3"
},
"funding": [
{
"url": "https://paypal.me/awobaz",
"type": "custom"
}
],
"time": "2023-02-22T16:52:55+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
@ -399,16 +461,16 @@
},
{
"name": "doctrine/dbal",
"version": "3.6.6",
"version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "63646ffd71d1676d2f747f871be31b7e921c7864"
"reference": "00d03067f07482f025d41ab55e4ba0db5eca2cdf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/63646ffd71d1676d2f747f871be31b7e921c7864",
"reference": "63646ffd71d1676d2f747f871be31b7e921c7864",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/00d03067f07482f025d41ab55e4ba0db5eca2cdf",
"reference": "00d03067f07482f025d41ab55e4ba0db5eca2cdf",
"shasum": ""
},
"require": {
@ -424,9 +486,9 @@
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "1.10.29",
"phpstan/phpstan": "1.10.35",
"phpstan/phpstan-strict-rules": "^1.5",
"phpunit/phpunit": "9.6.9",
"phpunit/phpunit": "9.6.13",
"psalm/plugin-phpunit": "0.18.4",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.7.2",
@ -492,7 +554,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.6.6"
"source": "https://github.com/doctrine/dbal/tree/3.7.0"
},
"funding": [
{
@ -508,20 +570,20 @@
"type": "tidelift"
}
],
"time": "2023-08-17T05:38:17+00:00"
"time": "2023-09-26T20:56:55+00:00"
},
{
"name": "doctrine/deprecations",
"version": "v1.1.1",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3"
"reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
"reference": "612a3ee5ab0d5dd97b7cf3874a6efe24325efac3",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
"reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931",
"shasum": ""
},
"require": {
@ -553,9 +615,9 @@
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/v1.1.1"
"source": "https://github.com/doctrine/deprecations/tree/1.1.2"
},
"time": "2023-06-03T09:27:29+00:00"
"time": "2023-09-27T20:04:15+00:00"
},
{
"name": "doctrine/event-manager",
@ -2054,16 +2116,16 @@
},
{
"name": "league/commonmark",
"version": "2.4.0",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048"
"reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
"reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
"reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
"shasum": ""
},
"require": {
@ -2156,7 +2218,7 @@
"type": "tidelift"
}
],
"time": "2023-03-24T15:16:10+00:00"
"time": "2023-08-30T16:55:00+00:00"
},
{
"name": "league/config",
@ -2242,16 +2304,16 @@
},
{
"name": "league/flysystem",
"version": "3.15.1",
"version": "3.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "a141d430414fcb8bf797a18716b09f759a385bed"
"reference": "4fdf372ca6b63c6e281b1c01a624349ccb757729"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a141d430414fcb8bf797a18716b09f759a385bed",
"reference": "a141d430414fcb8bf797a18716b09f759a385bed",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4fdf372ca6b63c6e281b1c01a624349ccb757729",
"reference": "4fdf372ca6b63c6e281b1c01a624349ccb757729",
"shasum": ""
},
"require": {
@ -2260,6 +2322,8 @@
"php": "^8.0.2"
},
"conflict": {
"async-aws/core": "<1.19.0",
"async-aws/s3": "<1.14.0",
"aws/aws-sdk-php": "3.209.31 || 3.210.0",
"guzzlehttp/guzzle": "<7.0",
"guzzlehttp/ringphp": "<1.1.1",
@ -2279,7 +2343,7 @@
"microsoft/azure-storage-blob": "^1.1",
"phpseclib/phpseclib": "^3.0.14",
"phpstan/phpstan": "^0.12.26",
"phpunit/phpunit": "^9.5.11",
"phpunit/phpunit": "^9.5.11|^10.0",
"sabre/dav": "^4.3.1"
},
"type": "library",
@ -2314,7 +2378,7 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.15.1"
"source": "https://github.com/thephpleague/flysystem/tree/3.16.0"
},
"funding": [
{
@ -2326,20 +2390,20 @@
"type": "github"
}
],
"time": "2023-05-04T09:04:26+00:00"
"time": "2023-09-07T19:22:17+00:00"
},
{
"name": "league/flysystem-local",
"version": "3.15.0",
"version": "3.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
"reference": "543f64c397fefdf9cfeac443ffb6beff602796b3"
"reference": "ec7383f25642e6fd4bb0c9554fc2311245391781"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/543f64c397fefdf9cfeac443ffb6beff602796b3",
"reference": "543f64c397fefdf9cfeac443ffb6beff602796b3",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ec7383f25642e6fd4bb0c9554fc2311245391781",
"reference": "ec7383f25642e6fd4bb0c9554fc2311245391781",
"shasum": ""
},
"require": {
@ -2374,7 +2438,7 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-local/issues",
"source": "https://github.com/thephpleague/flysystem-local/tree/3.15.0"
"source": "https://github.com/thephpleague/flysystem-local/tree/3.16.0"
},
"funding": [
{
@ -2386,7 +2450,7 @@
"type": "github"
}
],
"time": "2023-05-02T20:02:14+00:00"
"time": "2023-08-30T10:23:59+00:00"
},
{
"name": "league/mime-type-detection",
@ -2668,16 +2732,16 @@
},
{
"name": "nesbot/carbon",
"version": "2.69.0",
"version": "2.71.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "4308217830e4ca445583a37d1bf4aff4153fa81c"
"reference": "98276233188583f2ff845a0f992a235472d9466a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4308217830e4ca445583a37d1bf4aff4153fa81c",
"reference": "4308217830e4ca445583a37d1bf4aff4153fa81c",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/98276233188583f2ff845a0f992a235472d9466a",
"reference": "98276233188583f2ff845a0f992a235472d9466a",
"shasum": ""
},
"require": {
@ -2770,7 +2834,7 @@
"type": "tidelift"
}
],
"time": "2023-08-03T09:00:52+00:00"
"time": "2023-09-25T11:31:05+00:00"
},
{
"name": "nette/schema",
@ -2836,16 +2900,16 @@
},
{
"name": "nette/utils",
"version": "v4.0.1",
"version": "v4.0.2",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "9124157137da01b1f5a5a22d6486cb975f26db7e"
"reference": "cead6637226456b35e1175cc53797dd585d85545"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/9124157137da01b1f5a5a22d6486cb975f26db7e",
"reference": "9124157137da01b1f5a5a22d6486cb975f26db7e",
"url": "https://api.github.com/repos/nette/utils/zipball/cead6637226456b35e1175cc53797dd585d85545",
"reference": "cead6637226456b35e1175cc53797dd585d85545",
"shasum": ""
},
"require": {
@ -2867,8 +2931,7 @@
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
"ext-xml": "to use Strings::length() etc. when mbstring is not available"
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"type": "library",
"extra": {
@ -2917,9 +2980,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.1"
"source": "https://github.com/nette/utils/tree/v4.0.2"
},
"time": "2023-07-30T15:42:21+00:00"
"time": "2023-09-19T11:58:07+00:00"
},
{
"name": "nikic/php-parser",
@ -3362,16 +3425,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.27",
"version": "9.2.29",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1"
"reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1",
"reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76",
"reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76",
"shasum": ""
},
"require": {
@ -3428,7 +3491,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29"
},
"funding": [
{
@ -3436,7 +3499,7 @@
"type": "github"
}
],
"time": "2023-07-26T13:44:30+00:00"
"time": "2023-09-19T04:57:46+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -3681,16 +3744,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.11",
"version": "9.6.13",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "810500e92855eba8a7a5319ae913be2da6f957b0"
"reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0",
"reference": "810500e92855eba8a7a5319ae913be2da6f957b0",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be",
"reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be",
"shasum": ""
},
"require": {
@ -3705,7 +3768,7 @@
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"php": ">=7.3",
"phpunit/php-code-coverage": "^9.2.13",
"phpunit/php-code-coverage": "^9.2.28",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-invoker": "^3.1.1",
"phpunit/php-text-template": "^2.0.3",
@ -3764,7 +3827,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13"
},
"funding": [
{
@ -3780,7 +3843,7 @@
"type": "tidelift"
}
],
"time": "2023-08-19T07:10:56+00:00"
"time": "2023-09-19T05:39:22+00:00"
},
{
"name": "psr/cache",
@ -3984,16 +4047,16 @@
},
{
"name": "psr/http-client",
"version": "1.0.2",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": ""
},
"require": {
@ -4030,9 +4093,9 @@
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client/tree/1.0.2"
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-04-10T20:12:12+00:00"
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-factory",
@ -8920,16 +8983,16 @@
},
{
"name": "maximebf/debugbar",
"version": "v1.18.2",
"version": "v1.19.0",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
"reference": "17dcf3f6ed112bb85a37cf13538fd8de49f5c274"
"reference": "30f65f18f7ac086255a77a079f8e0dcdd35e828e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/17dcf3f6ed112bb85a37cf13538fd8de49f5c274",
"reference": "17dcf3f6ed112bb85a37cf13538fd8de49f5c274",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/30f65f18f7ac086255a77a079f8e0dcdd35e828e",
"reference": "30f65f18f7ac086255a77a079f8e0dcdd35e828e",
"shasum": ""
},
"require": {
@ -8980,9 +9043,9 @@
],
"support": {
"issues": "https://github.com/maximebf/php-debugbar/issues",
"source": "https://github.com/maximebf/php-debugbar/tree/v1.18.2"
"source": "https://github.com/maximebf/php-debugbar/tree/v1.19.0"
},
"time": "2023-02-04T15:27:00+00:00"
"time": "2023-09-19T19:53:10+00:00"
},
{
"name": "mockery/mockery",
@ -9159,16 +9222,16 @@
},
{
"name": "pdepend/pdepend",
"version": "2.14.0",
"version": "2.15.1",
"source": {
"type": "git",
"url": "https://github.com/pdepend/pdepend.git",
"reference": "1121d4b04af06e33e9659bac3a6741b91cab1de1"
"reference": "d12f25bcdfb7754bea458a4a5cb159d55e9950d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pdepend/pdepend/zipball/1121d4b04af06e33e9659bac3a6741b91cab1de1",
"reference": "1121d4b04af06e33e9659bac3a6741b91cab1de1",
"url": "https://api.github.com/repos/pdepend/pdepend/zipball/d12f25bcdfb7754bea458a4a5cb159d55e9950d0",
"reference": "d12f25bcdfb7754bea458a4a5cb159d55e9950d0",
"shasum": ""
},
"require": {
@ -9210,7 +9273,7 @@
],
"support": {
"issues": "https://github.com/pdepend/pdepend/issues",
"source": "https://github.com/pdepend/pdepend/tree/2.14.0"
"source": "https://github.com/pdepend/pdepend/tree/2.15.1"
},
"funding": [
{
@ -9218,26 +9281,26 @@
"type": "tidelift"
}
],
"time": "2023-05-26T13:15:18+00:00"
"time": "2023-09-28T12:00:56+00:00"
},
{
"name": "phpmd/phpmd",
"version": "2.13.0",
"version": "2.14.1",
"source": {
"type": "git",
"url": "https://github.com/phpmd/phpmd.git",
"reference": "dad0228156856b3ad959992f9748514fa943f3e3"
"reference": "442fc2c34edcd5198b442d8647c7f0aec3afabe8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpmd/phpmd/zipball/dad0228156856b3ad959992f9748514fa943f3e3",
"reference": "dad0228156856b3ad959992f9748514fa943f3e3",
"url": "https://api.github.com/repos/phpmd/phpmd/zipball/442fc2c34edcd5198b442d8647c7f0aec3afabe8",
"reference": "442fc2c34edcd5198b442d8647c7f0aec3afabe8",
"shasum": ""
},
"require": {
"composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0",
"ext-xml": "*",
"pdepend/pdepend": "^2.12.1",
"pdepend/pdepend": "^2.15.1",
"php": ">=5.3.9"
},
"require-dev": {
@ -9247,7 +9310,7 @@
"gregwar/rst": "^1.0",
"mikey179/vfsstream": "^1.6.8",
"phpunit/phpunit": "^4.8.36 || ^5.7.27",
"squizlabs/php_codesniffer": "^2.0"
"squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2"
},
"bin": [
"src/bin/phpmd"
@ -9284,6 +9347,7 @@
"description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.",
"homepage": "https://phpmd.org/",
"keywords": [
"dev",
"mess detection",
"mess detector",
"pdepend",
@ -9293,7 +9357,7 @@
"support": {
"irc": "irc://irc.freenode.org/phpmd",
"issues": "https://github.com/phpmd/phpmd/issues",
"source": "https://github.com/phpmd/phpmd/tree/2.13.0"
"source": "https://github.com/phpmd/phpmd/tree/2.14.1"
},
"funding": [
{
@ -9301,7 +9365,7 @@
"type": "tidelift"
}
],
"time": "2022-09-10T08:44:15+00:00"
"time": "2023-09-28T13:07:44+00:00"
},
{
"name": "squizlabs/php_codesniffer",
@ -9602,5 +9666,5 @@
"platform-overrides": {
"php": "8.0.2"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View file

@ -19,14 +19,17 @@
namespace Database\Factories;
use App\Account;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
use Awobaz\Compoships\Database\Eloquent\Factories\ComposhipsFactory;
use App\Account;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
class AccountFactory extends Factory
{
protected $model = Account::class;
use ComposhipsFactory;
public function definition()
{
@ -39,7 +42,7 @@ class AccountFactory extends Factory
'confirmation_key' => Str::random(WebAuthenticateController::$emailCodeSize),
'provisioning_token' => Str::random(WebAuthenticateController::$emailCodeSize),
'ip_address' => $this->faker->ipv4,
'created_at' => $this->faker->dateTime,
'created_at' => $this->faker->dateTimeBetween('-1 year'),
'dtmf_protocol' => array_rand(Account::$dtmfProtocols),
'activated' => true
];

View file

@ -22,7 +22,6 @@ namespace Database\Factories;
use App\Admin;
use App\Password;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class AdminFactory extends Factory
{

View file

@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\StatisticsMessage;
use Illuminate\Database\Eloquent\Factories\Factory;
use Awobaz\Compoships\Database\Eloquent\Factories\ComposhipsFactory;
class StatisticsMessageDeviceFactory extends Factory
{
use ComposhipsFactory;
public function definition(): array
{
$message = StatisticsMessage::factory()->create();
return [
'message_id' => $message->id,
'to_username' => $this->faker->userName(),
'to_domain' => $this->faker->domainName(),
'device_id' => $this->faker->uuid(),
'received_at' => $this->faker->dateTimeBetween('-1 year'),
'last_status' => 200,
];
}
}

View file

@ -3,9 +3,12 @@
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Awobaz\Compoships\Database\Eloquent\Factories\ComposhipsFactory;
class StatisticsMessageFactory extends Factory
{
use ComposhipsFactory;
public function definition(): array
{
return [

View file

@ -0,0 +1,58 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('statistics_calls', function (Blueprint $table) {
$table->index('from_domain');
$table->index('from_username');
$table->index('to_domain');
$table->index('to_username');
});
Schema::table('statistics_messages', function (Blueprint $table) {
$table->index('from_domain');
$table->index('from_username');
});
Schema::table('statistics_message_devices', function (Blueprint $table) {
$table->index('to_domain');
$table->index('to_username');
});
Schema::table('accounts', function (Blueprint $table) {
$table->index('domain');
$table->index('username');
});
}
public function down()
{
Schema::table('statistics_calls', function (Blueprint $table) {
$table->dropIndex('statistics_calls_from_domain_index');
$table->dropIndex('statistics_calls_from_username_index');
$table->dropIndex('statistics_calls_to_domain_index');
$table->dropIndex('statistics_calls_to_username_index');
});
Schema::table('statistics_messages', function (Blueprint $table) {
$table->dropIndex('statistics_messages_from_domain_index');
$table->dropIndex('statistics_messages_from_username_index');
});
Schema::table('statistics_message_devices', function (Blueprint $table) {
$table->dropIndex('statistics_message_devices_to_domain_index');
$table->dropIndex('statistics_message_devices_to_username_index');
});
Schema::table('accounts', function (Blueprint $table) {
$table->dropIndex('accounts_domain_index');
$table->dropIndex('accounts_username_index');
});
}
};

View file

@ -19,6 +19,7 @@
namespace Database\Seeders;
use App\Account;
use App\StatisticsCall;
use App\StatisticsCallDevice;
use App\StatisticsMessage;
@ -39,12 +40,11 @@ class StatisticsSeeder extends Seeder
StatisticsCall::truncate();
Schema::enableForeignKeyConstraints();
StatisticsMessage::factory()
->count(10000)
->create();
StatisticsCall::factory()
->count(10000)
Account::factory(10)
->hasStatisticsFromMessages(20)
->hasStatisticsToMessageDevices(20)
->hasStatisticsFromCalls(20)
->hasStatisticsToCalls(20)
->create();
}
}

View file

@ -760,7 +760,7 @@ select.list_toggle {
pointer-events: none;
}
#chart {
.chart {
min-height: 80vh;
}

View file

@ -21,8 +21,9 @@
<p title="{{ $account->updated_at }}">Updated on {{ $account->updated_at->format('d/m/Y') }}
@include('parts.tabs', [
'items' => [
route('admin.account.edit', $account->id, ['type' => 'messages']) => 'Information',
route('admin.account.device.index', $account->id, ['type' => 'accounts']) => 'Devices',
route('admin.account.edit', $account->id) => 'Information',
route('admin.account.device.index', $account->id) => 'Devices',
route('admin.account.statistics.show', $account->id) => 'Statistics',
],
])
@else
@ -41,7 +42,8 @@
<h2>Connexion</h2>
<div>
<input placeholder="Username" required="required" name="username" type="text"
value="@if ($account->id){{ $account->username }}@else{{ old('username') }}@endif" @if ($account->id) readonly @endif>
value="@if ($account->id) {{ $account->username }}@else{{ old('username') }} @endif"
@if ($account->id) readonly @endif>
<label for="username">Username</label>
@include('parts.errors', ['name' => 'username'])
</div>
@ -53,32 +55,37 @@
</div>
<div>
<input placeholder="John Doe" name="display_name" type="text" value="@if ($account->id){{ $account->display_name }}@else{{ old('display_name') }}@endif">
<input placeholder="John Doe" name="display_name" type="text"
value="@if ($account->id) {{ $account->display_name }}@else{{ old('display_name') }} @endif">
<label for="display_name">Display Name</label>
@include('parts.errors', ['name' => 'display_name'])
</div>
<div></div>
<div>
<input placeholder="Password" name="password" type="password" value="" autocomplete="new-password" @if (!$account->id)required @endif>
<input placeholder="Password" name="password" type="password" value="" autocomplete="new-password"
@if (!$account->id) required @endif>
<label for="password">{{ $account->id ? 'Password (fill to change)' : 'Password' }}</label>
@include('parts.errors', ['name' => 'password'])
</div>
<div>
<input placeholder="Password" name="password_confirmation" type="password" value="" autocomplete="off" @if (!$account->id)required @endif>
<input placeholder="Password" name="password_confirmation" type="password" value="" autocomplete="off"
@if (!$account->id) required @endif>
<label for="password_confirmation">Confirm password</label>
@include('parts.errors', ['name' => 'password_confirmation'])
</div>
<div>
<input placeholder="Email" name="email" type="email" value="@if ($account->id){{ $account->email }}@else{{ old('email') }}@endif">
<input placeholder="Email" name="email" type="email"
value="@if ($account->id) {{ $account->email }}@else{{ old('email') }} @endif">
<label for="email">Email</label>
@include('parts.errors', ['name' => 'email'])
</div>
<div>
<input placeholder="+12123123" name="phone" type="text" value="@if ($account->id){{ $account->phone }}@else{{ old('phone') }}@endif">
<input placeholder="+12123123" name="phone" type="text"
value="@if ($account->id) {{ $account->phone }}@else{{ old('phone') }} @endif">
<label for="phone">Phone</label>
@include('parts.errors', ['name' => 'phone'])
</div>

View file

@ -19,8 +19,9 @@
@include('parts.tabs', [
'items' => [
route('admin.account.edit', $account->id, ['type' => 'messages']) => 'Information',
route('admin.account.device.index', $account->id, ['type' => 'accounts']) => 'Devices',
route('admin.account.edit', $account->id) => 'Information',
route('admin.account.device.index', $account->id) => 'Devices',
route('admin.account.statistics.show', $account->id) => 'Statistics',
],
])

View file

@ -0,0 +1,72 @@
@extends('layouts.main')
@section('breadcrumb')
<li class="breadcrumb-item">
<a href="{{ route('admin.account.index') }}">Accounts</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.account.edit', $account) }}">{{ $account->identifier }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Statistics</li>
@endsection
@section('content')
<header>
<h1><i class="material-icons-outlined">people</i> {{ $account->identifier }}</h1>
</header>
@include('parts.tabs', [
'items' => [
route('admin.account.edit', $account->id) => 'Information',
route('admin.account.device.index', $account->id) => 'Devices',
route('admin.account.statistics.show', $account->id) => 'Statistics',
],
])
<div>
<form class="inline" method="POST" action="{{ route('admin.account.statistics.edit', $account) }}" accept-charset="UTF-8">
@csrf
@method('post')
<input type="hidden" name="by" value="{{ request()->get('by', 'day') }}">
<div>
<input type="date" name="from" value="{{ request()->get('from') }}" onchange="this.form.submit()">
<label for="from">From</label>
</div>
<div>
<input type="date" name="to" value="{{ request()->get('to') }}" onchange="this.form.submit()">
<label for="to">To</label>
</div>
<div>
<a href="{{ route('admin.account.statistics.show', ['account' => $account, 'by' => 'day'] + request()->only(['from', 'to', 'domain'])) }}"
class="chip @if (request()->get('by', 'day') == 'day') selected @endif">Day</a>
<a href="{{ route('admin.account.statistics.show', ['account' => $account, 'by' => 'week'] + request()->only(['from', 'to', 'domain'])) }}"
class="chip @if (request()->get('by', 'day') == 'week') selected @endif">Week</a>
<a href="{{ route('admin.account.statistics.show', ['account' => $account, 'by' => 'month'] + request()->only(['from', 'to', 'domain'])) }}"
class="chip @if (request()->get('by', 'day') == 'month') selected @endif">Month</a>
<a href="{{ route('admin.account.statistics.show', ['account' => $account, 'by' => 'year'] + request()->only(['from', 'to', 'domain'])) }}"
class="chip @if (request()->get('by', 'day') == 'year') selected @endif">Year</a>
</div>
</form>
</div>
<h2><i class="material-icons-outlined">message</i> Messages from the account</h2>
{!! $messagesFromGraph !!}
<h2><i class="material-icons-outlined">message</i> Messages to the account</h2>
{!! $messagesToGraph !!}
<h2><i class="material-icons-outlined">call</i> Calls from the account</h2>
{!! $callsFromGraph !!}
<h2><i class="material-icons-outlined">call</i> Calls to the account</h2>
{!! $callsToGraph !!}
@endsection

View file

@ -1,12 +0,0 @@
<div class="bar first" style="flex-basis: {{ percent($slice['phone'] - $slice['activated_phone'], $max) }}%"
data-value="{{ $slice['phone'] - $slice['activated_phone'] }}"
title="Unactivated phone: {{ $slice['phone'] - $slice['activated_phone'] }}"></div>
<div class="bar first activated" style="flex-basis: {{ percent($slice['activated_phone'], $max) }}%"
data-value="{{ $slice['activated_phone'] }}"
title="Activated phone: {{ $slice['activated_phone'] }}"></div>
<div class="bar second" style="flex-basis: {{ percent($slice['email'] - $slice['activated_email'], $max) }}%"
data-value="{{ $slice['email'] - $slice['activated_email'] }}"
title="Unactivated email: {{ $slice['email'] - $slice['activated_email'] }}"></div>
<div class="bar second activated" style="flex-basis: {{ percent($slice['activated_email'], $max) }}%"
data-value="{{ $slice['activated_email'] }}"
title="Activated email: {{ $slice['activated_email'] }}"></div>

View file

@ -0,0 +1,73 @@
<div>
<form class="inline" method="POST" action="{{ route('admin.statistics.edit') }}" accept-charset="UTF-8">
@csrf
@method('post')
<input type="hidden" name="by" value="{{ $request->get('by', 'day') }}">
<input type="hidden" name="type" value="{{ $type }}">
<div>
<input type="date" name="from" value="{{ $request->get('from') }}" onchange="this.form.submit()">
<label for="from">From</label>
</div>
<div>
<input type="date" name="to" value="{{ $request->get('to') }}" onchange="this.form.submit()">
<label for="to">To</label>
</div>
<div class="large on_desktop"></div>
@if (config('app.admins_manage_multi_domains'))
<div class="select">
<select name="domain" onchange="this.form.submit()">
<option value="">
Select a domain
</option>
@foreach ($domains as $d)
<option value="{{ $d }}"
@if (request()->get('domain', '') == $d) selected="selected" @endif>
{{ $d }}
</option>
@endforeach
</select>
<label for="domain">Domain</label>
</div>
@endif
@if ($type == 'accounts')
<div class="select">
<select name="contacts_list" onchange="this.form.submit()">
<option value="">
Select a contacts list
</option>
@foreach ($contacts_lists as $key => $name)
<option value="{{ $key }}"
@if (request()->get('contacts_list', '') == $key) selected="selected" @endif>
{{ $name }}
</option>
@endforeach
</select>
<label for="contacts_list">Contacts list</label>
</div>
@endif
<div>
<a href="{{ route('admin.statistics.show', ['by' => 'day', 'type' => $type] + $request->only(['from', 'to', 'domain', 'contacts_list'])) }}"
class="chip @if ($request->get('by', 'day') == 'day') selected @endif">Day</a>
<a href="{{ route('admin.statistics.show', ['by' => 'week', 'type' => $type] + $request->only(['from', 'to', 'domain', 'contacts_list'])) }}"
class="chip @if ($request->get('by', 'day') == 'week') selected @endif">Week</a>
<a href="{{ route('admin.statistics.show', ['by' => 'month', 'type' => $type] + $request->only(['from', 'to', 'domain', 'contacts_list'])) }}"
class="chip @if ($request->get('by', 'day') == 'month') selected @endif">Month</a>
<a href="{{ route('admin.statistics.show', ['by' => 'year', 'type' => $type] + $request->only(['from', 'to', 'domain', 'contacts_list'])) }}"
class="chip @if ($request->get('by', 'day') == 'year') selected @endif">Year</a>
</div>
<div class="oppose">
<a class="btn btn-secondary" href="{{ route('admin.statistics.show') }}">Reset</a>
<a class="btn btn-tertiary"
href="{{ route('admin.statistics.show', ['by' => $request->get('by', 'day'), 'type' => $type, 'export' => true] + $request->only(['from', 'to', 'domain'])) }}">
<i class="material-icons-outlined">download</i> Export
</a>
</div>
</form>
</div>

View file

@ -1,6 +0,0 @@
<div class="legend">
<div class="first">Unactivated phones</div>
<div class="first activated">Activated phones</div>
<div class="second">Unactivated emails</div>
<div class="second activated">Activated emails</div>
</div>

View file

@ -5,9 +5,9 @@
@endsection
@section('content')
<header>
<h1><i class="material-icons-outlined">analytics</i> Statistics</h1>
</header>
<header>
<h1><i class="material-icons-outlined">analytics</i> Statistics</h1>
</header>
@include('parts.tabs', [
'items' => [
@ -17,43 +17,7 @@
],
])
<div>
<form class="inline" method="POST" action="{{ route('admin.statistics.edit') }}" accept-charset="UTF-8">
@csrf
@method('post')
<input type="hidden" name="by" value="{{ $by }}">
<input type="hidden" name="type" value="{{ $type }}">
<div>
<input type="date" name="from" value="{{ $request->get('from') }}" onchange="this.form.submit()">
<label for="from">From</label>
</div>
<div>
<input type="date" name="to" value="{{ $request->get('to') }}" onchange="this.form.submit()">
<label for="to">To</label>
</div>
<div>
<a href="{{ route('admin.statistics.show', ['by' => 'day', 'type' => $type] + $request->only(['from', 'to'])) }}"
class="chip @if ($by == 'day') selected @endif">Day</a>
<a href="{{ route('admin.statistics.show', ['by' => 'week', 'type' => $type] + $request->only(['from', 'to'])) }}"
class="chip @if ($by == 'week') selected @endif">Week</a>
<a href="{{ route('admin.statistics.show', ['by' => 'month', 'type' => $type] + $request->only(['from', 'to'])) }}"
class="chip @if ($by == 'month') selected @endif">Month</a>
<a href="{{ route('admin.statistics.show', ['by' => 'year', 'type' => $type] + $request->only(['from', 'to'])) }}"
class="chip @if ($by == 'year') selected @endif">Year</a>
</div>
<div class="oppose">
<a class="btn btn-secondary" href="{{ route('admin.statistics.show') }}">Reset</a>
<a class="btn btn-tertiary"
href="{{ route('admin.statistics.show', ['by' => $by, 'type' => $type, 'export' => true] + $request->only(['from', 'to'])) }}">
<i class="material-icons-outlined">download</i> Export
</a>
</div>
</form>
</div>
@include('admin.statistics.parts.filters')
@include('parts.graph')
@endsection

View file

@ -1,27 +1,17 @@
<div id="chart" @if (isset($style))style="{{ $style }}" @endif></div>
@php($id = generatePin())
<div id="chart-{{ $id }}" class="chart"></div>
<script>
const config = {!! $jsonConfig !!};
chart = document.getElementById('chart');
chart = document.getElementById('chart-' + {{ $id }});
chart.innerHTML = '';
canvas = document.createElement('canvas');
canvas.id = 'myChart';
canvas.id = 'myChart-' + {{ $id }};
chart.appendChild(canvas);
@if (isset($withDataLabel) && $withDataLabel)
config.plugins = [ChartDataLabels];
@endif
@if (isset($showLabel) && $showLabel)
config.options.plugins.datalabels.formatter = function(value, context) {
return value + ' ' + context.dataset.label;
}
@endif
new Chart(
document.getElementById('myChart'),
config
document.getElementById('myChart-' + {{ $id }}),
{!! $jsonConfig !!}
);
</script>
</script>

View file

@ -31,6 +31,7 @@ use App\Http\Controllers\Admin\AccountDeviceController;
use App\Http\Controllers\Admin\AccountTypeController;
use App\Http\Controllers\Admin\AccountController as AdminAccountController;
use App\Http\Controllers\Admin\AccountImportController;
use App\Http\Controllers\Admin\AccountStatisticsController;
use App\Http\Controllers\Admin\ContactsListController;
use App\Http\Controllers\Admin\ContactsListContactController;
use App\Http\Controllers\Admin\StatisticsController;
@ -132,6 +133,7 @@ if (config('app.web_panel')) {
Route::get('/', 'index')->name('index');
Route::get('/{type?}', 'show')->name('show');
Route::post('/', 'edit')->name('edit');
//Route::post('search', 'search')->name('search');
});
Route::name('account.')->prefix('accounts')->group(function () {
@ -191,6 +193,11 @@ if (config('app.web_panel')) {
Route::delete('/', 'destroy')->name('destroy');
});
Route::name('statistics.')->prefix('{account}/statistics')->controller(AccountStatisticsController::class)->group(function () {
Route::get('/', 'show')->name('show');
Route::post('/', 'edit')->name('edit');
});
Route::name('action.')->prefix('{account}/actions')->controller(AccountActionController::class)->group(function () {
Route::get('create', 'create')->name('create');
Route::post('/', 'store')->name('store');