diff --git a/application/ADBRimoFcp/ADBRimoFcpController.php b/application/ADBRimoFcp/ADBRimoFcpController.php index f785a234d..5a1c36fb2 100644 --- a/application/ADBRimoFcp/ADBRimoFcpController.php +++ b/application/ADBRimoFcp/ADBRimoFcpController.php @@ -101,4 +101,28 @@ class ADBRimoFcpController extends TTCrud { $counts['upd'], $counts['fcpNF'], $counts['noFCP'], $counts['noExtId']); self::returnJson(['success' => true, 'message' => $msg]); } + + public function MapAction() { + Helper::renderVue($this, "ADBRimoFcpMap", "ADBRimoFcpMap", [ + "MAPBOX_KEY" => TT_MAPBOX_TILE_API_TOKEN, + ]); + } + + + public function getAllFCPsAction() { + $input = json_decode(file_get_contents('php://input'), true); + + $fcpList = ADBRimoFcp::getAll(); + $fcpData = array_map(function ($fcp) { + return [ + 'id' => $fcp->id, +// 'rimo_ex_state' => $fcp->rimo_ex_state, +// 'rimo_op_state' => $fcp->rimo_op_state, + 'gps_lat' => $fcp->gps_lat, + 'gps_long' => $fcp->gps_long + ]; + }, $fcpList); + + self::returnJson(['success' => true, 'data' => $fcpData]); + } } \ No newline at end of file diff --git a/application/Preorder/PreorderController.php b/application/Preorder/PreorderController.php index 309428100..bc4741032 100644 --- a/application/Preorder/PreorderController.php +++ b/application/Preorder/PreorderController.php @@ -1095,7 +1095,7 @@ class PreorderController extends mfBaseController { return array_map( fn($fcp) => ["id" => $fcp->name ?? null, "text" => $fcp->name ?? null, 'lat' => $fcp->gps_lat ?? null, 'lng' => $fcp->gps_long ?? null], - ADBRimoFcp::getAll(["netzgebiet_id" => $campaign->network->adb_netzgebiet_id]) ?? [] + ADBRimoFcp::getAll(["netzgebiet_id" => intval($campaign->network->adb_netzgebiet_id)]) ?? [] ); } diff --git a/application/Preorder/PreorderModel.php b/application/Preorder/PreorderModel.php index 30081f87c..94727c89c 100644 --- a/application/Preorder/PreorderModel.php +++ b/application/Preorder/PreorderModel.php @@ -1005,14 +1005,26 @@ class PreorderModel } } } - - if (!empty($filter['fcp'])) { + if (!empty($filter['fcp']) && array_key_exists("preordercampaign_id", $filter)) { $fcp = $filter['fcp']; $db = FronkDB::singleton(); + $campaign = new Preordercampaign($filter['preordercampaign_id']); if (is_array($fcp)) { - $items = array_map(fn($i) => "'" . $db->escape($i) . "'", array_filter($fcp)); - if ($items) $where .= " AND adb_hausnummer.rimo_fcp_name IN (" . implode(',', $items) . ")"; + $items = array_map(fn($i) => ADBRimoFcp::getAll([ + 'netzgebiet_id' => intval($campaign->network->adb_netzgebiet_id), + 'name' => $i])[0], array_filter($fcp)); + + + + $items = array_map(fn($i) => $i->id, array_filter($items)); + if ($items) $where .= " AND adb_hausnummer.fcp_id IN (" . implode(',', $items) . ")"; } else { + $fcp = ADBRimoFcp::getAll([ + 'netzgebiet_id' => intval($campaign->network->adb_netzgebiet_id), + 'name' => $fcp]); + if ($fcp) $fcp = $fcp[0]->id; + else $fcp = null; + $where .= " AND adb_hausnummer.rimo_fcp_name = '" . $db->escape($fcp) . "'"; } } diff --git a/public/bundler.php b/public/bundler.php index 2f6228deb..ef6b66338 100644 --- a/public/bundler.php +++ b/public/bundler.php @@ -49,6 +49,7 @@ $jsFiles = [ "plugins/vue/tt-components/tt-textarea.js", "plugins/vue/tt-components/tt-position-manager.js", "plugins/vue/tt-components/tt-tooltip.js", + "plugins/vue/tt-components/tt-map.js", ]; diff --git a/public/js/pages/ADBRimoFcp/ADBRimoFcp.js b/public/js/pages/ADBRimoFcp/ADBRimoFcp.js index 945feb06b..517613048 100644 --- a/public/js/pages/ADBRimoFcp/ADBRimoFcp.js +++ b/public/js/pages/ADBRimoFcp/ADBRimoFcp.js @@ -200,6 +200,7 @@ Vue.component('a-d-b-rimo-fcp', {
+
@@ -231,7 +232,8 @@ Vue.component('a-d-b-rimo-fcp', { showImportLocationsModal: false, networkAreas: window.TT_CONFIG?.CRUD_CONFIG?.columns?.find(col => col.key === 'netzgebiet_id')?.modal?.items || [], fcpApiUrl: window.TT_CONFIG['BASE_PATH'] + '/ADBRimoFcp/ImportFCPs', - locationsApiUrl: window.TT_CONFIG['BASE_PATH'] + '/ADBRimoFcp/ImportLocations' + locationsApiUrl: window.TT_CONFIG['BASE_PATH'] + '/ADBRimoFcp/ImportLocations', + window } }, methods: { diff --git a/public/js/pages/ADBRimoFcpMap/ADBRimoFcpMap.js b/public/js/pages/ADBRimoFcpMap/ADBRimoFcpMap.js new file mode 100644 index 000000000..5d2fd4747 --- /dev/null +++ b/public/js/pages/ADBRimoFcpMap/ADBRimoFcpMap.js @@ -0,0 +1,93 @@ +Vue.component('ADBRimoFcpMap', { + data() { + return { + mapMarkers: [], + isLoading: true, + error: null, + window, + fetchUrl: window.TT_CONFIG.BASE_PATH + '/ADBRimoFcp/getAllFCPs', + }; + }, + async created() { + await this.fetchAndPrepareData(); + }, + methods: { + async fetchAndPrepareData() { + this.isLoading = true; + this.error = null; + try { + const response = await axios.get(this.fetchUrl); + if (response.data && response.data.success && Array.isArray(response.data.data)) { + this.mapMarkers = response.data.data + .filter(fcp => fcp.gps_lat != null && fcp.gps_long != null) + .map(fcp => ({ + lat: fcp.gps_lat, + lng: fcp.gps_long, + options: { + asyncPopupContent: async (markerData) => { + const response = await axios.get(`${this.window.TT_CONFIG.BASE_PATH}/ADBRimoFcp/getById?id=${fcp.id}`); + const fullFcpData = response.data; + return ` +
+
${fullFcpData.name}
+ + + + + + + + + + + + + + + + + + + + + + + +
RIMO ID:${fullFcpData.rimo_id}
RIMO Ex State:${fullFcpData.rimo_ex_state}
RIMO Op State:${fullFcpData.rimo_op_state}
Building Type:${fullFcpData.building_type}
Coordinates: + + ${fullFcpData.gps_lat},
${fullFcpData.gps_long} +
+
+
+`; }, + } + })); + } else { + console.error("Invalid data format from API:", response.data); + this.error = "Invalid data format received."; + this.mapMarkers = []; + } + } catch (err) { + console.error("Error fetching FCP data:", err); + this.error = "Failed to load FCP locations."; + this.mapMarkers = []; + } finally { + this.isLoading = false; + } + } + }, + template: ` + + +
+ {{ error }} +
+ +
+ No FCP locations found. +
+
+ ` +}); \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-map.js b/public/plugins/vue/tt-components/tt-map.js new file mode 100644 index 000000000..43078f6c1 --- /dev/null +++ b/public/plugins/vue/tt-components/tt-map.js @@ -0,0 +1,185 @@ +Vue.component('tt-map', { + props: { + markersData: { + type: Array, + default: () => [] // Expecting [{ lat: Number, lng: Number, options: { maki?: Object, popup?: String, asyncPopupContent?: Function } }, ...] + }, + config: { + type: Object, + default: () => ({}) // User overrides for defaults + }, + loading: { + type: Boolean, + default: false + } + }, + data() { + return { + map: null, + markerLayer: null, + tileLayers: { streets: null, satellite: null }, + mapType: localStorage.getItem('tt-map-type') || 'streets', // Default to 'streets' or stored preference + internalLoading: true, + scriptsLoaded: false, + }; + }, + computed: { + isLoading() { + return this.internalLoading || this.loading; + }, + mapConfig() { + const defaults = { + center: [47.0707, 15.4395], + zoom: 13, + mapboxKey: window.TT_CONFIG?.MAPBOX_KEY, + streetsTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', + streetsTileId: 'mapbox/streets-v11', + satelliteTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', + satelliteTileId: 'mapbox/satellite-streets-v12', // Or 'mapbox/satellite-v9' + tileAttribution: '© Mapbox © OpenStreetMap', + clusterOptions: {}, + makiMarkerOptions: { icon: "marker", color: "#3b82f6", size: "m" } + }; + return { ...defaults, ...this.config }; // Merge user config over defaults + } + }, + async mounted() { + try { + await this.loadScripts(); + this.scriptsLoaded = true; + this.initializeMap(); + this.updateMarkers(); + this.internalLoading = false; + } catch (error) { + console.error("Map Initialization Error:", error); + this.internalLoading = false; + } + }, + methods: { + loadScripts() { + const scripts = [ + { type: 'link', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' }, + { type: 'script', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' }, + { type: 'script', url: 'https://unpkg.com/leaflet-makimarkers@3.1.0/Leaflet.MakiMarkers.js' }, + { type: 'link', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css' }, + { type: 'link', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css' }, + { type: 'script', url: 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js' } + ]; + + const promises = scripts.map(s => new Promise((resolve, reject) => { + let el; + if (s.type === 'script') { + el = document.createElement('script'); + el.src = s.url; el.async = false; el.onload = resolve; el.onerror = reject; + } else { + el = document.createElement('link'); + el.rel = 'stylesheet'; el.href = s.url; resolve(); + } + if (el) document.head.appendChild(el); else reject(); + })); + return Promise.all(promises); + }, + initializeMap() { + if (!this.scriptsLoaded || !L || !L.MarkerClusterGroup || !L.MakiMarkers || !this.mapConfig.mapboxKey) return; + + this.map = L.map(this.$refs.mapContainer, { preferCanvas: true }).setView(this.mapConfig.center, this.mapConfig.zoom); + L.MakiMarkers.accessToken = this.mapConfig.mapboxKey; + + this.tileLayers.streets = L.tileLayer(this.mapConfig.streetsTileUrl, { + attribution: this.mapConfig.tileAttribution, maxZoom: 18, id: this.mapConfig.streetsTileId, + tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey + }); + this.tileLayers.satellite = L.tileLayer(this.mapConfig.satelliteTileUrl, { + attribution: this.mapConfig.tileAttribution, maxZoom: 18, id: this.mapConfig.satelliteTileId, + tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey + }); + + this.tileLayers[this.mapType].addTo(this.map); // Add initial layer based on preference + + this.markerLayer = L.markerClusterGroup(this.mapConfig.clusterOptions); + this.map.addLayer(this.markerLayer); + + // Invalidate size after initial load if container might not have been ready + this.$nextTick(() => { + this.map.invalidateSize(); + }); + // Add resize listener + window.addEventListener('resize', this.handleResize); + }, + updateMarkers() { + if (!this.map || !this.markerLayer || !this.scriptsLoaded) return; + this.markerLayer.clearLayers(); + const markersToAdd = []; + this.markersData.forEach(data => { + if (data.lat != null && data.lng != null) { + const makiOptions = { ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) }; + const icon = L.MakiMarkers.icon(makiOptions); + const marker = L.marker([data.lat, data.lng], { icon: icon }); + + if (data.options?.popup) { + marker.bindPopup(data.options.popup); + } else if (data.options?.asyncPopupContent && typeof data.options.asyncPopupContent === 'function') { + marker.bindPopup(() => ''); // Initial content + marker.on('popupopen', async (e) => { + const popup = e.popup; + try { + const content = await data.options.asyncPopupContent(data); // Pass marker data to function + popup.setContent(content); + } catch (error) { + console.error("Error loading popup content:", error); + popup.setContent('
Failed to load content.
'); + } + popup.update(); // Adjust size + }); + } + + if (data.options?.tooltip) marker.bindTooltip(data.options.tooltip); + markersToAdd.push(marker); + } + }); + if (markersToAdd.length > 0) this.markerLayer.addLayers(markersToAdd); + }, + toggleMapType() { + this.map.removeLayer(this.tileLayers[this.mapType]); + this.mapType = this.mapType === 'streets' ? 'satellite' : 'streets'; + this.tileLayers[this.mapType].addTo(this.map); + localStorage.setItem('tt-map-type', this.mapType); + }, + handleResize() { + if (this.map) { + // Use debounce if resize events fire too rapidly + this.map.invalidateSize(); + } + } + }, + watch: { + markersData: { handler() { this.updateMarkers(); }, deep: true }, + loading(newVal) { + // Optional: Invalidate map size when loading finishes, in case container size changed + if (!newVal && this.map) { + this.$nextTick(() => this.map.invalidateSize()); + } + } + }, + beforeDestroy() { + window.removeEventListener('resize', this.handleResize); + if (this.map) { + this.map.remove(); + this.map = null; + } + }, + template: ` +
+
+ +
+
+ +
+ ` +}); \ No newline at end of file