diff --git a/Layout/default/AddressDB/Index.php b/Layout/default/AddressDB/Index.php
index 7e6d30357..549314120 100644
--- a/Layout/default/AddressDB/Index.php
+++ b/Layout/default/AddressDB/Index.php
@@ -225,7 +225,7 @@
Straße |
Hausnr. |
Stiege |
- Wohneinheiten |
+ Homes/Preorders |
Rimo-ID |
Rollout Jahr |
Rollout Info |
@@ -247,6 +247,12 @@
=count($address->wohneinheiten)?>
tool_building_type == 1) ? "EFH" : "MPH")?>">
tool_building_type == 1) ? "fa-home" : "fa-building")?>">
+
+
+
+ =PreorderModel::countActive(['adb_hausnummer_id' => $address->id])?>
+
+
|
=str_replace('_', '_', $address->rimo_id)?>
|
diff --git a/Layout/default/Preorder/Index.php b/Layout/default/Preorder/Index.php
index 3fa4d331d..216caf763 100644
--- a/Layout/default/Preorder/Index.php
+++ b/Layout/default/Preorder/Index.php
@@ -463,7 +463,7 @@ $pagination_entity_name = "Vorbestellungen";
+
+
+
+
+
+
@@ -702,4 +737,177 @@
}
+
+
+
+
\ No newline at end of file
diff --git a/Layout/default/VueViews/PreorderIFrame.php b/Layout/default/VueViews/PreorderIFrame.php
new file mode 100644
index 000000000..8059cb311
--- /dev/null
+++ b/Layout/default/VueViews/PreorderIFrame.php
@@ -0,0 +1,620 @@
+
+
+
+
+
+
+
+ Bestellformular
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/PreorderIFrame/PreorderIFrameController.php b/application/PreorderIFrame/PreorderIFrameController.php
new file mode 100644
index 000000000..9a9fde265
--- /dev/null
+++ b/application/PreorderIFrame/PreorderIFrameController.php
@@ -0,0 +1,133 @@
+preorderIFrameModel = new PreorderIFrameModel();
+ }
+
+ /**
+ * Serves the main order form HTML.
+ * This action injects the necessary configuration into the Vue app.
+ */
+ public function indexAction()
+ {
+ $clusterId = $this->request->get('clusterId', 'NULL');
+ $color = $this->request->get('color', 'blue');
+
+ $vue_config = [
+ 'baseUrl' => '/PreorderIFrame', // URL to this controller
+ 'clusterId' => $clusterId !== NULL ? intval($clusterId) : null,
+ 'color' => htmlspecialchars($color),
+ ];
+
+ $this->layout()->set("JSGlobals", $vue_config);
+ $this->layout()->setTemplate("VueViews/PreorderIFrame");
+ }
+
+ // --- API ENDPOINTS ---
+
+ public function getClustersAction() {
+ self::returnJson(['clusters' => $this->preorderIFrameModel->getClusters($_SERVER['HTTP_X_FRAME_REFERRER'])]);
+ }
+
+ public function findCityAction()
+ {
+ $allowedClusters = $this->preorderIFrameModel->getClusters($_SERVER['HTTP_X_FRAME_REFERRER']);
+
+ $zip = $this->request->get('zip');
+ $clusterId = $this->request->get('cluster_id');
+ $cities = $this->preorderIFrameModel->findCities($zip, $clusterId);
+ self::returnJson(['cities' => $cities]);
+ }
+
+ public function findStreetAction()
+ {
+// $this->checkOriginAndGetCampaign(); // Security check
+ $zip = $this->request->get('zip');
+ $city = $this->request->get('city');
+ $clusterId = $this->request->get('cluster_id');
+ $streets = $this->preorderIFrameModel->findStreets($zip, $city, $clusterId);
+ self::returnJson(['streets' => $streets]);
+ }
+
+ public function findAddressAction()
+ {
+ $addresses = $this->preorderIFrameModel->findAddresses($_GET);
+ self::returnJson(['addresses' => $addresses]);
+ }
+
+ public function submitOrderAction()
+ {
+ $requestBody = file_get_contents('php://input');
+ $preorderData = json_decode($requestBody, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) self::sendError("Invalid JSON data.");
+
+ $tt_network = NetworkModel::getFirst(['adb_network_id' => $preorderData['additionalData']['clusterId']]);
+ if (!$tt_network) self::sendError("No network found for the given cluster ID.");
+
+ $campaign = PreordercampaignModel::getFirst(['network_id' => $tt_network->id]);
+ if (!$campaign) self::sendError("No campaign found for the given cluster ID.");
+
+ $h = new ADBHausnummer($preorderData['address']['hausnummer_id']);
+ if (!$h->id) self::sendError("Invalid house number ID provided.");
+
+ $w = new ADBWohneinheit($preorderData['address']['wohneinheit_id']);
+ if ($preorderData['address']['wohneinheit_id'] && !$w->id) self::sendError("Invalid unit ID provided.");
+
+ $data = [];
+ $data['preordercampaign_id'] = $campaign->id;
+ $data['adb_hausnummer_id'] = $preorderData['address']['hausnummer_id'];
+ $data['adb_wohneinheit_id'] = $preorderData['address']['wohneinheit_id'];
+
+
+ $new_status = null;
+ if ($data['adb_wohneinheit_id'] && $w->id) {
+ $status_code = max($w->status->code, $w->hausnummer->status->code);
+ $new_status = PreorderstatusModel::getFirst(["code" => $status_code]);
+ } elseif ($data['adb_hausnummer_id'] && $h->id) {
+ $new_status = PreorderstatusModel::getFirst(["code" => $h->status->code]);
+ }
+ $data["status_id"] = $new_status ? $new_status->id : 1;
+
+ $data['type'] = $preorderData['connectionType'] === 'vorsorge' ? 'provision' : 'order';
+ $data['connection_type'] = $preorderData['customerType'] === 'business' ? 'business' : 'single-dwelling';
+
+ $data['accept_agb'] = $preorderData['acceptAgb'] ? 1 : 0;
+ $data['accept_dsgvo'] = $preorderData['acceptDsgvo'] ? 1 : 0;
+ $data['accept_marketing'] = $preorderData['acceptMarketing'] ? 1 : 0;
+ $data['accept_withdrawal'] = $preorderData['acceptWithdrawal'] ? 1 : 0;
+ $data['submit_request'] = json_encode($preorderData);
+
+ $data['firstname'] = trim($preorderData['customer']['firstname']);
+ $data['lastname'] = trim($preorderData['customer']['lastname']);
+ $data['company'] = (trim($preorderData['customer']['company'])) ?: null;
+ $data['street'] = (trim($preorderData['customer']['street'])) ?: null;
+ $data['housenumber'] = (trim($preorderData['customer']['housenumber'])) ?: null;
+ $data['zip'] = (trim($preorderData['customer']['zip'])) ?: null;
+ $data['city'] = (trim($preorderData['customer']['city'])) ?: null;
+ $data['phone'] = (trim($preorderData['customer']['phone'])) ?: null;
+ $data['email'] = (trim($preorderData['customer']['email'])) ?: null;
+
+ $data['edit_by'] = 1;
+ $data['create_by'] = 1;
+
+ $preorder = PreorderModel::create($data);
+ $preorder->createUcode();
+ $new_id = $preorder->save();
+
+ if (!$new_id) {
+ self::sendError("Failed to create preorder record.");
+ }
+
+ self::returnJson(['orderCode' => $preorder->ucode, 'status' => 'success']);
+ }
+}
\ No newline at end of file
diff --git a/application/PreorderIFrame/PreorderIFrameModel.php b/application/PreorderIFrame/PreorderIFrameModel.php
new file mode 100644
index 000000000..b789b8433
--- /dev/null
+++ b/application/PreorderIFrame/PreorderIFrameModel.php
@@ -0,0 +1,239 @@
+table = 'Preorder'; // Set a primary table if needed, though most methods define their own
+ }
+
+ /**
+ * Checks if an origin has rights for a given cluster and returns the campaign.
+ * @param int $clusterId The 'adb_netzgebiet_id' from the thetool.Network table.
+ * @param string|null $origin The requesting origin (e.g., https://www.example.com).
+ * @return array|null The campaign data if valid, otherwise null.
+ */
+ public function getCampaignByClusterIdAndOrigin(int $clusterId, ?string $origin): ?array
+ {
+ // A null origin is not allowed for security reasons
+ if (!$origin) {
+ return null;
+ }
+
+ $query = "
+ SELECT pc.*
+ FROM thetool.Preordercampaign pc
+ JOIN thetool.Network n ON pc.Network_id = n.id
+ WHERE n.adb_netzgebiet_id = ?
+ ";
+ $res = $this->db->query($query, [$clusterId]);
+ $campaign = $this->db->fetch_assoc($res);
+
+ if (!$campaign || empty($campaign['iframe_origins'])) {
+ return null;
+ }
+
+ $allowedOrigins = json_decode($campaign['iframe_origins'], true);
+ if (is_array($allowedOrigins) && in_array($origin, $allowedOrigins)) {
+ return $campaign;
+ }
+
+ return null;
+ }
+
+ public function getClusters($frame_referrer): array
+ {
+ $query = "
+ SELECT n.adb_netzgebiet_id as id, ng.name
+ FROM thetool.Preordercampaign pc
+ JOIN thetool.Network n ON pc.Network_id = n.id
+ JOIN addressdb.Netzgebiet ng ON n.adb_netzgebiet_id = ng.id
+ WHERE JSON_SEARCH(pc.iframe_origins, 'one', '" . $this->db->escape($frame_referrer) . "') IS NOT NULL
+ GROUP BY n.adb_netzgebiet_id, ng.name
+ ORDER BY ng.name ASC
+ ";
+
+ $res = $this->db->query($query);
+ return $this->db->fetch_all_assoc($res);
+ }
+
+ public function findCities(string $zip, int $adb_network_id): array
+ {
+ $query = "
+ SELECT DISTINCT o.name
+ FROM addressdb.Plz p
+ JOIN addressdb.Ortschaft o ON p.gemeinde_id = o.gemeinde_id
+ JOIN addressdb.Gemeinde g ON o.gemeinde_id = g.id
+ JOIN addressdb.GemeindeNetzgebiet gn ON g.id = gn.gemeinde_id
+ WHERE p.plzstring = " . $this->db->escape($zip) . " AND gn.netzgebiet_id = " . intval($adb_network_id) . "
+ ORDER BY o.name ASC
+ ";
+
+ $res = $this->db->query($query);
+ $cities = $this->db->fetch_all_assoc($res);
+ return array_column($cities, 'name');
+ }
+
+ /**
+ * Finds streets for a given ZIP, city, and cluster.
+ * @param string $zip
+ * @param string $city
+ * @param int $clusterId
+ * @return array
+ */
+ public function findStreets(string $zip, string $city, int $clusterId): array
+ {
+ $query = "
+ SELECT DISTINCT s.name
+ FROM addressdb.Strasse s
+ JOIN addressdb.Gemeinde g ON s.gemeinde_id = g.id
+ JOIN addressdb.Ortschaft o ON o.gemeinde_id = g.id AND o.name = '" . $this->db->escape($city) . "'
+ JOIN addressdb.GemeindeNetzgebiet gn ON g.id = gn.gemeinde_id
+ WHERE gn.netzgebiet_id = " . intval($clusterId) . "
+ AND EXISTS (
+ SELECT 1
+ FROM addressdb.Plz p
+ WHERE p.plzstring = " . $this->db->escape($zip) . "
+ AND p.gemeinde_id = o.gemeinde_id
+ )
+ ORDER BY s.name ASC
+ ";
+ $res = $this->db->query($query);
+ $streets = $this->db->fetch_all_assoc($res);
+ return array_column($streets, 'name');
+ }
+
+ public function findAddresses(array $params): array
+ {
+ $query = "
+ SELECT h.oaid, h.hausnummer, s.name as street, p.plzstring as zip, o.name as city, h.id as hausnummer_id, w.id as wohneinheit_id,
+ h.stiege, h.unit_count as building_unit_count, h.tool_building_type as building_type,
+ w.oaid as unit_oaid, w.stock, w.tuer, w.zusatz
+ FROM addressdb.Hausnummer h
+ JOIN addressdb.Strasse s ON h.strasse_id = s.id
+ JOIN addressdb.Plz p ON h.plz_id = p.id
+ JOIN addressdb.Ortschaft o ON s.gemeinde_id = o.gemeinde_id
+ LEFT JOIN addressdb.Wohneinheit w ON w.hausnummer_id = h.id
+ WHERE h.netzgebiet_id = " . intval($params['cluster_id']) . "
+ AND p.plzstring = " . $this->db->escape($params['zip']) . "
+ AND o.name = '" . $this->db->escape($params['city']) . "'
+ AND s.name = '" . $this->db->escape($params['street']) . "'
+ AND h.hausnummer = '" . $this->db->escape($params['housenumber']) . "'
+ ";
+
+ $results = $this->db->fetch_all_assoc($this->db->query($query));
+ if (empty($results)) return [];
+
+ $addresses = [];
+ $topCounter = 1;
+
+ if (count($results) > 1) {
+ foreach ($results as $row) {
+ $showText = $this->buildShowText($row, $topCounter++);
+ $addresses[] = [
+ 'oaid' => $row['unit_oaid'],
+ 'street' => $row['street'],
+ 'housenumber' => $row['hausnummer'],
+ 'hausnummer_id' => $row['hausnummer_id'],
+ 'wohneinheit_id' => $row['wohneinheit_id'],
+ 'building_type' => intval($row['building_type']),
+ 'zip' => $row['zip'],
+ 'city' => $row['city'],
+ 'stiege' => $row['stiege'],
+ 'stock' => $row['stock'],
+ 'tuer' => $row['tuer'],
+ 'zusatz' => $row['zusatz'],
+ 'building_unit_count' => $row['building_unit_count'],
+ 'showText' => $showText,
+ 'preorderTypes' => ['order']
+ ];
+ }
+ } else {
+ $row = $results[0];
+ $addresses[] = [
+ 'oaid' => $row['oaid'],
+ 'street' => $row['street'],
+ 'housenumber' => $row['hausnummer'],
+ 'hausnummer_id' => $row['hausnummer_id'],
+ 'wohneinheit_id' => $row['wohneinheit_id'],
+ 'building_type' => intval($row['building_type']),
+ 'zip' => $row['zip'],
+ 'city' => $row['city'],
+ 'stiege' => $row['stiege'],
+ 'stock' => $row['stock'],
+ 'tuer' => $row['tuer'],
+ 'zusatz' => $row['zusatz'],
+ 'building_unit_count' => $row['building_unit_count'],
+ 'showText' => $this->buildShowText($row, 1),
+ 'preorderTypes' => ['order']
+ ];
+ }
+
+ return $addresses;
+ }
+
+ private function buildShowText(array $row, int $counter): string
+ {
+ $parts = array_filter([
+ $row['stiege'] ? "Stiege {$row['stiege']}" : null,
+ $row['stock'] ? "Stock {$row['stock']}" : null,
+ $row['tuer'] ? "Tür {$row['tuer']}" : null,
+ $row['zusatz'] ?: null
+ ]);
+
+ return $parts ? implode(', ', $parts) : "Top {$counter}";
+ }
+
+ /**
+ * Creates a new preorder record in the database.
+ * @param array $data The validated preorder data from the form.
+ * @param int $campaignId The ID of the associated preorder campaign.
+ * @return array The result containing the new order code.
+ */
+ public function createPreorder(array $data, int $campaignId): array
+ {
+ $customer = $data['customer'];
+ $address = $data['address'];
+
+ // Generate a unique code for the preorder
+ $ucode = strtoupper(substr(md5(uniqid(rand(), true)), 0, 10));
+
+ $preorderData = [
+ 'ucode' => $ucode,
+ 'thetool.Preordercampaign_id' => $campaignId,
+ 'oaid' => $address['oaid'],
+ 'type' => $data['preorderType'],
+ 'connection_type' => $data['connectionType'],
+ 'contact_type' => $customer['type'],
+ 'firstname' => $customer['firstname'],
+ 'lastname' => $customer['lastname'],
+ 'company' => $customer['company'] ?? null,
+ 'street' => $customer['street'],
+ 'housenumber' => $customer['housenumber'],
+ 'zip' => $customer['zip'],
+ 'city' => $customer['city'],
+ 'phone' => $customer['phone'],
+ 'email' => $customer['email'],
+ 'address_info' => $data['address_info'],
+ 'accept_agb' => $data['acceptAgb'] ? 1 : 0,
+ 'accept_dsgvo' => $data['acceptDsgvo'] ? 1 : 0,
+ 'accept_marketing' => $data['acceptMarketing'] ? 1 : 0,
+ 'accept_withdrawal' => $data['acceptWithdrawal'] ? 1 : 0,
+ 'addon_data' => json_encode($data['additionalData']),
+ 'submit_type' => 'api',
+ 'submit_request' => json_encode($data),
+ 'order_date' => time(),
+ 'create' => time(),
+ 'edit' => time(),
+ 'create_by' => 0, // System user
+ 'edit_by' => 0, // System user
+ ];
+
+ $this->db->insert('Preorder', $preorderData);
+
+ return ['code' => $ucode];
+ }
+}
\ No newline at end of file
diff --git a/application/Preordercampaign/PreordercampaignController.php b/application/Preordercampaign/PreordercampaignController.php
index 6cf2e53f8..9507505fb 100644
--- a/application/Preordercampaign/PreordercampaignController.php
+++ b/application/Preordercampaign/PreordercampaignController.php
@@ -221,6 +221,8 @@ class PreordercampaignController extends mfBaseController {
$data["from_email_name"] = trim($r->from_email_name);
$data["from_email"] = trim($r->from_email);
$data["netowner_fibu_cost_code"] = trim($r->netowner_fibu_cost_code);
+ $data["iframe_origins"] = trim($r->iframe_origins);
+ $data["iframe_consents"] = trim($r->iframe_consents);
if($r->from) {
$data['from'] = self::dateToTimestamp($r->from);
@@ -473,8 +475,8 @@ class PreordercampaignController extends mfBaseController {
// Save Status Email Templates
foreach($r->mailtemplates as $status_code => $status_data) {
$mailtemplate_id = $status_data["mailtemplate_id"];
- $allow_on_skip = $status_data["allow_on_skip"];
- $prevent_previous = $status_data["prevent_previous"];
+ $allow_on_skip = $status_data["allow_on_skip"] ?? false;
+ $prevent_previous = $status_data["prevent_previous"] ?? false;
if(!$mailtemplate_id && !$allow_on_skip && !$prevent_previous) {
$mailtemplates_delete[] = $status_code;
diff --git a/db/migrations/20250617100000_add_preorder_cors_columns.php b/db/migrations/20250617100000_add_preorder_cors_columns.php
new file mode 100644
index 000000000..bb4ee7793
--- /dev/null
+++ b/db/migrations/20250617100000_add_preorder_cors_columns.php
@@ -0,0 +1,35 @@
+getEnvironment() == "thetool") {
+ $Preordercampaign = $this->table("Preordercampaign");
+ $Preordercampaign->addColumn('iframe_origins', 'json', [
+ 'null' => true,
+ 'default' => null,
+ 'comment' => 'JSON array of allowed CORS origins for the IFrame, e.g., ["https://partner-a.com", "https://partner-b.de"]'
+ ]);
+ $Preordercampaign->addColumn('iframe_consents', 'json', [
+ 'null' => true,
+ 'default' => null,
+ 'comment' => 'JSON array of consent information for the IFrame, e.g., [{url: "https://partner-a.com", text: "Partner A Consent", required: true, replace: "Consent"}, {url: "https://partner-b.de", text: "Partner B Consent", required: false, replace: "Consent"}]'
+ ]);
+
+ }
+ }
+
+ public function down(): void
+ {
+ if ($this->getEnvironment() == "thetool") {
+ $Preordercampaign = $this->table("Preordercampaign");
+ $Preordercampaign->removeColumn('iframe_origins')
+ ->removeColumn('iframe_consents')
+ ->update();
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/FronkDB/FronkDB.php b/lib/FronkDB/FronkDB.php
index 98dc060fd..1c60149a3 100644
--- a/lib/FronkDB/FronkDB.php
+++ b/lib/FronkDB/FronkDB.php
@@ -184,6 +184,23 @@ class FronkDB {
return false;
}
+ public function fetch_all_assoc($_res = false) {
+ $array = array();
+ $res = $this->result;
+
+ if($_res)
+ $res = $_res;
+
+ if(!$res)
+ return false;
+
+ while($row = mysqli_fetch_assoc($res)) {
+ $array[] = $row;
+ }
+
+ return $array;
+ }
+
public function insert($_table, $_data, $_forcestr = array(), $options = array()) {
if(empty($_table)) {
$this->lastError = "Error constructing INSERT: tablename ommited";