Update: 2026-06-22 00:31:28
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user