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