diff --git a/Layout/default/header.php b/Layout/default/header.php
index cf5329afd..34df75eb0 100644
--- a/Layout/default/header.php
+++ b/Layout/default/header.php
@@ -56,7 +56,7 @@
-
+
diff --git a/application/Cpeprovisioning/CpeprovisioningController.php b/application/Cpeprovisioning/CpeprovisioningController.php
index 0701e0316..682bbb35c 100644
--- a/application/Cpeprovisioning/CpeprovisioningController.php
+++ b/application/Cpeprovisioning/CpeprovisioningController.php
@@ -5,12 +5,11 @@ class CpeprovisioningController extends mfBaseController
protected function init()
{
$this->needlogin = true;
- $me = new User();
- $me->loadMe();
- $this->me = $me;
- $this->layout()->set("me", $me);
+ $this->me = new User();
+ $this->me->loadMe();
+ $this->layout()->set("me", $this->me);
- if (!$me->is(["Admin"])) {
+ if (!$this->me->is(["Admin"])) {
$this->redirect("Dashboard");
}
}
@@ -283,11 +282,275 @@ class CpeprovisioningController extends mfBaseController
$query["filter"] = $this->request->filter;
}
- $qs = http_build_query($query);
-
$this->layout()->setFlash("Eintrag erfolgreich gespeichert.", "success");
- $this->redirect("Cpeprovisioning", "Index", $qs);
-
+ $this->redirect("Cpeprovisioning", "Index", http_build_query($query));
}
-}
+ protected function apiSaveAction() {
+ try {
+ $p = json_decode(file_get_contents('php://input'), true);
+
+ $id = $p['id'] ?? null;
+ $mode = $id ? "edit" : "add";
+
+ if ($mode === 'edit') {
+ $cpe = new Cpeprovisioning($id);
+ if (!$cpe->id) throw new Exception("Eintrag nicht gefunden");
+ }
+
+ if (empty($p['termination_id']) && empty($p['order_id'])) throw new Exception("Anschluss oder Bestellung nicht gefunden");
+ if (!OrderProductModel::getFirst(["order_id" => $p['order_id'], "termination_id" => $p['termination_id']])) throw new Exception("Anschluss gehört nicht zur Bestellung");
+
+ if (!empty($p['ont_sn'])) {
+ $termination = new Termination($p['termination_id']);
+ $orig_sn = $termination->getWorkflowvalue("ont_sn", "string");
+
+ if ($orig_sn === null) {
+ if ($sn_item = WorkflowitemModel::getFirst(["name" => "ont_sn", "object_type" => "termination"])) {
+ $sn_item->setObjectId($p['termination_id']);
+ $termination->workflowitems["ont_sn"] = $sn_item;
+ } else {
+ $this->log->error("ont_sn workflow item not found");
+ }
+ }
+
+ if ($p['ont_sn'] !== $orig_sn && isset($termination->workflowitems["ont_sn"])) {
+ $termination->workflowitems["ont_sn"]->value->setValue($p['ont_sn']);
+ $termination->workflowitems["ont_sn"]->value->save();
+ }
+ }
+
+ $data = [
+ "termination_id" => $p['termination_id'] ?: null,
+ "order_id" => $p['order_id'],
+ "orderproduct_id" => $p['orderproduct_id'],
+ "routerconfig_finished" => (int)($p['routerconfig_finished'] ?? 0),
+ "shipping" => (int)($p['shipping'] ?? 0),
+ "routertype" => $p['routertype'] ?? null,
+ "wifi_ssid" => $p['wifi_ssid'] ?? null,
+ "wifi_pass" => $p['wifi_pass'] ?? null,
+ "mac" => $p['mac'] ?? null,
+ "note" => $p['note'] ?? null,
+ "edit_by" => $this->me->id,
+ ];
+
+ foreach (['ship_weight', 'ship_length', 'ship_width', 'ship_height'] as $key) $data[$key] = empty($p[$key]) ? null : $p[$key];
+ foreach (['public', 'nat', 'ipv6'] as $type) $data["vlan_{$type}"] = !empty($p['vlans'][$type]['checked']) ? ($p['vlans'][$type]['tag'] ?? null) : null;
+
+ if ($mode === 'add') {
+ $data["create_by"] = $this->me->id;
+ $cpe = CpeprovisioningModel::create($data);
+ } else $cpe->update($data);
+
+ if (!$cpe->save()) throw new Exception("Fehler beim Speichern");
+
+ if ($cpe->routerconfig_finished) {
+ $order_product = new OrderProduct($p['orderproduct_id']);
+
+ $shipping_text = $cpe->shipping
+ ? "zum Versand vorbereitet"
+ : "vorbereitet für Techniker zur Vorortinstallation";
+
+ $text = "CPE zu Produkt \"{$order_product->product->name}\" {$shipping_text}.\n\n"
+ . "Router: {$cpe->routertype}\n"
+ . "Zugangstyp: {$order_product->product->attributes['bras_type']->value}\n";
+
+ if ($cpe->vlan_public) $text .= "Vlan Public: {$cpe->vlan_public}\n";
+ if ($cpe->vlan_nat) $text .= "Vlan NAT: {$cpe->vlan_nat}\n";
+ if ($cpe->vlan_ipv6) $text .= "Vlan IPv6: {$cpe->vlan_ipv6}\n";
+
+ $journal = new OrderJournal();
+ $journal->order_id = $p['order_id'];
+ $journal->text = $text;
+ $journal->create_by = $this->me->id;
+ $journal->edit_by = $this->me->id;
+
+ if (!($journal_id = $journal->save())) {
+ $this->layout()->setFlash("Konnte nicht ins Bestelljournal schreiben!", "warning");
+ } else {
+ $cpe->order_journal_id = $journal_id;
+ $cpe->save();
+ }
+ }
+
+ self::returnJson(['success' => true, 'message' => 'Eintrag erfolgreich gespeichert.']);
+
+ } catch (Exception $e) {
+ http_response_code(400);
+ self::returnJson(['success' => false, 'message' => $e->getMessage()]);
+ }
+ }
+ protected function apiGetAction()
+ {
+ $p = json_decode(file_get_contents('php://input'), true) ?? [];
+
+ // --- Pagination and Sorting setup ---
+ $page = (int)($p['pagination']['page'] ?? 1);
+ $perPage = (int)($p['pagination']['per_page'] ?? 25);
+ $orderBy = $p['order']['key'] ?? null;
+ $orderDir = $p['order']['order'] ?? 'asc';
+
+ // Calculate start and end indexes for manual pagination, just like in the old indexAction
+ $start = ($page - 1) * $perPage;
+ $end = $start + $perPage;
+
+ // --- Data Fetching (same as before) ---
+ $searchFilter = $this->getPreparedFilter($p['filters'] ?? []);
+ // if routerconfig_finished === 0 then we can add key add-where to searchFilter and only show orders withing the last 180 days either by create or edit (caution this is unix timestamp)
+ if (isset($searchFilter['routerconfig_finished']) && !$searchFilter['routerconfig_finished']) {
+ $searchFilter['add-where'] = "`Order`.create > " . (time() - 365 * 86400) . " OR `Order`.edit > " . (time() - 365 * 86400);
+ }
+
+ $orders = OrderModel::search($searchFilter); // cpeprovisioning_enabled
+ // Use the same uncached precache from indexAction to get all potential products
+// $prefetched = OrderProductModel::precache("`Terminationstatus`.`code` < " . TT_TERMSTATUS_CONNECTED . " AND `ProducttechAttribute`.`name` = 'bras_type'");
+ $sqlWhere = "`ProducttechAttribute`.`name` = 'bras_type'";
+ // if filter routerconfig_finished === 0 then we can filter the precache by routerconfig_finished = 0 or is null
+ if (isset($searchFilter['routerconfig_finished']) && !$searchFilter['routerconfig_finished']) {
+// $sqlWhere .= " AND (`Cpeprovisioning`.`routerconfig_finished` = 0 OR `Cpeprovisioning`.`routerconfig_finished` IS NULL)";
+ }
+
+ $prefetched = OrderProductModel::precache($sqlWhere);
+
+ $paginatedRows = [];
+ $totalRows = 0;
+ $orderInfoCache = []; // Cache for order-level info to avoid repeated calculations
+
+ foreach ($orders as $order) {
+ // --- Apply the same initial filters as the old and new methods ---
+ if (($searchFilter["hide_delayed_finish"] ?? false) && $order->finish_after && $order->finish_after > strtotime('+31 days')) continue;
+ if (isset($prefetched['terminations'][$order->id][0]) && !$order->cpeprovisioning_enabled && $prefetched['terminations'][$order->id][0]['statuscode'] < TT_TERMSTATUS_CONNECTED) continue;
+ if (empty($prefetched[$order->id])) continue;
+
+ // Loop through the prefetched raw data, similar to indexAction
+ foreach ($prefetched[$order->id] as $opData) {
+ // --- Apply the same product-level filters ---
+ if (!is_array($opData) || ($opData['routerconfig_finished'] xor ($searchFilter['routerconfig_finished'] ?? false)) || empty($opData['attributes']['bras_type'])) continue;
+
+ // This item is a valid candidate for the list.
+ $totalRows++;
+
+ // *** THE CORE PERFORMANCE IMPROVEMENT ***
+ // If the current item is not on the page we want, skip the expensive processing below.
+ if ($totalRows <= $start || $totalRows > $end) {
+ continue;
+ }
+
+ // --- Now, do the expensive processing ONLY for the items on the current page ---
+
+ // Calculate and cache order-level info only when it's first needed for a visible item
+ if (!isset($orderInfoCache[$order->id])) {
+ $orderInfo = ['vot' => false, 'hw' => [], 'voip' => false];
+ foreach ($order->products as $prod) {
+ $attrs = $prod->product->attributes ?? [];
+ if (empty($attrs) || !is_array($attrs)) continue;
+
+ if ($attrs['hw_only']->value ?? false) $orderInfo['hw'][] = (int)$prod->amount . "x " . $prod->product->name;
+ if ($attrs['addon']->value ?? false) $orderInfo['hw'][] = $prod->product->name;
+ if ($attrs['voip_chan']->value ?? false) $orderInfo['voip'] = true;
+ if ($attrs['vot']->value ?? false) $orderInfo['vot'] = true;
+ }
+ $orderInfoCache[$order->id] = $orderInfo;
+ }
+ $orderInfo = $orderInfoCache[$order->id];
+
+ // Hydrate the full model object, but only for this one item
+ $product = OrderProductModel::getOne($opData['id']);
+ $term = $product->termination;
+ $attrs = $product->product->attributes;
+ $cpe = $product->cpeprovisioning;
+
+ $vlanPublicDefault = $term ? $term->getPop()->vlan_public : ($attrs['vlan_default_public']->value ?? null);
+ $vlanNatDefault = $term ? $term->getPop()->vlan_nat : ($attrs['vlan_default_nat']->value ?? null);
+ $vlanIpv6Default = $term ? $term->getPop()->vlan_ipv6 : ($attrs['vlan_default_ipv6']->value ?? null);
+
+ /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
+ $paginatedRows[] = [
+ 'id' => $product->id, 'order_id' => $product->order_id, 'termination_id' => $product->termination_id, 'orderproduct_id' => $product->id,
+ 'network' => $term->building->network->name ?? "{$order->owner->zip} {$order->owner->city}",
+ 'spin' => $order->owner->spin, 'customer' => $order->owner->getCompanyOrName(),
+ 'product_name' => $product->product->name, 'product_code' => $term->code ?? '',
+ 'access_type' => $attrs['bras_type']->value,
+ 'access_type_down' => $attrs["bw_down"]->value,
+ 'access_type_up' => $attrs["bw_up"]->value,
+ 'ont_deployed' => $term ? $term->getWorkflowValue("ont_deployed", "int") : 0,
+ 'ont_sn' => $term ? $term->getWorkflowValue("ont_sn", "string") : null,
+ 'vot' => $orderInfo['vot'] || $order->install_date,
+ 'hw' => !empty($orderInfo['hw']) ? implode("
", $orderInfo['hw']) : null,
+ 'voip' => $orderInfo['voip'],
+ 'note' => $order->note,
+ 'show_snopp_button' => ($attrs['hostnetwork_order']->value ?? 0) == 1 && !str_contains($product->product->name, 'XDSL'),
+ 'snopp_url' => 'https://snopp.breitband-steiermark.at/Termination/index?filter[status][]=connected&filter[address]=' . urlencode($order->owner->street),
+ 'vlans' => [
+ 'public' => ['tag' => $cpe->vlan_public ?? $vlanPublicDefault, 'checked' => $product->cpeprovisioning->vlan_public],
+ 'nat' => ['tag' => $cpe->vlan_nat ?? $vlanNatDefault, 'checked' => $product->cpeprovisioning->vlan_nat],
+ 'ipv6' => ['tag' => $cpe->vlan_ipv6 ?? $vlanIpv6Default, 'checked' => $product->cpeprovisioning->vlan_ipv6],
+ ],
+ 'cpe_id' => $cpe->id ?? null,
+ 'cpe_data' => $this->fixCpeData($cpe->data ?? null),
+ ];
+ }
+ }
+
+ // Sort the final (small) array of results for the current page
+ if ($orderBy) {
+ usort($paginatedRows, fn($a, $b) => ($orderDir === 'asc' ? 1 : -1) * strnatcasecmp($a[$orderBy] ?? '', $b[$orderBy] ?? ''));
+ }
+
+ // No need for array_slice, we already built the paginated list manually
+ self::returnJson([
+ 'rows' => $paginatedRows,
+ 'pagination' => [
+ 'page' => $page, 'per_page' => $perPage, 'total_rows' => $totalRows,
+ 'filtered_available' => $totalRows, 'total_pages' => ceil($totalRows / $perPage)
+ ]
+ ]);
+ }
+
+ protected function newIndexAction()
+ {
+ $this->layout()->set('additionalJS', ['js/pages/Cpeprovisioning/Cpeprovisioning.js']);
+ $this->layout()->set('additionalHead', ['']);
+
+ Helper::renderVue(
+ $this,
+ "Cpeprovisioning", // The root Vue component name
+ "CPE Provisioning", // The page title
+ [
+ // Pass API URLs and initial data to the frontend
+ "CPE_PROV_API_GET_URL" => $this->getUrl("Cpeprovisioning", "apiGet"),
+ "CPE_PROV_API_SAVE_URL" => $this->getUrl("Cpeprovisioning", "apiSave"),
+ "ORDER_URL" => $this->getUrl("Order"),
+ "NETWORKS" => NetworkModel::getAll(),
+ "ROUTER_OPTIONS" => [
+ // General Options
+ ['value' => 'eigener Router', 'text' => 'Eigener Router'],
+ ['value' => 'anderes CPE', 'text' => 'Anderes CPE'],
+ // PPPoE/DHCP Routers
+ ['value' => 'TP-Link Archer C80', 'text' => 'TP-Link Archer C80 (Inet, IPTV)'],
+ ['value' => 'FritzBox 4040', 'text' => 'FritzBox 4040 (Inet, IPTV)'],
+ ['value' => 'FritzBox 4050', 'text' => 'FritzBox 4050 (Inet, Phone IPTV)'],
+ ['value' => 'FritzBox 5530', 'text' => 'FritzBox 5530 (Inet FiberP2P, Phone, IPTV)'],
+ ['value' => 'FritzBox 7530', 'text' => 'FritzBox 7530 (Inet, Phone, IPTV)'],
+ ['value' => 'FritzBox 7590', 'text' => 'FritzBox 7590 (Inet, Phone, IPTV)'],
+ ['value' => 'FritzBox 7690', 'text' => 'FritzBox 7690 (Inet, Phone, IPTV)'],
+ // Static Routers
+ ['value' => 'Mikrotik HAP AC', 'text' => 'Mikrotik HAP AC (Inet, IPTV)'],
+ ['value' => 'Mikrotik HEX S', 'text' => 'Mikrotik HEX S (Inet, IPTV)'],
+ ['value' => 'Mikrotik RB3011', 'text' => 'Mikrotik RB3011 (Inet, IPTV)'],
+ // CMTS Routers
+ ['value' => 'FritzBox 6490 Cable', 'text' => 'FritzBox 6490 Cable (Inet, Phone, IPTV)'],
+ ]
+ ]
+ );
+ }
+
+ private function fixCpeData($data) {
+ if (!$data) return [];
+ $data->shipping = (bool)$data->shipping;
+ $data->routerconfig_finished = (bool)$data->routerconfig_finished;
+ return $data;
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/Order/OrderModel.php b/application/Order/OrderModel.php
index f491dae57..caee15c8a 100644
--- a/application/Order/OrderModel.php
+++ b/application/Order/OrderModel.php
@@ -529,6 +529,13 @@ class OrderModel {
}
}
+
+ if(array_key_exists("add-where", $filter)) {
+ $add_where = $filter['add-where'];
+ if($add_where) {
+ $where .= " AND ($add_where)";
+ }
+ }
//var_dump($filter, $where);exit;
return $where;
diff --git a/application/OrderProduct/OrderProductModel.php b/application/OrderProduct/OrderProductModel.php
index 98c3759f5..e9ab464d6 100644
--- a/application/OrderProduct/OrderProductModel.php
+++ b/application/OrderProduct/OrderProductModel.php
@@ -141,7 +141,7 @@ class OrderProductModel
return $items;
}
- public static function precache()
+ public static function precache($where = false): array
{
$items = [];
$db = FronkDB::singleton();
@@ -162,7 +162,7 @@ class OrderProductModel
";
//mfLoghandler::singleton()->debug($sql);
- $res = $db->query($sql);
+ $res = $db->query($sql . ($where ? " WHERE $where" : ""));
if ($db->num_rows($res)) {
$oldProduct = "";
$oldOrder = "";
diff --git a/db/migrations/20250721222300_cpeprov_add_new_indexes.php b/db/migrations/20250721222300_cpeprov_add_new_indexes.php
new file mode 100644
index 000000000..894fd5c05
--- /dev/null
+++ b/db/migrations/20250721222300_cpeprov_add_new_indexes.php
@@ -0,0 +1,52 @@
+getEnvironment() == 'thetool') {
+ $orderProduct = $this->table('OrderProduct');
+ $orderProduct->addIndex('product_id', ['name' => 'idx_product_id'])
+ ->addIndex('termination_id', ['name' => 'idx_termination_id'])
+ ->save();
+
+ $product = $this->table('Product');
+ $product->addIndex('producttech_id', ['name' => 'idx_producttech_id'])
+ ->save();
+
+ $productAttribute = $this->table('ProductAttribute');
+ $productAttribute->addIndex('producttechattribute_id', ['name' => 'idx_producttechattribute_id'])
+ ->save();
+
+ $termination = $this->table('Termination');
+ $termination->addIndex('status_id', ['name' => 'idx_status_id'])
+ ->save();
+ }
+ }
+
+ public function down(): void
+ {
+ if ($this->getEnvironment() == 'thetool') {
+ $orderProduct = $this->table('OrderProduct');
+ $orderProduct->removeIndexByName('idx_product_id')
+ ->removeIndexByName('idx_termination_id')
+ ->save();
+
+ $product = $this->table('Product');
+ $product->removeIndexByName('idx_producttech_id')
+ ->save();
+
+ $productAttribute = $this->table('ProductAttribute');
+ $productAttribute->removeIndexByName('idx_producttechattribute_id')
+ ->save();
+
+ $termination = $this->table('Termination');
+ $termination->removeIndexByName('idx_status_id')
+ ->save();
+ }
+ }
+}
diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile
index 432a43e52..8561a7318 100644
--- a/docker/php/Dockerfile
+++ b/docker/php/Dockerfile
@@ -37,3 +37,14 @@ RUN echo "* * * * * /root/clean_old_logs.sh" > /etc/cron.d/clean_old_logs && \
# Start Apache in the foreground
CMD ["apachectl", "-D", "FOREGROUND"]
+
+
+# Install XDEBUG
+# apt install -y php8.2-xdebug
+#
+# cat <<'EOF' > /etc/php/8.2/apache2/conf.d/99-xdebug-custom.ini
+ #[xdebug]
+ #xdebug.mode=profile
+ #xdebug.start_with_request=trigger
+ #xdebug.output_dir="/tmp/xdebug_profiles"
+ #EOF
\ No newline at end of file
diff --git a/docker/php/apache.conf b/docker/php/apache.conf
index 29f0d6673..dd7e83203 100644
--- a/docker/php/apache.conf
+++ b/docker/php/apache.conf
@@ -6,4 +6,24 @@