Update: 2026-06-22 00:31:28
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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