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 @@ wohneinheiten)?> tool_building_type == 1) ? "EFH" : "MPH")?>"> tool_building_type == 1) ? "fa-home" : "fa-building")?>"> + + + + $address->id])?> + + ', $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";

Liste aller Vorbestellungenname : "" ?>

- +
$filter['preordercampaign_id']]) ?>">Für Begleitschreiben - Status 145
+ +
+ +
+
+ +
+ + + + URL's wo der Bestell-Iframe eingebunden werden darf.
+ Beispiel: https://partner-a.com, https://partner-b.de
+
+
+
+ + +
+ +
+
+ +
+ + + + Zusätzliche Custom Zustimmungen mit Verlinkung + +
+
+ @@ -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";