defmodule DaProductAppWeb.TerminalLocationsLive do use DaProductAppWeb, :live_view alias DaProductApp.TerminalManagement @impl true def mount(_params, _session, socket) do # Subscribe to real-time updates if connected?(socket) do TerminalManagement.subscribe_to_device_updates() end socket = socket |> assign(:page_title, "Terminal Locations") |> assign(:current_page, "terminal_locations") |> assign(:loading, true) |> assign(:map_view, "street") # street, satellite, terrain |> assign(:show_clusters, true) |> assign(:show_heatmap, false) |> assign_initial_filters() |> load_location_data() {:ok, socket} end @impl true def handle_params(params, _url, socket) do socket = socket |> update_filters_from_params(params) |> load_location_data() {:noreply, socket} end @impl true def handle_event("update_filter", %{"filter" => filter_data}, socket) do socket = socket |> update_filters(filter_data) |> push_patch_with_filters() {:noreply, socket} end @impl true def handle_event("change_map_view", %{"view" => view}, socket) do {:noreply, assign(socket, :map_view, view)} end @impl true def handle_event("toggle_clusters", _params, socket) do {:noreply, assign(socket, :show_clusters, !socket.assigns.show_clusters)} end @impl true def handle_event("toggle_heatmap", _params, socket) do {:noreply, assign(socket, :show_heatmap, !socket.assigns.show_heatmap)} end @impl true def handle_event("clear_filters", _params, socket) do socket = socket |> assign_initial_filters() |> load_location_data() |> push_patch(to: ~p"/terminals/locations") {:noreply, socket} end @impl true def handle_event("export_locations", %{"format" => format}, socket) do case export_location_data(socket.assigns.filtered_devices, format) do {:ok, _} -> socket = put_flash(socket, :info, "Location data exported successfully") {:noreply, socket} {:error, reason} -> socket = put_flash(socket, :error, "Export failed: #{reason}") {:noreply, socket} end end @impl true def handle_event("create_geofence", params, socket) do case create_geofence_from_params(params) do {:ok, geofence} -> socket = socket |> put_flash(:info, "Geofence '#{geofence.name}' created successfully") |> load_location_data() {:noreply, socket} {:error, reason} -> socket = put_flash(socket, :error, "Failed to create geofence: #{reason}") {:noreply, socket} end end @impl true def handle_info({:device_update, _device_data}, socket) do socket = load_location_data(socket) {:noreply, socket} end defp assign_initial_filters(socket) do socket |> assign(:filters, %{ country: "AE", # Default to UAE region: "", # Emirate/State city: "", status: "", device_model: "", vendor: "", device_type: "", date_from: Date.add(Date.utc_today(), -30), date_to: Date.utc_today(), last_activity_days: 30 }) end defp update_filters_from_params(socket, params) do filters = socket.assigns.filters updated_filters = params |> Enum.reduce(filters, fn {key, value}, acc -> case key do "country" -> Map.put(acc, :country, value) "region" -> Map.put(acc, :region, value) "city" -> Map.put(acc, :city, value) "status" -> Map.put(acc, :status, value) "device_model" -> Map.put(acc, :device_model, value) "vendor" -> Map.put(acc, :vendor, value) "device_type" -> Map.put(acc, :device_type, value) "date_from" -> Map.put(acc, :date_from, parse_date(value)) "date_to" -> Map.put(acc, :date_to, parse_date(value)) "last_activity_days" -> Map.put(acc, :last_activity_days, parse_int(value, 30)) _ -> acc end end) assign(socket, :filters, updated_filters) end defp update_filters(socket, filter_data) do current_filters = socket.assigns.filters updated_filters = Map.merge(current_filters, atomize_keys(filter_data)) assign(socket, :filters, updated_filters) end defp load_location_data(socket) do filters = socket.assigns.filters # Get all devices with location data all_devices = TerminalManagement.list_terminals_with_latest_status() IO.inspect(length(all_devices), label: "Total devices from DB") # Apply filters and get location data filtered_devices = all_devices |> apply_filters(filters) |> Enum.map(&enrich_with_location/1) |> Enum.filter(&has_valid_location?/1) IO.inspect(length(filtered_devices), label: "Filtered devices with location") # Generate location analytics location_stats = generate_location_analytics(filtered_devices) # Get geographic options for filters geographic_options = get_geographic_options(filters.country) socket |> assign(:all_devices, all_devices) |> assign(:filtered_devices, filtered_devices) |> assign(:location_stats, location_stats) |> assign(:geographic_options, geographic_options) |> assign(:loading, false) end defp apply_filters(devices, filters) do devices |> filter_by_country(filters.country) |> filter_by_region(filters.region) |> filter_by_status(filters.status) |> filter_by_device_model(filters.device_model) |> filter_by_vendor(filters.vendor) |> filter_by_date_range(filters.date_from, filters.date_to) |> filter_by_last_activity(filters.last_activity_days) end defp enrich_with_location(device) do device_id = device[:id] || device.id case TerminalManagement.get_latest_terminal_location(device_id) do nil -> # If no location data, add default/mock coordinates for testing Map.merge(device, %{ lat: 25.2048 + (:rand.uniform() - 0.5) * 0.1, # Random around Dubai lng: 55.2708 + (:rand.uniform() - 0.5) * 0.1, address: "Location not available", location_timestamp: DateTime.utc_now() }) location -> Map.merge(device, %{ lat: parse_float(location.lat), lng: parse_float(location.lng), address: location.address || "Address not available", location_timestamp: location.timestamp || DateTime.utc_now() }) end end defp has_valid_location?(device) do lat = device[:lat] || device.lat lng = device[:lng] || device.lng # Check if we have valid coordinates valid_coords = lat != nil and lng != nil and is_number(lat) and is_number(lng) and lat >= -90 and lat <= 90 and lng >= -180 and lng <= 180 if not valid_coords do IO.inspect(device, label: "Device with invalid location") end valid_coords end defp generate_location_analytics(devices) do total_devices = length(devices) online_devices = Enum.count(devices, fn device -> status = device[:status] || device.status status in ["online", "Online", "connected"] end) # Group by emirates/regions by_region = Enum.group_by(devices, &extract_region_from_address/1) # Calculate coverage metrics coverage_areas = length(Map.keys(by_region)) avg_devices_per_area = if coverage_areas > 0, do: Float.round(total_devices / coverage_areas, 2), else: 0.0 # Device density analysis density_analysis = calculate_device_density(devices) %{ total_devices: total_devices, online_devices: online_devices, offline_devices: total_devices - online_devices, coverage_areas: coverage_areas, avg_devices_per_area: avg_devices_per_area, by_region: by_region, density_analysis: density_analysis, last_updated: DateTime.utc_now() } end defp get_geographic_options("AE") do %{ country_name: "United Arab Emirates", regions: [ %{code: "AZ", name: "Abu Dhabi", cities: ["Abu Dhabi", "Al Ain", "Madinat Zayed"]}, %{code: "DU", name: "Dubai", cities: ["Dubai", "Hatta"]}, %{code: "SH", name: "Sharjah", cities: ["Sharjah", "Khor Fakkan", "Kalba"]}, %{code: "AJ", name: "Ajman", cities: ["Ajman"]}, %{code: "UQ", name: "Umm Al Quwain", cities: ["Umm Al Quwain"]}, %{code: "RK", name: "Ras Al Khaimah", cities: ["Ras Al Khaimah"]}, %{code: "FU", name: "Fujairah", cities: ["Fujairah", "Dibba Al-Fujairah"]} ] } end defp get_geographic_options("IN") do %{ country_name: "India", regions: [ %{code: "KA", name: "Karnataka", cities: ["Bangalore", "Mysore", "Mangalore"]}, %{code: "MH", name: "Maharashtra", cities: ["Mumbai", "Pune", "Nagpur"]}, %{code: "DL", name: "Delhi", cities: ["New Delhi", "Delhi"]}, # Add more states as needed ] } end defp get_geographic_options(_), do: %{country_name: "Unknown", regions: []} # Filter functions defp filter_by_country(devices, ""), do: devices defp filter_by_country(devices, country) do Enum.filter(devices, fn device -> # This would need to be implemented based on how country is stored # For now, we'll use a simple area-based filter area = device[:area] || device.area area && String.contains?(String.upcase(area || ""), country) end) end defp filter_by_region(devices, ""), do: devices defp filter_by_region(devices, region) do Enum.filter(devices, fn device -> area = device[:area] || device.area area && String.contains?(String.downcase(area || ""), String.downcase(region)) end) end defp filter_by_status(devices, ""), do: devices defp filter_by_status(devices, status) do Enum.filter(devices, fn device -> device_status = device[:status] || device.status device_status == status end) end defp filter_by_device_model(devices, ""), do: devices defp filter_by_device_model(devices, model) do Enum.filter(devices, fn device -> device_model = device[:model] || device.model device_model == model end) end defp filter_by_vendor(devices, ""), do: devices defp filter_by_vendor(devices, vendor) do Enum.filter(devices, fn device -> device_vendor = device[:vendor] || device.vendor device_vendor == vendor end) end defp filter_by_date_range(devices, nil, nil), do: devices defp filter_by_date_range(devices, from_date, to_date) do Enum.filter(devices, fn device -> device_date = case device[:inserted_at] do %DateTime{} = dt -> DateTime.to_date(dt) nil -> nil _ -> nil end if device_date do Date.compare(device_date, from_date || ~D[1900-01-01]) != :lt && Date.compare(device_date, to_date || Date.utc_today()) != :gt else # Include devices without dates when filtering by date true end end) end defp filter_by_last_activity(devices, days) when is_integer(days) and days > 0 do cutoff_date = DateTime.add(DateTime.utc_now(), -days * 24 * 60 * 60, :second) Enum.filter(devices, fn device -> case device[:last_seen_at] do %DateTime{} = last_seen -> DateTime.compare(last_seen, cutoff_date) != :lt nil -> true # Include devices without last_seen_at data _ -> true end end) end defp filter_by_last_activity(devices, _), do: devices # Helper functions defp parse_float(nil), do: nil defp parse_float(value) when is_number(value), do: value defp parse_float(value) when is_binary(value) do case Float.parse(value) do {float_val, _} -> float_val :error -> nil end end defp parse_float(_), do: nil defp parse_date(nil), do: nil defp parse_date(date_string) when is_binary(date_string) do case Date.from_iso8601(date_string) do {:ok, date} -> date _ -> nil end end defp parse_int(value, default) when is_binary(value) do case Integer.parse(value) do {int_val, _} -> int_val :error -> default end end defp parse_int(value, _default) when is_integer(value), do: value defp parse_int(_, default), do: default defp atomize_keys(map) when is_map(map) do Map.new(map, fn {k, v} -> {String.to_atom(k), v} end) end defp extract_region_from_address(device) do # Extract region/emirate from address or area area = device[:area] || device.area address = device[:address] || device.address area || address || "Unknown" end defp calculate_device_density(devices) do # Simple density calculation - would be more sophisticated in real implementation %{ high_density_areas: 0, medium_density_areas: 0, low_density_areas: 0, coverage_gaps: [] } end defp push_patch_with_filters(socket) do filters = socket.assigns.filters query_params = build_query_params(filters) push_patch(socket, to: ~p"/terminals/locations?#{query_params}") end defp build_query_params(filters) do filters |> Enum.filter(fn {_k, v} -> v != nil and v != "" end) |> Enum.map(fn {k, v} -> {to_string(k), to_string(v)} end) |> URI.encode_query() end defp export_location_data(devices, format) do # Implementation for exporting location data {:ok, "exported"} end defp create_geofence_from_params(params) do # Implementation for creating geofences {:ok, %{name: params["name"]}} end @impl true def render(assigns) do ~H"""
Geographic distribution and location management
Total Devices
<%= @location_stats.total_devices %>
Online Devices
<%= @location_stats.online_devices %>
Coverage Areas
<%= @location_stats.coverage_areas %>
Avg per Area
<%= @location_stats.avg_devices_per_area %>