Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1a381a1
Оптимизация. Убрал неиспользуемый код.
boffart Nov 25, 2025
c14751c
Доработал контроль работы воркеров по cron
boffart Dec 3, 2025
0ad9edd
Доработка парсера cdr. Добавил таблицу для хранения статистики по оче…
boffart Dec 8, 2025
e20044a
Доработал запрос REST по очередям
boffart Dec 8, 2025
e4891d8
Реализовал отчет по очередям.
boffart Dec 8, 2025
117297b
Доработал экспорт отчета по очередям в PDF и Excel
boffart Dec 9, 2025
3d3b549
Доработал отображение элементов на форме.
boffart Dec 9, 2025
f23ccc7
Исправил ошибку вычисления даты окончания периода.
boffart Dec 9, 2025
ff36860
Поправил работу метода historyQueue
boffart Dec 9, 2025
ce220b7
Исправлены ошибки формирования журнала звонков.
boffart Dec 9, 2025
4418a26
Реализовал фильтр по очередям.
boffart Dec 9, 2025
a46f144
Доработал ConnectorDB.php для работы с большим объемом данных.
boffart Dec 9, 2025
5b62115
Доработал отборы по идентификатору очереди в отчете по очередям.
boffart Dec 9, 2025
b4254c8
Доработал переход из отчета по очередям в детализацию
boffart Dec 9, 2025
4d12ac8
Доработал отображение поля с мниимальной длительностью
boffart Dec 10, 2025
9b0f3e0
Поправил работу отчета с отбором "Без очередей".
boffart Dec 10, 2025
07dc7a9
Исправлена ошибка отображения currentMinBillSecComp элемента.
boffart Dec 10, 2025
d9ad0f5
Поправил расчет времени ожидания клиента ответа.
boffart Dec 10, 2025
b50ef5b
Поправил расчет времени ожидания клиента ответа.
boffart Dec 11, 2025
dca4c99
Расширил логирование ConnectorDB
boffart Dec 11, 2025
8c46840
Расширил логирование ConnectorDB
boffart Dec 11, 2025
7c518e0
Добавил замеры времени и логирование
boffart Dec 11, 2025
c2f8790
Оптимизация загрузки истории звонков.
boffart Dec 12, 2025
ab78454
Исправлено округление до сотых для процента пропущенных
boffart Dec 12, 2025
102a9b3
Исправид отображение "загрузки" для отчета. Поправил формирование отв…
boffart Dec 22, 2025
a3b607b
И правил проблему формирования отчета по исходящим сотрудникам при на…
boffart Dec 22, 2025
d5fd609
Добавил расчет процента по формуле 100* "Пропущенные" / "Входящие" + …
boffart Dec 22, 2025
4fa47d9
Поправил пробелмы двойного вызова формирования отчета. Оптимизация. П…
boffart Dec 22, 2025
604e58a
Исправил ошибку определения общего статуса звонка, когда все CDR проп…
boffart Dec 24, 2025
14ef7c0
Поправил скачивание аудио файла.
boffart Jan 22, 2026
a70f73f
Оптимизация формирования xlsx
boffart Jan 22, 2026
46ba3a9
Доработка скачивания медиа файла.
boffart Jan 22, 2026
a8b251b
Оптимизация формирования большого PGF документа на 1100 страниц.
boffart Jan 22, 2026
523514f
Рефакторинг ConnectorDB: безопасность, производительность, архитектура
Jan 24, 2026
5250d48
Исправлена выгрузка записей для звонков через IVR/очередь
Jan 24, 2026
2d4567b
Добавлен id записи в prettyFilename для уникальности
Jan 24, 2026
ff85b33
Добавлен onBeforeModuleDisable для обхода проблемы с большими БД
Jan 25, 2026
8d811eb
Добавлен onBeforeModuleEnable для обхода проблемы при включении модуля
Jan 25, 2026
e3f25dd
Добавлен обход deadlock в CallHistory при enable/disable модуля
Jan 25, 2026
5c10df0
Оптимизация размера модуля: удалены ненужные шрифты и тесты из vendor
Jan 25, 2026
534191a
Исправлена проблема "работает через раз" в getHistory ajax запросах
Jan 25, 2026
4bfa6b0
Исправлена сборка JS: использованы inline helpers вместо require()
Jan 25, 2026
dd84a40
Оптимизация getCountCdr: lazy-кэш и составной индекс
Jan 25, 2026
d71c5ad
Исправлена потеря CDR-записей при синхронизации и поддержка webm
Feb 3, 2026
bb0bb51
Добавлен скрипт сброса cdrOffset на начало дня с перезапуском Connect…
Feb 3, 2026
e9813fa
reset-cdr-offset: fix sqlite3 вывод и запуск safe.php в фоне
Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions App/Controllers/ModuleExtendedCDRsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Modules\ModuleExtendedCDRs\Lib\CacheManager;
use Modules\ModuleExtendedCDRs\Lib\GetReport;
use Modules\ModuleExtendedCDRs\Lib\HistoryParser;
use Modules\ModuleExtendedCDRs\Lib\RestAPI\Controllers\ApiController;
use Modules\ModuleExtendedCDRs\Models\CallHistory;
use Modules\ModuleExtendedCDRs\Models\ExportRules;
use Modules\ModuleExtendedCDRs\Models\ModuleExtendedCDRs;
Expand Down Expand Up @@ -145,8 +146,12 @@ public function indexAction(): void
$this->view->form = new ModuleExtendedCDRsForm($settings, $options);
$this->view->pick("{$this->moduleDir}/App/Views/index");

$queues = CallQueues::find(['columns' => ['uniqid As id', 'name']])->toArray();
if(!empty($queues)){
$queues[] = ['id' => '-', "name" => Util::translate('repModuleExtendedCDRs_WithOutQueues')];
}
// Список выбора очередей.
$this->view->queues = CallQueues::find(['columns' => ['id', 'name']]);
$this->view->queues = $queues;
$this->view->groups = [];

if(class_exists('\Modules\ModuleUsersGroups\Models\UsersGroups')){
Expand Down Expand Up @@ -183,6 +188,16 @@ public function indexAction(): void
}
}

foreach ($this->view->queues as $queue){
foreach ($filterNumbers as $number){
if('queue_'.$queue['id'] === $number){
$additionalFilter[] = [
'name' => $queue['name'],
'number' => $number
];
}
}
}
foreach ($this->view->users as $user){
foreach ($filterNumbers as $number){
if($user['number'] === $number){
Expand All @@ -193,7 +208,32 @@ public function indexAction(): void
}
}
}
$this->view->dateRangeSelector = $searchSettings['dateRangeSelector']??'';
$pDateRangeSelector = $_REQUEST['dateRangeSelector']??'';
if(!empty($pDateRangeSelector)){
$this->view->dateRangeSelector = $pDateRangeSelector;
$this->view->pDateRangeSelector = $pDateRangeSelector;
$additionalFilter = [];
foreach ($this->view->queues as $queue){
if($queue['id'] === $_REQUEST['queue']??'' ){
$additionalFilter[] = [
'name' => $queue['name'],
'number' => 'queue_'.$queue['id']
];
}
}
$this->view->pAdditionalFilterString = implode(',',array_column($additionalFilter, 'number'));
}else{
$this->view->pDateRangeSelector = '';
$this->view->pAdditionalFilterString = '-';
$this->view->dateRangeSelector = $searchSettings['dateRangeSelector']??'';
}

$pReportNameID = $_REQUEST['reportNameID']??'';
if($pReportNameID === ReportSettings::REPORT_MAIN){
$this->view->currentReportNameID = $pReportNameID;
$this->view->currentVariantId = null;
}

$this->view->additionalFilter = $additionalFilter;
$this->view->additionalFilterString = implode(',',array_column($additionalFilter, 'number'));
$this->view->accessData = $this->getUserData();
Expand All @@ -215,6 +255,12 @@ private function getVariantsReports(&$searchSettings)
'isMain' => 0,
'variantName' => Util::translate('repModuleExtendedCDRs_'.ReportSettings::REPORT_OUTGOING_EMPLOYEE_CALLS)
],
ReportSettings::REPORT_QUEUES => [
'searchText' => '{}',
'minBillSec' => 0,
'isMain' => 0,
'variantName' => Util::translate('repModuleExtendedCDRs_'.ReportSettings::REPORT_QUEUES)
],
];
$filterVariantReport = [
'userID=:userID:',
Expand Down Expand Up @@ -588,6 +634,21 @@ public function getHistoryAction(): void
$this->view->$key = $value;
}
}
public function getCdrQueueAction(): void
{
$this->view->draw = $this->request->get('draw');
$searchPhrase = $this->request->get('search');
$length = is_numeric($this->request->get('length'))?(int)$this->request->get('length'):null;

$gr = new GetReport();
$params = [];
$view = ApiController::aggregateCdrData($gr->historyQueue($searchPhrase['value']??'', $this->request->get('start'), $length));
foreach ($view as $key => $value) {
$this->view->$key = $value;
}
$this->view->params = $params;
$this->view->length = count($view);
}

/**
* Запрос отвчета по исходящим сотрудников
Expand All @@ -597,9 +658,9 @@ public function getOutgoingEmployeeCallsAction(): void
{
$this->view->draw = $this->request->get('draw');
$searchPhrase = $this->request->get('search');
$length = is_numeric($this->request->get('length'))?(int)$this->request->get('length'):null;
// $length = is_numeric($this->request->get('length'))?(int)$this->request->get('length'):null;
$gr = new GetReport();
$view = $gr->outgoingEmployeeCalls($searchPhrase['value']??'', $this->request->get('start'), $length);
$view = $gr->outgoingEmployeeCalls($searchPhrase['value']??'');
foreach ($view as $key => $value) {
$this->view->$key = $value;
}
Expand Down
61 changes: 57 additions & 4 deletions App/Views/index.volt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<div id="menu-reports" style="display: none;" class="ui fluid popup bottom left transition hidden">
<input id="currentReportNameID" type="hidden" name="filters" value="{{currentReportNameID}}">
<input id="currentVariantId" type="hidden" name="filters" value="{{currentVariantId}}">
<input id="pDateRangeSelector" type="hidden" name="filters" value="{{pDateRangeSelector}}">
<input id="pAdditionalFilterString" type="hidden" name="filters" value="{{pAdditionalFilterString}}">

<div class="ui four column relaxed equal height divided grid">
{% for variantId,variantsData in mainReports %}
Expand Down Expand Up @@ -122,9 +124,9 @@
</a>
</div>
</div>
<div class="ui two wide column" style="height: 40px;">
<div class="ui column" style="height: 40px; display: flex; justify-content: flex-end; flex: 1;">
<div class="ui action input">
<input style="height: 40px;" type="search" id="globalsearch" placeholder="{{ t._('repModuleExtendedCDRs_FindCallsPlaceholder') }}" aria-controls="KeysTable">
<input style="height: 40px; width: 200px; min-width: 150px;" type="search" id="globalsearch" placeholder="{{ t._('repModuleExtendedCDRs_FindCallsPlaceholder') }}" aria-controls="KeysTable">
<button style="height: 40px;" id="createExcelButton" class="ui icon basic button"> <i class="green file excel outline icon"></i></button>
<button style="height: 40px;" id="createPdfButton" class="ui icon basic button"> <i class="red file pdf outline icon"></i></button>
<button style="height: 40px;" id="downloadRecords" class="ui icon basic button"> <i class="download icon"></i></button>
Expand All @@ -134,7 +136,7 @@
</div>
</div>
</div>
<div class="ui row" style="padding-top:0; padding-left:9px">
<div class="ui row" style="padding-top: 10px; padding-left:9px; display: flex; justify-content: space-between; align-items: center;">
<div id='additionalFilter' class="ui multiple dropdown">
<input type="hidden" name="filters" value="{{additionalFilterString}}">
<i class="filter icon"></i>
Expand All @@ -152,11 +154,20 @@
<div class="scrolling menu">
{% for group in groups %}
<div class="item" data-value="group_{{ group['id'] }}">
<i class="users icon"></i>
<i class="sitemap icon"></i>
Отдел: {{ group['name'] }}
</div>
{% endfor %}
<h4 class="ui horizontal divider header"></h4>

{% for queue in queues %}
<div class="item" data-value="queue_{{ queue['id'] }}">
<i class="users icon"></i>
Очередь: {{ queue['name'] }}
</div>
{% endfor %}
<h4 class="ui horizontal divider header"></h4>

{% for user in users %}
<div class="item" data-value="{{ user['number'] }}">
<i class="user icon"></i>
Expand All @@ -168,6 +179,25 @@
</div>

</div>
<div id="minBillSecContainer" style="margin-right: 18px; display: flex; align-items: center; white-space: nowrap;">
<label style="margin-right: 10px; line-height: 32px; font-weight: 600;">{{ t._('repModuleExtendedCDRs_Form_minBillSec') }}:</label>
<div id="currentMinBillSecComp" class="ui basic compact button dropdown" data-value=">" style="height: 32px; min-width: 40px; padding: 8px 10px; line-height: 16px;">
&gt;
<div class="menu">
<div class="item" data-value=">">&gt;</div>
<div class="item" data-value=">=">&gt;=</div>
<div class="item" data-value="<">&lt;</div>
<div class="item" data-value="<=">&lt;=</div>
<div class="item" data-value="=">=</div>
</div>
</div>
<div class="ui mini right labeled input" style="margin-left: 5px; width: 110px;">
<input type="number" id="currentMinBillSec" placeholder="0" min="0" max="1000" maxlength="6" style="text-align: right; height: 32px; padding: 5px 8px; width: 60px;">
<div class="ui basic label" style="height: 32px; line-height: 20px; padding: 5px 10px; font-size: 12px;">
{{ t._('repModuleExtendedCDRs_Form_minBillSec_s') }}
</div>
</div>
</div>
</div>
</div>

Expand All @@ -191,6 +221,29 @@
</table>
</div>

<div id="CdrQueue-table-div">
<table id="CdrQueue-table" data-report-name="CdrQueue" class="ui small very compact single line unstackable celled striped table ">
<thead>
<tr>
<th class="one wide">{{ t._('repModuleExtendedCDRs_CdrQueue_Date') }}</th>
<th class="three wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_Queue') }}</th>
<th class="one wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_TotalCalls') }}</th>
<th class="one wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_Answered') }}</th>
<th class="one wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_Missed') }}</th>
<th class="one wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_AnsweredQueue') }}</th>
<th class="one wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_AvgWaitTime') }}</th>
<th class="one wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_AvgMissed') }}</th>
<th class="one wide center aligned">{{ t._('repModuleExtendedCDRs_CdrQueue_AvgWaitTimeQueue') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="9" class="dataTables_empty">{{ t._('dt_TableIsEmpty') }}</td>
</tr>
</tbody>
</table>
</div>

<br>
<div id="CallDetails-table-div">

Expand Down
138 changes: 138 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## CRITICAL RULES

- **Git commit and push ONLY with explicit user permission.** Never run `git commit` or `git push` without the user explicitly asking for it.

## Project Overview

ModuleExtendedCDRs is a MikoPBX extension module for Extended Call Detail Records management and reporting. It synchronizes CDR data from Asterisk, provides call history views, generates reports (PDF/XLSX/JSON), exports via webhooks, and delivers scheduled reports by email.

- **Language**: PHP 7.4.6+
- **Framework**: Phalcon 4.0 (MVC)
- **Namespace**: `Modules\ModuleExtendedCDRs\`
- **License**: GPL-3.0-or-later
- **Module ID**: `ModuleExtendedCDRs`

## Commands

```bash
# Install PHP dependencies
composer install

# Syntax check
php -l <file.php>

# Run test script (development)
php bin/test.php

# Background workers (run on PBX system)
php bin/ConnectorDB.php # CDR synchronization daemon
php bin/SyncRecords.php # Webhook export worker
php bin/report2email.php # Scheduled email reports
```

## Build & CI

GitHub Actions workflow (`.github/workflows/build.yml`) triggers on push to `master`/`develop` and uses the shared `mikopbx/.github-workflows` reusable workflow for building and publishing releases. Version is auto-incremented from initial version `1.33`.

## Architecture

### Data Flow

```
Asterisk CDR DB → ConnectorDB worker → SQLite (db/cdr.db)
↓
GetReport (formatting/filtering)
↓
API / PDF / XLSX / Webhook / Email
```

### Key Layers

| Layer | Location | Purpose |
|-------|----------|---------|
| Controllers | `App/Controllers/` | Admin panel request handling |
| Views | `App/Views/` | Volt templates for UI |
| Forms | `App/Forms/` | Phalcon form definitions |
| Business Logic | `Lib/` | Report generation, CDR parsing, caching |
| REST API | `Lib/RestAPI/Controllers/` | External API endpoints |
| Models | `Models/` | Phalcon ORM (5 models) |
| Workers | `bin/` | Background daemons (extend `WorkerBase`) |
| Setup | `Setup/` | Module install/uninstall |
| i18n | `Messages/` | 31 language translations |
| Assets | `public/assets/` | JS, CSS, images |

### Key Classes

- **`GetReport`** (`Lib/GetReport.php`) — Central report engine: `history()`, `historyDetail()`, `outgoingEmployeeCalls()`, `exportToPdf()`, `exportToXlsx()`
- **`ConnectorDB`** (`bin/ConnectorDB.php`) — Daemon syncing Asterisk CDR → local SQLite, extends `WorkerBase`
- **`HistoryParser`** (`Lib/HistoryParser.php`) — CDR parsing and call history assembly
- **`CacheManager`** (`Lib/CacheManager.php`) — Redis cache for sync progress tracking
- **`ExtendedCDRsConf`** (`Lib/ExtendedCDRsConf.php`) — REST API route registration and module config
- **`ExtendedCDRsMain`** (`Lib/ExtendedCDRsMain.php`) — Module lifecycle (`checkModuleWorkProperly`, `startAllServices`)
- **`ApiController`** (`Lib/RestAPI/Controllers/ApiController.php`) — REST endpoints for export/download

### Database

- **Module settings**: MikoPBX main DB (table `m_ModuleExtendedCDRs`)
- **CDR data**: SQLite at `db/cdr.db` (managed by `CdrDbProvider`)

### Models

- `ModuleExtendedCDRs` — Module configuration/settings
- `ReportSettings` — Per-user report variant preferences
- `CallHistory` — Call history records
- `ExportRules` — Webhook export rules
- `ExportResults` — Export result tracking

### REST API

Base path: `/pbxcore/api/modules/ModuleExtendedCDRs/`

- `GET /records` — Download call recordings
- `GET /exportHistory` — Export call history (JSON/PDF/XLSX)
- `GET /exportHistoryDetail` — Detailed CDR export
- `GET /exportOutgoingEmployeeCalls` — Employee call reports
- `GET /downloads` — File downloads

### Integration Points

- **MikoPBX Core**: AMI, Beanstalk queues, CDR database, module lifecycle hooks
- **Other Modules**: ModuleUsersUI (RBAC/ACL), ModuleUsersGroups (group filtering)
- **External**: Webhooks to arbitrary URLs via `ExportRules`

## JavaScript Build

Source files are in `public/assets/js/src/`. After modifying them, compiled files must be generated in `public/assets/js/`.

**IMPORTANT:** Only edit files in `src/` directory. Files in `public/assets/js/*.js` are auto-generated.

Build process uses Babel via PHPStorm File Watcher:
- See setup: https://docs.mikopbx.com/mikopbx-development/prepare-ide-tools/mac#phpstorm-setup-babel
- Babel path: `/Users/apor/Developement/MikoPBX/MikoPBXUtils/node_modules/.bin/babel`
- Presets: `airbnb`
- Source maps: enabled

To rebuild manually:
```bash
cd /Users/apor/Developement/MikoPBX/MikoPBXUtils && \
cp babel.config.json babel.config.json.bak && \
echo '{"presets":[["@babel/preset-env",{"targets":{"chrome":50,"ie":11,"firefox":45}}]]}' > babel.config.json && \
./node_modules/.bin/babel \
/Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleExtendedCDRs/public/assets/js/src/module-export-records-index.js \
--out-dir /Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleExtendedCDRs/public/assets/js/ \
--source-maps && \
mv babel.config.json.bak babel.config.json
```

## Conventions

- Module extends `PbxExtensionBase` for lifecycle, implements `CDRConfigInterface` for CDR hooks
- Workers extend `WorkerBase` with ping/heartbeat health checks
- REST routes registered via `getPBXCoreRESTAdditionalRoutes()` in `ExtendedCDRsConf`
- Cache keys prefixed with `ModuleExtendedCDRs_`
- Log files at `/var/log/pbx/ModuleExtendedCDRs/{class}.log` with rotation
- PSR-4 autoloading, root namespace maps to repository root
Loading