chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل

نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق».
نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا
`cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules ·
.dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore.

هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً
مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ.

⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا
صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم
توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:10:29 +03:00
co-authored by Claude Opus 5
commit 92dc6b3641
3654 changed files with 896478 additions and 0 deletions
@@ -0,0 +1,377 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:dio/dio.dart';
import 'package:get/get.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;
String _selectedSource = 'all';
String _selectedCountry = 'all';
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();
_fetchHeatmapData();
}
Future<void> _fetchHeatmapData() async {
setState(() => _isLoading = true);
try {
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 source = point['source'].toString();
Color markerColor;
switch (source) {
case 'geofence':
markerColor = Colors.green.withValues(alpha: 0.65);
break;
case 'silent_push':
markerColor = Colors.orange.withValues(alpha: 0.55);
break;
case 'app_usage':
default:
markerColor = Colors.blue.withValues(alpha: 0.5);
}
return CircleMarker(
point: LatLng(lat, lng),
color: markerColor,
borderStrokeWidth: 0,
useRadiusInMeter: true,
radius: 150,
);
}).toList();
_isLoading = false;
});
}
} catch (e) {
debugPrint("Error fetching heatmap: $e");
setState(() => _isLoading = false);
}
}
void _applyFilter() {
_fetchHeatmapData();
final center = _countryCenters[_selectedCountry] ?? _countryCenters['all']!;
_mapController.move(center, 11.0);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
const Text('خريطة النشاط الحرارية',
style: TextStyle(
color: Colors.black,
fontSize: 17,
fontWeight: FontWeight.bold)),
const Spacer(),
GestureDetector(
onTap: _applyFilter,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.refresh_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
],
),
),
Expanded(
child: Column(
children: [
Container(
color: cs.surfaceContainerHighest,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
children: [
_buildFilterRow(
cs: cs,
label: 'المصدر:',
options: const {
'all': 'الكل',
'geofence': '🟢 سياج',
'app_usage': '🔵 فتح',
'silent_push': '🟠 صامت',
},
selected: _selectedSource,
onChanged: (v) => setState(() => _selectedSource = v),
),
const SizedBox(height: 6),
_buildFilterRow(
cs: cs,
label: 'الدولة:',
options: const {
'all': 'الكل',
'JO': '🇯🇴 الأردن',
'SY': '🇸🇾 سوريا',
'EG': '🇪🇬 مصر',
'IQ': '🇮🇶 العراق',
},
selected: _selectedCountry,
onChanged: (v) => setState(() => _selectedCountry = v),
),
const SizedBox(height: 6),
Row(
children: [
Text('الفترة:', style: TextStyle(color: cs.onSurfaceVariant, 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
? cs.primary
: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _daysFilter == days
? cs.primary
: Colors.transparent,
),
),
child: Text(
'$days يوم',
style: TextStyle(
color: _daysFilter == days ? cs.onSurface : cs.onSurfaceVariant,
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: cs.primary,
foregroundColor: cs.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
),
],
),
],
),
),
Expanded(
child: Stack(
children: [
_isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: cs.primary),
const SizedBox(height: 12),
Text('جاري تحميل البيانات...', style: TextStyle(color: cs.onSurfaceVariant, 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.siromove.admin',
),
CircleLayer(circles: _markers),
],
),
Positioned(
bottom: 12,
right: 12,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_LegendItem(color: Colors.green, label: 'دخل منطقة سياج', cs: cs),
const SizedBox(height: 4),
_LegendItem(color: Colors.blue, label: 'فتح عادي للتطبيق', cs: cs),
const SizedBox(height: 4),
_LegendItem(color: Colors.orange, label: 'إيقاظ صامت', cs: cs),
],
),
),
),
Positioned(
top: 12,
left: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: cs.outline.withValues(alpha: 0.3)),
),
child: Text(
'${_markers.length} نقطة',
style: TextStyle(color: cs.onSurface, fontSize: 11, fontWeight: FontWeight.bold),
),
),
),
],
),
),
],
),
),
],
),
);
}
Widget _buildFilterRow({
required ColorScheme cs,
required String label,
required Map<String, String> options,
required String selected,
required ValueChanged<String> onChanged,
}) {
return Row(
children: [
Text(label, style: TextStyle(color: cs.onSurfaceVariant, 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 ? cs.primary : cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? cs.primary : Colors.transparent,
),
),
child: Text(
e.value,
style: TextStyle(
color: isSelected ? cs.onSurface : cs.onSurfaceVariant,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
);
}).toList(),
),
),
),
],
);
}
}
class _LegendItem extends StatelessWidget {
final Color color;
final String label;
final ColorScheme cs;
const _LegendItem({required this.color, required this.label, required this.cs});
@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: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
],
);
}
}
@@ -0,0 +1,971 @@
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:get/get.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../controller/admin/marketing_controller.dart';
class MarketingPage extends StatelessWidget {
const MarketingPage({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final controller = Get.put(MarketingController());
controller.fetchAnomalies();
controller.fetchCampaignsLog();
controller.fetchTelemetry();
controller.fetchPriceComparison();
controller.fetchPriceGapHeatmap();
return GetBuilder<MarketingController>(
builder: (c) {
return DefaultTabController(
length: 3,
child: Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Text('أتمتة التسويق الذكي (Siro AI Marketing)', style: TextStyle(color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w700)),
],
),
),
_buildCountrySelector(c, cs),
Container(
color: cs.surface,
child: TabBar(
indicatorColor: cs.primary,
labelColor: cs.primary,
unselectedLabelColor: cs.onSurfaceVariant,
labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
tabs: const [
Tab(icon: Icon(Icons.analytics_outlined, size: 20), text: 'شواذ الأسعار والمنافسين'),
Tab(icon: Icon(Icons.campaign_outlined, size: 20), text: 'سجل الحملات المنجزة'),
Tab(icon: Icon(Icons.settings_outlined, size: 20), text: 'إعدادات الأتمتة والتحكم'),
],
),
),
Expanded(
child: TabBarView(
children: [
_buildAnomaliesTab(context, c, cs),
_buildLogsTab(context, c, cs),
_buildConfigTab(context, c, cs),
],
),
),
],
),
),
);
},
);
}
Widget _buildCountrySelector(MarketingController c, ColorScheme cs) {
final countries = {
'All': 'الكل',
'SY': 'سوريا 🇸🇾',
'JO': 'الأردن 🇯🇴',
'EG': 'مصر 🇪🇬',
'IQ': 'العراق 🇮🇶',
};
return SizedBox(
height: 44,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
children: countries.entries.map((entry) {
final isSelected = c.selectedCountry == entry.key;
return Padding(
padding: const EdgeInsets.only(left: 8),
child: ChoiceChip(
label: Text(
entry.value,
style: TextStyle(
color: isSelected ? cs.onSurface : cs.onSurfaceVariant,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
selected: isSelected,
onSelected: (_) => c.changeCountry(entry.key),
selectedColor: cs.primary,
backgroundColor: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: isSelected ? cs.primary : cs.outline),
),
),
);
}).toList(),
),
);
}
Widget _buildAnomaliesTab(BuildContext context, MarketingController c, ColorScheme cs) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAIControlCard(context, c, cs),
_buildAIPredictionCard(context, c, cs),
_buildWhatIfSimulatorCard(context, c, cs),
_buildMarketShareChart(context, c, cs),
_buildComparisonChart(c, cs),
_buildPCIHeatmapSection(c, cs),
Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 8),
child: Row(
children: [
Container(
width: 3,
height: 14,
decoration: BoxDecoration(color: cs.primary, borderRadius: BorderRadius.circular(2)),
),
const SizedBox(width: 8),
Text(
'أحدث مقارنات أسعار المنافسين والفرص السوقية',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.bold),
),
],
),
),
if (c.isLoading && c.priceAnomalies.isEmpty)
SizedBox(
height: 200,
child: Center(child: CircularProgressIndicator(color: cs.primary)),
)
else if (c.priceAnomalies.isEmpty)
SizedBox(
height: 120,
child: Center(
child: Text('لا توجد شواذ سعرية مسجلة حالياً.', style: TextStyle(color: cs.onSurfaceVariant)),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: c.priceAnomalies.length,
itemBuilder: (context, index) {
final anomaly = c.priceAnomalies[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 20.0,
child: FadeInAnimation(
child: Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outline),
),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
anomaly['competitor_name'] ?? "منافس غير معروف",
style: TextStyle(color: cs.primary, fontWeight: FontWeight.bold, fontSize: 11),
),
),
Text(
anomaly['country_code'] ?? "",
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
),
],
),
const SizedBox(height: 12),
Row(
children: [
_buildStatItem('سعرنا', '${anomaly['our_price']}', const Color(0xFF10B981), cs),
const SizedBox(width: 24),
_buildStatItem('سعر المنافس', '${anomaly['competitor_price']}', cs.error, cs),
const SizedBox(width: 24),
_buildStatItem(
'الفارق',
((double.tryParse(anomaly['competitor_price']?.toString() ?? '0') ?? 0) - (double.tryParse(anomaly['our_price']?.toString() ?? '0') ?? 0)).toStringAsFixed(2),
cs.tertiary,
cs,
),
],
),
const SizedBox(height: 12),
Divider(color: cs.outline, height: 1),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'تاريخ الرصد: ${anomaly['detected_at'] ?? ""}',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10),
),
if (anomaly['anomaly_type'] != null)
Text(
anomaly['anomaly_type'].toString(),
style: TextStyle(
color: anomaly['anomaly_type'].toString().contains('high') ? cs.error : cs.tertiary,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
),
),
),
);
},
),
],
),
);
}
Widget _buildComparisonChart(MarketingController c, ColorScheme cs) {
final hourlyData = c.hourlyPriceData;
final siroBase = c.siroBasePrices;
final double siroSpeedPrice = (siroBase['speedPrice'] != null)
? double.tryParse(siroBase['speedPrice'].toString()) ?? 0.0
: 0.0;
List<FlSpot> competitorSpots = [];
if (hourlyData.isNotEmpty) {
for (int i = 0; i < hourlyData.length && i < 24; i++) {
final val = hourlyData[i];
final double avg = double.tryParse((val['avg_price_per_km'] ?? 0).toString()) ?? 0.0;
competitorSpots.add(FlSpot(i.toDouble(), avg));
}
}
if (competitorSpots.isEmpty) {
competitorSpots = const [FlSpot(0, 0), FlSpot(1, 0)];
}
final double maxY = competitorSpots.fold<double>(0, (p, s) => s.y > p ? s.y : p);
final double chartMaxY = maxY > 0 ? maxY * 1.3 : 10;
List<FlSpot> siroSpots = [];
if (siroSpeedPrice > 0) {
for (int i = 0; i < hourlyData.length; i++) {
siroSpots.add(FlSpot(i.toDouble(), siroSpeedPrice));
}
}
return Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outline),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'مقارنة تقلبات الأسعار اللحظية (ساعة/ريال)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: cs.onSurface),
),
const SizedBox(height: 4),
Text(
hourlyData.isNotEmpty
? 'مقارنة أسعار Siro مقابل متوسط المنافسين لآخر ${hourlyData.length} ساعة'
: 'مقارنة أسعار Siro مقابل متوسط المنافسين لآخر 24 ساعة',
style: TextStyle(fontSize: 10, color: cs.onSurfaceVariant),
),
const SizedBox(height: 24),
SizedBox(
height: 160,
child: LineChart(
LineChartData(
gridData: const FlGridData(show: false),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
if (value == 0 || value == chartMaxY / 2 || value == chartMaxY) {
return Text('${value.toInt()}', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 9));
}
return const SizedBox.shrink();
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 22,
getTitlesWidget: (value, meta) {
final idx = value.toInt();
if (idx % 4 == 0 && idx < 24) {
return Text('${idx}h', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 8));
}
return const SizedBox.shrink();
},
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: (hourlyData.length - 1).toDouble(),
minY: 0,
maxY: chartMaxY,
lineBarsData: [
if (siroSpots.length > 1)
LineChartBarData(
spots: siroSpots,
isCurved: true,
color: cs.primary,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: cs.primary.withValues(alpha: 0.1),
),
),
LineChartBarData(
spots: competitorSpots,
isCurved: true,
color: cs.error,
barWidth: 2,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: cs.error.withValues(alpha: 0.05),
),
),
],
),
),
),
],
),
),
);
}
Widget _buildPCIHeatmapSection(MarketingController c, ColorScheme cs) {
final heatmapList = c.heatmapData;
List<Map<String, dynamic>> regions = [];
if (heatmapList.isNotEmpty) {
for (final item in heatmapList) {
final double pci = double.tryParse(item['pci'].toString()) ?? 1.0;
final pct = ((1 - pci) * 100).abs().toStringAsFixed(1);
final isCheaper = pci < 1;
regions.add({
'name': 'شبكة (${item['lat']}, ${item['lng']})',
'pci': pci,
'weight': item['weight'] ?? 0.0,
'desc': isCheaper ? 'أرخص بـ $pct%' : 'أغلى بـ $pct%',
'samples': item['sample_size'] ?? 0,
});
}
}
if (regions.isEmpty) {
regions = [
{'name': 'لا توجد بيانات حرارية كافية', 'pci': 1.0, 'weight': 0.0, 'desc': 'يتم جمع البيانات حالياً', 'samples': 0},
];
}
return Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outline),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'خارطة الفجوات ومؤشر التنافسية السعري (PCI)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: cs.onSurface),
),
const SizedBox(height: 12),
...regions.map((region) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(region['name'] as String, style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: cs.onSurface)),
),
const SizedBox(width: 8),
Text(region['desc'] as String, style: TextStyle(fontSize: 10, color: (region['pci'] as double) < 1 ? const Color(0xFF10B981) : cs.error)),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: ((region['weight'] as double) + 1) / 2,
backgroundColor: const Color(0xFF10B981).withValues(alpha: 0.3),
color: (region['pci'] as double) < 1 ? const Color(0xFF10B981) : cs.error,
minHeight: 6,
),
),
],
),
);
}),
],
),
),
);
}
Widget _buildWhatIfSimulatorCard(BuildContext context, MarketingController c, ColorScheme cs) {
final TextEditingController priceCtrl = TextEditingController();
return Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: cs.outline)),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.science_outlined, color: cs.primary, size: 20),
const SizedBox(width: 8),
Text('محاكي تغيير الأسعار الذكي', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: cs.onSurface)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: priceCtrl,
keyboardType: TextInputType.number,
style: TextStyle(color: cs.onSurface, fontSize: 13),
decoration: InputDecoration(
hintText: 'سعر الكيلومتر المقترح',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
filled: true,
fillColor: cs.surfaceContainerLow,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () {
if (priceCtrl.text.isNotEmpty) c.runWhatIfSimulation(priceCtrl.text);
},
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('محاكاة', style: TextStyle(fontSize: 12)),
),
],
),
if (c.simulatedPci != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: c.simulatorRecommendationStatus == 'success' ? const Color(0xFF10B981).withValues(alpha: 0.1) : (c.simulatorRecommendationStatus == 'danger' ? cs.error.withValues(alpha: 0.1) : cs.tertiary.withValues(alpha: 0.1)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('مؤشر PCI المتوقع:', style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)),
Text(c.simulatedPci.toString(), style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: cs.onSurface)),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('الحصة السوقية (أرخص):', style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)),
Text('${c.simulatedMarketShare}%', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: cs.onSurface)),
],
),
const SizedBox(height: 8),
Text(
c.simulatorRecommendationMessage ?? '',
style: TextStyle(
fontSize: 11,
color: c.simulatorRecommendationStatus == 'success' ? const Color(0xFF10B981) : (c.simulatorRecommendationStatus == 'danger' ? cs.error : cs.tertiary)
),
),
],
),
),
]
],
),
),
);
}
Widget _buildLogsTab(BuildContext context, MarketingController c, ColorScheme cs) {
if (c.isLoading && c.campaignsLog.isEmpty) {
return Center(child: CircularProgressIndicator(color: cs.primary));
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: c.campaignsLog.length,
itemBuilder: (context, index) {
final log = c.campaignsLog[index];
final channel = log['channel']?.toString().toUpperCase() ?? 'SMS';
Color channelColor = cs.tertiary;
if (channel == 'SMS') channelColor = const Color(0xFF10B981);
if (channel == 'WHATSAPP') channelColor = const Color(0xFF25D366);
return Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outline),
),
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: channelColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
channel,
style: TextStyle(color: channelColor, fontWeight: FontWeight.bold, fontSize: 11),
),
),
Text(
log['created_at']?.toString() ?? "",
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
),
],
),
const SizedBox(height: 12),
Text(
log['campaign_name']?.toString() ?? "حملة استعادة تلقائية",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: cs.onSurface),
),
const SizedBox(height: 8),
Text(
log['message_body']?.toString() ?? "",
style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant, height: 1.4),
),
const SizedBox(height: 12),
Divider(color: cs.outline, height: 1),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.people_outline_rounded, size: 16, color: cs.onSurfaceVariant),
const SizedBox(width: 6),
Text(
'المستهدفون: ${log['users_targeted'] ?? 0}',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
),
],
),
if (log['promo_code'] != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: cs.primary.withValues(alpha: 0.2)),
),
child: Text(
'كود: ${log['promo_code']}',
style: TextStyle(color: cs.primary, fontWeight: FontWeight.bold, fontSize: 11),
),
),
],
),
],
),
),
);
},
);
}
Widget _buildConfigTab(BuildContext context, MarketingController c, ColorScheme cs) {
final promptCtrl = TextEditingController(text: c.systemPrompt);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outline),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.bar_chart_rounded, color: cs.tertiary, size: 20),
const SizedBox(width: 8),
Text(
'مراقب استهلاك الرموز والطلبات (API Telemetry)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: cs.onSurface),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildTelemetryItem('طلبات الـ API', '${c.apiRequestsCount}', cs.onSurface, cs),
_buildTelemetryItem('الرموز (Tokens)', '${c.totalTokensUsed}', cs.onSurface, cs),
_buildTelemetryItem('التكلفة التقديرية', '\$${c.estimatedCostUSD.toStringAsFixed(5)}', const Color(0xFF10B981), cs),
],
),
],
),
),
),
const SizedBox(height: 16),
Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outline),
),
child: SwitchListTile(
title: Text(
'التشغيل الآلي بالكامل (Full Autopilot)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: cs.onSurface),
),
subtitle: Text(
c.isAutopilotEnabled
? 'يقوم النظام بإرسال الرسائل الترويجية فور رصد فرصة سعرية دون تدخل.'
: 'النظام في وضع شبه الآلي (يتطلب مراجعة المدير قبل الإرسال).',
style: TextStyle(fontSize: 10, color: cs.onSurfaceVariant),
),
value: c.isAutopilotEnabled,
activeThumbColor: cs.primary,
onChanged: c.toggleAutopilot,
),
),
const SizedBox(height: 16),
Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: cs.outline),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(Icons.edit_note_rounded, color: cs.primary, size: 20),
const SizedBox(width: 8),
Text(
'تعديل التوجيه الأساسي للذكاء الاصطناعي (System Prompt)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: cs.onSurface),
),
],
),
const SizedBox(height: 12),
TextField(
controller: promptCtrl,
maxLines: 4,
style: TextStyle(fontSize: 12, color: cs.onSurface),
decoration: InputDecoration(
filled: true,
fillColor: cs.surfaceContainerLow,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: cs.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: cs.outline),
),
),
),
const SizedBox(height: 12),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () => c.savePrompt(promptCtrl.text),
child: const Text('حفظ التوجيهات الجديدة', style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
),
),
],
),
);
}
Widget _buildTelemetryItem(String label, String value, Color valueColor, ColorScheme cs) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(color: valueColor, fontSize: 14, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildStatItem(String label, String value, Color color, ColorScheme cs) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11)),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(color: color, fontSize: 15, fontWeight: FontWeight.bold),
),
],
);
}
Widget _buildAIControlCard(BuildContext context, MarketingController c, ColorScheme cs) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
cs.surfaceContainerLow,
cs.surfaceContainerHighest,
],
),
border: Border.all(color: cs.outlineVariant),
boxShadow: [
BoxShadow(
color: cs.primary.withValues(alpha: 0.08),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.psychology_outlined, color: cs.primary, size: 24),
),
const SizedBox(width: 12),
Text(
'مركز حملات الذكاء الاصطناعي (Gemini)',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.bold, fontSize: 15),
),
],
),
const SizedBox(height: 12),
Text(
'يقوم نظام Autopilot بتحليل الشواذ السعرية لمنافسينا في مختلف المناطق الجغرافية، وصياغة عروض وحملات تسويقية ملائمة ومخصصة لإرسالها فورياً للعملاء المستهدفين.',
style: TextStyle(color: cs.onSurface.withValues(alpha: 0.7), fontSize: 11, height: 1.5),
),
const SizedBox(height: 20),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 4,
),
onPressed: c.isLoading ? null : () => c.triggerAICampaign(),
icon: c.isLoading
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(color: cs.onPrimary, strokeWidth: 2),
)
: const Icon(Icons.offline_bolt_rounded),
label: const Text(
'إطلاق حملة استعادة ذكية فوراً',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
);
}
Widget _buildAIPredictionCard(BuildContext context, MarketingController c, ColorScheme cs) {
if (c.aiPredictionMessage == null) return const SizedBox.shrink();
return Card(
color: cs.tertiary.withValues(alpha: 0.05),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: cs.tertiary.withValues(alpha: 0.3))),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.psychology, color: cs.tertiary, size: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('توقعات الذكاء الاصطناعي (Siro AI Prediction)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: cs.tertiary)),
const SizedBox(height: 6),
Text(
c.aiPredictionMessage!,
style: TextStyle(fontSize: 11, color: cs.onSurface, height: 1.5),
),
],
),
),
],
),
),
);
}
Widget _buildMarketShareChart(BuildContext context, MarketingController c, ColorScheme cs) {
if (c.marketShareData.isEmpty) return const SizedBox.shrink();
return Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: cs.outline)),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تطور الحصة السوقية (آخر 12 أسبوع)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: cs.onSurface),
),
const SizedBox(height: 16),
SizedBox(
height: 180,
child: LineChart(
LineChartData(
gridData: const FlGridData(show: false),
titlesData: const FlTitlesData(
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 30)),
bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: c.marketShareData.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), double.tryParse(e.value['market_share'].toString()) ?? 0.0);
}).toList(),
isCurved: true,
color: cs.primary,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(show: true, color: cs.primary.withValues(alpha: 0.1)),
),
],
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,170 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import '../../../constant/links.dart';
class SocialIntelligenceController extends GetxController {
var isLoading = true.obs;
var reports = [].obs;
@override
void onInit() {
super.onInit();
fetchReports();
}
Future<void> fetchReports() async {
try {
isLoading(true);
final url = Uri.parse(
'${AppLink.server}/marketing_engine/index.php?action=get_reports');
final response = await http.get(url);
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'success') {
reports.value = data['data'] ?? [];
}
}
} catch (e) {
print('Error fetching reports: $e');
} finally {
isLoading(false);
}
}
}
class SocialIntelligenceScreen extends StatelessWidget {
const SocialIntelligenceScreen({super.key});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final controller = Get.put(SocialIntelligenceController());
return Scaffold(
backgroundColor: cs.surface,
body: Column(
children: [
Container(
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
child: Row(
children: [
GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.arrow_back_ios_new_rounded,
color: cs.onSurfaceVariant, size: 16),
),
),
const SizedBox(width: 12),
Text('استخبارات السوشيال ميديا',
style: TextStyle(
color: cs.onSurface,
fontSize: 17,
fontWeight: FontWeight.bold)),
const Spacer(),
GestureDetector(
onTap: () => controller.fetchReports(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: cs.outline),
),
child: Icon(Icons.refresh,
color: cs.onSurfaceVariant, size: 16),
),
),
],
),
),
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(color: cs.primary));
}
if (controller.reports.isEmpty) {
return Center(
child: Text(
'لا توجد تقارير حالياً',
style: TextStyle(color: cs.onSurfaceVariant),
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.reports.length,
itemBuilder: (context, index) {
final report = controller.reports[index];
final isWeekly = report['is_weekly'].toString() == '1';
return Card(
color: cs.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: isWeekly ? cs.primary : cs.outline,
width: isWeekly ? 1.5 : 1.0),
),
margin: const EdgeInsets.only(bottom: 16),
child: ExpansionTile(
iconColor: cs.primary,
collapsedIconColor: cs.onSurfaceVariant,
title: Text(
isWeekly
? '📈 تقرير السوق الأسبوعي الشامل'
: '📊 تقرير ${report['platform'].toString().toUpperCase()}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: isWeekly ? cs.primary : cs.onSurface,
fontSize: 14,
),
),
subtitle: Text(
report['created_at'].toString(),
style: TextStyle(
color: cs.onSurfaceVariant, fontSize: 11),
),
children: [
Container(
padding: const EdgeInsets.all(16),
width: double.infinity,
decoration: BoxDecoration(
border: Border(top: BorderSide(color: cs.outline)),
),
child: Directionality(
textDirection: TextDirection.rtl,
child: HtmlWidget(
report['report_html'] ?? '',
textStyle: TextStyle(
color: cs.onSurface,
fontSize: 13,
height: 1.6),
),
),
)
],
),
);
},
);
}),
),
],
),
);
}
}