[VoiceCallHistory]
- implemented VoiceCallHistoryJob for running the jobs - implement /VoiceCallHistory for displaying, importing the Voice Call History - added date-range-picker to tt-table (still todo)
This commit is contained in:
88
Layout/default/VoiceCallHistory/Index.php
Normal file
88
Layout/default/VoiceCallHistory/Index.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php /** @noinspection PhpUndefinedClassInspection
|
||||
* @var string $mfLayoutPackage
|
||||
* @var TYPE_NAME $git_merge_ts
|
||||
*/
|
||||
|
||||
//additional css /css/views/RaspberryDisplay.css
|
||||
|
||||
$JSGlobals = ["BASE_URL" => self::getUrl("Domain"),
|
||||
"DASHBOARD_URL" => self::getUrl("Dashboard"),
|
||||
"MFAPPNAME" => MFAPPNAME_SLUG,
|
||||
"PAGE_TITLE" => "Domains",
|
||||
"PATH" => [
|
||||
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
|
||||
["text" => "Voice Calls History", "href" => self::getUrl("VoiceCallHistory")]
|
||||
],
|
||||
"VOICE_CALL_HISTORY_API_URL" => self::getUrl("VoiceCallHistory/api"),
|
||||
];
|
||||
|
||||
$additionalJS = ["plugins/vue/vue.js",
|
||||
"plugins/axios/axios.min.js",
|
||||
"plugins/vue/tt-components/tt-page-title.js",
|
||||
"plugins/vue/tt-components/tt-table.js",
|
||||
];
|
||||
$additionalCSS = [
|
||||
'plugins/vue/tt-components/css/tt-table.css',
|
||||
];
|
||||
|
||||
include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
|
||||
|
||||
<div id="app">
|
||||
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
|
||||
|
||||
<tt-table :fetch-url="window['TT_CONFIG']['VOICE_CALL_HISTORY_API_URL'] + '?do=getCalls'" :table-config="VoiceCallHistoryTableConfig"
|
||||
small ref="table">
|
||||
<template v-slot:top-buttons>
|
||||
<button type="button" class="btn btn-primary" @click="importCallsFromToday">
|
||||
<template v-if="importCallsFromTodayLoading">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
Re-Import Calls from Today
|
||||
</template>
|
||||
</button>
|
||||
|
||||
</template>
|
||||
</tt-table>
|
||||
</div>
|
||||
|
||||
<!-- TODO: download from cdn to local -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-select@1.13.14/dist/css/bootstrap-select.min.css">
|
||||
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
window: window,
|
||||
VoiceCallHistoryTableConfig: {
|
||||
headers: [
|
||||
{text: "UID", key: "uid"},
|
||||
{text: "Voice Account", key: "voice_account"},
|
||||
{text: "Start", key: "start", filter: "dateRange"},
|
||||
{text: "Source", key: "source"},
|
||||
{text: "Destination", key: "destination"},
|
||||
{text: "Billable", key: "billable"},
|
||||
{text: "Duration", key: "duration"},
|
||||
],
|
||||
tableHeader: 'Voice Call History',
|
||||
},
|
||||
importCallsFromTodayLoading: false,
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
async importCallsFromToday() {
|
||||
this.importCallsFromTodayLoading = true;
|
||||
await axios.get(window['TT_CONFIG']['VOICE_CALL_HISTORY_API_URL'] + '?do=importCallsFromToday');
|
||||
this.$refs.table.fetchData();
|
||||
this.importCallsFromTodayLoading = false;
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>
|
||||
|
||||
@@ -125,7 +125,8 @@
|
||||
</a>
|
||||
<ul class="submenu">
|
||||
<?php if($me->isAdmin() || $me->can("Voipnumbering")): ?><li><a href="<?=self::getUrl("Voicenumberblock")?>"><i class="fad fa-fw fa-phone-office text-info"></i> Rufnummernblöcke</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("Voiceplan")): ?><li><a href="<?=self::getUrl("Voiceplan")?>"><i class="fas fa-fw fa-phone-arrow-up-right text-info"></i> Sprachtarife</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("Voiceplan")): ?><li><a href="<?=self::getUrl("Voiceplan")?>"><i class="fas fa-fw fa-phone-arrow-up-right text-info"></i> Sprachtarife</a></li><?php endif; ?>
|
||||
<?php if($me->isAdmin() || $me->can("VoiceCallHistory")): ?><li><a href="<?=self::getUrl("VoiceCallHistory")?>"><i class="fas fa-fw fa-phone-arrow-down-left text-info"></i> Voice Call History</a></li><?php endif; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
9
application/VoiceCallHistory/VoiceCallHistory.php
Normal file
9
application/VoiceCallHistory/VoiceCallHistory.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class VoiceCallHistory extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
96
application/VoiceCallHistory/VoiceCallHistoryController.php
Normal file
96
application/VoiceCallHistory/VoiceCallHistoryController.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
//display errors
|
||||
//ini_set('display_errors', 1);
|
||||
//ini_set('display_startup_errors', 1);
|
||||
//error_reporting(E_ALL);
|
||||
|
||||
class VoiceCallHistoryController extends mfBaseController {
|
||||
private User $me;
|
||||
private string $VOICE_PORTAL_HOST = "vportal.xinon.at";
|
||||
private string $VOICE_PORTAL_API_KEY = "2f9mpw3oamALg7gSgtWUTCKNZ01fFRDh";
|
||||
private string $VOICE_PORTAL_USERNAME = "700342020";
|
||||
|
||||
private KolmisoftMore $kolmisoftMore;
|
||||
|
||||
|
||||
protected function init(): void {
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
$this->layout()->set("me", $me);
|
||||
$this->me = $me;
|
||||
|
||||
if (!$this->me->isAdmin()) {
|
||||
$this->redirect("dashboard");
|
||||
}
|
||||
|
||||
$this->kolmisoftMore = new KolmisoftMore($this->VOICE_PORTAL_HOST, $this->VOICE_PORTAL_API_KEY, $this->VOICE_PORTAL_USERNAME);
|
||||
|
||||
}
|
||||
|
||||
protected function indexAction(): void {
|
||||
$this->layout()->setTemplate("VoiceCallHistory/Index");
|
||||
}
|
||||
|
||||
protected function apiAction() {
|
||||
$do = $this->request->do;
|
||||
|
||||
if (!$this->me->isAdmin()) {
|
||||
$this->redirect("dashboard");
|
||||
}
|
||||
|
||||
switch ($do) {
|
||||
case "getCalls":
|
||||
$return = $this->getCalls();
|
||||
break;
|
||||
case "importCallsFromToday":
|
||||
$return = $this->importCallsFromToday();
|
||||
break;
|
||||
default:
|
||||
$return = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$return) {
|
||||
$return = [
|
||||
"status" => "error",
|
||||
"message" => "Invalid request."
|
||||
];
|
||||
}
|
||||
|
||||
die(json_encode($return));
|
||||
}
|
||||
|
||||
private function importCallsFromToday(): array {
|
||||
$startDate = strtotime(date("Y-m-d 8:00:00"));
|
||||
$endDate = strtotime(date("Y-m-d 9:00:59"));
|
||||
|
||||
$callHistory = $this->kolmisoftMore->getVoiceCallHistory($startDate, $endDate);
|
||||
|
||||
return VoiceCallHistoryModel::importCallsFromKolmisoft($callHistory);
|
||||
}
|
||||
|
||||
private function getCalls(): array {
|
||||
$json = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$filters = $json['filters'] ?? [];
|
||||
$page = $json['pagination']['page'] ?? 1;
|
||||
$perPage = $json['pagination']['per_page'] ?? 10;
|
||||
|
||||
$calls = VoiceCallHistoryModel::getVoiceCallHistory($filters, $perPage, $perPage * $page - $perPage);
|
||||
$totalRows = VoiceCallHistoryModel::countVoiceCallHistory($filters);
|
||||
|
||||
return [
|
||||
"rows" => $calls,
|
||||
"pagination" => [
|
||||
"page" => $page,
|
||||
"total_pages" => ceil($totalRows / $perPage),
|
||||
"per_page" => $perPage,
|
||||
"total_rows" => intval($totalRows)
|
||||
]
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
146
application/VoiceCallHistory/VoiceCallHistoryModel.php
Normal file
146
application/VoiceCallHistory/VoiceCallHistoryModel.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
class VoiceCallHistoryModel {
|
||||
public $uid;
|
||||
public $voice_account;
|
||||
public $start;
|
||||
public $source;
|
||||
public $destination;
|
||||
public $billable;
|
||||
public $duration;
|
||||
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $field => $value) {
|
||||
if (property_exists(get_called_class(), $field)) {
|
||||
$this->$field = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SQL Filter condition (space separated) for a given column.
|
||||
*
|
||||
* @param string|null $filterValue The filter value to match against.
|
||||
* @param string $columnName The name of the column in the database table.
|
||||
* @return string The SQL condition generated based on the filter value and column name.
|
||||
*/
|
||||
public static function generateFilterCondition(?string $filterValue, string $columnName): string {
|
||||
$sql = "";
|
||||
if (!empty($filterValue)) {
|
||||
$filterItems = explode(" ", $filterValue);
|
||||
foreach ($filterItems as $item) {
|
||||
$sql .= " AND `$columnName` LIKE '%" . $item . "%'";
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
//TODO: combine these two functions into one
|
||||
public static function importCallsFromKolmisoft($callHistory): array {
|
||||
{
|
||||
|
||||
$modifiedCallHistory = [];
|
||||
for ($i = 0; $i < count($callHistory); $i++) {
|
||||
$call = $callHistory[$i];
|
||||
$date = date("Y-m-d H:i:s", strtotime($call["calldate2"] . " UTC"));
|
||||
$date = new DateTime($date, new DateTimeZone('UTC'));
|
||||
$date->setTimezone(new DateTimeZone('Europe/Vienna'));
|
||||
$date = $date->format('Y-m-d H:i:s');
|
||||
|
||||
$modifiedCall = [
|
||||
"uid" => $call["uniqueid"],
|
||||
"voice_account" => strpos($call["clid"], "Anonymous") !== false ? 'Anonymous' : preg_replace('/[^0-9]/', '', explode(" ", $call["clid"])[0]),
|
||||
"start" => $date,
|
||||
"source" => strpos($call["clid"], "nymous") !== false ? 'Anonymous' : str_replace("+", "", $call["src"]),
|
||||
"destination" => $call["dst"],
|
||||
"state" => preg_replace('/[^0-9]/', '', explode("(", $call["dispod"])[1]),
|
||||
"billable" => gettype($call["did"]) === "string" ? 0 : 1,
|
||||
"duration" => $call["nice_billsec"],
|
||||
];
|
||||
$modifiedCallHistory[] = $modifiedCall;
|
||||
}
|
||||
|
||||
$voiceHistoryImport = VoiceCallHistoryModel::importVoiceCallHistory($modifiedCallHistory);
|
||||
|
||||
if ($callHistory === false) {
|
||||
return [
|
||||
"status" => "error",
|
||||
"message" => "Failed to import calls."
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"status" => "success",
|
||||
"message" => $voiceHistoryImport["message"]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public static function importVoiceCallHistory($callHistory): array {
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
$sql = /** @lang text */ "INSERT INTO `VoiceCallHistory` (`uid`, `voice_account`, `start`, `source`, `destination`, `state`, `billable`, `duration`) VALUES ";
|
||||
$values = [];
|
||||
foreach ($callHistory as $voiceCall) {
|
||||
$uid = $voiceCall['uid'];
|
||||
$valueStr = "(" .
|
||||
($voiceCall['uid'] === 'Anonymous' ? 'NULL' : "'$uid'") . ", " .
|
||||
($voiceCall['voice_account'] === 'Anonymous' ? 'NULL' : "'" . $voiceCall['voice_account']. "'" ) . ", '" .
|
||||
$voiceCall['start'] . "', '" .
|
||||
($voiceCall['source'] === 'Anonymous' ? "NULL" : $voiceCall['source']) . "', '" .
|
||||
($voiceCall['destination'] === 'Anonymous' ? "NULL" : $voiceCall['destination'] ) . "', " .
|
||||
$voiceCall['state'] . ", " .
|
||||
$voiceCall['billable'] . ", " .
|
||||
$voiceCall['duration'] . ")";
|
||||
|
||||
$values[] = $valueStr;
|
||||
}
|
||||
|
||||
$sql .= implode(", ", $values);
|
||||
$sql .= " ON DUPLICATE KEY UPDATE duration = VALUES(duration)";
|
||||
|
||||
$db->query($sql);
|
||||
|
||||
return [
|
||||
"message" => "Imported " . count($callHistory) . " calls to the history."
|
||||
];
|
||||
}
|
||||
|
||||
public static function getSqlFilter($filters): string {
|
||||
|
||||
$sql = "";
|
||||
if (isset($filters['start']['from']) && isset($filters['start']['to'])) {
|
||||
$sql = " AND `start` >= FROM_UNIXTIME(" . $filters['start']['from'] . ") AND `start` <= FROM_UNIXTIME(" . $filters['start']['to'] . ")";
|
||||
}
|
||||
$sql .= isset($filters['end']) ? " AND `start` <= '" . $filters['end'] . "'" : "";
|
||||
$sql .= isset($filters['source']) ? self::generateFilterCondition($filters['source'], "source") : "";
|
||||
$sql .= isset($filters['destination']) ? self::generateFilterCondition($filters['destination'], "destination") : "";
|
||||
$sql .= isset($filters['billable']) ? self::generateFilterCondition($filters['billable'], "billable") : "";
|
||||
$sql .= isset($filters['duration']) ? self::generateFilterCondition($filters['duration'], "duration") : "";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public static function getVoiceCallHistory($filters, $limit = null, $offset = 0): array {
|
||||
$db = FronkDB::singleton();
|
||||
$sql = "SELECT * FROM `VoiceCallHistory` WHERE 1 " . self::getSqlFilter($filters);
|
||||
$sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset;
|
||||
|
||||
$result = $db->query($sql);
|
||||
$rows = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$rows[] = new VoiceCallHistoryModel($row);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function countVoiceCallHistory($filters) {
|
||||
$db = FronkDB::singleton();
|
||||
$sql = "SELECT COUNT(*) as `total_rows` FROM `VoiceCallHistory` WHERE 1 " . self::getSqlFilter($filters);
|
||||
$result = $db->query($sql);
|
||||
return $result->fetch_assoc()['total_rows'];
|
||||
}
|
||||
}
|
||||
9
application/VoiceCallHistoryJob/VoiceCallHistoryJob.php
Normal file
9
application/VoiceCallHistoryJob/VoiceCallHistoryJob.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @property mixed|null $name
|
||||
*/
|
||||
class VoiceCallHistoryJob extends mfBaseModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
//display errors
|
||||
//ini_set('display_errors', 1);
|
||||
//ini_set('display_startup_errors', 1);
|
||||
//error_reporting(E_ALL);
|
||||
|
||||
class VoiceCallHistoryJobController extends mfBaseController {
|
||||
private User $me;
|
||||
private string $VOICE_PORTAL_HOST = "vportal.xinon.at";
|
||||
private string $VOICE_PORTAL_API_KEY = "2f9mpw3oamALg7gSgtWUTCKNZ01fFRDh";
|
||||
private string $VOICE_PORTAL_USERNAME = "700342020";
|
||||
|
||||
private KolmisoftMore $kolmisoftMore;
|
||||
|
||||
|
||||
protected function init(): void {
|
||||
$me = new User();
|
||||
$me->loadMe();
|
||||
$this->layout()->set("me", $me);
|
||||
$this->me = $me;
|
||||
|
||||
if (!$this->me->isAdmin()) {
|
||||
$this->redirect("dashboard");
|
||||
}
|
||||
|
||||
$this->kolmisoftMore = new KolmisoftMore($this->VOICE_PORTAL_HOST, $this->VOICE_PORTAL_API_KEY, $this->VOICE_PORTAL_USERNAME);
|
||||
|
||||
}
|
||||
|
||||
protected function indexAction(): void {
|
||||
$this->layout()->setTemplate("VoiceCallHistoryJob/Index");
|
||||
}
|
||||
|
||||
protected function apiAction() {
|
||||
$do = $this->request->do;
|
||||
|
||||
if (!$this->me->isAdmin()) {
|
||||
$this->redirect("dashboard");
|
||||
}
|
||||
|
||||
switch ($do) {
|
||||
case "runJobs":
|
||||
$return = $this->runJobs();
|
||||
break;
|
||||
case "importCallsFromToday":
|
||||
$return = $this->importCallsFromToday();
|
||||
break;
|
||||
default:
|
||||
$return = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$return) {
|
||||
$return = [
|
||||
"status" => "error",
|
||||
"message" => "Invalid request."
|
||||
];
|
||||
}
|
||||
|
||||
die(json_encode($return));
|
||||
}
|
||||
|
||||
private function runJobs(): array {
|
||||
VoiceCallHistoryJobModel::createJobsUntilToday();
|
||||
|
||||
$jobs = VoiceCallHistoryJobModel::getJobsNotDone();
|
||||
|
||||
$messages = [
|
||||
"success" => [],
|
||||
"error" => []
|
||||
];
|
||||
foreach ($jobs as $job) {
|
||||
$startDate = strtotime(date("Y-m-d 00:00:00", strtotime($job->date)));
|
||||
$endDate = strtotime(date("Y-m-d 00:00:00", strtotime($job->date . " +1 day")));
|
||||
|
||||
$callHistory = $this->kolmisoftMore->getVoiceCallHistory($startDate, $endDate);
|
||||
|
||||
$importedCalls = VoiceCallHistoryModel::importCallsFromKolmisoft($callHistory);
|
||||
|
||||
if ($importedCalls) {
|
||||
$messages["success"][$job->date] = $importedCalls["message"];
|
||||
VoiceCallHistoryJobModel::updateJobStatus($job->id, "success");
|
||||
} else {
|
||||
$messages["error"][$job->date] = "Failed to import calls for job $job->id.";
|
||||
VoiceCallHistoryJobModel::updateJobStatus($job->id, "failed");
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
86
application/VoiceCallHistoryJob/VoiceCallHistoryJobModel.php
Normal file
86
application/VoiceCallHistoryJob/VoiceCallHistoryJobModel.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
class VoiceCallHistoryJobModel {
|
||||
public $id;
|
||||
public $date;
|
||||
public $status;
|
||||
public $create;
|
||||
public $edit;
|
||||
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $field => $value) {
|
||||
if (property_exists(get_called_class(), $field)) {
|
||||
$this->$field = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SQL Filter condition (space separated) for a given column.
|
||||
*
|
||||
* @param string|null $filterValue The filter value to match against.
|
||||
* @param string $columnName The name of the column in the database table.
|
||||
* @return string The SQL condition generated based on the filter value and column name.
|
||||
*/
|
||||
public static function generateFilterCondition(?string $filterValue, string $columnName): string {
|
||||
$sql = "";
|
||||
if (!empty($filterValue)) {
|
||||
$filterItems = explode(" ", $filterValue);
|
||||
foreach ($filterItems as $item) {
|
||||
$sql .= " AND `$columnName` LIKE '%" . $item . "%'";
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public static function createJobsUntilToday(): array {
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
// $i = first day of the month; $i <= today; $i += 1 day
|
||||
$values = [];
|
||||
for ($i = strtotime(date("Y-m-01")); $i <= strtotime(date("Y-m-d")); $i += 86400) {
|
||||
$values[] = "('" . date("Y-m-d", $i) . "')";
|
||||
}
|
||||
|
||||
$valueStr = implode(", ", $values);
|
||||
|
||||
$db->query("INSERT INTO `VoiceCallHistoryJob` (`date`) VALUES $valueStr ON DUPLICATE KEY UPDATE date=VALUES(date)");
|
||||
$db->query("UPDATE `VoiceCallHistoryJob` SET `status` = 'created', `finished` = NULL WHERE `date` = '" . date("Y-m-d") . "'");
|
||||
|
||||
return [
|
||||
"message" => "Created " . count($values) . " jobs."
|
||||
];
|
||||
}
|
||||
|
||||
public static function updateJobStatus($id, $status): array {
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
$escapedStatus = $db->escape($status);
|
||||
$escapedId = $db->escape($id);
|
||||
|
||||
$finished = $status == "success" ? ", `finished` = NOW()" : "";
|
||||
|
||||
$db->query("UPDATE `VoiceCallHistoryJob` SET `status` = '$escapedStatus' $finished WHERE `id` = $escapedId");
|
||||
|
||||
return [
|
||||
"message" => "Updated job $id status to $status."
|
||||
];
|
||||
}
|
||||
|
||||
public static function getJobsNotDone(): array {
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
$query = $db->query("SELECT * FROM `VoiceCallHistoryJob` WHERE `status` = 'created' OR `status` = 'failed' OR `status` = 'pending' ORDER BY `date`");
|
||||
|
||||
$items = [];
|
||||
if($db->num_rows($query)) {
|
||||
while($data = $db->fetch_object($query)) {
|
||||
$items[] = new VoiceCallHistoryJobModel($data);
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
}
|
||||
42
db/migrations/20240410150500_add_voice_call_history.php
Normal file
42
db/migrations/20240410150500_add_voice_call_history.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php /** @noinspection ALL */
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddVoiceCallHistory extends AbstractMigration {
|
||||
public function up(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
|
||||
//VoiceCallHistory Table
|
||||
$voiceCallHistoryTable = $this->table("VoiceCallHistory", ["signed" => true]);
|
||||
$voiceCallHistoryTable->addColumn("id", "integer", ["identity" => true]);
|
||||
$voiceCallHistoryTable->addColumn("uid", "string", ["null" => false, "limit" => 255]);
|
||||
$voiceCallHistoryTable->addColumn("voice_account", "string", ["null" => false, "limit" => 255]);
|
||||
$voiceCallHistoryTable->addColumn("start", "datetime", ["null" => false]);
|
||||
$voiceCallHistoryTable->addColumn("source", "string", ["null" => true, "limit" => 255]);
|
||||
$voiceCallHistoryTable->addColumn("destination", "string", ["null" => true, "limit" => 255]);
|
||||
$voiceCallHistoryTable->addColumn("state", "integer", ["null" => true]);
|
||||
$voiceCallHistoryTable->addColumn("billable", "integer", ["null" => false, "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]);
|
||||
$voiceCallHistoryTable->addColumn("duration", "integer", ["null" => false]);
|
||||
|
||||
//VoiceCallHistory Table Indexes
|
||||
$voiceCallHistoryTable->addIndex(["billable"]);
|
||||
$voiceCallHistoryTable->addIndex(["voice_account"]);
|
||||
|
||||
//VoiceCallHistoryJob Table
|
||||
$voiceCallHistoryJobTable = $this->table("VoiceCallHistoryJob", ["signed" => true]);
|
||||
$voiceCallHistoryJobTable->addColumn("id", "integer", ["identity" => true]);
|
||||
$voiceCallHistoryJobTable->addColumn("date", "date", ["null" => false]);
|
||||
$voiceCallHistoryJobTable->addColumn("status", "enum", ["values" => ["created", "pending", "running", "success", "failed"], "default" => "created", "null" => false]);
|
||||
$voiceCallHistoryJobTable->addColumn("finished", "date", ["null" => true]);
|
||||
$voiceCallHistoryJobTable->addIndex(["date"], ["unique" => true]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {
|
||||
if ($this->getEnvironment() == "thetool") {
|
||||
$this->table("VoiceCallHistory")->drop()->save();
|
||||
$this->table("VoiceCallHistoryJob")->drop()->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,84 @@
|
||||
* @property {string} class - The CSS class(es) applied to the column.
|
||||
*/
|
||||
|
||||
//TODO: export this to its own file
|
||||
Vue.component('tt-date-range', {
|
||||
template: `
|
||||
<input type="text" class="form-control form-control-sm" ref="input" @click="initialize" style="cursor: pointer;background-color: #ffffff">
|
||||
`, props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
inputValue: '',
|
||||
isInitialized: false,
|
||||
locale: {
|
||||
"format": "DD.MM.YYYY HH:mm",
|
||||
"separator": " - ",
|
||||
"applyLabel": "Übernehmen",
|
||||
"cancelLabel": "Abbrechen",
|
||||
"fromLabel": "Von",
|
||||
"toLabel": "Bis",
|
||||
"customRangeLabel": "Benutzerdefiniert",
|
||||
"weekLabel": "W",
|
||||
"daysOfWeek": [
|
||||
"So",
|
||||
"Mo",
|
||||
"Di",
|
||||
"Mi",
|
||||
"Do",
|
||||
"Fr",
|
||||
"Sa"
|
||||
],
|
||||
"monthNames": [
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember"
|
||||
],
|
||||
"firstDay": 1
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initialize() {
|
||||
if (!this.isInitialized) {
|
||||
this.isInitialized = true;
|
||||
$(this.$refs.input).daterangepicker({
|
||||
autoUpdateInput: true,
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
locale: this.locale,
|
||||
});
|
||||
|
||||
this.$refs.input.click();
|
||||
|
||||
const _this = this;
|
||||
$(this.$refs.input).on('apply.daterangepicker', function(ev, picker) {
|
||||
console.log('now emitting chang', picker.startDate.unix(), picker.endDate.unix());
|
||||
_this.$emit('change', {
|
||||
target: {
|
||||
value: {
|
||||
from: picker.startDate.unix() + 7200,
|
||||
to: picker.endDate.unix() + 7200
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
$(this.$refs.input).off('apply.daterangepicker');
|
||||
},
|
||||
})
|
||||
|
||||
Vue.component('tt-table', {
|
||||
template: `
|
||||
<div class="card tt-table-card">
|
||||
@@ -65,7 +143,7 @@ Vue.component('tt-table', {
|
||||
</li>
|
||||
</ul>
|
||||
<span class="text-center"
|
||||
v-text="Math.min(pagination.page * pagination.per_page - pagination.per_page + 1, pagination.total_rows)
|
||||
v-text="Math.max(pagination.page * pagination.per_page - pagination.per_page + 1, pagination.total_rows)
|
||||
+ ' bis ' + Math.min(pagination.page * pagination.per_page, pagination.total_rows) + ' von ' + pagination.total_rows"></span>
|
||||
<select v-model="pagination.per_page" v-on:change="fetchRows(1)" class="form-control form-control-sm">
|
||||
<option value="10">10</option>
|
||||
@@ -82,7 +160,7 @@ Vue.component('tt-table', {
|
||||
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" v-for="column in columns" style="vertical-align: top; text-align: center">
|
||||
<th scope="col" v-for="column in columns" :style="'vertical-align: top; text-align: center;' + (column.filter === 'dateRange' ? 'min-width: 260px;' : '')">
|
||||
<div style="text-align:center">{{ column.text }}</div>
|
||||
<input v-if="column.filter === 'search'" type=text v-on:input="applyFilter($event, column.key)"
|
||||
class="form-control form-control-sm">
|
||||
@@ -93,6 +171,7 @@ Vue.component('tt-table', {
|
||||
{{ filterOption.text }}
|
||||
</option>
|
||||
</select>
|
||||
<tt-date-range v-if="column.filter === 'dateRange'" v-on:change="applyFilter($event, column.key)"></tt-date-range>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
Reference in New Issue
Block a user