diff --git a/Layout/default/Callcenterstats/Index.php b/Layout/default/Callcenterstats/Index.php
new file mode 100644
index 000000000..761daf63f
--- /dev/null
+++ b/Layout/default/Callcenterstats/Index.php
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Eingehend
+
+
+
+ | Anzahl eingehende Calls: |
+ =$out["count"]?> |
+
+
+ | Sekunden gesamt: |
+ =$out["seconds"]?> |
+
+
+ | Verrechenbare Minuten: |
+ =$out["billable"]?> |
+
+
+ | Kosten: |
+ € =$out["cost"]?> |
+
+
+
+
+
+
+
Ausgehend
+
+
+
+ | Anzahl ausgehende Calls: |
+ =$in["count"]?> |
+
+
+ | Sekunden gesamt: |
+ =$in["seconds"]?> |
+
+
+ | Verrechenbare Minuten: |
+ =$in["billable"]?> |
+
+
+ | Kosten: |
+ € =$in["cost"]?> |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Layout/default/menu.php b/Layout/default/menu.php
index 0324b0d51..2c076da33 100644
--- a/Layout/default/menu.php
+++ b/Layout/default/menu.php
@@ -113,10 +113,11 @@
"> Vorbestellkampagnen Verrechnung
isAdmin() || !empty(json_decode((new WorkerFlag($me->id, "constructionConsent_projects"))->value() ?? '[]'))): ?>
- "> Zustimmungserklärungen
+ "> Zustimmungserklärungen
is(["Admin"])): ?>"> Emailaussendungen
+ is(["Admin"])): ?>"> Callcenter Auswertung
is(["Admin"])): ?>"> Asset Management
diff --git a/application/CallcenterIdentity/CallcenterIdentity.php b/application/CallcenterIdentity/CallcenterIdentity.php
new file mode 100644
index 000000000..b7acd7f91
--- /dev/null
+++ b/application/CallcenterIdentity/CallcenterIdentity.php
@@ -0,0 +1,198 @@
+$name == null) {
+
+ if($name == "inumbers") {
+ $inumbers = CallcenterIdentityIncomingnumber::search(["callcenteridentity_id" => $this->id]);
+ if($inumbers) {
+ $this->inumbers = $inumbers;
+ }
+ return $this->inumbers;
+ }
+
+ $classname = ucfirst($name);
+ $idfield = $name . "_id";
+ $this->$name = mfValuecache::singleton()->get("mfObjectmodel-$name-" . $this->$idfield);
+ if(!$this->$name) {
+ $this->$name = new $classname($this->$idfield);
+ }
+
+ if($this->$name->id) {
+ mfValuecache::singleton()->set("mfObjectmodel-$name-" . $this->$name->id, $this->$name);
+ return $this->$name;
+ } else {
+ return null;
+ }
+ }
+
+ return $this->$name;
+
+ }
+
+ /********************************
+ * Begin static Model functions
+ */
+
+ public static function create(Array $data) {
+ $model = new CallcenterIdentity();
+
+ $table_fields = [
+ "name", "number", "display",
+ "create_by","edit_by","create","edit"
+ ];
+
+ foreach($data as $field => $value) {
+ if(in_array($field, $table_fields)) {
+ $model->$field = $value;
+ }
+ }
+
+ $me = new User();
+ $me->loadMe();
+
+ if($model->create_by === null) {
+ $model->create_by = $me->id;
+ }
+ if($model->edit_by === null) {
+ $model->edit_by = $me->id;
+ }
+
+ return $model;
+ }
+
+
+ public static function getAll() {
+ $items = [];
+
+ $db = FronkDB::singleton();
+
+ $res = $db->select("CallcenterIdentity", "*", "1 = 1 ORDER BY `order`");
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[] = new CallcenterIdentity($data);
+ }
+ }
+ return $items;
+
+ }
+
+ public static function getFirst($filter) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT * FROM CallcenterIdentity
+ WHERE $where
+ ORDER BY `order` LIMIT 1";
+ //var_dump($sql);exit;
+ mfLoghandler::singleton()->debug($sql);
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ $item = new CallcenterIdentity($data);
+ if($item->id) {
+ return $item;
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public static function count($filter) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT COUNT(*) as cnt FROM CallcenterIdentity
+ WHERE $where";
+
+ //mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ return $data->cnt;
+ }
+ return 0;
+ }
+
+ public static function search($filter, $limit = false, $order = false) {
+ //var_dump($filter);exit;
+ $items = [];
+
+ if(!$order) {
+ $order = "`order` ASC";
+ }
+
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT * FROM CallcenterIdentity
+ WHERE $where
+ ORDER BY $order";
+
+ if(is_array($limit) && count($limit)) {
+ if(is_numeric($limit['start']) && is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['start'].", ".$limit['count'];
+ } elseif(is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['count'];
+ }
+ }
+
+ mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[$data->id] = new CallcenterIdentity($data);
+ }
+ }
+
+ return $items;
+ }
+
+ private static function getSqlFilter($filter) {
+ $where = "1=1 ";
+
+ if(array_key_exists("order", $filter)) {
+ $order = $filter['order'];
+ if(is_numeric($order)) {
+ $where .= " AND CallcenterIdentity.order=$order";
+ }
+ }
+
+ if(array_key_exists("number", $filter)) {
+ $number = FronkDB::singleton()->escape($filter['number']);
+ if($number) {
+ $where .= " AND CallcenterIdentity.number LIKE '$number'";
+ }
+ }
+
+ if(array_key_exists("name", $filter)) {
+ $name = FronkDB::singleton()->escape($filter['name']);
+ if($name) {
+ $where .= " AND CallcenterIdentity.name LIKE '$name'";
+ }
+ }
+
+ if(array_key_exists("display", $filter)) {
+ $display = FronkDB::singleton()->escape($filter['display']);
+ if($display) {
+ $where .= " AND CallcenterIdentity.display LIKE '$display'";
+ }
+ }
+
+ if(array_key_exists("add-where", $filter)) {
+ $where .= " ".$filter['add-where'];
+ }
+
+ //var_dump($filter, $where);exit;
+ return $where;
+ }
+
+}
\ No newline at end of file
diff --git a/application/CallcenterIdentity/CallcenterIdentityController.php b/application/CallcenterIdentity/CallcenterIdentityController.php
new file mode 100644
index 000000000..3cc5ffea0
--- /dev/null
+++ b/application/CallcenterIdentity/CallcenterIdentityController.php
@@ -0,0 +1,26 @@
+needlogin = true;
+ $me = new User();
+ $me->loadMe();
+ $this->me = $me;
+ $this->layout()->set("me", $me);
+
+ if(!$me->is("Admin")) {
+ $this->redirect("Dashboard");
+ }
+ }
+
+ protected function indexAction() {
+ $this->layout()->setTemplate("Callcenterstats/Index");
+
+ $identities = CallcenterIdentity::getAll();
+ $this->layout()->set("identities", $identities);
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/application/CallcenterIdentityIncomingnumber/CallcenterIdentityIncomingnumber.php b/application/CallcenterIdentityIncomingnumber/CallcenterIdentityIncomingnumber.php
new file mode 100644
index 000000000..c44f969c6
--- /dev/null
+++ b/application/CallcenterIdentityIncomingnumber/CallcenterIdentityIncomingnumber.php
@@ -0,0 +1,190 @@
+$name == null) {
+
+
+ $classname = ucfirst($name);
+ $idfield = $name . "_id";
+ $this->$name = mfValuecache::singleton()->get("mfObjectmodel-$name-" . $this->$idfield);
+ if(!$this->$name) {
+ $this->$name = new $classname($this->$idfield);
+ }
+
+ if($this->$name->id) {
+ mfValuecache::singleton()->set("mfObjectmodel-$name-" . $this->$name->id, $this->$name);
+ return $this->$name;
+ } else {
+ return null;
+ }
+ }
+
+ return $this->$name;
+
+ }
+
+ /********************************
+ * Begin static Model functions
+ */
+
+ public static function create(Array $data) {
+ $model = new CallcenterIdentityIncomingnumber();
+
+ $table_fields = [
+ "callcenteridentity_id", "number", "display",
+ "create_by","edit_by","create","edit"
+ ];
+
+ foreach($data as $field => $value) {
+ if(in_array($field, $table_fields)) {
+ $model->$field = $value;
+ }
+ }
+
+ $me = new User();
+ $me->loadMe();
+
+ if($model->create_by === null) {
+ $model->create_by = $me->id;
+ }
+ if($model->edit_by === null) {
+ $model->edit_by = $me->id;
+ }
+
+ return $model;
+ }
+
+
+ public static function getAll() {
+ $items = [];
+
+ $db = FronkDB::singleton();
+
+ $res = $db->select("CallcenterIdentityIncomingnumber", "*", "1 = 1 ORDER BY `number`");
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[] = new CallcenterIdentityIncomingnumber($data);
+ }
+ }
+ return $items;
+
+ }
+
+ public static function getFirst($filter) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT * FROM CallcenterIdentityIncomingnumber
+ ORDER BY `number` LIMIT 1";
+ //var_dump($sql);exit;
+ mfLoghandler::singleton()->debug($sql);
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ $item = new CallcenterIdentityIncomingnumber($data);
+ if($item->id) {
+ return $item;
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public static function count($filter) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT COUNT(*) as cnt FROM CallcenterIdentityIncomingnumber
+ WHERE $where";
+
+ //mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ return $data->cnt;
+ }
+ return 0;
+ }
+
+ public static function search($filter, $limit = false, $order = false) {
+ //var_dump($filter);exit;
+ $items = [];
+
+ if(!$order) {
+ $order = "`number` ASC";
+ }
+
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT * FROM CallcenterIdentityIncomingnumber
+ WHERE $where
+ ORDER BY $order";
+
+ if(is_array($limit) && count($limit)) {
+ if(is_numeric($limit['start']) && is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['start'].", ".$limit['count'];
+ } elseif(is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['count'];
+ }
+ }
+
+ mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[$data->id] = new CallcenterIdentityIncomingnumber($data);
+ }
+ }
+
+ return $items;
+ }
+
+ private static function getSqlFilter($filter) {
+ $where = "1=1 ";
+
+ if(array_key_exists("callcenteridentity_id", $filter)) {
+ $callcenteridentity_id = $filter['callcenteridentity_id'];
+ if(is_numeric($callcenteridentity_id)) {
+ $where .= " AND CallcenterIdentityIncomingnumber.callcenteridentity_id=$callcenteridentity_id";
+ }
+ }
+
+ if(array_key_exists("number", $filter)) {
+ $number = FronkDB::singleton()->escape($filter['number']);
+ if($number) {
+ $where .= " AND CallcenterIdentityIncomingnumber.number LIKE '$number'";
+ }
+ }
+
+ if(array_key_exists("name", $filter)) {
+ $name = FronkDB::singleton()->escape($filter['name']);
+ if($name) {
+ $where .= " AND CallcenterIdentityIncomingnumber.name LIKE '$name'";
+ }
+ }
+
+ if(array_key_exists("display", $filter)) {
+ $display = FronkDB::singleton()->escape($filter['display']);
+ if($display) {
+ $where .= " AND CallcenterIdentityIncomingnumber.display LIKE '$display'";
+ }
+ }
+
+ if(array_key_exists("add-where", $filter)) {
+ $where .= " ".$filter['add-where'];
+ }
+
+ //var_dump($filter, $where);exit;
+ return $where;
+ }
+
+}
\ No newline at end of file
diff --git a/application/Callcenterstats/CallcenterstatsController.php b/application/Callcenterstats/CallcenterstatsController.php
new file mode 100644
index 000000000..f81df557e
--- /dev/null
+++ b/application/Callcenterstats/CallcenterstatsController.php
@@ -0,0 +1,158 @@
+needlogin = true;
+ $me = new User();
+ $me->loadMe();
+ $this->me = $me;
+ $this->layout()->set("me", $me);
+
+ if(!$me->is("Admin")) {
+ $this->redirect("Dashboard");
+ }
+ }
+
+ protected function indexAction() {
+ $this->layout()->setTemplate("Callcenterstats/Index");
+
+ $identities = CallcenterIdentity::getAll();
+ $this->layout()->set("identities", $identities);
+
+ $firstOfLastMonth = new DateTime("now");
+ $firstOfLastMonth->setTime(0,0,0);
+ $firstOfLastMonth->modify("first day of this month");
+ $firstOfLastMonth->modify("-1 month");
+
+ $lastOfLastMonth = clone($firstOfLastMonth);
+ $lastOfLastMonth->modify("last day of this month");
+
+ $today = new DateTime("now");
+ $today->setTime(23,59,59);
+
+ $this->layout()->set("from", $firstOfLastMonth);
+ $this->layout()->set("to", $lastOfLastMonth);
+ $this->layout()->set("today", $today);
+ $this->layout()->set("selected_identities", []);
+
+ if($this->request->run) {
+ return $this->runAction();
+ }
+ }
+
+ protected function runAction() {
+ $identities = $this->request->identities;
+ $from = $this->request->from;
+ $to = $this->request->to;
+ $direction = $this->request->direction;
+
+ $from_ts = self::dateToTimestamp($from);
+ $to_ts = self::dateToTimestamp($to);
+
+ $from_date = new DateTime("@".$from_ts);
+ $from_date->setTimezone(new DateTimeZone("Europe/Vienna"));
+ $to_date = new DateTime("@".$to_ts);
+ $to_date->setTimezone(new DateTimeZone("Europe/Vienna"));
+
+ $this->layout()->set("from", $from_date);
+ $this->layout()->set("to", $to_date);
+
+ $this->layout()->set("selected_identities", $identities);
+
+ $calls = [
+ "in" => [
+ "count" => 0,
+ "seconds" => 0,
+ "billable" => 0,
+ "cost" => 0,
+ ],
+ "out" => [
+ "count" => 0,
+ "seconds" => 0,
+ "billable" => 0,
+ "cost" => 0,
+ ],
+ ];
+
+ $perMinute = 1;
+
+ // Eingehend
+ $destinations = [];
+ foreach($identities as $num) {
+ $ident = CallcenterIdentity::getFirst(["number" => $num]);
+ foreach($ident->inumbers as $inumber) {
+ $inum = $inumber->number;
+ $destinations[] = $inum;
+ $destinations[] = substr($inum, 2);
+ $destinations[] = "0".substr($inum, 2);
+ }
+ }
+
+
+ $sql = "SELECT * FROM VoiceCallHistory WHERE
+ `start` >= '".date("Y-m-d", $from_ts)."' AND `start` <= '".date("Y-m-d", $to_ts)." 23:59:59'
+ AND `destination` IN ('".join("','", $destinations)."')
+ ";
+ //echo "$sql\n
";
+ $res = $this->db->query($sql);
+ while($data = $this->db->fetch_object($res)) {
+ //var_dump($data);
+ $calls["in"]["count"]++;
+ $calls["in"]["seconds"] += (int)$data->duration;
+
+ // 1 euro pro minute
+ $cost = (int)($data->duration / 60);
+ if($data->duration % 60) {
+ // wenn nächste Minute angefangen hat, +1 euro
+ $cost++;
+ }
+ //echo "cost: $cost\n
";
+ $calls["in"]["billable"] += $cost;
+ $calls["in"]["cost"] += $cost * $perMinute;
+ }
+ $this->layout()->set("in", $calls["in"]);
+ //var_dump($calls["in"]);
+
+
+ // ausgehend
+ $sources = [];
+ foreach($identities as $num) {
+ $ident = CallcenterIdentity::getFirst(["number" => $num]);
+ $sources[] = $ident->number;
+ $sources[] = substr($ident->number, 2);
+ $sources[] = "0".substr($ident->number, 2);
+ }
+
+
+ $sql = "SELECT * FROM VoiceCallHistory WHERE
+ `start` >= '".date("Y-m-d", $from_ts)."' AND `start` <= '".date("Y-m-d", $to_ts)." 23:59:59'
+ AND `source` IN ('".join("','", $sources)."')
+ ";
+ //echo "$sql\n
";
+
+ $res = $this->db->query($sql);
+ while($data = $this->db->fetch_object($res)) {
+ $calls["out"]["count"]++;
+ $calls["out"]["seconds"] += $data->duration;
+
+ // 1 euro pro minute
+ $cost = (int)($data->duration / 60);
+ if($data->duration % 60) {
+ // wenn nächste Minute angefangen hat, +1 euro
+ $cost++;
+ }
+ $calls["out"]["billable"] += $cost;
+ $calls["out"]["cost"] += $cost*$perMinute;
+ }
+ $this->layout()->set("out", $calls["out"]);
+ //var_dump($calls["out"]);
+
+ //exit;
+
+
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/db/migrations/20250717135228_callcenter_identity.php b/db/migrations/20250717135228_callcenter_identity.php
new file mode 100644
index 000000000..b13daa243
--- /dev/null
+++ b/db/migrations/20250717135228_callcenter_identity.php
@@ -0,0 +1,51 @@
+getEnvironment() == "thetool") {
+ $ci = $this->table("CallcenterIdentity");
+ $ci->addColumn("name", "string", ["null" => false, "length" => 64]);
+ $ci->addColumn("order", "integer", ["null" => true]);
+ $ci->addColumn("number", "string", ["null" => false, "length" => 64]);
+ $ci->addColumn("display", "string", ["null" => false, "length" => 64]);
+ $ci->addColumn("color", "string", ["null" => false, "length" => 64]);
+ $ci->addColumn("create_by", "integer", ["null" => false]);
+ $ci->addColumn("edit_by", "integer", ["null" => false]);
+ $ci->addColumn("create", "integer", ["null" => false]);
+ $ci->addColumn("edit", "integer", ["null" => false]);
+ $ci->create();
+
+ $ciin = $this->table("CallcenterIdentityIncomingnumber");
+ $ciin->addColumn("callcenteridentity_id", "integer", ["null" => false]);
+ $ciin->addColumn("number", "string", ["null" => false, "length" => 64]);
+ $ciin->addColumn("display", "string", ["null" => false, "length" => 64]);
+ $ciin->addColumn("create_by", "integer", ["null" => false]);
+ $ciin->addColumn("edit_by", "integer", ["null" => false]);
+ $ciin->addColumn("create", "integer", ["null" => false]);
+ $ciin->addColumn("edit", "integer", ["null" => false]);
+ $ciin->create();
+
+ }
+
+ if($this->getEnvironment() == "addressdb") {
+
+ }
+ }
+
+ public function down(): void
+ {
+ if($this->getEnvironment() == "thetool") {
+ $this->table("CallcenterIdentityIncomingnumber")->drop()->save();
+ $this->table("CallcenterIdentity")->drop()->save();
+ }
+
+ if($this->getEnvironment() == "addressdb") {
+
+ }
+ }
+}
diff --git a/lib/mvcfronk/mfBase/mfBaseController.php b/lib/mvcfronk/mfBase/mfBaseController.php
index 29a34f171..6def4f80c 100644
--- a/lib/mvcfronk/mfBase/mfBaseController.php
+++ b/lib/mvcfronk/mfBase/mfBaseController.php
@@ -7,13 +7,18 @@ class mfBaseController
{
protected $log;
protected $needlogin = false;
+ /** @var mfRequest */
protected $request;
private $mfAction;
+ /** @var FronkDB */
private $mfDBI;
+ /** @var Layout */
private $mfLayout;
private $mfMenu;
private $mfUser;
+ /** @var mixed|null Called module (Controller) */
protected $mod;
+ /** @var mixed|null Called action method in Controller */
protected $action;
public function __construct($params = NULL)