إصلاح أخطاء شاشة الكباتن من تحديث الذكاء الاصطناعي
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -493,6 +494,7 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildChartsSection(data, controller, cs),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
@@ -500,6 +502,206 @@ class _AdminHomePageState extends State<AdminHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChartsSection(
|
||||
dynamic data, DashboardController controller, ColorScheme cs) {
|
||||
final completed = (data['completed_rides'] ?? 0).toDouble();
|
||||
final cancelled = (data['cancelled_rides'] ?? 0).toDouble();
|
||||
final total = completed + cancelled;
|
||||
final completedPercent = total > 0 ? (completed / total * 100).toStringAsFixed(1) : '0.0';
|
||||
|
||||
final comfort = (data['comfort'] ?? 0).toDouble();
|
||||
final speed = (data['speed'] ?? 0).toDouble();
|
||||
final lady = (data['lady'] ?? 0).toDouble();
|
||||
final maxBar = [comfort, speed, lady].reduce((a, b) => a > b ? a : b);
|
||||
final barMaxY = maxBar > 0 ? (maxBar * 1.3).ceilToDouble() : 10.0;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 0, 4, 10),
|
||||
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: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.2,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 35,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: completed,
|
||||
color: cs.primary,
|
||||
radius: 55,
|
||||
title: '',
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: cancelled,
|
||||
color: cs.error,
|
||||
radius: 55,
|
||||
title: '',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'$completedPercent%',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
total.toInt().toString(),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: barMaxY,
|
||||
barTouchData: BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (_) => cs.surfaceContainerHighest,
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
return BarTooltipItem(
|
||||
rod.toY.toInt().toString(),
|
||||
TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final labels = ['Comfort', 'Speed', 'Lady'];
|
||||
if (value.toInt() < labels.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
labels[value.toInt()],
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
gridData: const FlGridData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: [
|
||||
BarChartGroupData(
|
||||
x: 0,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: comfort,
|
||||
color: cs.tertiary,
|
||||
width: 28,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
BarChartGroupData(
|
||||
x: 1,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: speed,
|
||||
color: const Color(0xFFF59E0B),
|
||||
width: 28,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
BarChartGroupData(
|
||||
x: 2,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: lady,
|
||||
color: const Color(0xFFF48FB1),
|
||||
width: 28,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHighlightCard(_HighlightData h, ColorScheme cs) {
|
||||
return GlassContainer(
|
||||
width: 155,
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/admin/live_analytics_controller.dart';
|
||||
|
||||
class LiveAnalyticsPage extends StatelessWidget {
|
||||
@@ -13,15 +12,16 @@ class LiveAnalyticsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = Get.put(LiveAnalyticsController());
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('لوحة البيانات التفاعلية',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
backgroundColor: AppColor.surface,
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
@@ -34,13 +34,13 @@ class LiveAnalyticsPage extends StatelessWidget {
|
||||
onPressed: () => c.fetchAll(),
|
||||
),
|
||||
],
|
||||
bottom: const TabBar(
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
indicatorColor: AppColor.accent,
|
||||
labelColor: AppColor.accent,
|
||||
unselectedLabelColor: AppColor.textSecondary,
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||
tabs: [
|
||||
indicatorColor: cs.primary,
|
||||
labelColor: cs.primary,
|
||||
unselectedLabelColor: cs.onSurfaceVariant,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
|
||||
tabs: const [
|
||||
Tab(text: 'الخريطة التفاعلية', icon: Icon(Icons.map_rounded, size: 18)),
|
||||
Tab(text: 'السوق والتسعير', icon: Icon(Icons.trending_up_rounded, size: 18)),
|
||||
Tab(text: 'الأسطول والجودة', icon: Icon(Icons.local_taxi_rounded, size: 18)),
|
||||
@@ -51,8 +51,8 @@ class LiveAnalyticsPage extends StatelessWidget {
|
||||
body: GetBuilder<LiveAnalyticsController>(
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColor.accent),
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: cs.primary),
|
||||
);
|
||||
}
|
||||
if (ctrl.allData.isEmpty) {
|
||||
@@ -60,16 +60,16 @@ class LiveAnalyticsPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_off_rounded,
|
||||
size: 48, color: AppColor.textMuted),
|
||||
Icon(Icons.cloud_off_rounded,
|
||||
size: 48, color: cs.onSurfaceVariant),
|
||||
const SizedBox(height: 12),
|
||||
Text('لا توجد بيانات لتاريخ ${ctrl.selectedDate}',
|
||||
style: const TextStyle(color: AppColor.textSecondary)),
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
const SizedBox(height: 8),
|
||||
if (ctrl.availableDates.isNotEmpty)
|
||||
Text('آخر تاريخ متاح: ${ctrl.availableDates.first}',
|
||||
style: const TextStyle(
|
||||
color: AppColor.accent, fontSize: 13)),
|
||||
style: TextStyle(
|
||||
color: cs.primary, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -89,6 +89,7 @@ class LiveAnalyticsPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _pickDate(BuildContext context, LiveAnalyticsController c) async {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.tryParse(c.selectedDate) ?? DateTime.now(),
|
||||
@@ -96,7 +97,7 @@ class LiveAnalyticsPage extends StatelessWidget {
|
||||
lastDate: DateTime.now(),
|
||||
builder: (ctx, child) => Theme(
|
||||
data: ThemeData.dark().copyWith(
|
||||
colorScheme: const ColorScheme.dark(primary: AppColor.accent),
|
||||
colorScheme: ColorScheme.dark(primary: cs.primary),
|
||||
),
|
||||
child: child!,
|
||||
),
|
||||
@@ -129,12 +130,13 @@ class _StatCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -143,14 +145,14 @@ class _StatCard extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 16, color: AppColor.textMuted),
|
||||
Icon(icon, size: 16, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColor.textMuted,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
@@ -162,17 +164,17 @@ class _StatCard extends StatelessWidget {
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColor.textPrimary)),
|
||||
color: cs.onSurface)),
|
||||
),
|
||||
if (sub != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(sub!,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: subColor ?? AppColor.textSecondary),
|
||||
color: subColor ?? cs.onSurfaceVariant),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
@@ -187,13 +189,14 @@ class _SectionTitle extends StatelessWidget {
|
||||
const _SectionTitle(this.title);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, top: 8),
|
||||
child: Text(title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.textPrimary)),
|
||||
color: cs.onSurface)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -211,22 +214,23 @@ class _ChartCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.textSecondary)),
|
||||
color: cs.onSurfaceVariant)),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: height, child: child),
|
||||
],
|
||||
@@ -281,6 +285,7 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final c = widget.ctrl;
|
||||
final rt = c.realtime;
|
||||
|
||||
@@ -293,7 +298,6 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// KPI Cards
|
||||
GridView.count(
|
||||
crossAxisCount: MediaQuery.of(context).size.width > 600 ? 5 : 2,
|
||||
shrinkWrap: true,
|
||||
@@ -316,7 +320,7 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
label: 'إيرادات اليوم',
|
||||
value: _fmt(revToday),
|
||||
sub: '${revChange >= 0 ? '+' : ''}${revChange.toStringAsFixed(1)}% عن الأمس',
|
||||
subColor: revChange >= 0 ? AppColor.success : AppColor.danger,
|
||||
subColor: revChange >= 0 ? const Color(0xFF10B981) : cs.error,
|
||||
icon: Icons.attach_money_rounded,
|
||||
),
|
||||
_StatCard(
|
||||
@@ -334,18 +338,18 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const _SectionTitle('خريطة فجوة العرض والطلب'),
|
||||
_buildLayerToggles(),
|
||||
_buildLayerToggles(cs),
|
||||
const SizedBox(height: 8),
|
||||
_buildMap(c),
|
||||
_buildMap(c, cs),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
_buildHourlyChart(c),
|
||||
_buildFunnelChart(c),
|
||||
_buildHourlyChart(c, cs),
|
||||
_buildFunnelChart(c, cs),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLayerToggles() {
|
||||
Widget _buildLayerToggles(ColorScheme cs) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
@@ -354,17 +358,17 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
(v) => setState(() => showGap = v)),
|
||||
_LayerChip('كثافة الطلب', showHeatmap, Colors.orange,
|
||||
(v) => setState(() => showHeatmap = v)),
|
||||
_LayerChip('مواقع الكباتن', showSupply, AppColor.success,
|
||||
_LayerChip('مواقع الكباتن', showSupply, const Color(0xFF10B981),
|
||||
(v) => setState(() => showSupply = v)),
|
||||
_LayerChip('أسعار المنافسين', showPricing, AppColor.info,
|
||||
_LayerChip('أسعار المنافسين', showPricing, cs.tertiary,
|
||||
(v) => setState(() => showPricing = v)),
|
||||
_LayerChip('أعلى المناطق', showZones, AppColor.accent,
|
||||
_LayerChip('أعلى المناطق', showZones, cs.primary,
|
||||
(v) => setState(() => showZones = v)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMap(LiveAnalyticsController c) {
|
||||
Widget _buildMap(LiveAnalyticsController c, ColorScheme cs) {
|
||||
final gapCells = (c.gap['cells'] as List?) ?? [];
|
||||
final heatmapData = c.heatmap['data'];
|
||||
final pricingGrids = c.pricing['grids'];
|
||||
@@ -382,8 +386,8 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
markers.add(CircleMarker(
|
||||
point: LatLng(lat, lng),
|
||||
color: gap > 0
|
||||
? Colors.redAccent.withOpacity(0.3 + intensity * 0.5)
|
||||
: AppColor.success.withOpacity(0.4),
|
||||
? Colors.redAccent.withValues(alpha: 0.3 + intensity * 0.5)
|
||||
: const Color(0xFF10B981).withValues(alpha: 0.4),
|
||||
borderStrokeWidth: 0,
|
||||
radius: 12 + intensity * 8,
|
||||
));
|
||||
@@ -399,7 +403,7 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (lat == 0 || lng == 0) continue;
|
||||
markers.add(CircleMarker(
|
||||
point: LatLng(lat, lng),
|
||||
color: Colors.orange.withOpacity(0.35),
|
||||
color: Colors.orange.withValues(alpha: 0.35),
|
||||
borderStrokeWidth: 0,
|
||||
radius: 5,
|
||||
));
|
||||
@@ -416,9 +420,9 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (lat == 0 || lng == 0) continue;
|
||||
markers.add(CircleMarker(
|
||||
point: LatLng(lat, lng),
|
||||
color: AppColor.success.withOpacity(0.5),
|
||||
color: const Color(0xFF10B981).withValues(alpha: 0.5),
|
||||
borderStrokeWidth: 1,
|
||||
borderColor: Colors.white.withOpacity(0.4),
|
||||
borderColor: Colors.white.withValues(alpha: 0.4),
|
||||
radius: 6 + supply.clamp(0, 10).toDouble(),
|
||||
));
|
||||
}
|
||||
@@ -435,9 +439,9 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (lat == 0 || lng == 0) continue;
|
||||
markers.add(CircleMarker(
|
||||
point: LatLng(lat, lng),
|
||||
color: AppColor.info.withOpacity(0.5),
|
||||
color: cs.tertiary.withValues(alpha: 0.5),
|
||||
borderStrokeWidth: 1,
|
||||
borderColor: Colors.white.withOpacity(0.3),
|
||||
borderColor: Colors.white.withValues(alpha: 0.3),
|
||||
radius: 8,
|
||||
));
|
||||
}
|
||||
@@ -451,9 +455,9 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (lat == 0 || lng == 0) continue;
|
||||
markers.add(CircleMarker(
|
||||
point: LatLng(lat, lng),
|
||||
color: AppColor.accent.withOpacity(0.5),
|
||||
color: cs.primary.withValues(alpha: 0.5),
|
||||
borderStrokeWidth: 2,
|
||||
borderColor: Colors.white.withOpacity(0.5),
|
||||
borderColor: Colors.white.withValues(alpha: 0.5),
|
||||
radius: 10 + (rides / 10).clamp(0, 15).toDouble(),
|
||||
));
|
||||
}
|
||||
@@ -485,7 +489,7 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHourlyChart(LiveAnalyticsController c) {
|
||||
Widget _buildHourlyChart(LiveAnalyticsController c, ColorScheme cs) {
|
||||
final hours = (c.hourly['hours'] as List?) ?? [];
|
||||
if (hours.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
@@ -497,7 +501,7 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
return BarChartGroupData(x: e.key, barRods: [
|
||||
BarChartRodData(
|
||||
toY: _toDouble(e.value['rides']),
|
||||
color: AppColor.accent.withOpacity(0.7),
|
||||
color: cs.primary.withValues(alpha: 0.7),
|
||||
width: 10,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
|
||||
),
|
||||
@@ -511,8 +515,8 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
final i = v.toInt();
|
||||
if (i < 0 || i >= hours.length) return const SizedBox();
|
||||
return Text('${hours[i]['hour']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -530,17 +534,17 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFunnelChart(LiveAnalyticsController c) {
|
||||
Widget _buildFunnelChart(LiveAnalyticsController c, ColorScheme cs) {
|
||||
final statuses = (c.funnel['statuses'] as List?) ?? [];
|
||||
if (statuses.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final colors = {
|
||||
'Finished': AppColor.success,
|
||||
'wait': AppColor.warning,
|
||||
'started': AppColor.info,
|
||||
'arrived': AppColor.accent,
|
||||
'cancelled': AppColor.danger,
|
||||
'timeout': AppColor.textMuted,
|
||||
'Finished': const Color(0xFF10B981),
|
||||
'wait': cs.tertiary,
|
||||
'started': cs.tertiary,
|
||||
'arrived': cs.primary,
|
||||
'cancelled': cs.error,
|
||||
'timeout': cs.onSurfaceVariant,
|
||||
};
|
||||
|
||||
return _ChartCard(
|
||||
@@ -557,7 +561,7 @@ class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin {
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white),
|
||||
color: colors[status] ?? AppColor.textMuted,
|
||||
color: colors[status] ?? cs.onSurfaceVariant,
|
||||
radius: 60,
|
||||
);
|
||||
}).toList(),
|
||||
@@ -579,18 +583,19 @@ class _LayerChip extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return FilterChip(
|
||||
label: Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: active ? Colors.white : AppColor.textSecondary)),
|
||||
color: active ? Colors.white : cs.onSurfaceVariant)),
|
||||
selected: active,
|
||||
onSelected: onChanged,
|
||||
selectedColor: color.withOpacity(0.3),
|
||||
selectedColor: color.withValues(alpha: 0.3),
|
||||
checkmarkColor: color,
|
||||
backgroundColor: AppColor.surface,
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
side: BorderSide(
|
||||
color: active ? color.withOpacity(0.5) : AppColor.divider),
|
||||
color: active ? color.withValues(alpha: 0.5) : cs.outline),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
);
|
||||
}
|
||||
@@ -606,6 +611,7 @@ class _MarketTab extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final mkt = ctrl.market;
|
||||
final countries = mkt['countries'] as Map<String, dynamic>? ?? {};
|
||||
String latestPci = '—';
|
||||
@@ -653,14 +659,14 @@ class _MarketTab extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildCompetitorChart(),
|
||||
_buildPciChart(countries),
|
||||
_buildMarketShareChart(countries),
|
||||
_buildCompetitorChart(cs),
|
||||
_buildPciChart(countries, cs),
|
||||
_buildMarketShareChart(countries, cs),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompetitorChart() {
|
||||
Widget _buildCompetitorChart(ColorScheme cs) {
|
||||
final hourly = (ctrl.competitor['hourly'] as List?) ?? [];
|
||||
if (hourly.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
@@ -670,11 +676,11 @@ class _MarketTab extends StatelessWidget {
|
||||
}
|
||||
|
||||
final compColors = [
|
||||
AppColor.danger,
|
||||
AppColor.info,
|
||||
AppColor.warning,
|
||||
AppColor.success,
|
||||
AppColor.accent,
|
||||
cs.error,
|
||||
cs.tertiary,
|
||||
cs.tertiary,
|
||||
const Color(0xFF10B981),
|
||||
cs.primary,
|
||||
];
|
||||
final hours = <String>{};
|
||||
for (var r in hourly) {
|
||||
@@ -720,8 +726,8 @@ class _MarketTab extends StatelessWidget {
|
||||
final i = v.toInt();
|
||||
if (i < 0 || i >= hourList.length) return const SizedBox();
|
||||
return Text(hourList[i],
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -736,7 +742,7 @@ class _MarketTab extends StatelessWidget {
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (_) =>
|
||||
FlLine(color: AppColor.divider, strokeWidth: 0.5),
|
||||
FlLine(color: cs.outline, strokeWidth: 0.5),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
),
|
||||
@@ -744,7 +750,7 @@ class _MarketTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPciChart(Map<String, dynamic> countries) {
|
||||
Widget _buildPciChart(Map<String, dynamic> countries, ColorScheme cs) {
|
||||
if (countries.isEmpty) return const SizedBox.shrink();
|
||||
final rows = (countries.values.first as List?) ?? [];
|
||||
if (rows.isEmpty) return const SizedBox.shrink();
|
||||
@@ -759,13 +765,13 @@ class _MarketTab extends StatelessWidget {
|
||||
return FlSpot(
|
||||
e.key.toDouble(), _toDouble(e.value['average_pci']));
|
||||
}).toList(),
|
||||
color: AppColor.accent,
|
||||
color: cs.primary,
|
||||
barWidth: 2,
|
||||
dotData: const FlDotData(show: true),
|
||||
isCurved: true,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppColor.accent.withOpacity(0.08),
|
||||
color: cs.primary.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -778,8 +784,8 @@ class _MarketTab extends StatelessWidget {
|
||||
if (i < 0 || i >= rows.length) return const SizedBox();
|
||||
final d = rows[i]['report_date']?.toString() ?? '';
|
||||
return Text(d.length >= 10 ? d.substring(5) : d,
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -797,7 +803,7 @@ class _MarketTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMarketShareChart(Map<String, dynamic> countries) {
|
||||
Widget _buildMarketShareChart(Map<String, dynamic> countries, ColorScheme cs) {
|
||||
if (countries.isEmpty) return const SizedBox.shrink();
|
||||
final rows = (countries.values.first as List?) ?? [];
|
||||
if (rows.isEmpty) return const SizedBox.shrink();
|
||||
@@ -810,7 +816,7 @@ class _MarketTab extends StatelessWidget {
|
||||
return BarChartGroupData(x: e.key, barRods: [
|
||||
BarChartRodData(
|
||||
toY: _toDouble(e.value['market_share_percent']),
|
||||
color: const Color(0xFF00CEC9).withOpacity(0.7),
|
||||
color: const Color(0xFF00CEC9).withValues(alpha: 0.7),
|
||||
width: 14,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(4)),
|
||||
@@ -826,8 +832,8 @@ class _MarketTab extends StatelessWidget {
|
||||
if (i < 0 || i >= rows.length) return const SizedBox();
|
||||
final d = rows[i]['report_date']?.toString() ?? '';
|
||||
return Text(d.length >= 10 ? d.substring(5) : d,
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -856,6 +862,7 @@ class _FleetTab extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final rt = ctrl.realtime;
|
||||
final complaintsList = (ctrl.complaints['by_type'] as List?) ?? [];
|
||||
final totalComplaints =
|
||||
@@ -892,27 +899,27 @@ class _FleetTab extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
_buildComplaintsChart(complaintsList),
|
||||
_buildRevenueProfitChart(),
|
||||
_buildComplaintsChart(complaintsList, cs),
|
||||
_buildRevenueProfitChart(cs),
|
||||
|
||||
if (topZones.isNotEmpty) ...[
|
||||
const _SectionTitle('أعلى المناطق نشاطاً (آخر 4 أسابيع)'),
|
||||
_buildZonesTable(topZones),
|
||||
_buildZonesTable(topZones, cs),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildComplaintsChart(List complaintsList) {
|
||||
Widget _buildComplaintsChart(List complaintsList, ColorScheme cs) {
|
||||
if (complaintsList.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final colors = [
|
||||
AppColor.danger,
|
||||
AppColor.warning,
|
||||
AppColor.info,
|
||||
AppColor.accent,
|
||||
AppColor.success,
|
||||
AppColor.textMuted,
|
||||
cs.error,
|
||||
cs.tertiary,
|
||||
cs.tertiary,
|
||||
cs.primary,
|
||||
const Color(0xFF10B981),
|
||||
cs.onSurfaceVariant,
|
||||
const Color(0xFFfd79a8),
|
||||
];
|
||||
|
||||
@@ -940,7 +947,7 @@ class _FleetTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRevenueProfitChart() {
|
||||
Widget _buildRevenueProfitChart(ColorScheme cs) {
|
||||
final daily = (ctrl.revenue['daily'] as List?) ?? [];
|
||||
if (daily.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
@@ -955,7 +962,7 @@ class _FleetTab extends StatelessWidget {
|
||||
return FlSpot(
|
||||
e.key.toDouble(), _toDouble(e.value['total_revenue']));
|
||||
}).toList(),
|
||||
color: AppColor.info,
|
||||
color: cs.tertiary,
|
||||
barWidth: 2,
|
||||
dotData: const FlDotData(show: false),
|
||||
isCurved: true,
|
||||
@@ -965,7 +972,7 @@ class _FleetTab extends StatelessWidget {
|
||||
return FlSpot(
|
||||
e.key.toDouble(), _toDouble(e.value['company_profit']));
|
||||
}).toList(),
|
||||
color: AppColor.success,
|
||||
color: const Color(0xFF10B981),
|
||||
barWidth: 2,
|
||||
dotData: const FlDotData(show: false),
|
||||
isCurved: true,
|
||||
@@ -981,8 +988,8 @@ class _FleetTab extends StatelessWidget {
|
||||
if (i < 0 || i >= daily.length) return const SizedBox();
|
||||
final d = daily[i]['date']?.toString() ?? '';
|
||||
return Text(d.length >= 10 ? d.substring(5) : d,
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -997,7 +1004,7 @@ class _FleetTab extends StatelessWidget {
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (_) =>
|
||||
FlLine(color: AppColor.divider, strokeWidth: 0.5),
|
||||
FlLine(color: cs.outline, strokeWidth: 0.5),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
),
|
||||
@@ -1005,50 +1012,50 @@ class _FleetTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildZonesTable(List topZones) {
|
||||
Widget _buildZonesTable(List topZones, ColorScheme cs) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: DataTable(
|
||||
columnSpacing: 20,
|
||||
headingRowColor: WidgetStateProperty.all(AppColor.surfaceElevated),
|
||||
columns: const [
|
||||
DataColumn(
|
||||
headingRowColor: WidgetStateProperty.all(cs.surfaceContainerHighest),
|
||||
columns: [
|
||||
const DataColumn(
|
||||
label: Text('#',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: AppColor.textSecondary))),
|
||||
DataColumn(
|
||||
TextStyle(fontSize: 12))),
|
||||
const DataColumn(
|
||||
label: Text('الموقع',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: AppColor.textSecondary))),
|
||||
DataColumn(
|
||||
TextStyle(fontSize: 12))),
|
||||
const DataColumn(
|
||||
label: Text('رحلات',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: AppColor.textSecondary))),
|
||||
DataColumn(
|
||||
TextStyle(fontSize: 12))),
|
||||
const DataColumn(
|
||||
label: Text('متوسط السعر',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: AppColor.textSecondary))),
|
||||
TextStyle(fontSize: 12))),
|
||||
],
|
||||
rows: topZones.asMap().entries.map((e) {
|
||||
final z = e.value;
|
||||
return DataRow(cells: [
|
||||
DataCell(Text('${e.key + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppColor.textPrimary))),
|
||||
fontSize: 12))),
|
||||
DataCell(Text(
|
||||
'${_toDouble(z['lat']).toStringAsFixed(4)}, ${_toDouble(z['lng']).toStringAsFixed(4)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppColor.textPrimary))),
|
||||
fontSize: 12))),
|
||||
DataCell(Text(_fmt(z['rides']),
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppColor.textPrimary))),
|
||||
fontSize: 12))),
|
||||
DataCell(Text(_toDouble(z['avg_price']).toStringAsFixed(2),
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppColor.textPrimary))),
|
||||
fontSize: 12))),
|
||||
]);
|
||||
}).toList(),
|
||||
),
|
||||
@@ -1066,12 +1073,13 @@ class _GrowthTab extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final gr = ctrl.growth;
|
||||
final totals = gr['totals'] as Map<String, dynamic>? ?? {};
|
||||
final weeks = (ctrl.weekly['weeks'] as List?) ?? [];
|
||||
|
||||
String weekChange = '';
|
||||
Color weekColor = AppColor.textSecondary;
|
||||
Color weekColor = cs.onSurfaceVariant;
|
||||
if (weeks.length >= 2) {
|
||||
final last = _toInt(weeks.last['rides']);
|
||||
final prev = _toInt(weeks[weeks.length - 2]['rides']);
|
||||
@@ -1079,7 +1087,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
final pct = ((last - prev) / prev * 100).toStringAsFixed(1);
|
||||
weekChange = '${double.parse(pct) >= 0 ? '+' : ''}$pct% عن الأسبوع السابق';
|
||||
weekColor =
|
||||
double.parse(pct) >= 0 ? AppColor.success : AppColor.danger;
|
||||
double.parse(pct) >= 0 ? const Color(0xFF10B981) : cs.error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,15 +1123,15 @@ class _GrowthTab extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildGrowthChart(),
|
||||
_buildWeeklyChart(weeks),
|
||||
_buildRetentionChart(),
|
||||
_buildRevenueChart(),
|
||||
_buildGrowthChart(cs),
|
||||
_buildWeeklyChart(weeks, cs),
|
||||
_buildRetentionChart(cs),
|
||||
_buildRevenueChart(cs),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGrowthChart() {
|
||||
Widget _buildGrowthChart(ColorScheme cs) {
|
||||
final passengers = (ctrl.growth['passengers'] as List?) ?? [];
|
||||
final drivers = (ctrl.growth['drivers'] as List?) ?? [];
|
||||
if (passengers.isEmpty && drivers.isEmpty) return const SizedBox.shrink();
|
||||
@@ -1142,13 +1150,13 @@ class _GrowthTab extends StatelessWidget {
|
||||
spots: passengers.asMap().entries.map((e) {
|
||||
return FlSpot(e.key.toDouble(), _toDouble(e.value['count']));
|
||||
}).toList(),
|
||||
color: AppColor.accent,
|
||||
color: cs.primary,
|
||||
barWidth: 2,
|
||||
dotData: const FlDotData(show: false),
|
||||
isCurved: true,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppColor.accent.withOpacity(0.08),
|
||||
color: cs.primary.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
LineChartBarData(
|
||||
@@ -1161,7 +1169,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
isCurved: true,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: const Color(0xFF00CEC9).withOpacity(0.08),
|
||||
color: const Color(0xFF00CEC9).withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1175,8 +1183,8 @@ class _GrowthTab extends StatelessWidget {
|
||||
if (i < 0 || i >= passengers.length) return const SizedBox();
|
||||
final d = passengers[i]['date']?.toString() ?? '';
|
||||
return Text(d.length >= 10 ? d.substring(5) : d,
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1194,7 +1202,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeeklyChart(List weeks) {
|
||||
Widget _buildWeeklyChart(List weeks, ColorScheme cs) {
|
||||
if (weeks.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return _ChartCard(
|
||||
@@ -1205,7 +1213,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
return BarChartGroupData(x: e.key, barRods: [
|
||||
BarChartRodData(
|
||||
toY: _toDouble(e.value['rides']),
|
||||
color: AppColor.accent.withOpacity(0.7),
|
||||
color: cs.primary.withValues(alpha: 0.7),
|
||||
width: 12,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(4)),
|
||||
@@ -1221,8 +1229,8 @@ class _GrowthTab extends StatelessWidget {
|
||||
if (i < 0 || i >= weeks.length) return const SizedBox();
|
||||
final d = weeks[i]['week_start']?.toString() ?? '';
|
||||
return Text(d.length >= 10 ? d.substring(5) : d,
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1240,7 +1248,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRetentionChart() {
|
||||
Widget _buildRetentionChart(ColorScheme cs) {
|
||||
final cohorts = (ctrl.retention['cohorts'] as List?) ?? [];
|
||||
if (cohorts.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
@@ -1253,7 +1261,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
return BarChartGroupData(x: e.key, barRods: [
|
||||
BarChartRodData(
|
||||
toY: _toDouble(e.value['active_passengers']),
|
||||
color: AppColor.accent.withOpacity(opacity),
|
||||
color: cs.primary.withValues(alpha: opacity),
|
||||
width: 14,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(4)),
|
||||
@@ -1266,8 +1274,8 @@ class _GrowthTab extends StatelessWidget {
|
||||
showTitles: true,
|
||||
getTitlesWidget: (v, _) {
|
||||
return Text('w${v.toInt()}',
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1285,7 +1293,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRevenueChart() {
|
||||
Widget _buildRevenueChart(ColorScheme cs) {
|
||||
final daily = (ctrl.revenue['daily'] as List?) ?? [];
|
||||
if (daily.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
@@ -1300,13 +1308,13 @@ class _GrowthTab extends StatelessWidget {
|
||||
return FlSpot(
|
||||
e.key.toDouble(), _toDouble(e.value['total_revenue']));
|
||||
}).toList(),
|
||||
color: AppColor.info,
|
||||
color: cs.tertiary,
|
||||
barWidth: 2,
|
||||
dotData: const FlDotData(show: false),
|
||||
isCurved: true,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppColor.info.withOpacity(0.08),
|
||||
color: cs.tertiary.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1320,8 +1328,8 @@ class _GrowthTab extends StatelessWidget {
|
||||
if (i < 0 || i >= daily.length) return const SizedBox();
|
||||
final d = daily[i]['date']?.toString() ?? '';
|
||||
return Text(d.length >= 10 ? d.substring(5) : d,
|
||||
style: const TextStyle(
|
||||
fontSize: 9, color: AppColor.textMuted));
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: cs.onSurfaceVariant));
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1336,7 +1344,7 @@ class _GrowthTab extends StatelessWidget {
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (_) =>
|
||||
FlLine(color: AppColor.divider, strokeWidth: 0.5),
|
||||
FlLine(color: cs.outline, strokeWidth: 0.5),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
),
|
||||
|
||||
@@ -16,19 +16,38 @@ class ComplaintListPage extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: Text('إدارة الشكاوى'.tr, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
|
||||
leading: GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurface),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Obx(() {
|
||||
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('إدارة الشكاوى'.tr,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Obx(() {
|
||||
if (controller.delayedComplaints.isNotEmpty) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
@@ -85,7 +104,10 @@ class ComplaintListPage extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildComplaintCard(BuildContext context, dynamic c) {
|
||||
|
||||
@@ -504,28 +504,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
centerTitle: true,
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.onSurface.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
"نظام تتبع الكابتن",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cs.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
foregroundColor: cs.onPrimary,
|
||||
),
|
||||
backgroundColor: cs.surface,
|
||||
body: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
@@ -542,6 +521,37 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
MarkerLayer(markers: _markers),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDashboard(cs),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -241,28 +241,6 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: Obx(() {
|
||||
if (controller.isTracking.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
title: Text(
|
||||
"مراقبة الرحلات",
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
body: Obx(() {
|
||||
if (!controller.isTracking.value) {
|
||||
return _buildSearchForm(context, controller, cs);
|
||||
@@ -274,108 +252,143 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
|
||||
Widget _buildSearchForm(
|
||||
BuildContext context, RideMonitorController controller, ColorScheme cs) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.primary.withValues(alpha: 0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
return 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.primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Icon(Icons.radar_rounded, size: 60, color: cs.primary),
|
||||
child: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: cs.onSurfaceVariant, size: 16),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"تتبع رحلة نشطة",
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"مراقبة الرحلات",
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"أدخل رقم هاتف السائق أو الراكب للبدء",
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline, width: 2),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.primary.withValues(alpha: 0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.phoneInputController,
|
||||
keyboardType: TextInputType.phone,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: "مثال: 0992952235...",
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 18, horizontal: 20),
|
||||
prefixIcon:
|
||||
Icon(Icons.phone_rounded, color: cs.primary),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.radar_rounded, size: 60, color: cs.primary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"تتبع رحلة نشطة",
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"أدخل رقم هاتف السائق أو الراكب للبدء",
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline, width: 2),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.phoneInputController,
|
||||
keyboardType: TextInputType.phone,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: "مثال: 0992952235...",
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 18, horizontal: 20),
|
||||
prefixIcon:
|
||||
Icon(Icons.phone_rounded, color: cs.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.startSearch,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.primary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"بدء المراقبة",
|
||||
style: TextStyle(
|
||||
color: cs.onPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.startSearch,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.primary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"بدء المراقبة",
|
||||
style: TextStyle(
|
||||
color: cs.onPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ class _EmployeeCard extends StatelessWidget {
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
color: cs.shadow.withValues(alpha: 0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -227,7 +227,7 @@ class _EmployeeCard extends StatelessWidget {
|
||||
cs.primary, cs),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(Icons.location_on_outlined,
|
||||
employee['site'] ?? 'غير محدد', Colors.blueGrey, cs),
|
||||
employee['site'] ?? 'غير محدد', cs.onSurfaceVariant, cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 {
|
||||
@@ -100,27 +101,54 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'خريطة النشاط الحرارية',
|
||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
|
||||
),
|
||||
backgroundColor: cs.surface,
|
||||
foregroundColor: cs.onSurface,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
tooltip: 'تحديث',
|
||||
onPressed: _applyFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: cs.surfaceContainerHighest,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
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(
|
||||
@@ -273,7 +301,10 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterRow({
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 '../../../constant/colors.dart';
|
||||
import '../../../controller/admin/marketing_controller.dart';
|
||||
|
||||
class MarketingPage extends StatelessWidget {
|
||||
@@ -481,7 +480,7 @@ class MarketingPage extends StatelessWidget {
|
||||
hintText: 'سعر الكيلومتر المقترح',
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
||||
filled: true,
|
||||
fillColor: AppColor.surfaceElevated,
|
||||
fillColor: cs.surfaceContainerLow,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
@@ -737,7 +736,7 @@ class MarketingPage extends StatelessWidget {
|
||||
style: TextStyle(fontSize: 12, color: cs.onSurface),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: AppColor.surfaceElevated,
|
||||
fillColor: cs.surfaceContainerLow,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: cs.outline),
|
||||
@@ -806,11 +805,11 @@ class MarketingPage extends StatelessWidget {
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColor.surfaceElevated,
|
||||
cs.surfaceContainerLow,
|
||||
cs.surfaceContainerHighest,
|
||||
],
|
||||
),
|
||||
border: Border.all(color: AppColor.accentBorder),
|
||||
border: Border.all(color: cs.outlineVariant),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.primary.withValues(alpha: 0.08),
|
||||
|
||||
@@ -46,23 +46,50 @@ class SocialIntelligenceScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'استخبارات السوشيال ميديا (Social Intelligence)',
|
||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16, color: cs.onSurface),
|
||||
),
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
foregroundColor: cs.onSurface,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh, color: cs.onSurface),
|
||||
onPressed: () => controller.fetchReports(),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
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));
|
||||
@@ -135,6 +162,9 @@ class SocialIntelligenceScreen extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,62 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: _buildAppBar(cs),
|
||||
body: GetBuilder<PackageController>(
|
||||
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),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.system_update_rounded,
|
||||
color: cs.primary, size: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text('تحديث التطبيق',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => packageController.fetchPackages(),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(height: 1, color: cs.outline),
|
||||
Expanded(
|
||||
child: GetBuilder<PackageController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
@@ -41,71 +95,10 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar(ColorScheme cs) {
|
||||
return AppBar(
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
leading: GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
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),
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.system_update_rounded,
|
||||
color: cs.primary, size: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'تحديث التطبيق',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
GestureDetector(
|
||||
onTap: () => packageController.fetchPackages(),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
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: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Container(height: 1, color: cs.outline),
|
||||
),
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPackageCard(
|
||||
|
||||
@@ -16,7 +16,6 @@ class Passengrs extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool isSuperAdmin =
|
||||
|
||||
@@ -16,45 +16,67 @@ class KazanEditorPage extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: Text('تعديل أسعار كازان'.tr, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
|
||||
leading: GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurface),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildCountryDropdown(cs),
|
||||
Expanded(
|
||||
child: Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
|
||||
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('⚙️ الإعدادات العامة', cs),
|
||||
_buildGeneralSettings(cs),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة', cs),
|
||||
_buildKmPricesGrid(cs),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)', cs),
|
||||
_buildMinutePrices(cs),
|
||||
const SizedBox(height: 32),
|
||||
MyElevatedButton(
|
||||
title: '💾 حفظ جميع التعديلات',
|
||||
icon: Icons.save_rounded,
|
||||
onPressed: () => _handleSave(),
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
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('تعديل أسعار كازان'.tr,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCountryDropdown(cs),
|
||||
Expanded(
|
||||
child: Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
|
||||
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('⚙️ الإعدادات العامة', cs),
|
||||
_buildGeneralSettings(cs),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة', cs),
|
||||
_buildKmPricesGrid(cs),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)', cs),
|
||||
_buildMinutePrices(cs),
|
||||
const SizedBox(height: 32),
|
||||
MyElevatedButton(
|
||||
title: '💾 حفظ جميع التعديلات',
|
||||
icon: Icons.save_rounded,
|
||||
onPressed: () => _handleSave(),
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -11,15 +11,36 @@ class AuditLogsPage extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: Text('سجل العمليات (Audit Logs)',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: cs.onSurface)),
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
),
|
||||
body: GetBuilder<SecurityV2Controller>(
|
||||
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('سجل العمليات (Audit Logs)',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GetBuilder<SecurityV2Controller>(
|
||||
init: SecurityV2Controller(),
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) {
|
||||
@@ -57,7 +78,10 @@ class AuditLogsPage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogItem(Map<String, dynamic> log, ColorScheme cs) {
|
||||
|
||||
@@ -8,7 +8,6 @@ class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(AnalyticsV2Controller());
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
|
||||
Reference in New Issue
Block a user