Update: 2026-06-30 23:32:14

This commit is contained in:
Hamza-Ayed
2026-06-30 23:32:15 +03:00
parent 808066f4a6
commit d2ce4bdb16
7 changed files with 277 additions and 17 deletions

View File

@@ -0,0 +1,76 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php'; // Includes db connection
$zone_name = filterRequest('zone_name');
$latitude = filterRequest('latitude');
$longitude = filterRequest('longitude');
$radius_meters = filterRequest('radius_meters');
$country_code = filterRequest('country_code');
$priority = filterRequest('priority') ?? 1;
if (empty($zone_name) || empty($latitude) || empty($longitude) || empty($radius_meters) || empty($country_code)) {
echo json_encode(["status" => "error", "message" => "Missing required fields"]);
exit;
}
try {
// 1. Check for overlapping zones
// Using Haversine formula directly in SQL to find any zone where distance < (new_radius + existing_radius)
$sql = "
SELECT id, zone_name, radius_meters,
(
6371000 * acos(
cos(radians(:new_lat)) * cos(radians(latitude)) *
cos(radians(longitude) - radians(:new_lng)) +
sin(radians(:new_lat)) * sin(radians(latitude))
)
) AS distance_meters
FROM geofence_zones
WHERE is_active = 1 AND country_code = :country_code
HAVING distance_meters < (radius_meters + :new_radius)
LIMIT 1
";
$stmt = $con->prepare($sql);
$stmt->bindValue(':new_lat', (float) $latitude);
$stmt->bindValue(':new_lng', (float) $longitude);
$stmt->bindValue(':new_radius', (int) $radius_meters, PDO::PARAM_INT);
$stmt->bindValue(':country_code', $country_code);
$stmt->execute();
$overlapping_zone = $stmt->fetch(PDO::FETCH_ASSOC);
if ($overlapping_zone) {
echo json_encode([
"status" => "error",
"message" => "Zone overlaps with existing zone: " . $overlapping_zone['zone_name'],
"overlap_details" => $overlapping_zone
]);
exit;
}
// 2. Insert new zone
$insert_sql = "INSERT INTO geofence_zones (zone_name, latitude, longitude, radius_meters, priority, country_code)
VALUES (:zone_name, :lat, :lng, :radius, :priority, :country)";
$insert_stmt = $con->prepare($insert_sql);
$insert_stmt->bindValue(':zone_name', $zone_name);
$insert_stmt->bindValue(':lat', (float) $latitude);
$insert_stmt->bindValue(':lng', (float) $longitude);
$insert_stmt->bindValue(':radius', (int) $radius_meters, PDO::PARAM_INT);
$insert_stmt->bindValue(':priority', (int) $priority, PDO::PARAM_INT);
$insert_stmt->bindValue(':country', $country_code);
$insert_stmt->execute();
echo json_encode([
"status" => "success",
"message" => "Geofence zone added successfully",
"zone_id" => $con->lastInsertId()
]);
} catch (Exception $e) {
error_log("Error adding geofence zone: " . $e->getMessage());
echo json_encode(["status" => "error", "message" => "Server error"]);
}
?>

View File

@@ -0,0 +1,29 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php';
// Optional filter: days
$days = filterRequest('days') ?? 7;
try {
$sql = "SELECT latitude, longitude, source, created_at
FROM passenger_opening_locations
WHERE created_at >= DATE_SUB(NOW(), INTERVAL :days DAY)
ORDER BY created_at DESC LIMIT 5000"; // Limit to prevent massive payloads
$stmt = $con->prepare($sql);
$stmt->bindValue(':days', (int) $days, PDO::PARAM_INT);
$stmt->execute();
$locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
"status" => "success",
"data" => $locations
]);
} catch (Exception $e) {
error_log("Error fetching heatmap data: " . $e->getMessage());
echo json_encode(["status" => "error", "message" => "Server error"]);
}
?>

View File

@@ -22,6 +22,10 @@ class LocationIntelligenceEngine {
* @return array Optional new geofence regions to register on user's device
*/
public function processLocationUpdate($passengerId, $lat, $lng, $source = 'app_usage', $batteryLevel = null) {
if (!function_exists('sendFCM_Internal')) {
require_once __DIR__ . '/../../functions.php';
}
// 1. Update Database
$this->updateLocationDatabase($passengerId, $lat, $lng, $source, $batteryLevel);
@@ -72,13 +76,7 @@ class LocationIntelligenceEngine {
$stmt->execute([':lat' => $lat, ':lng' => $lng]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
private function evaluateCampaignOpportunity($passengerId, $zone, $source) {
// Avoid spamming if source is silent_push, maybe only do it for geofence or app_usage
if ($source === 'silent_push') {
return; // Don't trigger active campaigns on silent push to save battery and avoid weird timing
}
// 1. Check if passenger received a campaign recently (Anti-Spam)
$sqlSpamCheck = "SELECT COUNT(*) FROM marketing_campaigns_log
WHERE passenger_id = :pid
@@ -89,16 +87,37 @@ class LocationIntelligenceEngine {
$spamCount = intval($stmtSpam->fetchColumn());
if ($spamCount == 0) {
// 2. Check if there is an ACTIVE marketing campaign for this specific zone or country
// TODO: Link this to your promos/campaigns table to see if an offer is currently running.
// DO NOT send a generic "Welcome" notification as it is annoying to users.
// We only send a push if there's a real incentive (e.g. discount code).
// 2. Fetch Active Campaign (Placeholder for real campaign DB logic)
// Currently, we just send a generic welcome to the zone if priority is high.
if ($zone['priority'] >= 1) {
$this->sendCampaignNotification($passengerId, $zone);
}
}
}
// Example of what will be here:
// $campaign = $this->getActiveCampaignForZone($zone['id']);
// if ($campaign) {
// $this->sendCampaignNotification($passengerId, $campaign);
// }
private function sendCampaignNotification($passengerId, $zone) {
// Get passenger token
$sql = "SELECT users_token, country_code FROM users WHERE users_id = :pid";
$stmt = $this->db->prepare($sql);
$stmt->execute([':pid' => $passengerId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && !empty($user['users_token'])) {
$title = "مرحباً بك في " . $zone['zone_name'] . " \u{1F389}";
$body = "اطلب رحلتك الآن من " . $zone['zone_name'] . " واستمتع بتجربة سيرو!";
// Send Push
sendFCM_Internal([$user['users_token']], $title, $body, ['type' => 'geofence_promo'], '');
// Log it
$logSql = "INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by)
VALUES (:pid, 'push', :country, :region, 'geofence_trigger')";
$logStmt = $this->db->prepare($logSql);
$logStmt->execute([
':pid' => $passengerId,
':country' => $user['country_code'] ?? 'JO',
':region' => $zone['zone_name']
]);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:dio/dio.dart';
import 'package:siro_admin/constant/links.dart';
class HeatmapPage extends StatefulWidget {
const HeatmapPage({super.key});
@override
State<HeatmapPage> createState() => _HeatmapPageState();
}
class _HeatmapPageState extends State<HeatmapPage> {
List<CircleMarker> _markers = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_fetchHeatmapData();
}
Future<void> _fetchHeatmapData() async {
try {
final response = await Dio().get('${AppLink.server}/Admin/geofence/get_heatmap.php?days=7');
if (response.statusCode == 200 && response.data['status'] == 'success') {
final data = response.data['data'] as List;
setState(() {
_markers = data.map((point) {
final lat = double.tryParse(point['latitude'].toString()) ?? 0.0;
final lng = double.tryParse(point['longitude'].toString()) ?? 0.0;
final source = point['source'].toString();
// Color logic:
// Geofence trigger = Green (High intent/Campaign)
// App Usage = Blue (Normal usage)
// Silent Push = Orange (Background wake)
Color markerColor = Colors.blue.withOpacity(0.5);
if (source == 'geofence') {
markerColor = Colors.green.withOpacity(0.7);
} else if (source == 'silent_push') {
markerColor = Colors.orange.withOpacity(0.5);
}
return CircleMarker(
point: LatLng(lat, lng),
color: markerColor,
borderStrokeWidth: 0,
useRadiusInMeter: true,
radius: 150, // 150 meters radius for visualization
);
}).toList();
_isLoading = false;
});
}
} catch (e) {
debugPrint("Error fetching heatmap: $e");
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('خريطة النشاط الحرارية (Heatmap)'),
centerTitle: true,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: FlutterMap(
options: const MapOptions(
initialCenter: LatLng(31.9522, 35.9334), // Default to Amman
initialZoom: 12.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.siro.admin',
),
CircleLayer(
circles: _markers,
),
],
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:geolocator/geolocator.dart';
import 'package:siro_rider/services/geofencing_service.dart';
class GeoLocation {
Future<Position> getCurrentLocation() async {
@@ -28,7 +29,16 @@ class GeoLocation {
}
// When we reach here, permissions are granted and we can fetch the location.
return await Geolocator.getCurrentPosition(
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
// Update Geofences based on the fresh location
try {
SiroGeofencingService.syncZonesWithServer(position.latitude, position.longitude);
} catch (e) {
// Ignore errors so it doesn't break the main location flow
}
return position;
}
}

View File

@@ -1,5 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:geolocator/geolocator.dart';
import 'package:siro_rider/constant/links.dart';
import 'package:siro_rider/app_bindings.dart';
import 'package:siro_rider/controller/functions/crud.dart';
import 'package:siro_rider/views/home/HomePage/contact_us.dart';
@@ -36,6 +39,36 @@ DbSql sql = DbSql.instance;
Future<void> backgroundMessageHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
Log.print("Handling a background message: ${message.messageId}");
if (message.data.isNotEmpty) {
try {
await GetStorage.init();
final storage = GetStorage();
final passengerId = storage.read('passenger_id') ?? '';
if (passengerId.toString().isEmpty) return;
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return;
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) return;
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final url = Uri.parse('${AppLink.server}/api/location/sync_location.php');
await http.post(url, body: {
'passenger_id': passengerId.toString(),
'lat': position.latitude.toString(),
'lng': position.longitude.toString(),
'source': 'silent_push',
});
// Update geofences on device silently
await SiroGeofencingService.syncZonesWithServer(position.latitude, position.longitude);
} catch (e) {
Log.print("Silent push location update failed: $e");
}
}
}
void main() {

View File

@@ -72,7 +72,7 @@ class SiroGeofencingService {
try {
for (var region in regions) {
final geofence = Geofence(
id: region['zone_name'],
id: region['id'].toString(),
location: Location(
latitude: double.parse(region['latitude'].toString()),
longitude: double.parse(region['longitude'].toString())),