Update: 2026-06-22 00:31:28

This commit is contained in:
Hamza-Ayed
2026-06-22 00:31:29 +03:00
parent e73be65a72
commit efe26c95be
19 changed files with 2635 additions and 32 deletions

View File

@@ -345,6 +345,11 @@ import 'box_name.dart';class AppLink {
static String getCampaignsLog = "$server/Admin/marketing/get_campaigns_log.php";
static String getTelemetry = "$server/Admin/marketing/get_telemetry.php";
static String getPriceComparison = "$server/Admin/marketing/get_price_comparison.php";
static String whatIfSimulator = "$server/Admin/marketing/what_if_simulator.php";
static String getPriceGapHeatmap = "$server/Admin/marketing/get_price_gap_heatmap.php";
static String winbackHotspotTargets = "$server/Admin/marketing/winback_hotspot_targets.php";
static String getMarketShareAnalytics = "$server/Admin/marketing/get_market_share_analytics.php";
static String aiPricePrediction = "$server/Admin/marketing/ai_price_prediction.php";
static String saveDriverDestination = "$server/ride/location/save_driver_destination.php";
static String paymentServerV2 = 'https://walletintaleq.intaleq.xyz/v2/main';
static String realtimeDashboardV2 = "$server/Admin/v2/realtime_dashboard.php";

View File

@@ -28,6 +28,10 @@ class MarketingController extends GetxController {
fetchAnomalies();
fetchCampaignsLog();
fetchPriceComparison();
fetchPriceGapHeatmap();
fetchMarketShareAnalytics();
fetchAiPricePrediction();
fetchWinbackTargets();
}
// --- Autopilot Status Toggle ---
@@ -187,4 +191,158 @@ class MarketingController extends GetxController {
update();
}
}
// --- What-If Simulator State ---
double? simulatedPci;
double? simulatedMarketShare;
String? simulatorRecommendationStatus;
String? simulatorRecommendationMessage;
Future<void> runWhatIfSimulation(String proposedSpeedPrice) async {
isLoading = true;
update();
try {
Map<String, dynamic> params = {
'speed_price': proposedSpeedPrice
};
if (selectedCountry != 'All') {
params['country_code'] = selectedCountry;
} else {
params['country_code'] = 'SY'; // Fallback
}
var res = await CRUD().post(
link: AppLink.whatIfSimulator,
payload: params,
);
if (res is Map && res['status'] == 'success') {
final data = res['message'];
if (data is Map) {
simulatedPci = (data['simulated_pci'] ?? 0.0).toDouble();
simulatedMarketShare = (data['simulated_market_share_percent'] ?? 0.0).toDouble();
simulatorRecommendationStatus = data['recommendation_status'];
simulatorRecommendationMessage = data['recommendation_message'];
}
} else {
Get.snackbar("Simulation Error", res['message'] ?? "Failed to run simulation");
}
} catch (e) {
Get.snackbar("Error", "Network error during simulation");
} finally {
isLoading = false;
update();
}
}
// --- Heatmap State ---
List heatmapData = [];
double currentSiroPriceHeatmap = 0.0;
Future<void> fetchPriceGapHeatmap() async {
try {
Map<String, dynamic> params = {};
if (selectedCountry != 'All') {
params['country_code'] = selectedCountry;
} else {
params['country_code'] = 'SY'; // Fallback
}
var res = await CRUD().post(
link: AppLink.getPriceGapHeatmap,
payload: params,
);
if (res is Map && res['status'] == 'success') {
final data = res['message'];
if (data is Map) {
heatmapData = data['heatmap_data'] ?? [];
currentSiroPriceHeatmap = (data['current_siro_price_per_km'] ?? 0.0).toDouble();
update();
}
}
} catch (e) {
// Silently fail
}
}
// --- Market Share Analytics ---
List marketShareData = [];
Future<void> fetchMarketShareAnalytics() async {
try {
Map<String, dynamic> params = {};
if (selectedCountry != 'All') {
params['country_code'] = selectedCountry;
} else {
params['country_code'] = 'SY';
}
var res = await CRUD().post(
link: AppLink.getMarketShareAnalytics,
payload: params,
);
if (res is Map && res['status'] == 'success') {
marketShareData = res['historical_data'] ?? [];
update();
}
} catch (e) {
// Ignore
}
}
// --- AI Price Prediction ---
String? aiPredictionMessage;
List predictedSurgeHours = [];
Future<void> fetchAiPricePrediction() async {
try {
Map<String, dynamic> params = {};
if (selectedCountry != 'All') {
params['country_code'] = selectedCountry;
} else {
params['country_code'] = 'SY';
}
var res = await CRUD().post(
link: AppLink.aiPricePrediction,
payload: params,
);
if (res is Map && res['status'] == 'success') {
aiPredictionMessage = res['ai_analysis_message'];
predictedSurgeHours = res['predicted_surge_hours'] ?? [];
update();
}
} catch (e) {
// Ignore
}
}
// --- Win-Back Targets ---
List winbackTargets = [];
int winbackTotalCount = 0;
Future<void> fetchWinbackTargets() async {
try {
Map<String, dynamic> params = {};
if (selectedCountry != 'All') {
params['country_code'] = selectedCountry;
} else {
params['country_code'] = 'SY';
}
var res = await CRUD().post(
link: AppLink.winbackHotspotTargets,
payload: params,
);
if (res is Map && res['status'] == 'success') {
winbackTargets = res['targets'] ?? [];
winbackTotalCount = res['total_targets'] ?? 0;
update();
}
} catch (e) {
// Ignore
}
}
@override
void onInit() {
super.onInit();
// Initially fetch these too if needed, but fetch them specifically when country changes
}
}

View File

@@ -27,6 +27,7 @@ class MarketingPage extends StatelessWidget {
controller.fetchCampaignsLog();
controller.fetchTelemetry();
controller.fetchPriceComparison();
controller.fetchPriceGapHeatmap();
return GetBuilder<MarketingController>(
builder: (c) {
@@ -124,6 +125,9 @@ class MarketingPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAIControlCard(context, c),
_buildAIPredictionCard(context, c),
_buildWhatIfSimulatorCard(context, c),
_buildMarketShareChart(context, c),
_buildComparisonChart(),
_buildPCIHeatmapSection(),
Padding(
@@ -386,33 +390,28 @@ class MarketingPage extends StatelessWidget {
Widget _buildPCIHeatmapSection() {
return GetBuilder<MarketingController>(
builder: (c) {
final pciData = c.pciRegions;
final siroBase = c.siroBasePrices;
final double siroSpeedPrice = (siroBase['speedPrice'] != null)
? double.tryParse(siroBase['speedPrice'].toString()) ?? 0.0
: 0.0;
final heatmapList = c.heatmapData;
final siroSpeedPrice = c.currentSiroPriceHeatmap;
List<Map<String, dynamic>> regions = [];
if (pciData is List && pciData.isNotEmpty) {
for (final item in pciData) {
final double compAvg = double.tryParse((item['avg_price_per_km'] ?? 0).toString()) ?? 0.0;
final pci = siroSpeedPrice > 0 && compAvg > 0
? (siroSpeedPrice / compAvg).clamp(0.5, 1.5)
: 1.0;
if (heatmapList is List && 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['competitor_name'] ?? 'منافس'} (${item['lat_group'] ?? ''}, ${item['lng_group'] ?? ''})',
'name': 'شبكة (${item['lat']}, ${item['lng']})',
'pci': pci,
'desc': isCheaper ? 'أرخص بنسبة $pct% من المنافسين' : 'أغلى بنسبة $pct% من المنافسين',
'samples': item['samples'] ?? 0,
'weight': item['weight'] ?? 0.0,
'desc': isCheaper ? 'أرخص بـ $pct%' : 'أغلى بـ $pct%',
'samples': item['sample_size'] ?? 0,
});
}
}
if (regions.isEmpty) {
regions = [
{'name': 'لا توجد بيانات', 'pci': 1.0, 'desc': 'يتم جمع البيانات خلال 24 ساعة', 'samples': 0},
{'name': 'لا توجد بيانات حرارية كافية', 'pci': 1.0, 'weight': 0.0, 'desc': 'يتم جمع البيانات حالياً', 'samples': 0},
];
}
@@ -439,6 +438,8 @@ class MarketingPage extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -447,16 +448,16 @@ class MarketingPage extends StatelessWidget {
child: Text(region['name'] as String, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: _textPrimary)),
),
const SizedBox(width: 8),
Text(region['desc'] as String, style: const TextStyle(fontSize: 10, color: _success)),
Text(region['desc'] as String, style: TextStyle(fontSize: 10, color: (region['pci'] as double) < 1 ? _success : _danger)),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (1 - pciVal).clamp(0.0, 1.0),
backgroundColor: AppColor.surfaceElevated,
color: _success,
value: ((region['weight'] as double) + 1) / 2, // Map -1..1 to 0..1
backgroundColor: _success.withOpacity(0.3),
color: (region['pci'] as double) < 1 ? _success : _danger,
minHeight: 6,
),
),
@@ -472,24 +473,104 @@ class MarketingPage extends StatelessWidget {
);
}
Widget _buildWhatIfSimulatorCard(BuildContext context, MarketingController c) {
final TextEditingController priceCtrl = TextEditingController();
return Card(
color: _surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: const BorderSide(color: _divider)),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.science_outlined, color: _accent, size: 20),
SizedBox(width: 8),
Text('محاكي تغيير الأسعار الذكي', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: _textPrimary)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: priceCtrl,
keyboardType: TextInputType.number,
style: const TextStyle(color: _textPrimary, fontSize: 13),
decoration: InputDecoration(
hintText: 'سعر الكيلومتر المقترح',
hintStyle: const TextStyle(color: _textSecondary, fontSize: 12),
filled: true,
fillColor: AppColor.surfaceElevated,
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: _accent,
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' ? _success.withOpacity(0.1) : (c.simulatorRecommendationStatus == 'danger' ? _danger.withOpacity(0.1) : _info.withOpacity(0.1)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('مؤشر PCI المتوقع:', style: TextStyle(fontSize: 12, color: _textSecondary)),
Text(c.simulatedPci.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _textPrimary)),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('الحصة السوقية (أرخص):', style: TextStyle(fontSize: 12, color: _textSecondary)),
Text('${c.simulatedMarketShare}%', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: _textPrimary)),
],
),
const SizedBox(height: 8),
Text(
c.simulatorRecommendationMessage ?? '',
style: TextStyle(
fontSize: 11,
color: c.simulatorRecommendationStatus == 'success' ? _success : (c.simulatorRecommendationStatus == 'danger' ? _danger : _info)
),
),
],
),
),
]
],
),
),
);
}
Widget _buildLogsTab(BuildContext context, MarketingController c) {
if (c.isLoading && c.campaignsLog.isEmpty) {
return const Center(child: CircularProgressIndicator(color: _accent));
}
if (c.campaignsLog.isEmpty) {
return const Center(
child: Text('سجل الحملات فارغ حالياً.', style: TextStyle(color: _textSecondary)),
);
}
return ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
itemCount: c.campaignsLog.length,
itemBuilder: (context, index) {
final log = c.campaignsLog[index];
final channel = log['channel']?.toString().toUpperCase() ?? "FCM";
Color channelColor = _info;
if (channel == 'SMS') channelColor = _success;
if (channel == 'WHATSAPP') channelColor = const Color(0xFF25D366);
@@ -807,4 +888,150 @@ class MarketingPage extends StatelessWidget {
),
);
}
Widget _buildAIPredictionCard(BuildContext context, MarketingController c) {
if (c.aiPredictionMessage == null) return const SizedBox.shrink();
return Card(
color: _info.withOpacity(0.05),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: _info.withOpacity(0.3))),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.psychology, color: _info, size: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('توقعات الذكاء الاصطناعي (Siro AI Prediction)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: _info)),
const SizedBox(height: 6),
Text(
c.aiPredictionMessage!,
style: const TextStyle(fontSize: 11, color: _textPrimary, height: 1.5),
),
],
),
),
],
),
),
);
}
Widget _buildMarketShareChart(BuildContext context, MarketingController c) {
if (c.marketShareData.isEmpty) return const SizedBox.shrink();
return Card(
color: _surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: const BorderSide(color: _divider)),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'تطور الحصة السوقية (آخر 12 أسبوع)',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: _textPrimary),
),
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: _accent,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(show: true, color: _accent.withOpacity(0.1)),
),
],
),
),
),
],
),
),
);
}
Widget _buildWinbackCampaignsSection(BuildContext context, MarketingController c) {
return Card(
color: _surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: const BorderSide(color: _divider)),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.radar, color: _accent, size: 24),
SizedBox(width: 8),
Text('حملات استعادة العملاء (Win-Back)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: _textPrimary)),
],
),
const SizedBox(height: 8),
const Text(
'يتم البحث عن الركاب المنقطعين عن التطبيق والذين يتواجدون حالياً بالقرب من مناطق تشهد أسعار ذروة لدى المنافسين.',
style: TextStyle(color: _textSecondary, fontSize: 11),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => c.fetchWinbackTargets(),
icon: const Icon(Icons.person_search, size: 18),
label: const Text('بحث عن أهداف حالية'),
style: ElevatedButton.styleFrom(
backgroundColor: _accent.withOpacity(0.1),
foregroundColor: _accent,
elevation: 0,
),
),
),
if (c.winbackTotalCount > 0) ...[
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
// TODO: Implement trigger specific winback campaign
Get.snackbar("Campaign Triggered", "SMS sent to ${c.winbackTotalCount} targets");
},
icon: const Icon(Icons.send),
label: Text('إرسال SMS لـ ${c.winbackTotalCount}'),
style: ElevatedButton.styleFrom(
backgroundColor: _success,
foregroundColor: Colors.white,
),
),
),
]
],
),
],
),
),
);
}
}