Update: 2026-07-02 05:27:04
This commit is contained in:
@@ -7,6 +7,7 @@ import 'views/admin/pricing/kazan_editor_page.dart';
|
||||
import 'views/admin/complaints/complaint_list_page.dart';
|
||||
import 'views/admin/drivers/driver_documents_review_page.dart';
|
||||
import 'views/admin/marketing/marketing_page.dart';
|
||||
import 'views/admin/marketing/heatmap_page.dart';
|
||||
|
||||
List<GetPage<dynamic>> routes = [
|
||||
GetPage(name: "/", page: () => const AdminHomePage()),
|
||||
@@ -17,5 +18,5 @@ List<GetPage<dynamic>> routes = [
|
||||
GetPage(name: "/complaints", page: () => ComplaintListPage()),
|
||||
GetPage(name: "/driver-docs", page: () => DriverDocsReviewPage()),
|
||||
GetPage(name: "/marketing", page: () => const MarketingPage()),
|
||||
GetPage(name: "/heatmap", page: () => const HeatmapPage()),
|
||||
];
|
||||
|
||||
|
||||
@@ -15,6 +15,22 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
List<CircleMarker> _markers = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
// ─── فلاتر ───
|
||||
String _selectedSource = 'all'; // all | geofence | app_usage | silent_push
|
||||
String _selectedCountry = 'all'; // all | JO | SY | EG | IQ
|
||||
int _daysFilter = 7;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
// مراكز كل دولة للانتقال السريع
|
||||
final Map<String, LatLng> _countryCenters = {
|
||||
'all': const LatLng(31.9522, 35.9334), // عمان
|
||||
'JO': const LatLng(31.9522, 35.9334),
|
||||
'SY': const LatLng(33.5138, 36.2765),
|
||||
'EG': const LatLng(30.0444, 31.2357),
|
||||
'IQ': const LatLng(33.3152, 44.3661),
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -22,27 +38,40 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
}
|
||||
|
||||
Future<void> _fetchHeatmapData() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final response = await Dio().get('${AppLink.server}/Admin/geofence/get_heatmap.php?days=7');
|
||||
|
||||
final queryParams = {
|
||||
'days': _daysFilter.toString(),
|
||||
if (_selectedSource != 'all') 'source': _selectedSource,
|
||||
if (_selectedCountry != 'all') 'country_code': _selectedCountry,
|
||||
};
|
||||
|
||||
final response = await Dio().get(
|
||||
'${AppLink.server}/Admin/geofence/get_heatmap.php',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
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 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);
|
||||
|
||||
// ألوان حسب المصدر
|
||||
Color markerColor;
|
||||
switch (source) {
|
||||
case 'geofence':
|
||||
markerColor = Colors.green.withOpacity(0.65); // 🟢 دخل منطقة
|
||||
break;
|
||||
case 'silent_push':
|
||||
markerColor = Colors.orange.withOpacity(0.55); // 🟠 إيقاظ صامت
|
||||
break;
|
||||
case 'app_usage':
|
||||
default:
|
||||
markerColor = Colors.blue.withOpacity(0.5); // 🔵 فتح عادي
|
||||
}
|
||||
|
||||
return CircleMarker(
|
||||
@@ -50,7 +79,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
color: markerColor,
|
||||
borderStrokeWidth: 0,
|
||||
useRadiusInMeter: true,
|
||||
radius: 150, // 150 meters radius for visualization
|
||||
radius: 150,
|
||||
);
|
||||
}).toList();
|
||||
_isLoading = false;
|
||||
@@ -58,36 +87,265 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error fetching heatmap: $e");
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyFilter() {
|
||||
_fetchHeatmapData();
|
||||
// تحريك الخريطة لمركز الدولة المختارة
|
||||
final center = _countryCenters[_selectedCountry] ?? _countryCenters['all']!;
|
||||
_mapController.move(center, 11.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0F0F1A),
|
||||
appBar: AppBar(
|
||||
title: const Text('خريطة النشاط الحرارية (Heatmap)'),
|
||||
title: const Text(
|
||||
'خريطة النشاط الحرارية',
|
||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
|
||||
),
|
||||
backgroundColor: const Color(0xFF1A1A2E),
|
||||
foregroundColor: Colors.white,
|
||||
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,
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
tooltip: 'تحديث',
|
||||
onPressed: _applyFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// ─── شريط الفلاتر ───
|
||||
Container(
|
||||
color: const Color(0xFF1A1A2E),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// فلتر المصدر
|
||||
_buildFilterRow(
|
||||
label: 'المصدر:',
|
||||
options: const {
|
||||
'all': 'الكل',
|
||||
'geofence': '🟢 سياج',
|
||||
'app_usage': '🔵 فتح',
|
||||
'silent_push': '🟠 صامت',
|
||||
},
|
||||
selected: _selectedSource,
|
||||
onChanged: (v) => setState(() => _selectedSource = v),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// فلتر الدولة
|
||||
_buildFilterRow(
|
||||
label: 'الدولة:',
|
||||
options: const {
|
||||
'all': 'الكل',
|
||||
'JO': '🇯🇴 الأردن',
|
||||
'SY': '🇸🇾 سوريا',
|
||||
'EG': '🇪🇬 مصر',
|
||||
'IQ': '🇮🇶 العراق',
|
||||
},
|
||||
selected: _selectedCountry,
|
||||
onChanged: (v) => setState(() => _selectedCountry = v),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// فلتر الفترة الزمنية + زر تطبيق
|
||||
Row(
|
||||
children: [
|
||||
const Text('الفترة:', style: TextStyle(color: Colors.white54, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 8),
|
||||
...[1, 7, 30].map((days) => Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _daysFilter = days),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _daysFilter == days
|
||||
? const Color(0xFF6366F1)
|
||||
: const Color(0xFF2D2D42),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: _daysFilter == days
|
||||
? const Color(0xFF818CF8)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'$days يوم',
|
||||
style: TextStyle(
|
||||
color: _daysFilter == days ? Colors.white : Colors.white54,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _applyFilter,
|
||||
icon: const Icon(Icons.filter_alt_rounded, size: 14),
|
||||
label: const Text('تطبيق', style: TextStyle(fontSize: 11)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ─── الخريطة ───
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
_isLoading
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: Color(0xFF6366F1)),
|
||||
SizedBox(height: 12),
|
||||
Text('جاري تحميل البيانات...', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: _countryCenters[_selectedCountry] ?? _countryCenters['all']!,
|
||||
initialZoom: 12.0,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.siro.admin',
|
||||
),
|
||||
CircleLayer(circles: _markers),
|
||||
],
|
||||
),
|
||||
|
||||
// ─── مفتاح الألوان ───
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A2E).withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_LegendItem(color: Colors.green, label: 'دخل منطقة سياج'),
|
||||
SizedBox(height: 4),
|
||||
_LegendItem(color: Colors.blue, label: 'فتح عادي للتطبيق'),
|
||||
SizedBox(height: 4),
|
||||
_LegendItem(color: Colors.orange, label: 'إيقاظ صامت'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ─── عدد النقاط ───
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A2E).withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white12),
|
||||
),
|
||||
child: Text(
|
||||
'${_markers.length} نقطة',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterRow({
|
||||
required String label,
|
||||
required Map<String, String> options,
|
||||
required String selected,
|
||||
required ValueChanged<String> onChanged,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: options.entries.map((e) {
|
||||
final isSelected = selected == e.key;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: GestureDetector(
|
||||
onTap: () => onChanged(e.key),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF6366F1) : const Color(0xFF2D2D42),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF818CF8) : Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
e.value,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : Colors.white54,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LegendItem extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
const _LegendItem({required this.color, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 10)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user