إصلاح توافق فلاتر مع Theme الجديد
This commit is contained in:
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"6c0baaebf70e0148f485f27d5616b3d3382da7
|
||||
|
||||
_flutter.loader.load({
|
||||
serviceWorkerSettings: {
|
||||
serviceWorkerVersion: "658515598" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
||||
serviceWorkerVersion: "1676594277" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:siro_admin/views/admin/drivers/driver_tracker_screen.dart';
|
||||
import 'package:siro_admin/views/admin/quality/blacklist_page.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/theme.dart';
|
||||
import '../../controller/admin/dashboard_controller.dart';
|
||||
import '../../controller/admin/static_controller.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
|
||||
@@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../../../controller/admin/captain_admin_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import 'captain_details.dart';
|
||||
|
||||
class CaptainsPage extends StatelessWidget {
|
||||
@@ -21,159 +19,84 @@ class CaptainsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Search for Captain'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(context).primaryColor.withOpacity(0.03),
|
||||
Colors.white,
|
||||
],
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
_buildSearchSection(context, cs),
|
||||
Expanded(
|
||||
child: GetBuilder<CaptainAdminController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return _buildLoadingState(cs);
|
||||
}
|
||||
|
||||
final message = controller.captainData['message'];
|
||||
|
||||
if (message == null) {
|
||||
return _buildEmptyState(cs);
|
||||
}
|
||||
|
||||
final List<dynamic> captains =
|
||||
message is List ? message : [message];
|
||||
|
||||
if (captains.isEmpty) {
|
||||
return _buildEmptyState(cs);
|
||||
}
|
||||
|
||||
return _buildResultsList(context, captains, cs);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeaderSection(context),
|
||||
Expanded(
|
||||
child: GetBuilder<CaptainAdminController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return _buildLoadingState();
|
||||
}
|
||||
|
||||
final message = controller.captainData['message'];
|
||||
|
||||
if (message == null) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
// 🔥 الحل هنا: توحيد الشكل إلى List
|
||||
final List<dynamic> captains =
|
||||
message is List ? message : [message];
|
||||
|
||||
if (captains.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return _buildResultsList(context, captains);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ================= HEADER =================
|
||||
|
||||
Widget _buildHeaderSection(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.manage_search_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Find Captain'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Search by phone number'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildModernSearchBar(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModernSearchBar(BuildContext context) {
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
keyboardType: TextInputType.phone,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
decoration: InputDecoration(
|
||||
hintText: '0990000000'.tr,
|
||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
|
||||
prefixIcon: Icon(Icons.phone_android_rounded,
|
||||
color: Colors.grey[400], size: 22),
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
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),
|
||||
),
|
||||
onSubmitted: (_) => _performSearch(),
|
||||
child: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: cs.onSurfaceVariant, size: 16),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Material(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: _performSearch,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Icon(Icons.search, color: Colors.white, size: 24),
|
||||
),
|
||||
),
|
||||
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.drive_eta_rounded,
|
||||
color: cs.primary, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'قائمة الكباتن',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -181,6 +104,58 @@ class CaptainsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchSection(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
keyboardType: TextInputType.phone,
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 14),
|
||||
cursorColor: cs.primary,
|
||||
decoration: InputDecoration(
|
||||
hintText: '0990000000',
|
||||
hintStyle: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 13),
|
||||
prefixIcon: Icon(Icons.phone_android_rounded,
|
||||
color: cs.onSurfaceVariant, size: 20),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 14),
|
||||
),
|
||||
onSubmitted: (_) => _performSearch(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Material(
|
||||
color: cs.primary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
onTap: _performSearch,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Icon(Icons.search_rounded,
|
||||
color: Colors.white, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _performSearch() {
|
||||
final phone = searchController.text.trim();
|
||||
if (phone.isNotEmpty) {
|
||||
@@ -188,37 +163,98 @@ class CaptainsPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ================= RESULTS =================
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('جاري التحميل...',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultsList(BuildContext context, List<dynamic> captains) {
|
||||
Widget _buildEmptyState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.person_off_outlined,
|
||||
size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا يوجد كباتن',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultsList(
|
||||
BuildContext context, List<dynamic> captains, ColorScheme cs) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Search Results'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
Container(
|
||||
width: 3,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'نتائج البحث',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Container(height: 1, color: cs.outline),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: cs.primary.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Text(
|
||||
'${captains.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: cs.primary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -228,12 +264,12 @@ class CaptainsPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 80),
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
|
||||
itemCount: captains.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final captain = captains[index] as Map<String, dynamic>;
|
||||
return _buildModernCaptainCard(context, captain);
|
||||
return _buildCaptainCard(context, captain, cs);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -241,10 +277,10 @@ class CaptainsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ================= CARD =================
|
||||
|
||||
Widget _buildModernCaptainCard(
|
||||
BuildContext context, Map<String, dynamic> captain) {
|
||||
Widget _buildCaptainCard(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> captain,
|
||||
ColorScheme cs) {
|
||||
final String fullName =
|
||||
'${captain['first_name'] ?? ''} ${captain['last_name'] ?? ''}';
|
||||
final String phone = captain['phone']?.toString() ?? '';
|
||||
@@ -252,67 +288,103 @@ class CaptainsPage extends StatelessWidget {
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
Get.to(() => const CaptainDetailsPage(),
|
||||
arguments: {'data': captain});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Theme.of(context).primaryColor.withOpacity(0.8),
|
||||
Theme.of(context).primaryColor,
|
||||
cs.primary.withValues(alpha: 0.8),
|
||||
cs.primary,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.primary.withValues(alpha: 0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person_rounded,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fullName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(phone),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.phone_rounded,
|
||||
size: 13, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
phone,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isSuperAdmin && email != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(email),
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.email_outlined,
|
||||
size: 12,
|
||||
color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
email,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 14, color: cs.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -320,16 +392,4 @@ class CaptainsPage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ================= STATES =================
|
||||
|
||||
Widget _buildLoadingState() {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return const Center(
|
||||
child: Text("No captains found"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
import 'package:siro_admin/constant/box_name.dart';
|
||||
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../constant/char_map.dart';
|
||||
|
||||
import '../../../controller/drivers/driver_not_active_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import 'package:siro_admin/constant/info.dart';
|
||||
import 'package:siro_admin/constant/char_map.dart';
|
||||
import 'package:siro_admin/controller/drivers/driver_not_active_controller.dart';
|
||||
import 'package:siro_admin/main.dart';
|
||||
import 'package:siro_admin/print.dart';
|
||||
import 'package:siro_admin/constant/theme.dart';
|
||||
import 'driver_details_not_active_page.dart';
|
||||
|
||||
class DriversPendingPage extends StatelessWidget {
|
||||
@@ -18,35 +16,256 @@ class DriversPendingPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
controller.getDriversPending();
|
||||
Log.print(
|
||||
': ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Drivers Pending")),
|
||||
body: GetBuilder<DriverController>(
|
||||
id: 'drivers',
|
||||
builder: (c) {
|
||||
if (c.drivers.isEmpty) {
|
||||
return Center(
|
||||
child: const Text('no drivers found yet',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: c.drivers.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final d = c.drivers[i];
|
||||
return ListTile(
|
||||
title: Text(d["first_name"] + d['last_name'] ?? ""),
|
||||
subtitle: Text(d["phone"] ?? ""),
|
||||
onTap: () {
|
||||
Get.to(() => DriverDetailsPage(driverId: d["id"].toString()));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
Expanded(
|
||||
child: GetBuilder<DriverController>(
|
||||
id: 'drivers',
|
||||
builder: (c) {
|
||||
if (c.isLoading) {
|
||||
return _buildLoadingState(cs);
|
||||
}
|
||||
|
||||
if (c.drivers.isEmpty) {
|
||||
return _buildEmptyState(cs);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => controller.getDriversPending(),
|
||||
color: cs.primary,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 80),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: c.drivers.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (ctx, i) {
|
||||
final d = c.drivers[i];
|
||||
return _buildDriverCard(d, cs);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
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: const Color(0xFFF59E0B).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.hourglass_top_rounded,
|
||||
color: const Color(0xFFF59E0B), size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'طلبات الكباتن المعلقة',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('جاري تحميل الطلبات...',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.how_to_reg_rounded,
|
||||
size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا يوجد طلبات تسجيل حالياً',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverCard(dynamic driver, ColorScheme cs) {
|
||||
final String fullName =
|
||||
'${driver['first_name'] ?? ''} ${driver['last_name'] ?? ''}'.trim();
|
||||
final String phone = driver['phone'] ?? '';
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
Get.to(() => DriverDetailsPage(driverId: driver['id'].toString()));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
const Color(0xFFF59E0B).withValues(alpha: 0.20),
|
||||
const Color(0xFFF59E0B).withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF59E0B).withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
fullName.isNotEmpty ? fullName[0].toUpperCase() : 'D',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFF59E0B),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fullName,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF59E0B)
|
||||
.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.hourglass_top_rounded,
|
||||
size: 11,
|
||||
color: const Color(0xFFF59E0B)),
|
||||
const SizedBox(width: 3),
|
||||
const Text(
|
||||
'معلق',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFFF59E0B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.phone_rounded,
|
||||
size: 12, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
phone,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 14, color: cs.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/complaint_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
|
||||
@@ -14,81 +12,101 @@ class ComplaintListPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'إدارة الشكاوى'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Obx(() {
|
||||
if (controller.delayedComplaints.isNotEmpty) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.danger.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColor.danger.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, color: AppColor.danger),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'هناك ${controller.delayedComplaints.length} شكاوى لم يتم حلها منذ أكثر من أسبوع!',
|
||||
style: AppStyle.body.copyWith(color: AppColor.danger, fontWeight: FontWeight.bold),
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
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(() {
|
||||
if (controller.delayedComplaints.isNotEmpty) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.error.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.error.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: cs.error),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'هناك ${controller.delayedComplaints.length} شكاوى لم يتم حلها منذ أكثر من أسبوع!',
|
||||
style: GoogleFonts.inter(color: cs.error, fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Obx(() => MyElevatedButton(
|
||||
title: controller.showOnlyDelayed.value ? 'عرض جميع الشكاوى' : 'عرض الشكاوى المتأخرة فقط',
|
||||
onPressed: () => controller.showOnlyDelayed.toggle(),
|
||||
kolor: AppColor.danger,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
Obx(() {
|
||||
final list = controller.showOnlyDelayed.value ? controller.delayedComplaints : controller.complaintList;
|
||||
if (controller.isLoading.value && list.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => controller.getComplaints(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
final complaint = list[index];
|
||||
return _buildComplaintCard(context, complaint);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Obx(() => MyElevatedButton(
|
||||
title: controller.showOnlyDelayed.value ? 'عرض جميع الشكاوى' : 'عرض الشكاوى المتأخرة فقط',
|
||||
onPressed: () => controller.showOnlyDelayed.toggle(),
|
||||
kolor: cs.error,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final list = controller.showOnlyDelayed.value ? controller.delayedComplaints : controller.complaintList;
|
||||
if (controller.isLoading.value && list.isEmpty) {
|
||||
return Center(child: CircularProgressIndicator(color: cs.primary));
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => controller.getComplaints(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
final complaint = list[index];
|
||||
return _buildComplaintCard(context, complaint);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildComplaintCard(BuildContext context, dynamic c) {
|
||||
Color statusColor = _getStatusColor(c['statusComplaint']);
|
||||
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
Color statusColor = _getStatusColor(c['statusComplaint'], cs);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: AppStyle.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: _buildStatusIndicator(c['statusComplaint'], statusColor),
|
||||
leading: _buildStatusIndicator(c['statusComplaint'], statusColor, cs),
|
||||
title: Text(
|
||||
c['description']?.toString() ?? 'بدون وصف',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.title,
|
||||
style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -96,18 +114,18 @@ class ComplaintListPage extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'النوع: ${c['complaint_type'] ?? 'عام'} | الرحلة: ${c['ride_id']}',
|
||||
style: AppStyle.caption,
|
||||
style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person_rounded, size: 12, color: AppColor.textSecondary),
|
||||
Icon(Icons.person_rounded, size: 12, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 4),
|
||||
Text(c['passengerName'] ?? 'غير معروف', style: AppStyle.caption),
|
||||
Text(c['passengerName'] ?? 'غير معروف', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
const SizedBox(width: 12),
|
||||
Icon(Icons.drive_eta_rounded, size: 12, color: AppColor.textSecondary),
|
||||
Icon(Icons.drive_eta_rounded, size: 12, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 4),
|
||||
Text(c['driverName'] ?? 'غير معروف', style: AppStyle.caption),
|
||||
Text(c['driverName'] ?? 'غير معروف', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -118,12 +136,12 @@ class ComplaintListPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(color: AppColor.divider),
|
||||
_buildInfoRow('الوصف', c['description'] ?? 'لا يوجد وصف'),
|
||||
Divider(color: cs.outline),
|
||||
_buildInfoRow('الوصف', c['description'] ?? 'لا يوجد وصف', cs),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('الحل الحالي', c['resolution'] ?? 'لم يتم الحل بعد'),
|
||||
_buildInfoRow('الحل الحالي', c['resolution'] ?? 'لم يتم الحل بعد', cs),
|
||||
const SizedBox(height: 12),
|
||||
_buildRideDetails(c),
|
||||
_buildRideDetails(c, cs),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
@@ -131,7 +149,7 @@ class ComplaintListPage extends StatelessWidget {
|
||||
child: MyElevatedButton(
|
||||
title: 'تحديث الحالة / حل الشكوى',
|
||||
onPressed: () => _showResolveDialog(context, c),
|
||||
kolor: AppColor.accent,
|
||||
kolor: cs.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -144,14 +162,14 @@ class ComplaintListPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicator(String? status, Color color) {
|
||||
Widget _buildStatusIndicator(String? status, Color color, ColorScheme cs) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
color: color.withValues(alpha: 0.12),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color.withOpacity(0.25)),
|
||||
border: Border.all(color: color.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(
|
||||
status == 'Resolved' ? Icons.check_circle_rounded : Icons.pending_rounded,
|
||||
@@ -161,33 +179,33 @@ class ComplaintListPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
Widget _buildInfoRow(String label, String value, ColorScheme cs) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: AppStyle.caption.copyWith(color: AppColor.accent)),
|
||||
Text(label, style: GoogleFonts.inter(color: cs.primary, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: AppStyle.body),
|
||||
Text(value, style: GoogleFonts.inter(color: cs.onSurface, fontSize: 14)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideDetails(dynamic c) {
|
||||
Widget _buildRideDetails(dynamic c, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surfaceElevated,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildSmallStat('السعر', '${c['priceOfRide']} ل.س'),
|
||||
_buildSmallStat('التقييم', '${c['avgRatingDriverFromPassengers'] ?? 0}★'),
|
||||
_buildSmallStat('النوع', c['ascarType'] ?? 'N/A'),
|
||||
_buildSmallStat('السعر', '${c['priceOfRide']} ل.س', cs),
|
||||
_buildSmallStat('التقييم', '${c['avgRatingDriverFromPassengers'] ?? 0}★', cs),
|
||||
_buildSmallStat('النوع', c['ascarType'] ?? 'N/A', cs),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -195,26 +213,27 @@ class ComplaintListPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSmallStat(String label, String value) {
|
||||
Widget _buildSmallStat(String label, String value, ColorScheme cs) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: AppStyle.caption.copyWith(fontSize: 10)),
|
||||
Text(label, style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 10)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: AppStyle.number.copyWith(fontSize: 12)),
|
||||
Text(value, style: GoogleFonts.jetBrainsMono(fontWeight: FontWeight.bold, fontSize: 12, color: cs.onSurface)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(String? status) {
|
||||
Color _getStatusColor(String? status, ColorScheme cs) {
|
||||
switch (status) {
|
||||
case 'Open': return AppColor.danger;
|
||||
case 'In Progress': return AppColor.warning;
|
||||
case 'Resolved': return AppColor.success;
|
||||
default: return AppColor.textSecondary;
|
||||
case 'Open': return cs.error;
|
||||
case 'In Progress': return cs.tertiary;
|
||||
case 'Resolved': return const Color(0xFF10B981);
|
||||
default: return cs.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
|
||||
void _showResolveDialog(BuildContext context, dynamic c) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final TextEditingController resController = TextEditingController(text: c['resolution']);
|
||||
String selectedStatus = c['statusComplaint'] ?? 'Open';
|
||||
|
||||
@@ -222,16 +241,16 @@ class ComplaintListPage extends StatelessWidget {
|
||||
StatefulBuilder(
|
||||
builder: (context, setModalState) => Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColor.surfaceElevated,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('تحديث حالة الشكوى', style: AppStyle.headTitle),
|
||||
Text('تحديث حالة الشكوى', style: GoogleFonts.cairo(fontWeight: FontWeight.bold, color: cs.onSurface, fontSize: 18)),
|
||||
const SizedBox(height: 20),
|
||||
_buildStatusDropdown(selectedStatus, (val) {
|
||||
_buildStatusDropdown(selectedStatus, cs, (val) {
|
||||
setModalState(() => selectedStatus = val!);
|
||||
}),
|
||||
const SizedBox(height: 20),
|
||||
@@ -263,21 +282,21 @@ class ComplaintListPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusDropdown(String current, Function(String?) onChanged) {
|
||||
Widget _buildStatusDropdown(String current, ColorScheme cs, Function(String?) onChanged) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 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: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: current,
|
||||
isExpanded: true,
|
||||
dropdownColor: AppColor.surfaceElevated,
|
||||
dropdownColor: cs.surfaceContainerHighest,
|
||||
items: ['Open', 'In Progress', 'Resolved']
|
||||
.map((s) => DropdownMenuItem(value: s, child: Text(s.tr, style: AppStyle.body)))
|
||||
.map((s) => DropdownMenuItem(value: s, child: Text(s.tr, style: GoogleFonts.inter(color: cs.onSurface, fontSize: 14))))
|
||||
.toList(),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/theme.dart';
|
||||
import 'package:siro_admin/controller/admin/dashboard_v2_controller.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:siro_admin/views/widgets/glass_container.dart';
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/driver_docs_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/snackbar.dart';
|
||||
import '../../../constant/links.dart';
|
||||
@@ -15,67 +13,86 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'مراجعة طلبات التسجيل'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Obx(() => controller.isLoading.value && controller.pendingDrivers.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: controller.pendingDrivers.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.how_to_reg_rounded, size: 64, color: AppColor.textMuted),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا يوجد طلبات تسجيل حالياً', style: AppStyle.subtitle),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () => controller.getPendingDrivers(),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification scrollInfo) {
|
||||
if (!controller.isLoading.value &&
|
||||
!controller.isMoreLoading.value &&
|
||||
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 200) {
|
||||
controller.loadMore();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.pendingDrivers.length + (controller.hasMore.value ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == controller.pendingDrivers.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
final driver = controller.pendingDrivers[index];
|
||||
return _buildDriverCard(context, driver);
|
||||
},
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
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: Obx(() => controller.isLoading.value && controller.pendingDrivers.isEmpty
|
||||
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||
: controller.pendingDrivers.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: cs.onSurfaceVariant.withValues(alpha: 0.1)),
|
||||
child: Icon(Icons.how_to_reg_rounded, size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا يوجد طلبات تسجيل حالياً', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () => controller.getPendingDrivers(),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification scrollInfo) {
|
||||
if (!controller.isLoading.value &&
|
||||
!controller.isMoreLoading.value &&
|
||||
scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 200) {
|
||||
controller.loadMore();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.pendingDrivers.length + (controller.hasMore.value ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == controller.pendingDrivers.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator(color: cs.primary)),
|
||||
);
|
||||
}
|
||||
final driver = controller.pendingDrivers[index];
|
||||
return _buildDriverCard(context, driver);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverCard(BuildContext context, dynamic driver) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: AppStyle.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.accentSoft,
|
||||
child: Text(driver['first_name']?[0] ?? 'D', style: const TextStyle(color: AppColor.accent)),
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
child: Text(driver['first_name']?[0] ?? 'D', style: TextStyle(color: cs.primary)),
|
||||
),
|
||||
title: Text('${driver['first_name']} ${driver['last_name']}', style: AppStyle.title),
|
||||
subtitle: Text(driver['phone'] ?? '', style: AppStyle.caption),
|
||||
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16, color: AppColor.textSecondary),
|
||||
title: Text('${driver['first_name']} ${driver['last_name']}', style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
|
||||
subtitle: Text(driver['phone'] ?? '', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
trailing: Icon(Icons.arrow_forward_ios_rounded, size: 16, color: cs.onSurfaceVariant),
|
||||
onTap: () => _showDriverDetails(context, driver['id'].toString()),
|
||||
),
|
||||
);
|
||||
@@ -88,45 +105,59 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
final driver = details['driver'];
|
||||
final List docs = details['documents'];
|
||||
|
||||
Get.to(() => MyScafolld(
|
||||
title: 'تفاصيل السائق',
|
||||
isleading: true,
|
||||
body: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDriverHeader(driver),
|
||||
const SizedBox(height: 24),
|
||||
Text('الوثائق المرفوعة', style: AppStyle.title),
|
||||
const SizedBox(height: 12),
|
||||
...docs.map((doc) => _buildDocCard(doc)),
|
||||
const SizedBox(height: 32),
|
||||
MyElevatedButton(
|
||||
title: 'اعتماد وتفعيل الحساب',
|
||||
icon: Icons.check_circle_rounded,
|
||||
kolor: AppColor.success,
|
||||
onPressed: () async {
|
||||
bool success = await controller.approveDriver(id);
|
||||
if (success) {
|
||||
Get.back();
|
||||
mySnackbarSuccess('تم تفعيل حساب السائق بنجاح');
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
Get.to(() => Builder(builder: (ctx) {
|
||||
final cs = Theme.of(ctx).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: Text('تفاصيل السائق', 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: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDriverHeader(driver, cs),
|
||||
const SizedBox(height: 24),
|
||||
Text('الوثائق المرفوعة', style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
|
||||
const SizedBox(height: 12),
|
||||
...docs.map((doc) => _buildDocCard(doc, cs)),
|
||||
const SizedBox(height: 32),
|
||||
MyElevatedButton(
|
||||
title: 'اعتماد وتفعيل الحساب',
|
||||
icon: Icons.check_circle_rounded,
|
||||
kolor: const Color(0xFF10B981),
|
||||
onPressed: () async {
|
||||
bool success = await controller.approveDriver(id);
|
||||
if (success) {
|
||||
Get.back();
|
||||
mySnackbarSuccess('تم تفعيل حساب السائق بنجاح');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
Widget _buildDriverHeader(dynamic driver) {
|
||||
Widget _buildDriverHeader(dynamic driver, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: AppStyle.elevatedCard,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -135,28 +166,28 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${driver['first_name']} ${driver['last_name']}', style: AppStyle.headTitle),
|
||||
Text(driver['phone'] ?? '', style: AppStyle.subtitle),
|
||||
Text('${driver['first_name']} ${driver['last_name']}', style: GoogleFonts.cairo(fontWeight: FontWeight.bold, color: cs.onSurface, fontSize: 18)),
|
||||
Text(driver['phone'] ?? '', style: GoogleFonts.inter(color: cs.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.warning.withOpacity(0.2),
|
||||
color: cs.tertiary.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text('Pending', style: AppStyle.caption.copyWith(color: AppColor.warning)),
|
||||
child: Text('Pending', style: GoogleFonts.inter(color: cs.tertiary, fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 32, color: AppColor.divider),
|
||||
Divider(height: 32, color: cs.outline),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildSmallInfo('الرقم الوطني', driver['national_number'] ?? 'N/A'),
|
||||
_buildSmallInfo('الجنس', driver['gender'] ?? 'N/A'),
|
||||
_buildSmallInfo('تاريخ الميلاد', driver['birthdate'] ?? 'N/A'),
|
||||
_buildSmallInfo('الرقم الوطني', driver['national_number'] ?? 'N/A', cs),
|
||||
_buildSmallInfo('الجنس', driver['gender'] ?? 'N/A', cs),
|
||||
_buildSmallInfo('تاريخ الميلاد', driver['birthdate'] ?? 'N/A', cs),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -164,27 +195,30 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSmallInfo(String label, String value) {
|
||||
Widget _buildSmallInfo(String label, String value, ColorScheme cs) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: AppStyle.caption.copyWith(fontSize: 10)),
|
||||
Text(label, style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 10)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: AppStyle.body.copyWith(fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
Text(value, style: GoogleFonts.inter(fontSize: 12, fontWeight: FontWeight.bold, color: cs.onSurface)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocCard(dynamic doc) {
|
||||
Widget _buildDocCard(dynamic doc, ColorScheme cs) {
|
||||
String imageUrl = doc['link'] ?? '';
|
||||
// Ensure URL is absolute
|
||||
if (!imageUrl.startsWith('http')) {
|
||||
imageUrl = '${AppLink.server}/upload/drivers/$imageUrl';
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: AppStyle.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -192,9 +226,9 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.file_present_rounded, color: AppColor.accent, size: 20),
|
||||
Icon(Icons.file_present_rounded, color: cs.primary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(doc['doc_type'] ?? 'وثيقة', style: AppStyle.title.copyWith(fontSize: 14)),
|
||||
Text(doc['doc_type'] ?? 'وثيقة', style: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -207,8 +241,8 @@ class DriverDocsReviewPage extends StatelessWidget {
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
height: 200,
|
||||
color: AppColor.surfaceElevated,
|
||||
child: const Center(child: Icon(Icons.broken_image_rounded, size: 48, color: AppColor.textMuted)),
|
||||
color: cs.surfaceContainerHighest,
|
||||
child: Center(child: Icon(Icons.broken_image_rounded, size: 48, color: cs.onSurfaceVariant)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,30 +3,22 @@ import 'package:get/get.dart';
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/controller/functions/wallet.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
|
||||
import '../../../constant/links.dart'; // تأكد من المسار
|
||||
|
||||
// --- Controller: المسؤول عن المنطق (البحث، الفحص، الإضافة) ---
|
||||
class DriverGiftCheckerController extends GetxController {
|
||||
// للتحكم في حقل النص
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
|
||||
// لعرض النتائج وحالة التحميل
|
||||
var statusLog = "".obs;
|
||||
var isLoading = false.obs;
|
||||
|
||||
// قائمة السائقين (سنقوم بتحميلها للبحث عن الـ ID)
|
||||
List<dynamic> driversCache = [];
|
||||
|
||||
|
||||
// 1. تحميل قائمة السائقين لاستخراج الـ ID منها
|
||||
Future<void> fetchDriverCache() async {
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: '${AppLink.server}/Admin/driver/getDriverGiftPayment.php',
|
||||
payload: {'phone': phoneController.text.trim()},
|
||||
);
|
||||
// print('response: ${response}');
|
||||
|
||||
if (response != 'failure') {
|
||||
driversCache = (response['message']);
|
||||
@@ -36,7 +28,6 @@ class DriverGiftCheckerController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// --- الدالة الرئيسية التي تنفذ العملية المطلوبة ---
|
||||
Future<void> processDriverGift() async {
|
||||
String phoneInput = phoneController.text.trim();
|
||||
|
||||
@@ -49,13 +40,11 @@ class DriverGiftCheckerController extends GetxController {
|
||||
statusLog.value = "جاري البحث عن السائق...";
|
||||
|
||||
try {
|
||||
// الخطوة 1: استخراج الـ ID بناءً على رقم الهاتف
|
||||
var driver = driversCache.firstWhere(
|
||||
(d) {
|
||||
String dbPhone =
|
||||
d['phone'].toString().replaceAll(RegExp(r'[^0-9]'), '');
|
||||
String inputPhone = phoneInput.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
// قارن آخر 9 أرقام لتجاوز مشكلة 09 مقابل 963
|
||||
if (dbPhone.length >= 9 && inputPhone.length >= 9) {
|
||||
return dbPhone.substring(dbPhone.length - 9) ==
|
||||
inputPhone.substring(inputPhone.length - 9);
|
||||
@@ -77,17 +66,8 @@ class DriverGiftCheckerController extends GetxController {
|
||||
statusLog.value =
|
||||
"✅ تم العثور على السائق: $driverName (ID: $driverId)\nجاري فحص رصيد الهدايا...";
|
||||
|
||||
// الخطوة 2: فحص السيرفر هل الهدية موجودة؟
|
||||
// bool hasGift = await _checkIfGiftExistsOnServer(driverId);
|
||||
|
||||
// if (hasGift) {
|
||||
// statusLog.value +=
|
||||
// "\n⚠️ هذا السائق لديه هدية الافتتاح (30,000) مسبقاً. لم يتم اتخاذ إجراء.";
|
||||
// } else {
|
||||
// الخطوة 3: إضافة الهدية
|
||||
statusLog.value += "\n🎁 الهدية غير موجودة. جاري الإضافة...";
|
||||
await _addGiftToDriver(driverId, phoneInput, "300");
|
||||
// }
|
||||
} catch (e) {
|
||||
statusLog.value = "حدث خطأ غير متوقع: $e";
|
||||
} finally {
|
||||
@@ -95,130 +75,258 @@ class DriverGiftCheckerController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// دالة إضافة الهدية باستخدام WalletController الموجود عندك
|
||||
Future<void> _addGiftToDriver(
|
||||
String driverId, String phone, String amount) async {
|
||||
final wallet = Get.put(WalletController());
|
||||
|
||||
// استخدام الدالة الموجودة في نظامك
|
||||
await wallet.addDrivergift300('new driver', driverId, amount, phone);
|
||||
|
||||
// statusLog.value += "\n✅ تمت إضافة مبلغ $amount ل.س بنجاح!";
|
||||
|
||||
// إضافة تنبيه مرئي
|
||||
// Get.snackbar("تم بنجاح", "تمت إضافة هدية الافتتاح للسائق",
|
||||
// backgroundColor: Colors.green, colorText: Colors.white);
|
||||
}
|
||||
}
|
||||
|
||||
// --- View: واجهة المستخدم ---
|
||||
class DriverGiftCheckPage extends StatelessWidget {
|
||||
const DriverGiftCheckPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حقن الكنترولر
|
||||
final controller = Get.put(DriverGiftCheckerController());
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8FAFC),
|
||||
appBar: AppBar(
|
||||
title: const Text("فحص ومنح هدية الافتتاح",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: const Color(0xFF0F172A), // نفس لون الهيدر السابق
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// كارد الإدخال
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 10)
|
||||
],
|
||||
),
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.card_giftcard,
|
||||
size: 50, color: Colors.amber),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"أدخل رقم الهاتف للتحقق",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// حقل الإدخال
|
||||
TextField(
|
||||
controller: controller.phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'مثال: 0912345678',
|
||||
prefixIcon: const Icon(Icons.phone),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// زر التنفيذ
|
||||
Obx(() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: () => controller.processDriverGift(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: controller.isLoading.value
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white)
|
||||
: const Text("تحقق ومنح الهدية (30,000)",
|
||||
style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
)),
|
||||
_buildInputCard(context, controller, cs),
|
||||
const SizedBox(height: 24),
|
||||
_buildLogSection(controller, cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const SizedBox(height: 30),
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
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: const Color(0xFFF59E0B).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.card_giftcard_rounded,
|
||||
color: const Color(0xFFF59E0B), size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'هدايا وعروض الكباتن',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// منطقة عرض النتائج (Log)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Obx(() => Text(
|
||||
controller.statusLog.value.isEmpty
|
||||
? "بانتظار العملية..."
|
||||
: controller.statusLog.value,
|
||||
style: const TextStyle(
|
||||
color: Colors.greenAccent,
|
||||
fontFamily: 'monospace',
|
||||
height: 1.5),
|
||||
)),
|
||||
),
|
||||
Widget _buildInputCard(
|
||||
BuildContext context, DriverGiftCheckerController controller, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.shadow.withValues(alpha: 0.06),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFFF59E0B).withValues(alpha: 0.20),
|
||||
const Color(0xFFF59E0B).withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
|
||||
),
|
||||
child: const Icon(Icons.card_giftcard_rounded,
|
||||
size: 40, color: Color(0xFFF59E0B)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"أدخل رقم الهاتف للتحقق",
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"سيتم التحقق ومنح هدية الافتتاح (300)",
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 14),
|
||||
cursorColor: cs.primary,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'مثال: 0912345678',
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
prefixIcon: Icon(Icons.phone_rounded,
|
||||
color: cs.onSurfaceVariant, size: 20),
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Obx(() => ElevatedButton(
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: () => controller.processDriverGift(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF59E0B),
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: cs.outline,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: controller.isLoading.value
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white, strokeWidth: 2),
|
||||
)
|
||||
: const Text("تحقق ومنح الهدية",
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogSection(DriverGiftCheckerController controller, ColorScheme cs) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.terminal_rounded,
|
||||
color: cs.primary, size: 14),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'سجل العمليات',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0A0A0B),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
constraints: const BoxConstraints(minHeight: 200),
|
||||
child: SingleChildScrollView(
|
||||
child: Obx(() => Text(
|
||||
controller.statusLog.value.isEmpty
|
||||
? "بانتظار العملية..."
|
||||
: controller.statusLog.value,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4ADE80),
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
height: 1.6,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,30 +21,24 @@ class SiroTrackerScreen extends StatefulWidget {
|
||||
|
||||
class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
with TickerProviderStateMixin {
|
||||
// === Map Controller ===
|
||||
final MapController _mapController = MapController();
|
||||
List<Marker> _markers = [];
|
||||
|
||||
// === State Variables ===
|
||||
bool isLiveMode = true;
|
||||
bool isLoading = false;
|
||||
String lastUpdated = "جاري التحميل...";
|
||||
|
||||
// === Counters ===
|
||||
int liveCount = 0;
|
||||
int dayCount = 0;
|
||||
Timer? _timer;
|
||||
|
||||
// === Animation Controllers ===
|
||||
late AnimationController _fadeController;
|
||||
late AnimationController _scaleController;
|
||||
|
||||
// === Admin Info ===
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool get isSuperAdmin =>
|
||||
myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
// === URLs ===
|
||||
final String _baseDir = "${AppLink.server}/ride/location/";
|
||||
|
||||
@override
|
||||
@@ -90,10 +84,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
}
|
||||
|
||||
void _showSnackBar(String message) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: const Color(0xFF2C3E50),
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
@@ -160,6 +155,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
}
|
||||
|
||||
void _buildMarkers(List<dynamic> drivers) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
List<Marker> newMarkers = [];
|
||||
|
||||
for (var d in drivers) {
|
||||
@@ -192,7 +188,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
cancelled: cancelled,
|
||||
);
|
||||
},
|
||||
child: _buildMarkerWidget(heading),
|
||||
child: _buildMarkerWidget(heading, cs),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -203,7 +199,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildMarkerWidget(double heading) {
|
||||
Widget _buildMarkerWidget(double heading, ColorScheme cs) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
@@ -214,17 +210,17 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: isLiveMode
|
||||
? [const Color(0xFF27AE60), const Color(0xFF229954)]
|
||||
: [const Color(0xFF3498DB), const Color(0xFF2980B9)],
|
||||
? [const Color(0xFF10B981), const Color(0xFF10B981)]
|
||||
: [cs.tertiary, cs.tertiary],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isLiveMode
|
||||
? const Color(0xFF27AE60)
|
||||
: const Color(0xFF3498DB))
|
||||
.withOpacity(0.5),
|
||||
? const Color(0xFF10B981)
|
||||
: cs.tertiary)
|
||||
.withValues(alpha: 0.5),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
@@ -235,7 +231,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
angle: heading * (math.pi / 180),
|
||||
child: Icon(
|
||||
Icons.navigation,
|
||||
color: Colors.white,
|
||||
color: cs.onPrimary,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
@@ -252,6 +248,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
required String completed,
|
||||
required String cancelled,
|
||||
}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
@@ -260,14 +257,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.white, Colors.grey.shade50],
|
||||
colors: [cs.surface, cs.surfaceContainerHighest],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
color: cs.shadow.withValues(alpha: 0.15),
|
||||
blurRadius: 30,
|
||||
spreadRadius: 5,
|
||||
)
|
||||
@@ -284,38 +281,38 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isLiveMode
|
||||
? [const Color(0xFF27AE60), const Color(0xFF229954)]
|
||||
: [const Color(0xFF3498DB), const Color(0xFF2980B9)],
|
||||
? [const Color(0xFF10B981), const Color(0xFF10B981)]
|
||||
: [cs.tertiary, cs.tertiary],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.person_outline, color: Colors.white, size: 20),
|
||||
Icon(Icons.person_outline, color: cs.onPrimary, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
Text(
|
||||
"معلومات الكابتن",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: cs.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildInfoCard(Icons.person, "الاسم", name),
|
||||
_buildInfoCard(Icons.person, "الاسم", name, cs),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoCard(Icons.badge, "المعرف", driverId),
|
||||
_buildInfoCard(Icons.badge, "المعرف", driverId, cs),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoCard(Icons.speed, "السرعة", "$speed كم/س"),
|
||||
_buildInfoCard(Icons.speed, "السرعة", "$speed كم/س", cs),
|
||||
const SizedBox(height: 20),
|
||||
_buildStatsContainer(completed, cancelled),
|
||||
_buildStatsContainer(completed, cancelled, cs),
|
||||
if (isSuperAdmin) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildPhoneButton(phone),
|
||||
_buildPhoneButton(phone, cs),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
@@ -323,8 +320,8 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2C3E50),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: cs.onSurface,
|
||||
foregroundColor: cs.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -348,19 +345,20 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(IconData icon, String label, String value) {
|
||||
Widget _buildInfoCard(
|
||||
IconData icon, String label, String value, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade200,
|
||||
color: cs.outline,
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: cs.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
)
|
||||
],
|
||||
@@ -370,10 +368,10 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: const Color(0xFF2C3E50), size: 20),
|
||||
child: Icon(icon, color: cs.onSurface, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
@@ -383,17 +381,17 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2C3E50),
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -403,34 +401,36 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsContainer(String completed, String cancelled) {
|
||||
Widget _buildStatsContainer(
|
||||
String completed, String cancelled, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.grey.shade50, Colors.grey.shade100],
|
||||
colors: [cs.surfaceContainerHighest, cs.surfaceContainerHighest],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildStatItem("✓ مكتملة", completed, const Color(0xFF27AE60)),
|
||||
_buildStatItem("✓ مكتملة", completed, const Color(0xFF10B981), cs),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: Colors.grey.shade300,
|
||||
color: cs.outline,
|
||||
),
|
||||
_buildStatItem("✕ ملغاة", cancelled, const Color(0xFFE74C3C)),
|
||||
_buildStatItem("✕ ملغاة", cancelled, cs.error, cs),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value, Color color) {
|
||||
Widget _buildStatItem(
|
||||
String label, String value, Color color, ColorScheme cs) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
@@ -446,7 +446,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -454,7 +454,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneButton(String phone) {
|
||||
Widget _buildPhoneButton(String phone, ColorScheme cs) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (phone.isNotEmpty) _makePhoneCall(phone);
|
||||
@@ -465,14 +465,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [const Color(0xFFFFA500), const Color(0xFFFF8C00)],
|
||||
colors: [cs.tertiary, cs.tertiary],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFFFA500).withOpacity(0.4),
|
||||
color: cs.tertiary.withValues(alpha: 0.4),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
@@ -481,13 +481,13 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.call, color: Colors.white, size: 20),
|
||||
Icon(Icons.call, color: cs.onPrimary, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
phone,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: cs.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
@@ -502,6 +502,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
@@ -511,20 +512,19 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2C3E50).withOpacity(0.9),
|
||||
color: cs.onSurface.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
// backdropFilter: const BackdropFilter(blur: 10),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"نظام تتبع الكابتن",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
color: cs.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
foregroundColor: Colors.white,
|
||||
foregroundColor: cs.onPrimary,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
@@ -542,13 +542,13 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
MarkerLayer(markers: _markers),
|
||||
],
|
||||
),
|
||||
_buildDashboard(),
|
||||
_buildDashboard(cs),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDashboard() {
|
||||
Widget _buildDashboard(ColorScheme cs) {
|
||||
return Positioned(
|
||||
top: 100,
|
||||
right: 16,
|
||||
@@ -559,11 +559,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
child: Container(
|
||||
width: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
color: cs.shadow.withValues(alpha: 0.15),
|
||||
blurRadius: 30,
|
||||
spreadRadius: 5,
|
||||
)
|
||||
@@ -573,15 +573,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFF2C3E50),
|
||||
const Color(0xFF34495E)
|
||||
],
|
||||
colors: [cs.onSurface, cs.onSurfaceVariant],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
@@ -589,12 +585,11 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Icon(Icons.dashboard,
|
||||
color: Colors.white, size: 22),
|
||||
const Text(
|
||||
Icon(Icons.dashboard, color: cs.onPrimary, size: 22),
|
||||
Text(
|
||||
"لوحة التحكم",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: cs.onPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -607,7 +602,6 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Mode Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -618,7 +612,8 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
setState(() => isLiveMode = false);
|
||||
fetchData();
|
||||
},
|
||||
const Color(0xFF3498DB),
|
||||
cs.tertiary,
|
||||
cs,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -630,36 +625,35 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
setState(() => isLiveMode = true);
|
||||
fetchData();
|
||||
},
|
||||
const Color(0xFF27AE60),
|
||||
const Color(0xFF10B981),
|
||||
cs,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Stats
|
||||
_buildStatRow(
|
||||
icon: Icons.live_tv,
|
||||
label: "نشط الآن (مباشر)",
|
||||
value: liveCount.toString(),
|
||||
color: const Color(0xFF27AE60),
|
||||
color: const Color(0xFF10B981),
|
||||
cs: cs,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatRow(
|
||||
icon: Icons.history,
|
||||
label: "إجمالي اليوم",
|
||||
value: dayCount.toString(),
|
||||
color: const Color(0xFF3498DB),
|
||||
color: cs.tertiary,
|
||||
cs: cs,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Last Update
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -670,7 +664,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
: "تحديث: $lastUpdated",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -679,23 +673,24 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
? Icons.hourglass_bottom
|
||||
: Icons.check_circle,
|
||||
size: 14,
|
||||
color: isLoading ? Colors.orange : Colors.green,
|
||||
color: isLoading
|
||||
? cs.tertiary
|
||||
: const Color(0xFF10B981),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Refresh Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : fetchData,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2C3E50),
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Colors.grey.shade300,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: cs.onSurface,
|
||||
foregroundColor: cs.onPrimary,
|
||||
disabledBackgroundColor: cs.outline,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
@@ -705,13 +700,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (isLoading)
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
cs.onPrimary,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -746,6 +742,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
bool active,
|
||||
VoidCallback onTap,
|
||||
Color color,
|
||||
ColorScheme cs,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
@@ -757,21 +754,21 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
decoration: BoxDecoration(
|
||||
gradient: active
|
||||
? LinearGradient(
|
||||
colors: [color, color.withOpacity(0.8)],
|
||||
colors: [color, color.withValues(alpha: 0.8)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
)
|
||||
: null,
|
||||
color: active ? null : Colors.grey.shade100,
|
||||
color: active ? null : cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: active ? color : Colors.grey.shade300,
|
||||
color: active ? color : cs.outline,
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: active
|
||||
? [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.3),
|
||||
color: color.withValues(alpha: 0.3),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 1,
|
||||
)
|
||||
@@ -781,7 +778,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: active ? Colors.white : Colors.grey.shade700,
|
||||
color: active ? cs.onPrimary : cs.onSurfaceVariant,
|
||||
fontWeight: active ? FontWeight.bold : FontWeight.w600,
|
||||
fontSize: 13,
|
||||
),
|
||||
@@ -795,13 +792,14 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
required String label,
|
||||
required String value,
|
||||
required Color color,
|
||||
required ColorScheme cs,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 18),
|
||||
@@ -815,7 +813,7 @@ class _SiroTrackerScreenState extends State<SiroTrackerScreen>
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,14 +4,9 @@ import 'package:get/get.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
// Keep your specific imports
|
||||
import 'package:siro_admin/controller/functions/crud.dart';
|
||||
import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 1. DATA MODELS
|
||||
/// --------------------------------------------------------------------------
|
||||
|
||||
class DriverLocation {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
@@ -38,32 +33,21 @@ class DriverLocation {
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 2. GETX CONTROLLER
|
||||
/// --------------------------------------------------------------------------
|
||||
|
||||
/// تطبيع رقم الهاتف تلقائياً حسب الدولة
|
||||
/// مثال: 0992952235 ← 963992952235 (سوريا)
|
||||
/// مثال: 079XXXXXXX ← 96279XXXXXXX (أردن)
|
||||
/// مثال: 010XXXXXXXX ← 2010XXXXXXXX (مصر)
|
||||
String normalizePhone(String input) {
|
||||
final clean = input.replaceAll(RegExp(r'\D+'), '');
|
||||
|
||||
// Syria: 099XXXXXXX or 9639XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('09')) {
|
||||
return '963${clean.substring(1)}';
|
||||
}
|
||||
if (clean.length == 12 && clean.startsWith('963')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('9')) return '963$clean';
|
||||
|
||||
// Jordan: 079XXXXXXX or 9627XXXXXXX
|
||||
if (clean.length == 10 && clean.startsWith('07')) {
|
||||
return '962${clean.substring(1)}';
|
||||
}
|
||||
if (clean.length == 12 && clean.startsWith('962')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('7')) return '962$clean';
|
||||
|
||||
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
|
||||
if (clean.length == 11 && clean.startsWith('01')) {
|
||||
return '20${clean.substring(1)}';
|
||||
}
|
||||
@@ -73,32 +57,26 @@ String normalizePhone(String input) {
|
||||
}
|
||||
|
||||
class RideMonitorController extends GetxController {
|
||||
// CONFIGURATION
|
||||
final String apiUrl = "${AppLink.server}/Admin/rides/monitorRide.php";
|
||||
|
||||
// INPUT CONTROLLERS
|
||||
final TextEditingController phoneInputController = TextEditingController();
|
||||
|
||||
// OBSERVABLES
|
||||
var isTracking = false.obs;
|
||||
var isLoading = false.obs;
|
||||
var hasError = false.obs;
|
||||
var errorMessage = ''.obs;
|
||||
|
||||
// Driver & Ride Data
|
||||
var driverLocation = Rxn<DriverLocation>();
|
||||
var driverName = "Unknown Driver".obs;
|
||||
var rideStatus = "Waiting...".obs;
|
||||
|
||||
// Route Data
|
||||
var startPoint = Rxn<LatLng>();
|
||||
var endPoint = Rxn<LatLng>();
|
||||
var routePolyline = <LatLng>[].obs; // List of points for the line
|
||||
var routePolyline = <LatLng>[].obs;
|
||||
|
||||
// Map Variables
|
||||
final MapController mapController = MapController();
|
||||
Timer? _timer;
|
||||
bool _isFirstLoad = true; // To trigger auto-fit bounds only on first success
|
||||
bool _isFirstLoad = true;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
@@ -107,15 +85,12 @@ class RideMonitorController extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// --- ACTIONS ---
|
||||
|
||||
void startSearch() {
|
||||
if (phoneInputController.text.trim().isEmpty) {
|
||||
mySnackbarWarning("يرجى إدخال رقم الهاتف أولاً");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
hasError.value = false;
|
||||
errorMessage.value = '';
|
||||
driverLocation.value = null;
|
||||
@@ -126,14 +101,11 @@ class RideMonitorController extends GetxController {
|
||||
rideStatus.value = "جاري التحميل...";
|
||||
_isFirstLoad = true;
|
||||
|
||||
// Switch UI
|
||||
isTracking.value = true;
|
||||
isLoading.value = true;
|
||||
|
||||
// Start fetching
|
||||
fetchRideData();
|
||||
|
||||
// Start Polling
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
||||
fetchRideData();
|
||||
@@ -144,7 +116,6 @@ class RideMonitorController extends GetxController {
|
||||
_timer?.cancel();
|
||||
isTracking.value = false;
|
||||
isLoading.value = false;
|
||||
// phoneInputController.clear(); // اختياري: يمكنك إبقائه لتسهيل البحث مرة أخرى
|
||||
}
|
||||
|
||||
Future<void> fetchRideData() async {
|
||||
@@ -152,7 +123,6 @@ class RideMonitorController extends GetxController {
|
||||
if (phone.isEmpty) return;
|
||||
|
||||
try {
|
||||
// تطبيع رقم الهاتف تلقائياً حسب الدولة
|
||||
String normalizedPhone = normalizePhone(phone);
|
||||
final response = await CRUD().post(
|
||||
link: apiUrl,
|
||||
@@ -168,17 +138,14 @@ class RideMonitorController extends GetxController {
|
||||
final data =
|
||||
jsonResponse['message'] ?? jsonResponse['data'] ?? jsonResponse;
|
||||
|
||||
// 1. Parse Driver Info
|
||||
if (data['driver_details'] != null) {
|
||||
driverName.value =
|
||||
data['driver_details']['fullname'] ?? "سائق غير معروف";
|
||||
}
|
||||
|
||||
// 2. Parse Ride Info & Route
|
||||
if (data['ride_details'] != null) {
|
||||
rideStatus.value = data['ride_details']['status'] ?? "غير معروف";
|
||||
|
||||
// Parse Start/End Locations (Format: "lat,lng")
|
||||
String? startStr = data['ride_details']['start_location'];
|
||||
String? endStr = data['ride_details']['end_location'];
|
||||
|
||||
@@ -188,20 +155,17 @@ class RideMonitorController extends GetxController {
|
||||
if (s != null && e != null) {
|
||||
startPoint.value = s;
|
||||
endPoint.value = e;
|
||||
routePolyline.value = [s, e]; // Straight line for now
|
||||
routePolyline.value = [s, e];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Parse Live Location
|
||||
final locData = data['driver_location'];
|
||||
if (locData is Map<String, dynamic>) {
|
||||
final newLocation = DriverLocation.fromJson(locData);
|
||||
driverLocation.value = newLocation;
|
||||
|
||||
// 4. Update Camera Bounds
|
||||
_updateMapBounds();
|
||||
} else {
|
||||
// Even if no live driver, we might want to show the route
|
||||
if (startPoint.value != null && endPoint.value != null) {
|
||||
_updateMapBounds();
|
||||
}
|
||||
@@ -227,7 +191,6 @@ class RideMonitorController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse "lat,lng" string
|
||||
LatLng? _parseLatLngString(String? str) {
|
||||
if (str == null || !str.contains(',')) return null;
|
||||
try {
|
||||
@@ -240,7 +203,6 @@ class RideMonitorController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// Logic to fit start, end, and driver on screen
|
||||
void _updateMapBounds() {
|
||||
if (!_isFirstLoad) return;
|
||||
|
||||
@@ -264,48 +226,36 @@ class RideMonitorController extends GetxController {
|
||||
);
|
||||
_isFirstLoad = false;
|
||||
} catch (e) {
|
||||
// Map Controller not ready yet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 3. UI SCREEN (Modern Light Theme)
|
||||
/// --------------------------------------------------------------------------
|
||||
|
||||
class RideMonitorScreen extends StatelessWidget {
|
||||
const RideMonitorScreen({super.key});
|
||||
|
||||
// 🎨 الألوان العصرية (Modern Palette)
|
||||
final Color backgroundColor = const Color(0xFFF4F7FE);
|
||||
final Color primaryColor = const Color(0xFF4318FF);
|
||||
final Color textPrimary = const Color(0xFF2B3674);
|
||||
final Color textSecondary = const Color(0xFFA3AED0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final RideMonitorController controller = Get.put(RideMonitorController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: backgroundColor,
|
||||
// الإبقاء على AppBar فقط في شاشة البحث
|
||||
backgroundColor: cs.surface,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: Obx(() {
|
||||
if (controller.isTracking.value) {
|
||||
return const SizedBox
|
||||
.shrink(); // إخفاء الـ AppBar في وضع التتبع للخريطة الكاملة
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: textPrimary),
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
title: Text(
|
||||
"مراقبة الرحلات",
|
||||
style: TextStyle(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
@@ -315,18 +265,15 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
),
|
||||
body: Obx(() {
|
||||
if (!controller.isTracking.value) {
|
||||
return _buildSearchForm(context, controller);
|
||||
return _buildSearchForm(context, controller, cs);
|
||||
}
|
||||
return _buildMapTrackingView(context, controller);
|
||||
return _buildMapTrackingView(context, controller, cs);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// واجهة البحث (Search View)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildSearchForm(
|
||||
BuildContext context, RideMonitorController controller) {
|
||||
BuildContext context, RideMonitorController controller, ColorScheme cs) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
@@ -335,11 +282,11 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: primaryColor.withOpacity(0.08),
|
||||
color: cs.primary.withValues(alpha: 0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
@@ -351,17 +298,16 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.1),
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child:
|
||||
Icon(Icons.radar_rounded, size: 60, color: primaryColor),
|
||||
child: Icon(Icons.radar_rounded, size: 60, color: cs.primary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"تتبع رحلة نشطة",
|
||||
style: TextStyle(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -370,7 +316,7 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Text(
|
||||
"أدخل رقم هاتف السائق أو الراكب للبدء",
|
||||
style: TextStyle(
|
||||
color: textSecondary,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -378,27 +324,27 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
border: Border.all(color: cs.outline, width: 2),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.phoneInputController,
|
||||
keyboardType: TextInputType.phone,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: TextStyle(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: "مثال: 0992952235...",
|
||||
hintStyle: TextStyle(color: textSecondary),
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 18, horizontal: 20),
|
||||
prefixIcon:
|
||||
Icon(Icons.phone_rounded, color: primaryColor),
|
||||
Icon(Icons.phone_rounded, color: cs.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -409,16 +355,16 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.startSearch,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
backgroundColor: cs.primary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
"بدء المراقبة",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: cs.onPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -433,11 +379,8 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// واجهة الخريطة (Map View)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildMapTrackingView(
|
||||
BuildContext context, RideMonitorController controller) {
|
||||
BuildContext context, RideMonitorController controller, ColorScheme cs) {
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
@@ -451,45 +394,36 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.siromove.admin',
|
||||
),
|
||||
|
||||
// 1. ROUTE LINE (Polyline)
|
||||
if (controller.routePolyline.isNotEmpty)
|
||||
PolylineLayer(
|
||||
polylines: [
|
||||
Polyline(
|
||||
points: controller.routePolyline,
|
||||
strokeWidth: 6.0,
|
||||
color: primaryColor.withOpacity(0.9),
|
||||
color: cs.primary.withValues(alpha: 0.9),
|
||||
borderStrokeWidth: 2.0,
|
||||
borderColor: primaryColor.withOpacity(0.3),
|
||||
borderColor: cs.primary.withValues(alpha: 0.3),
|
||||
strokeCap: StrokeCap.round,
|
||||
strokeJoin: StrokeJoin.round,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 2. START & END MARKERS
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
// Start Point (Green Dot)
|
||||
if (controller.startPoint.value != null)
|
||||
Marker(
|
||||
point: controller.startPoint.value!,
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: _buildPointMarker(const Color(0xFF10B981)),
|
||||
child: _buildPointMarker(const Color(0xFF10B981), cs),
|
||||
),
|
||||
|
||||
// End Point (Red Dot)
|
||||
if (controller.endPoint.value != null)
|
||||
Marker(
|
||||
point: controller.endPoint.value!,
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: _buildPointMarker(const Color(0xFFEF4444)),
|
||||
child: _buildPointMarker(const Color(0xFFEF4444), cs),
|
||||
),
|
||||
|
||||
// Driver Car Marker
|
||||
if (controller.driverLocation.value != null)
|
||||
Marker(
|
||||
point: LatLng(
|
||||
@@ -507,11 +441,11 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: cs.onSurface.withValues(alpha: 0.2),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
@@ -519,7 +453,7 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
),
|
||||
child: Icon(
|
||||
Icons.directions_car_rounded,
|
||||
color: primaryColor,
|
||||
color: cs.primary,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
@@ -528,13 +462,13 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
"${controller.driverLocation.value!.speed.toInt()} كم",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: cs.onPrimary,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -549,48 +483,44 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// زر التراجع (إيقاف التتبع) أعلى الشاشة
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 10,
|
||||
right: 20, // أو left حسب لغة التطبيق
|
||||
right: 20,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: cs.onSurface.withValues(alpha: 0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.close_rounded, color: textPrimary, size: 24),
|
||||
icon: Icon(Icons.close_rounded, color: cs.onSurface, size: 24),
|
||||
onPressed: controller.stopTracking,
|
||||
tooltip: "إيقاف المراقبة",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// LOADING OVERLAY (Smooth Frosted Glass like)
|
||||
if (controller.isLoading.value &&
|
||||
controller.driverLocation.value == null &&
|
||||
controller.startPoint.value == null)
|
||||
Container(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
color: cs.surface.withValues(alpha: 0.8),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: primaryColor, strokeWidth: 3),
|
||||
color: cs.primary, strokeWidth: 3),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"جاري تحديد الموقع...",
|
||||
style: TextStyle(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
@@ -599,19 +529,17 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ERROR OVERLAY
|
||||
if (controller.hasError.value)
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: cs.onSurface.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
@@ -623,17 +551,17 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
color: cs.error.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.error_outline_rounded,
|
||||
color: Colors.red, size: 40),
|
||||
child: Icon(Icons.error_outline_rounded,
|
||||
color: cs.error, size: 40),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"حدث خطأ",
|
||||
style: TextStyle(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -642,7 +570,7 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Text(
|
||||
controller.errorMessage.value,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: textSecondary, height: 1.5),
|
||||
style: TextStyle(color: cs.onSurfaceVariant, height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
@@ -650,8 +578,8 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.stopTracking,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: textPrimary,
|
||||
backgroundColor: cs.surface,
|
||||
foregroundColor: cs.onSurface,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -666,8 +594,6 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// INFO CARD (Bottom Floating Card)
|
||||
if (!controller.hasError.value && !controller.isLoading.value)
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
@@ -675,11 +601,11 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
right: 20,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
color: cs.onSurface.withValues(alpha: 0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
@@ -696,11 +622,11 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.1),
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Icon(Icons.person_rounded,
|
||||
color: primaryColor, size: 28),
|
||||
color: cs.primary, size: 28),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
@@ -710,7 +636,7 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Text(
|
||||
controller.driverName.value,
|
||||
style: TextStyle(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -735,7 +661,7 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Text(
|
||||
controller.rideStatus.value,
|
||||
style: TextStyle(
|
||||
color: textSecondary,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -759,17 +685,19 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Icons.speed_rounded,
|
||||
"${controller.driverLocation.value!.speed.toStringAsFixed(1)} كم/س",
|
||||
const Color(0xFF3B82F6),
|
||||
cs,
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 30,
|
||||
color: Colors.grey.withOpacity(0.2)),
|
||||
color: cs.outline.withValues(alpha: 0.2)),
|
||||
_buildModernInfoBadge(
|
||||
Icons.access_time_rounded,
|
||||
controller.driverLocation.value!.updatedAt
|
||||
.split(' ')
|
||||
.last,
|
||||
const Color(0xFF8B5CF6),
|
||||
cs,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -781,13 +709,13 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
color: primaryColor, strokeWidth: 2),
|
||||
color: cs.primary, strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
"جاري الاتصال بالسائق...",
|
||||
style: TextStyle(
|
||||
color: primaryColor,
|
||||
color: cs.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
),
|
||||
@@ -803,12 +731,10 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Helper Widgets ---
|
||||
|
||||
Widget _buildPointMarker(Color color) {
|
||||
Widget _buildPointMarker(Color color, ColorScheme cs) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.3),
|
||||
color: color.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
@@ -818,10 +744,10 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
border: Border.all(color: cs.surface, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.5),
|
||||
color: color.withValues(alpha: 0.5),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 1,
|
||||
)
|
||||
@@ -832,13 +758,14 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModernInfoBadge(IconData icon, String text, Color iconColor) {
|
||||
Widget _buildModernInfoBadge(
|
||||
IconData icon, String text, Color iconColor, ColorScheme cs) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withOpacity(0.1),
|
||||
color: iconColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: iconColor),
|
||||
@@ -847,11 +774,11 @@ class RideMonitorScreen extends StatelessWidget {
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textDirection: TextDirection.ltr, // للحفاظ على اتجاه الأرقام
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/links.dart';
|
||||
import 'package:siro_admin/controller/employee_controller/employee_controller.dart';
|
||||
import 'package:siro_admin/controller/functions/upload_image copy.dart'; // تأكد من مسار الملف الصحيح
|
||||
import 'package:siro_admin/controller/functions/upload_image copy.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class EmployeePage extends StatelessWidget {
|
||||
@@ -10,27 +10,21 @@ class EmployeePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حقن الكنترولر
|
||||
Get.put(EmployeeController());
|
||||
|
||||
// ألوان الثيم
|
||||
const Color bgColor = Color(0xFF0A0E27);
|
||||
const Color cardColor = Color(0xFF1A1F3A);
|
||||
const Color primaryAccent = Color(0xFF6366F1);
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
backgroundColor: cs.surface,
|
||||
body: GetBuilder<EmployeeController>(
|
||||
builder: (controller) {
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// 1. App Bar
|
||||
SliverAppBar(
|
||||
expandedHeight: 100,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
backgroundColor: bgColor,
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
titlePadding: const EdgeInsets.only(bottom: 16),
|
||||
@@ -40,19 +34,19 @@ class EmployeePage extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: primaryAccent.withOpacity(0.2),
|
||||
color: cs.primary.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.badge_rounded,
|
||||
color: Colors.white, size: 18),
|
||||
child: Icon(Icons.badge_rounded,
|
||||
color: cs.onSurface, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
Text(
|
||||
'الموظفون',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
color: cs.onSurface,
|
||||
fontFamily: 'Segoe UI',
|
||||
),
|
||||
),
|
||||
@@ -65,8 +59,8 @@ class EmployeePage extends StatelessWidget {
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
primaryAccent.withOpacity(0.15),
|
||||
bgColor,
|
||||
cs.primary.withValues(alpha: 0.15),
|
||||
cs.surface,
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -74,18 +68,17 @@ class EmployeePage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// 2. قائمة الموظفين
|
||||
if (controller.employee.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.people_outline,
|
||||
size: 60, color: Colors.white24),
|
||||
SizedBox(height: 16),
|
||||
size: 60, color: cs.onSurface.withValues(alpha: 0.24)),
|
||||
const SizedBox(height: 16),
|
||||
Text("لا يوجد موظفين حالياً",
|
||||
style: TextStyle(color: Colors.white54)),
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -100,8 +93,7 @@ class EmployeePage extends StatelessWidget {
|
||||
return _EmployeeCard(
|
||||
employee: employee,
|
||||
index: index,
|
||||
cardColor: cardColor,
|
||||
primaryAccent: primaryAccent,
|
||||
cs: cs,
|
||||
);
|
||||
},
|
||||
childCount: controller.employee.length,
|
||||
@@ -118,25 +110,22 @@ class EmployeePage extends StatelessWidget {
|
||||
controller.id = controller.generateRandomId(8);
|
||||
Get.to(() => _EmployeeFormScreen(controller: controller));
|
||||
},
|
||||
backgroundColor: primaryAccent,
|
||||
child: const Icon(Icons.person_add_rounded, color: Colors.white),
|
||||
backgroundColor: cs.primary,
|
||||
child: Icon(Icons.person_add_rounded, color: cs.onPrimary),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// === بطاقة الموظف (تصميم جديد ومحسن) ===
|
||||
class _EmployeeCard extends StatelessWidget {
|
||||
final Map<String, dynamic> employee;
|
||||
final int index;
|
||||
final Color cardColor;
|
||||
final Color primaryAccent;
|
||||
final ColorScheme cs;
|
||||
|
||||
const _EmployeeCard({
|
||||
required this.employee,
|
||||
required this.index,
|
||||
required this.cardColor,
|
||||
required this.primaryAccent,
|
||||
required this.cs,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -147,12 +136,12 @@ class _EmployeeCard extends StatelessWidget {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.05)),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -168,28 +157,27 @@ class _EmployeeCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// الصف العلوي: الحالة + أيقونة
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.05),
|
||||
color: cs.outline.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.person,
|
||||
color: Colors.white.withOpacity(0.7), size: 20),
|
||||
color: cs.onSurface.withValues(alpha: 0.7), size: 20),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border:
|
||||
Border.all(color: statusColor.withOpacity(0.3)),
|
||||
Border.all(color: statusColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
employee['status'] ?? 'Unknown',
|
||||
@@ -208,23 +196,21 @@ class _EmployeeCard extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// الاسم في سطر كامل ومميز
|
||||
Text(
|
||||
employee['name'] ?? 'Unknown',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Divider(height: 1, color: Colors.white10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Divider(height: 1, color: cs.outline.withValues(alpha: 0.1)),
|
||||
),
|
||||
|
||||
// تفاصيل التعليم والهاتف والموقع (مع دعم تعدد الأسطر)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -233,23 +219,22 @@ class _EmployeeCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(Icons.phone_iphone_rounded,
|
||||
employee['phone'] ?? '', Colors.white54),
|
||||
const SizedBox(height: 12), // مسافة أكبر بين العناصر
|
||||
employee['phone'] ?? '', cs.onSurfaceVariant, cs),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(
|
||||
Icons.school_outlined,
|
||||
employee['education'] ?? 'غير محدد',
|
||||
primaryAccent),
|
||||
const SizedBox(height: 12), // مسافة أكبر
|
||||
cs.primary, cs),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow(Icons.location_on_outlined,
|
||||
employee['site'] ?? 'غير محدد', Colors.blueGrey),
|
||||
employee['site'] ?? 'غير محدد', Colors.blueGrey, cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// زر الاتصال الجانبي
|
||||
const SizedBox(width: 16),
|
||||
Material(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
color: const Color(0xFF10B981).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
@@ -258,7 +243,7 @@ class _EmployeeCard extends StatelessWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: const Icon(Icons.call,
|
||||
color: Colors.green, size: 24),
|
||||
color: Color(0xFF10B981), size: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -272,25 +257,23 @@ class _EmployeeCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String text, Color iconColor) {
|
||||
Widget _buildInfoRow(IconData icon, String text, Color iconColor, ColorScheme cs) {
|
||||
return Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start, // محاذاة الأيقونة مع بداية النص
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2), // ضبط بسيط لموقع الأيقونة
|
||||
child: Icon(icon, size: 16, color: iconColor.withOpacity(0.8)),
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Icon(icon, size: 16, color: iconColor.withValues(alpha: 0.8)),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
color: cs.onSurface.withValues(alpha: 0.7),
|
||||
fontSize: 13,
|
||||
height: 1.5, // تباعد الأسطر لسهولة القراءة
|
||||
height: 1.5,
|
||||
),
|
||||
// تم إزالة maxLines و overflow للسماح بالنص بالنزول لأسطر متعددة
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -305,25 +288,23 @@ class _EmployeeCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// === شاشة إضافة موظف ===
|
||||
class _EmployeeFormScreen extends StatelessWidget {
|
||||
final EmployeeController controller;
|
||||
const _EmployeeFormScreen({required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const Color bgColor = Color(0xFF0A0E27);
|
||||
const Color inputColor = Color(0xFF1A1F3A);
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text("إضافة موظف جديد",
|
||||
style: TextStyle(color: Colors.white)),
|
||||
backgroundColor: bgColor,
|
||||
title: Text("إضافة موظف جديد",
|
||||
style: TextStyle(color: cs.onSurface)),
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
@@ -339,6 +320,7 @@ class _EmployeeFormScreen extends StatelessWidget {
|
||||
child: _UploadButton(
|
||||
title: "الهوية (أمام)",
|
||||
icon: Icons.credit_card,
|
||||
cs: cs,
|
||||
onPressed: () async {
|
||||
await ImageController().choosImage(AppLink.uploadEgypt,
|
||||
'idFrontEmployee', controller.id);
|
||||
@@ -350,6 +332,7 @@ class _EmployeeFormScreen extends StatelessWidget {
|
||||
child: _UploadButton(
|
||||
title: "الهوية (خلف)",
|
||||
icon: Icons.credit_card_outlined,
|
||||
cs: cs,
|
||||
onPressed: () async {
|
||||
await ImageController().choosImage(AppLink.uploadEgypt,
|
||||
'idbackEmployee', controller.id);
|
||||
@@ -360,20 +343,20 @@ class _EmployeeFormScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildModernTextField(
|
||||
controller.name, "الاسم الكامل", Icons.person, inputColor),
|
||||
controller.name, "الاسم الكامل", Icons.person, cs),
|
||||
const SizedBox(height: 16),
|
||||
_buildModernTextField(
|
||||
controller.phone, "رقم الهاتف", Icons.phone, inputColor,
|
||||
controller.phone, "رقم الهاتف", Icons.phone, cs,
|
||||
type: TextInputType.phone),
|
||||
const SizedBox(height: 16),
|
||||
_buildModernTextField(controller.education, "التعليم / الملاحظات",
|
||||
Icons.school, inputColor),
|
||||
Icons.school, cs),
|
||||
const SizedBox(height: 16),
|
||||
_buildModernTextField(controller.site, "الموقع / العنوان",
|
||||
Icons.location_on, inputColor),
|
||||
Icons.location_on, cs),
|
||||
const SizedBox(height: 16),
|
||||
_buildModernTextField(controller.status, "الحالة (مثال: ممتاز)",
|
||||
Icons.star, inputColor),
|
||||
Icons.star, cs),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -385,14 +368,14 @@ class _EmployeeFormScreen extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
backgroundColor: cs.primary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text("حفظ البيانات",
|
||||
child: Text("حفظ البيانات",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: cs.onPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
@@ -405,23 +388,23 @@ class _EmployeeFormScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildModernTextField(TextEditingController controller, String hint,
|
||||
IconData icon, Color fillColor,
|
||||
IconData icon, ColorScheme cs,
|
||||
{TextInputType type = TextInputType.text}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: type,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
maxLines: null, // السماح بتعدد الأسطر عند الإدخال أيضاً
|
||||
style: TextStyle(color: cs.onSurface),
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
labelText: hint,
|
||||
labelStyle: TextStyle(color: Colors.white.withOpacity(0.5)),
|
||||
prefixIcon: Icon(icon, color: Colors.white38, size: 20),
|
||||
labelStyle: TextStyle(color: cs.onSurface.withValues(alpha: 0.5)),
|
||||
prefixIcon: Icon(icon, color: cs.onSurfaceVariant, size: 20),
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
@@ -437,9 +420,10 @@ class _UploadButton extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
final ColorScheme cs;
|
||||
|
||||
const _UploadButton(
|
||||
{required this.title, required this.icon, required this.onPressed});
|
||||
{required this.title, required this.icon, required this.onPressed, required this.cs});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -449,21 +433,21 @@ class _UploadButton extends StatelessWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1F3A),
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF6366F1).withOpacity(0.3),
|
||||
color: cs.primary.withValues(alpha: 0.3),
|
||||
style: BorderStyle.solid),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: const Color(0xFF6366F1), size: 30),
|
||||
Icon(icon, color: cs.primary, size: 30),
|
||||
const SizedBox(height: 8),
|
||||
Text(title,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12)),
|
||||
style: TextStyle(color: cs.onSurface.withValues(alpha: 0.7), fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
const Text("اضغط للرفع",
|
||||
style: TextStyle(color: Colors.white38, fontSize: 10)),
|
||||
Text("اضغط للرفع",
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -471,22 +455,21 @@ class _UploadButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// === شاشة التفاصيل ===
|
||||
class EmployeeDetails extends StatelessWidget {
|
||||
final int index;
|
||||
const EmployeeDetails({super.key, required this.index});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const Color bgColor = Color(0xFF0A0E27);
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title:
|
||||
const Text('تفاصيل الموظف', style: TextStyle(color: Colors.white)),
|
||||
backgroundColor: bgColor,
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
Text('تفاصيل الموظف', style: TextStyle(color: cs.onSurface)),
|
||||
backgroundColor: cs.surface,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
elevation: 0,
|
||||
),
|
||||
body: GetBuilder<EmployeeController>(
|
||||
@@ -497,24 +480,26 @@ class EmployeeDetails extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("الهوية الأمامية",
|
||||
Text("الهوية الأمامية",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
_buildImageViewer(
|
||||
'${AppLink.server}/card_image/idFrontEmployee-$employeeId.jpg',
|
||||
cs,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const Text("الهوية الخلفية",
|
||||
Text("الهوية الخلفية",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
_buildImageViewer(
|
||||
'${AppLink.server}/card_image/idbackEmployee-$employeeId.jpg',
|
||||
cs,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -524,14 +509,14 @@ class EmployeeDetails extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageViewer(String url) {
|
||||
Widget _buildImageViewer(String url, ColorScheme cs) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1F3A),
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -539,23 +524,23 @@ class EmployeeDetails extends StatelessWidget {
|
||||
url,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.broken_image_rounded,
|
||||
color: Colors.white24, size: 50),
|
||||
SizedBox(height: 8),
|
||||
color: cs.onSurface.withValues(alpha: 0.24), size: 50),
|
||||
const SizedBox(height: 8),
|
||||
Text("فشل تحميل الصورة",
|
||||
style: TextStyle(color: Colors.white24)),
|
||||
style: TextStyle(color: cs.onSurface.withValues(alpha: 0.24))),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF6366F1)));
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: cs.primary));
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -126,47 +126,51 @@ class _ErrorListPageState extends State<ErrorListPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F4F8),
|
||||
appBar: _buildAppBar(),
|
||||
backgroundColor: cs.surface,
|
||||
appBar: _buildAppBar(cs),
|
||||
body: Column(
|
||||
children: [
|
||||
_SearchBar(
|
||||
controller: _phoneCtrl,
|
||||
onSearch: _searchByPhone,
|
||||
onClear: _clearSearch,
|
||||
cs: cs,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildBody(),
|
||||
child: _buildBody(cs),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
PreferredSizeWidget _buildAppBar(ColorScheme cs) {
|
||||
return AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
foregroundColor: cs.onSurface,
|
||||
centerTitle: true,
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
color: cs.onSurface.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||
border: Border.all(color: cs.onSurface.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 22),
|
||||
Icon(Icons.error_outline, color: cs.error, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
Text(
|
||||
"سجل الأخطاء",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -175,11 +179,11 @@ class _ErrorListPageState extends State<ErrorListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
Widget _buildBody(ColorScheme cs) {
|
||||
if (_loading) {
|
||||
return const Center(
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF0F172A)),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(cs.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -189,13 +193,13 @@ class _ErrorListPageState extends State<ErrorListPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 64, color: Colors.red.shade300),
|
||||
Icon(Icons.error_outline, size: 64, color: cs.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMsg!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
color: cs.error,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
@@ -209,15 +213,15 @@ class _ErrorListPageState extends State<ErrorListPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
const Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 64,
|
||||
color: Colors.green.shade300,
|
||||
color: Color(0xFF10B981),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
Text(
|
||||
'لا توجد سجلات أخطاء',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
style: TextStyle(fontSize: 16, color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -232,12 +236,12 @@ class _ErrorListPageState extends State<ErrorListPage> {
|
||||
await _fetchLast20();
|
||||
}
|
||||
},
|
||||
color: const Color(0xFF0F172A),
|
||||
color: cs.primary,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _ErrorTile(_items[index], index);
|
||||
return _ErrorTile(_items[index], index, cs);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -254,11 +258,13 @@ class _SearchBar extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final VoidCallback onSearch;
|
||||
final VoidCallback onClear;
|
||||
final ColorScheme cs;
|
||||
|
||||
const _SearchBar({
|
||||
required this.controller,
|
||||
required this.onSearch,
|
||||
required this.onClear,
|
||||
required this.cs,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -270,18 +276,16 @@ class _SearchBarState extends State<_SearchBar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = widget.cs;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.white, Colors.grey.shade50],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
blurRadius: 16,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
@@ -303,15 +307,13 @@ class _SearchBarState extends State<_SearchBar> {
|
||||
keyboardType: TextInputType.phone,
|
||||
textDirection: TextDirection.rtl,
|
||||
onSubmitted: (_) => widget.onSearch(),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
style: TextStyle(fontSize: 15, color: cs.onSurface),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'بحث برقم الهاتف',
|
||||
hintStyle: TextStyle(color: Colors.grey.shade400),
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: _isFocused
|
||||
? const Color(0xFF0F172A)
|
||||
: Colors.grey.shade400,
|
||||
color: _isFocused ? cs.primary : cs.onSurfaceVariant,
|
||||
),
|
||||
suffixIcon: widget.controller.text.isNotEmpty
|
||||
? InkWell(
|
||||
@@ -320,28 +322,28 @@ class _SearchBarState extends State<_SearchBar> {
|
||||
setState(() {});
|
||||
},
|
||||
child: Icon(Icons.close,
|
||||
color: Colors.grey.shade400, size: 20),
|
||||
color: cs.onSurfaceVariant, size: 20),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
BorderSide(color: cs.outline, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.grey.shade300, width: 1),
|
||||
BorderSide(color: cs.outline, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF0F172A),
|
||||
borderSide: BorderSide(
|
||||
color: cs.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
fillColor: cs.surfaceContainerHighest,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
@@ -352,8 +354,8 @@ class _SearchBarState extends State<_SearchBar> {
|
||||
ElevatedButton(
|
||||
onPressed: widget.onSearch,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -372,9 +374,9 @@ class _SearchBarState extends State<_SearchBar> {
|
||||
OutlinedButton(
|
||||
onPressed: widget.onClear,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF0F172A),
|
||||
side: const BorderSide(
|
||||
color: Color(0xFF0F172A),
|
||||
foregroundColor: cs.primary,
|
||||
side: BorderSide(
|
||||
color: cs.primary,
|
||||
width: 1.5,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -395,12 +397,12 @@ class _SearchBarState extends State<_SearchBar> {
|
||||
class _ErrorTile extends StatelessWidget {
|
||||
final ErrorLog item;
|
||||
final int index;
|
||||
final ColorScheme cs;
|
||||
|
||||
const _ErrorTile(this.item, this.index);
|
||||
const _ErrorTile(this.item, this.index, this.cs);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// تحديد الألوان والأيقونات بناءً على نوع المستخدم
|
||||
final isDriver = item.userType.toLowerCase().contains('driver') ||
|
||||
item.userType.toLowerCase().contains('سائق');
|
||||
|
||||
@@ -410,22 +412,18 @@ class _ErrorTile extends StatelessWidget {
|
||||
final userTypeLabel = isDriver ? "سائق" : "راكب";
|
||||
|
||||
final userTypeBgColor = isDriver
|
||||
? const Color(0xFF10B981).withOpacity(0.1)
|
||||
: const Color(0xFFF59E0B).withOpacity(0.1);
|
||||
? const Color(0xFF10B981).withValues(alpha: 0.1)
|
||||
: const Color(0xFFF59E0B).withValues(alpha: 0.1);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.white, Colors.grey.shade50],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200, width: 1),
|
||||
border: Border.all(color: cs.outline, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.06),
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 1,
|
||||
)
|
||||
@@ -435,7 +433,6 @@ class _ErrorTile extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Stack(
|
||||
children: [
|
||||
// خط علوي ملون
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
@@ -446,7 +443,7 @@ class _ErrorTile extends StatelessWidget {
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFFEF4444),
|
||||
Colors.red.shade400,
|
||||
cs.error,
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
@@ -459,7 +456,6 @@ class _ErrorTile extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// الصف الأول: رقم الخطأ ونوع المستخدم
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -493,7 +489,7 @@ class _ErrorTile extends StatelessWidget {
|
||||
'#${item.id}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -501,14 +497,13 @@ class _ErrorTile extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// عنوان الخطأ (قابل للنسخ)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
color: cs.error.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.red.shade200,
|
||||
color: cs.error.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -516,7 +511,7 @@ class _ErrorTile extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded,
|
||||
size: 18, color: Colors.red.shade600),
|
||||
size: 18, color: cs.error),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
@@ -524,7 +519,7 @@ class _ErrorTile extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red.shade900,
|
||||
color: cs.error,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
@@ -534,15 +529,14 @@ class _ErrorTile extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// التفاصيل (إن وجدت)
|
||||
if (item.details.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade200,
|
||||
color: cs.outline,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -550,14 +544,14 @@ class _ErrorTile extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline,
|
||||
size: 16, color: Colors.grey.shade600),
|
||||
size: 16, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
item.details,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade700,
|
||||
color: cs.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
@@ -568,7 +562,6 @@ class _ErrorTile extends StatelessWidget {
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// معلومات تقنية
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
@@ -578,25 +571,29 @@ class _ErrorTile extends StatelessWidget {
|
||||
icon: Icons.phone,
|
||||
label: 'الهاتف',
|
||||
value: item.phone,
|
||||
color: Colors.blue,
|
||||
color: cs.primary,
|
||||
cs: cs,
|
||||
),
|
||||
_buildInfoBadge(
|
||||
icon: Icons.person_outline,
|
||||
label: 'المعرف',
|
||||
value: item.userId,
|
||||
color: Colors.purple,
|
||||
cs: cs,
|
||||
),
|
||||
_buildInfoBadge(
|
||||
icon: Icons.devices,
|
||||
label: 'Path',
|
||||
value: item.device,
|
||||
color: Colors.orange,
|
||||
cs: cs,
|
||||
),
|
||||
_buildInfoBadge(
|
||||
icon: Icons.schedule,
|
||||
label: 'التاريخ',
|
||||
value: item.createdAt,
|
||||
color: Colors.teal,
|
||||
cs: cs,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -614,13 +611,14 @@ class _ErrorTile extends StatelessWidget {
|
||||
required String label,
|
||||
required String value,
|
||||
required Color color,
|
||||
required ColorScheme cs,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
color: color.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -635,7 +633,7 @@ class _ErrorTile extends StatelessWidget {
|
||||
text: "$label: ",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: color.withOpacity(0.7),
|
||||
color: color.withValues(alpha: 0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
import 'package:siro_admin/controller/admin/financial_v2_controller.dart';
|
||||
|
||||
class FinancialV2Page extends StatelessWidget {
|
||||
@@ -8,19 +7,21 @@ class FinancialV2Page extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final controller = Get.put(FinancialV2Controller());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('الإدارة المالية المتقدمة',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: AppColor.surface,
|
||||
title: Text('الإدارة المالية المتقدمة',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: cs.onSurface)),
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
icon: Icon(Icons.refresh_rounded, color: cs.onSurface),
|
||||
onPressed: () => controller.fetchAllFinancials(),
|
||||
)
|
||||
],
|
||||
@@ -28,8 +29,8 @@ class FinancialV2Page extends StatelessWidget {
|
||||
body: GetBuilder<FinancialV2Controller>(
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColor.accent));
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: cs.primary));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
@@ -37,13 +38,13 @@ class FinancialV2Page extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMainFinancialStats(ctrl.stats),
|
||||
_buildMainFinancialStats(ctrl.stats, cs),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionTitle('طرق الدفع'),
|
||||
_buildPaymentMethodBreakdown(ctrl.stats),
|
||||
_buildSectionTitle('طرق الدفع', cs),
|
||||
_buildPaymentMethodBreakdown(ctrl.stats, cs),
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle('تسويات الكباتن (مستحقات معلقة)'),
|
||||
_buildSettlementsList(ctrl.settlements),
|
||||
_buildSectionTitle('تسويات الكباتن (مستحقات معلقة)', cs),
|
||||
_buildSettlementsList(ctrl.settlements, cs),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
@@ -53,13 +54,13 @@ class FinancialV2Page extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
Widget _buildSectionTitle(String title, ColorScheme cs) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: AppColor.textPrimary,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -67,14 +68,15 @@ class FinancialV2Page extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMainFinancialStats(Map<String, dynamic> stats) {
|
||||
Widget _buildMainFinancialStats(Map<String, dynamic> stats, ColorScheme cs) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildFinancialCard(
|
||||
'إجمالي عمولة المنصة',
|
||||
'${stats['total_platform_commission'] ?? 0} ج.م',
|
||||
Icons.account_balance_rounded,
|
||||
AppColor.accent,
|
||||
cs.primary,
|
||||
cs,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
@@ -84,7 +86,8 @@ class FinancialV2Page extends StatelessWidget {
|
||||
'إجمالي دخل الكباتن',
|
||||
'${stats['total_driver_pay'] ?? 0}',
|
||||
Icons.person_pin_rounded,
|
||||
AppColor.info,
|
||||
cs.tertiary,
|
||||
cs,
|
||||
isSmall: true,
|
||||
),
|
||||
),
|
||||
@@ -94,7 +97,8 @@ class FinancialV2Page extends StatelessWidget {
|
||||
'إجمالي الإيرادات',
|
||||
'${stats['total_revenue'] ?? 0}',
|
||||
Icons.payments_rounded,
|
||||
AppColor.success,
|
||||
const Color(0xFF10B981),
|
||||
cs,
|
||||
isSmall: true,
|
||||
),
|
||||
),
|
||||
@@ -105,18 +109,18 @@ class FinancialV2Page extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildFinancialCard(
|
||||
String title, String value, IconData icon, Color color,
|
||||
String title, String value, IconData icon, Color color, ColorScheme cs,
|
||||
{bool isSmall = false}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(isSmall ? 16 : 24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.2)),
|
||||
border: Border.all(color: color.withValues(alpha: 0.2)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColor.surface, color.withOpacity(0.05)],
|
||||
colors: [cs.surfaceContainerHighest, color.withValues(alpha: 0.05)],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -124,7 +128,7 @@ class FinancialV2Page extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(icon, color: color, size: isSmall ? 20 : 28),
|
||||
@@ -135,12 +139,12 @@ class FinancialV2Page extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 12)),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
color: AppColor.textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: isSmall ? 18 : 24,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
@@ -151,7 +155,7 @@ class FinancialV2Page extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentMethodBreakdown(Map<String, dynamic> stats) {
|
||||
Widget _buildPaymentMethodBreakdown(Map<String, dynamic> stats, ColorScheme cs) {
|
||||
double cash = double.tryParse(stats['cash_payments'].toString()) ?? 0;
|
||||
double digital = double.tryParse(stats['digital_payments'].toString()) ?? 0;
|
||||
double total = cash + digital;
|
||||
@@ -160,21 +164,21 @@ class FinancialV2Page extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildPaymentBar('نقدي (Cash)', cash, total, AppColor.warning),
|
||||
_buildPaymentBar('نقدي (Cash)', cash, total, cs.tertiary, cs),
|
||||
const SizedBox(height: 16),
|
||||
_buildPaymentBar('إلكتروني / محفظة', digital, total, AppColor.info),
|
||||
_buildPaymentBar('إلكتروني / محفظة', digital, total, cs.tertiary, cs),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentBar(
|
||||
String label, double value, double total, Color color) {
|
||||
String label, double value, double total, Color color, ColorScheme cs) {
|
||||
double percent = value / total;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -183,8 +187,7 @@ class FinancialV2Page extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style:
|
||||
const TextStyle(color: AppColor.textPrimary, fontSize: 13)),
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 13)),
|
||||
Text('${value.toStringAsFixed(0)} ج.م',
|
||||
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
@@ -194,7 +197,7 @@ class FinancialV2Page extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: percent,
|
||||
backgroundColor: AppColor.divider,
|
||||
backgroundColor: cs.outline,
|
||||
color: color,
|
||||
minHeight: 8,
|
||||
),
|
||||
@@ -203,9 +206,11 @@ class FinancialV2Page extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettlementsList(List<dynamic> settlements) {
|
||||
Widget _buildSettlementsList(List<dynamic> settlements, ColorScheme cs) {
|
||||
if (settlements.isEmpty) {
|
||||
return const Center(child: Text('لا توجد تسويات معلقة'));
|
||||
return Center(
|
||||
child: Text('لا توجد تسويات معلقة',
|
||||
style: TextStyle(color: cs.onSurfaceVariant)));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
@@ -218,9 +223,9 @@ class FinancialV2Page extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -229,36 +234,36 @@ class FinancialV2Page extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${s['first_name']} ${s['last_name']}',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textPrimary,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Text(s['phone'] ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 12)),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
Text('${s['total_rides']} رحلة مكتملة',
|
||||
style: const TextStyle(
|
||||
color: AppColor.info, fontSize: 11)),
|
||||
style: TextStyle(
|
||||
color: cs.tertiary, fontSize: 11)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Text('المستحقات',
|
||||
Text('المستحقات',
|
||||
style: TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 10)),
|
||||
color: cs.onSurfaceVariant, fontSize: 10)),
|
||||
Text('${s['total_earned']} ج.م',
|
||||
style: const TextStyle(
|
||||
color: AppColor.accent,
|
||||
style: TextStyle(
|
||||
color: cs.primary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.accent.withOpacity(0.1),
|
||||
foregroundColor: AppColor.accent,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
foregroundColor: cs.primary,
|
||||
elevation: 0,
|
||||
minimumSize: const Size(80, 32),
|
||||
shape: RoundedRectangleBorder(
|
||||
|
||||
@@ -15,16 +15,14 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
List<CircleMarker> _markers = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
// ─── فلاتر ───
|
||||
String _selectedSource = 'all'; // all | geofence | app_usage | silent_push
|
||||
String _selectedCountry = 'all'; // all | JO | SY | EG | IQ
|
||||
String _selectedSource = 'all';
|
||||
String _selectedCountry = 'all';
|
||||
int _daysFilter = 7;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
|
||||
// مراكز كل دولة للانتقال السريع
|
||||
final Map<String, LatLng> _countryCenters = {
|
||||
'all': const LatLng(31.9522, 35.9334), // عمان
|
||||
'all': const LatLng(31.9522, 35.9334),
|
||||
'JO': const LatLng(31.9522, 35.9334),
|
||||
'SY': const LatLng(33.5138, 36.2765),
|
||||
'EG': const LatLng(30.0444, 31.2357),
|
||||
@@ -60,18 +58,17 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
final lng = double.tryParse(point['longitude'].toString()) ?? 0.0;
|
||||
final source = point['source'].toString();
|
||||
|
||||
// ألوان حسب المصدر
|
||||
Color markerColor;
|
||||
switch (source) {
|
||||
case 'geofence':
|
||||
markerColor = Colors.green.withOpacity(0.65); // 🟢 دخل منطقة
|
||||
markerColor = Colors.green.withValues(alpha: 0.65);
|
||||
break;
|
||||
case 'silent_push':
|
||||
markerColor = Colors.orange.withOpacity(0.55); // 🟠 إيقاظ صامت
|
||||
markerColor = Colors.orange.withValues(alpha: 0.55);
|
||||
break;
|
||||
case 'app_usage':
|
||||
default:
|
||||
markerColor = Colors.blue.withOpacity(0.5); // 🔵 فتح عادي
|
||||
markerColor = Colors.blue.withValues(alpha: 0.5);
|
||||
}
|
||||
|
||||
return CircleMarker(
|
||||
@@ -93,22 +90,23 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
|
||||
void _applyFilter() {
|
||||
_fetchHeatmapData();
|
||||
// تحريك الخريطة لمركز الدولة المختارة
|
||||
final center = _countryCenters[_selectedCountry] ?? _countryCenters['all']!;
|
||||
_mapController.move(center, 11.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0F0F1A),
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'خريطة النشاط الحرارية',
|
||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
|
||||
),
|
||||
backgroundColor: const Color(0xFF1A1A2E),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: cs.surface,
|
||||
foregroundColor: cs.onSurface,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -120,14 +118,13 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// ─── شريط الفلاتر ───
|
||||
Container(
|
||||
color: const Color(0xFF1A1A2E),
|
||||
color: cs.surfaceContainerHighest,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// فلتر المصدر
|
||||
_buildFilterRow(
|
||||
cs: cs,
|
||||
label: 'المصدر:',
|
||||
options: const {
|
||||
'all': 'الكل',
|
||||
@@ -139,8 +136,8 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
onChanged: (v) => setState(() => _selectedSource = v),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// فلتر الدولة
|
||||
_buildFilterRow(
|
||||
cs: cs,
|
||||
label: 'الدولة:',
|
||||
options: const {
|
||||
'all': 'الكل',
|
||||
@@ -153,10 +150,9 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
onChanged: (v) => setState(() => _selectedCountry = v),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// فلتر الفترة الزمنية + زر تطبيق
|
||||
Row(
|
||||
children: [
|
||||
const Text('الفترة:', style: TextStyle(color: Colors.white54, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
Text('الفترة:', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 8),
|
||||
...[1, 7, 30].map((days) => Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
@@ -166,19 +162,19 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _daysFilter == days
|
||||
? const Color(0xFF6366F1)
|
||||
: const Color(0xFF2D2D42),
|
||||
? cs.primary
|
||||
: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: _daysFilter == days
|
||||
? const Color(0xFF818CF8)
|
||||
? cs.primary
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'$days يوم',
|
||||
style: TextStyle(
|
||||
color: _daysFilter == days ? Colors.white : Colors.white54,
|
||||
color: _daysFilter == days ? cs.onSurface : cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -192,8 +188,8 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
icon: const Icon(Icons.filter_alt_rounded, size: 14),
|
||||
label: const Text('تطبيق', style: TextStyle(fontSize: 11)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
@@ -204,18 +200,17 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// ─── الخريطة ───
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
_isLoading
|
||||
? const Center(
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(color: Color(0xFF6366F1)),
|
||||
SizedBox(height: 12),
|
||||
Text('جاري تحميل البيانات...', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
||||
CircularProgressIndicator(color: cs.primary),
|
||||
const SizedBox(height: 12),
|
||||
Text('جاري تحميل البيانات...', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -234,44 +229,42 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
],
|
||||
),
|
||||
|
||||
// ─── مفتاح الألوان ───
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A2E).withOpacity(0.9),
|
||||
color: cs.surfaceContainerHighest.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white12),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: const Column(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_LegendItem(color: Colors.green, label: 'دخل منطقة سياج'),
|
||||
SizedBox(height: 4),
|
||||
_LegendItem(color: Colors.blue, label: 'فتح عادي للتطبيق'),
|
||||
SizedBox(height: 4),
|
||||
_LegendItem(color: Colors.orange, label: 'إيقاظ صامت'),
|
||||
_LegendItem(color: Colors.green, label: 'دخل منطقة سياج', cs: cs),
|
||||
const SizedBox(height: 4),
|
||||
_LegendItem(color: Colors.blue, label: 'فتح عادي للتطبيق', cs: cs),
|
||||
const SizedBox(height: 4),
|
||||
_LegendItem(color: Colors.orange, label: 'إيقاظ صامت', cs: cs),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ─── عدد النقاط ───
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1A1A2E).withOpacity(0.9),
|
||||
color: cs.surfaceContainerHighest.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white12),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
'${_markers.length} نقطة',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -284,6 +277,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
}
|
||||
|
||||
Widget _buildFilterRow({
|
||||
required ColorScheme cs,
|
||||
required String label,
|
||||
required Map<String, String> options,
|
||||
required String selected,
|
||||
@@ -291,7 +285,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
@@ -306,16 +300,16 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF6366F1) : const Color(0xFF2D2D42),
|
||||
color: isSelected ? cs.primary : cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF818CF8) : Colors.transparent,
|
||||
color: isSelected ? cs.primary : Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
e.value,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : Colors.white54,
|
||||
color: isSelected ? cs.onSurface : cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -335,7 +329,8 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
||||
class _LegendItem extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
const _LegendItem({required this.color, required this.label});
|
||||
final ColorScheme cs;
|
||||
const _LegendItem({required this.color, required this.label, required this.cs});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -344,7 +339,7 @@ class _LegendItem extends StatelessWidget {
|
||||
children: [
|
||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 10)),
|
||||
Text(label, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
|
||||
class SocialIntelligenceController extends GetxController {
|
||||
@@ -42,36 +41,38 @@ class SocialIntelligenceScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final controller = Get.put(SocialIntelligenceController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'استخبارات السوشيال ميديا (Social Intelligence)',
|
||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
|
||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16, color: cs.onSurface),
|
||||
),
|
||||
backgroundColor: AppColor.bg,
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
foregroundColor: AppColor.textPrimary,
|
||||
foregroundColor: cs.onSurface,
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
icon: Icon(Icons.refresh, color: cs.onSurface),
|
||||
onPressed: () => controller.fetchReports(),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColor.accent));
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: cs.primary));
|
||||
}
|
||||
|
||||
if (controller.reports.isEmpty) {
|
||||
return const Center(
|
||||
return Center(
|
||||
child: Text(
|
||||
'لا توجد تقارير حالياً',
|
||||
style: TextStyle(color: AppColor.textSecondary),
|
||||
style: TextStyle(color: cs.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -84,45 +85,45 @@ class SocialIntelligenceScreen extends StatelessWidget {
|
||||
final isWeekly = report['is_weekly'].toString() == '1';
|
||||
|
||||
return Card(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: isWeekly ? AppColor.accent : AppColor.divider,
|
||||
color: isWeekly ? cs.primary : cs.outline,
|
||||
width: isWeekly ? 1.5 : 1.0),
|
||||
),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: ExpansionTile(
|
||||
iconColor: AppColor.accent,
|
||||
collapsedIconColor: AppColor.textSecondary,
|
||||
iconColor: cs.primary,
|
||||
collapsedIconColor: cs.onSurfaceVariant,
|
||||
title: Text(
|
||||
isWeekly
|
||||
? '📈 تقرير السوق الأسبوعي الشامل'
|
||||
: '📊 تقرير ${report['platform'].toString().toUpperCase()}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isWeekly ? AppColor.accent : AppColor.textPrimary,
|
||||
color: isWeekly ? cs.primary : cs.onSurface,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
report['created_at'].toString(),
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 11),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 11),
|
||||
),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: AppColor.divider)),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: cs.outline)),
|
||||
),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: HtmlWidget(
|
||||
report['report_html'] ?? '',
|
||||
textStyle: const TextStyle(
|
||||
color: AppColor.textPrimary,
|
||||
textStyle: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 13,
|
||||
height: 1.6),
|
||||
),
|
||||
|
||||
@@ -7,19 +7,6 @@ import 'package:siro_admin/views/widgets/snackbar.dart';
|
||||
|
||||
import '../../print.dart';
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// DESIGN TOKENS (same as AdminHomePage)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
const Color _bg = Color(0xFF0D1117);
|
||||
const Color _surface = Color(0xFF161B22);
|
||||
const Color _surfaceElevated = Color(0xFF1C2333);
|
||||
const Color _accent = Color(0xFF00D4AA);
|
||||
const Color _warning = Color(0xFFFFCB6B);
|
||||
const Color _info = Color(0xFF82AAFF);
|
||||
const Color _textPrimary = Color(0xFFE6EDF3);
|
||||
const Color _textSecondary = Color(0xFF7D8590);
|
||||
const Color _divider = Color(0xFF21262D);
|
||||
|
||||
class PackageUpdateScreen extends StatelessWidget {
|
||||
PackageUpdateScreen({super.key});
|
||||
|
||||
@@ -27,19 +14,20 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: _bg,
|
||||
appBar: _buildAppBar(),
|
||||
backgroundColor: cs.surface,
|
||||
appBar: _buildAppBar(cs),
|
||||
body: GetBuilder<PackageController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: _accent, strokeWidth: 2),
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: cs.primary, strokeWidth: 2),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.packages.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
return _buildEmptyState(cs);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
@@ -48,7 +36,7 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final package = controller.packages[index];
|
||||
return _buildPackageCard(context, package, controller);
|
||||
return _buildPackageCard(context, package, controller, cs);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -56,10 +44,9 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────── APP BAR ───────────────────────────
|
||||
PreferredSizeWidget _buildAppBar() {
|
||||
PreferredSizeWidget _buildAppBar(ColorScheme cs) {
|
||||
return AppBar(
|
||||
backgroundColor: _bg,
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
leading: GestureDetector(
|
||||
@@ -67,12 +54,12 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: _surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: _divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: _textSecondary, size: 16),
|
||||
child: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: cs.onSurfaceVariant, size: 16),
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
@@ -80,18 +67,18 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(7),
|
||||
decoration: BoxDecoration(
|
||||
color: _accent.withOpacity(0.12),
|
||||
color: cs.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
border: Border.all(color: _accent.withOpacity(0.25)),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: const Icon(Icons.system_update_rounded,
|
||||
color: _accent, size: 16),
|
||||
child: Icon(Icons.system_update_rounded,
|
||||
color: cs.primary, size: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
Text(
|
||||
'تحديث التطبيق',
|
||||
style: TextStyle(
|
||||
color: _textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -105,25 +92,24 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: _divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: const Icon(Icons.refresh_rounded,
|
||||
color: _textSecondary, size: 18),
|
||||
child: Icon(Icons.refresh_rounded,
|
||||
color: cs.onSurfaceVariant, size: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Container(height: 1, color: _divider),
|
||||
child: Container(height: 1, color: cs.outline),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────── PACKAGE CARD ───────────────────────────
|
||||
Widget _buildPackageCard(
|
||||
BuildContext context, dynamic package, PackageController controller) {
|
||||
BuildContext context, dynamic package, PackageController controller, ColorScheme cs) {
|
||||
final platform = package['platform']?.toString() ?? '';
|
||||
final isAndroid = platform.toLowerCase().contains('android');
|
||||
final isIOS = platform.toLowerCase().contains('ios');
|
||||
@@ -131,8 +117,8 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
final Color platformColor = isAndroid
|
||||
? const Color(0xFF4CAF50)
|
||||
: isIOS
|
||||
? _info
|
||||
: _warning;
|
||||
? cs.tertiary
|
||||
: cs.tertiary;
|
||||
final IconData platformIcon = isAndroid
|
||||
? Icons.android_rounded
|
||||
: isIOS
|
||||
@@ -142,20 +128,19 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showUpdateDialog(context, package, controller),
|
||||
onTap: () => _showUpdateDialog(context, package, controller, cs),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
splashColor: _accent.withOpacity(0.06),
|
||||
splashColor: cs.primary.withValues(alpha: 0.06),
|
||||
highlightColor: Colors.transparent,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Platform Icon
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
@@ -164,27 +149,24 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
platformColor.withOpacity(0.20),
|
||||
platformColor.withOpacity(0.06),
|
||||
platformColor.withValues(alpha: 0.20),
|
||||
platformColor.withValues(alpha: 0.06),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(13),
|
||||
border: Border.all(color: platformColor.withOpacity(0.25)),
|
||||
border: Border.all(color: platformColor.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(platformIcon, color: platformColor, size: 22),
|
||||
),
|
||||
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
package['appName']?.toString() ?? '—',
|
||||
style: const TextStyle(
|
||||
color: _textPrimary,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -195,31 +177,29 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
_buildTag(platform, platformColor),
|
||||
const SizedBox(width: 6),
|
||||
_buildVersionBadge(
|
||||
package['version']?.toString() ?? '?'),
|
||||
package['version']?.toString() ?? '?', cs),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Update button
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: _accent.withOpacity(0.10),
|
||||
color: cs.primary.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: _accent.withOpacity(0.25)),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.edit_rounded, color: _accent, size: 13),
|
||||
SizedBox(width: 5),
|
||||
children: [
|
||||
Icon(Icons.edit_rounded, color: cs.primary, size: 13),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'تعديل',
|
||||
style: TextStyle(
|
||||
color: _accent,
|
||||
color: cs.primary,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -238,7 +218,7 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.10),
|
||||
color: color.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
@@ -252,17 +232,17 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVersionBadge(String version) {
|
||||
Widget _buildVersionBadge(String version, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: _divider,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'v$version',
|
||||
style: const TextStyle(
|
||||
color: _textSecondary,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'monospace',
|
||||
@@ -271,8 +251,7 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────── EMPTY STATE ───────────────────────────
|
||||
Widget _buildEmptyState() {
|
||||
Widget _buildEmptyState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -280,30 +259,29 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: _surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: _divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: const Icon(Icons.inventory_2_outlined,
|
||||
color: _textSecondary, size: 32),
|
||||
child: Icon(Icons.inventory_2_outlined,
|
||||
color: cs.onSurfaceVariant, size: 32),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('لا توجد حزم متاحة',
|
||||
Text('لا توجد حزم متاحة',
|
||||
style: TextStyle(
|
||||
color: _textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 6),
|
||||
const Text('اسحب للأسفل لإعادة التحميل',
|
||||
style: TextStyle(color: _textSecondary, fontSize: 12)),
|
||||
Text('اسحب للأسفل لإعادة التحميل',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────── UPDATE DIALOG ───────────────────────────
|
||||
void _showUpdateDialog(
|
||||
BuildContext context, dynamic package, PackageController controller) {
|
||||
BuildContext context, dynamic package, PackageController controller, ColorScheme cs) {
|
||||
controller.versionController.clear();
|
||||
|
||||
Get.dialog(
|
||||
@@ -311,9 +289,9 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _surfaceElevated,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: _divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black54,
|
||||
@@ -327,107 +305,95 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: _accent.withOpacity(0.12),
|
||||
color: cs.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _accent.withOpacity(0.25)),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: const Icon(Icons.system_update_rounded,
|
||||
color: _accent, size: 20),
|
||||
child: Icon(Icons.system_update_rounded,
|
||||
color: cs.primary, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
'تحديث الإصدار',
|
||||
style: TextStyle(
|
||||
color: _textPrimary,
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
package['appName']?.toString() ?? '',
|
||||
style: const TextStyle(
|
||||
color: _textSecondary, fontSize: 11),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
Container(height: 1, color: _divider),
|
||||
Container(height: 1, color: cs.outline),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Current info
|
||||
Row(
|
||||
children: [
|
||||
_buildInfoChip(Icons.devices_rounded,
|
||||
package['platform']?.toString() ?? '', _info),
|
||||
package['platform']?.toString() ?? '', cs.tertiary),
|
||||
const SizedBox(width: 8),
|
||||
_buildInfoChip(Icons.tag_rounded,
|
||||
'الحالي: ${package['version']}', _warning),
|
||||
'الحالي: ${package['version']}', cs.tertiary),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Input label
|
||||
const Text(
|
||||
Text(
|
||||
'الإصدار الجديد',
|
||||
style: TextStyle(
|
||||
color: _textSecondary,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Text input
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _bg,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.versionController,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
style: const TextStyle(
|
||||
color: _textPrimary,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 15,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: package['version'].toString(),
|
||||
hintStyle: const TextStyle(
|
||||
color: _textSecondary,
|
||||
hintStyle: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
prefixIcon:
|
||||
const Icon(Icons.tag_rounded, color: _accent, size: 18),
|
||||
Icon(Icons.tag_rounded, color: cs.primary, size: 18),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 14, vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Actions
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -437,12 +403,12 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: _divider),
|
||||
side: BorderSide(color: cs.outline),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'إلغاء',
|
||||
style: TextStyle(color: _textSecondary, fontSize: 13),
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -450,11 +416,11 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Obx(() => ElevatedButton.icon(
|
||||
icon: controller.isLoading.value
|
||||
? const SizedBox(
|
||||
? SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
color: cs.onPrimary,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
@@ -464,8 +430,8 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _accent,
|
||||
foregroundColor: _bg,
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.surface,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -495,9 +461,9 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
color: color.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.2)),
|
||||
border: Border.all(color: color.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -518,9 +484,6 @@ class PackageUpdateScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CONTROLLER
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
class PackageController extends GetxController {
|
||||
List packages = [];
|
||||
var isLoading = false.obs;
|
||||
|
||||
@@ -2,14 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/passenger_admin_controller.dart';
|
||||
import '../../../main.dart'; // للوصول إلى box
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/snackbar.dart';
|
||||
import 'passenger_details_page.dart';
|
||||
|
||||
@@ -21,141 +15,36 @@ class Passengrs extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 1. منطق السوبر أدمن
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||
bool isSuperAdmin =
|
||||
myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Passengers Management'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
// استخدام Expanded أو Container بطول الشاشة لتجنب المشاكل
|
||||
SizedBox(
|
||||
height: Get.height, // تأمين مساحة العمل
|
||||
child: GetBuilder<PassengerAdminController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// --- قسم الإحصائيات والجوائز (Dashboard) ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildDashboardCard(context, controller),
|
||||
),
|
||||
|
||||
// --- عنوان القائمة ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"All Passengers".tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
"${controller.passengersData['message']?.length ?? 0} Users",
|
||||
style:
|
||||
TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// --- قائمة الركاب ---
|
||||
// استخدام Expanded هنا هو الحل الجذري لمكلة Overflow
|
||||
Expanded(
|
||||
child: _buildPassengersList(controller, isSuperAdmin),
|
||||
),
|
||||
|
||||
// مساحة سفلية صغيرة لضمان عدم التصاق القائمة بالحافة
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- تصميم بطاقة الإحصائيات (Dashboard) ---
|
||||
Widget _buildDashboardCard(
|
||||
BuildContext context, PassengerAdminController controller) {
|
||||
// جلب العدد بأمان
|
||||
final String countValue = (controller.passengersData['message'] != null &&
|
||||
controller.passengersData['message'].isNotEmpty)
|
||||
? controller.passengersData['message'][0]['countPassenger']
|
||||
?.toString() ??
|
||||
'0'
|
||||
: '0';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.groups_rounded,
|
||||
color: AppColor.primaryColor, size: 30),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Total Passengers'.tr,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
Text(
|
||||
countValue,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 45,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.card_giftcard,
|
||||
color: Colors.white, size: 20),
|
||||
label: Text('Add Prize to Gold Passengers'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.yellowColor, // لون ذهبي
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () {
|
||||
_showAddPrizeDialog(controller);
|
||||
_buildAppBar(context, cs),
|
||||
Expanded(
|
||||
child: GetBuilder<PassengerAdminController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return _buildLoadingState(cs);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildDashboardCard(context, controller, cs),
|
||||
_buildListHeader(context, controller, cs),
|
||||
Expanded(
|
||||
child: _buildPassengersList(
|
||||
controller, isSuperAdmin, cs),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -164,20 +53,252 @@ class Passengrs extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// --- بناء قائمة الركاب ---
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
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.groups_rounded, color: cs.primary, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'إدارة الركاب',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('جاري تحميل الركاب...',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDashboardCard(
|
||||
BuildContext context,
|
||||
PassengerAdminController controller,
|
||||
ColorScheme cs) {
|
||||
final String countValue =
|
||||
(controller.passengersData['message'] != null &&
|
||||
controller.passengersData['message'].isNotEmpty)
|
||||
? controller.passengersData['message'][0]['countPassenger']
|
||||
?.toString() ??
|
||||
'0'
|
||||
: '0';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.primary.withValues(alpha: 0.06),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
cs.primary.withValues(alpha: 0.20),
|
||||
cs.primary.withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: cs.primary.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.groups_rounded,
|
||||
color: cs.primary, size: 28),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'إجمالي الركاب',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
countValue,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.card_giftcard,
|
||||
color: Colors.white, size: 18),
|
||||
label: Text('إضافة جائزة لركاب الذهب',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF59E0B),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () {
|
||||
_showAddPrizeDialog(controller, cs);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListHeader(
|
||||
BuildContext context,
|
||||
PassengerAdminController controller,
|
||||
ColorScheme cs) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||
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.onSurface,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Container(height: 1, color: cs.outline),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: cs.primary.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Text(
|
||||
'${controller.passengersData['message']?.length ?? 0}',
|
||||
style: TextStyle(
|
||||
color: cs.primary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPassengersList(
|
||||
PassengerAdminController controller, bool isSuperAdmin) {
|
||||
final List<dynamic> passengers = controller.passengersData['message'] ?? [];
|
||||
PassengerAdminController controller,
|
||||
bool isSuperAdmin,
|
||||
ColorScheme cs) {
|
||||
final List<dynamic> passengers =
|
||||
controller.passengersData['message'] ?? [];
|
||||
|
||||
if (passengers.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.person_off_outlined, size: 60, color: Colors.grey[300]),
|
||||
const SizedBox(height: 10),
|
||||
Text("No passengers found".tr,
|
||||
style: TextStyle(color: Colors.grey[400])),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.person_off_outlined,
|
||||
size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا يوجد ركاب',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -187,132 +308,171 @@ class Passengrs extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: passengers.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final user = passengers[index];
|
||||
return _buildPassengerItem(user, isSuperAdmin);
|
||||
return _buildPassengerItem(user, isSuperAdmin, cs);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- عنصر الراكب الواحد (Card) ---
|
||||
Widget _buildPassengerItem(dynamic user, bool isSuperAdmin) {
|
||||
Widget _buildPassengerItem(
|
||||
dynamic user, bool isSuperAdmin, ColorScheme cs) {
|
||||
String firstName = user['first_name'] ?? '';
|
||||
String lastName = user['last_name'] ?? '';
|
||||
String fullName = '$firstName $lastName'.trim();
|
||||
if (fullName.isEmpty) fullName = 'Unknown User';
|
||||
if (fullName.isEmpty) fullName = 'مستخدم';
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
// الانتقال للتفاصيل مع تمرير صلاحية الأدمن
|
||||
Get.to(
|
||||
() => const PassengerDetailsPage(),
|
||||
arguments: {'data': user, 'isSuperAdmin': isSuperAdmin},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
// Avatar
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
child: Text(
|
||||
fullName.isNotEmpty ? fullName[0].toUpperCase() : 'U',
|
||||
style: TextStyle(
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
cs.primary.withValues(alpha: 0.20),
|
||||
cs.primary.withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: cs.primary.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
fullName.isNotEmpty
|
||||
? fullName[0].toUpperCase()
|
||||
: 'U',
|
||||
style: TextStyle(
|
||||
color: cs.primary,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
|
||||
// Info
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
fullName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Stats Row
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.star_rounded,
|
||||
size: 16, color: Colors.amber[700]),
|
||||
Text(
|
||||
" ${user['ratingPassenger'] ?? '0.0'} ",
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF59E0B)
|
||||
.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.star_rounded,
|
||||
size: 12, color: Color(0xFFF59E0B)),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
user['ratingPassenger']?.toString() ??
|
||||
'0.0',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFFF59E0B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.directions_car,
|
||||
size: 14, color: Colors.grey[400]),
|
||||
Text(
|
||||
" ${user['countPassengerRide'] ?? '0'} Trips",
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey[600]),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.directions_car_rounded,
|
||||
size: 12, color: cs.primary),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
'${user['countPassengerRide'] ?? '0'} رحلة',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: cs.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
// Phone Number (Masked logic)
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.phone_iphone,
|
||||
size: 12, color: Colors.grey[400]),
|
||||
Icon(Icons.phone_iphone_rounded,
|
||||
size: 12, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
_formatPhoneNumber(
|
||||
user['phone'].toString(), isSuperAdmin),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
fontFamily: 'monospace'),
|
||||
fontSize: 11,
|
||||
color: cs.onSurfaceVariant,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Email (Show only if Super Admin)
|
||||
if (isSuperAdmin && user['email'] != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
user['email'],
|
||||
style:
|
||||
TextStyle(fontSize: 10, color: Colors.grey[400]),
|
||||
style: TextStyle(
|
||||
fontSize: 10, color: cs.onSurfaceVariant),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
]
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Arrow
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 16, color: Colors.grey[300]),
|
||||
size: 14, color: cs.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -321,63 +481,89 @@ class Passengrs extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// --- دالة تنسيق الرقم (إظهار آخر 4 أرقام لغير الأدمن) ---
|
||||
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
|
||||
if (isSuperAdmin) return phone; // إظهار الرقم كاملاً للسوبر أدمن
|
||||
|
||||
// لغير الأدمن
|
||||
if (isSuperAdmin) return phone;
|
||||
if (phone.length <= 4) return phone;
|
||||
String lastFour = phone.substring(phone.length - 4);
|
||||
String masked = '*' * (phone.length - 4);
|
||||
return '$masked$lastFour'; // النتيجة: *******5678
|
||||
return '$masked$lastFour';
|
||||
}
|
||||
|
||||
// --- دالة إضافة الجوائز ---
|
||||
void _showAddPrizeDialog(PassengerAdminController controller) {
|
||||
// التحقق من يوم السبت
|
||||
void _showAddPrizeDialog(
|
||||
PassengerAdminController controller, ColorScheme cs) {
|
||||
if (DateTime.now().weekday == DateTime.saturday) {
|
||||
Get.defaultDialog(
|
||||
title: 'Add Prize'.tr,
|
||||
titleStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
title: 'إضافة جائزة',
|
||||
titleStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
color: cs.onSurface),
|
||||
backgroundColor: cs.surface,
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
content: Form(
|
||||
key: controller.formPrizeKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Add Points to Gold Passengers wallet'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
'إضافة نقاط لمحفظة ركاب الذهب',
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: controller.passengerPrizeController,
|
||||
keyboardType: TextInputType.number,
|
||||
style: TextStyle(color: cs.onSurface),
|
||||
cursorColor: cs.primary,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'مبلغ الجائزة',
|
||||
hintText: '1000...',
|
||||
filled: true,
|
||||
fillColor: cs.surfaceContainerHighest,
|
||||
labelStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: cs.outline),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: cs.primary, width: 1.5),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 14),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
MyTextForm(
|
||||
controller: controller.passengerPrizeController,
|
||||
label: 'Prize Amount'.tr,
|
||||
hint: '1000...',
|
||||
type: TextInputType.number,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: SizedBox(
|
||||
width: 120,
|
||||
child: MyElevatedButton(
|
||||
title: 'Add',
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (controller.formPrizeKey.currentState!.validate()) {
|
||||
controller.addPassengerPrizeToWalletSecure();
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
child: const Text('إضافة',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child:
|
||||
Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('إلغاء',
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
mySnackbarWarning('Prizes can only be added on Saturdays.'.tr);
|
||||
mySnackbarWarning('يمكن إضافة الجوائز يوم السبت فقط');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/kazan_controller.dart';
|
||||
import '../../../views/widgets/snackbar.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
class KazanEditorPage extends StatelessWidget {
|
||||
@@ -14,63 +12,72 @@ class KazanEditorPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'تعديل أسعار كازان'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Column(
|
||||
children: [
|
||||
_buildCountryDropdown(),
|
||||
Expanded(
|
||||
child: Obx(() => controller.isLoading.value && controller.kazanData.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionHeader('⚙️ الإعدادات العامة'),
|
||||
_buildGeneralSettings(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('🚗 أسعار الكيلومتر لكل نوع سيارة'),
|
||||
_buildKmPricesGrid(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionHeader('⏱️ أسعار الدقيقة (حسب وقت اليوم)'),
|
||||
_buildMinutePrices(),
|
||||
const SizedBox(height: 32),
|
||||
MyElevatedButton(
|
||||
title: '💾 حفظ جميع التعديلات',
|
||||
icon: Icons.save_rounded,
|
||||
onPressed: () => _handleSave(),
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
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),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountryDropdown() {
|
||||
Widget _buildCountryDropdown(ColorScheme cs) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Obx(() => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: controller.selectedCountry.value,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.keyboard_arrow_down_rounded),
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
style: GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16, color: cs.onSurface),
|
||||
items: controller.countries.map((c) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: c['name'],
|
||||
@@ -92,13 +99,14 @@ class KazanEditorPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 1. الإعدادات العامة
|
||||
// ====================================================================
|
||||
Widget _buildGeneralSettings() {
|
||||
Widget _buildGeneralSettings(ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: AppStyle.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSliderItem(
|
||||
@@ -106,31 +114,30 @@ class KazanEditorPage extends StatelessWidget {
|
||||
'kazanPercent',
|
||||
'النسبة المئوية التي تقتطعها المنصة من كل رحلة',
|
||||
Icons.percent_rounded,
|
||||
cs,
|
||||
),
|
||||
const Divider(height: 32, color: AppColor.divider),
|
||||
Divider(height: 32, color: cs.outline),
|
||||
_buildPriceInputRow(
|
||||
'سعر الوقود المرجعي',
|
||||
'fuelPrice',
|
||||
'السعر المستخدم في حسابات تعويض الوقود',
|
||||
Icons.local_gas_station_rounded,
|
||||
cs,
|
||||
),
|
||||
const Divider(height: 32, color: AppColor.divider),
|
||||
Divider(height: 32, color: cs.outline),
|
||||
_buildPriceInputRow(
|
||||
'رمز العملة',
|
||||
'currency',
|
||||
'مثال: SYP, EGP, JOD, IQD',
|
||||
Icons.currency_exchange_rounded,
|
||||
cs,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 2. أسعار الكيلومتر - كل نوع سيارة له حقل خاص به
|
||||
// ====================================================================
|
||||
Widget _buildKmPricesGrid() {
|
||||
// 🆕 9 أنواع سيارات - كل واحد له عمود سعر مستقل
|
||||
Widget _buildKmPricesGrid(ColorScheme cs) {
|
||||
final Map<String, Map<String, dynamic>> priceFields = {
|
||||
'speedPrice': {
|
||||
'label': 'Speed ⚡',
|
||||
@@ -180,13 +187,17 @@ class KazanEditorPage extends StatelessWidget {
|
||||
};
|
||||
|
||||
return Container(
|
||||
decoration: AppStyle.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3, // 3 أعمدة بدلاً من 2
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1.8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
@@ -196,14 +207,14 @@ class KazanEditorPage extends StatelessWidget {
|
||||
String key = priceFields.keys.elementAt(index);
|
||||
var field = priceFields[key]!;
|
||||
return _buildKmPriceCard(
|
||||
key, field['label'], field['icon'], field['color']);
|
||||
key, field['label'], field['icon'], field['color'], cs);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKmPriceCard(
|
||||
String key, String label, IconData icon, Color color) {
|
||||
String key, String label, IconData icon, Color color, ColorScheme cs) {
|
||||
final TextEditingController textController =
|
||||
TextEditingController(text: _getValue(key));
|
||||
|
||||
@@ -225,8 +236,7 @@ class KazanEditorPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: AppStyle.caption
|
||||
.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.bold, color: cs.onSurfaceVariant),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -240,7 +250,7 @@ class KazanEditorPage extends StatelessWidget {
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.number.copyWith(fontSize: 13, color: color),
|
||||
style: GoogleFonts.jetBrainsMono(fontSize: 13, fontWeight: FontWeight.bold, color: color),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
@@ -256,10 +266,7 @@ class KazanEditorPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 3. أسعار الدقيقة (حسب وقت اليوم)
|
||||
// ====================================================================
|
||||
Widget _buildMinutePrices() {
|
||||
Widget _buildMinutePrices(ColorScheme cs) {
|
||||
final Map<String, Map<String, dynamic>> minuteFields = {
|
||||
'normalMinPrice': {
|
||||
'label': 'Normal (سعر الدقيقة العادي)',
|
||||
@@ -282,7 +289,11 @@ class KazanEditorPage extends StatelessWidget {
|
||||
};
|
||||
|
||||
return Container(
|
||||
decoration: AppStyle.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: minuteFields.entries.map((entry) {
|
||||
@@ -293,6 +304,7 @@ class KazanEditorPage extends StatelessWidget {
|
||||
entry.key,
|
||||
entry.value['desc'],
|
||||
entry.value['icon'],
|
||||
cs,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -300,15 +312,12 @@ class KazanEditorPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// دوال مساعدة للـ UI
|
||||
// ====================================================================
|
||||
Widget _buildSectionHeader(String title) {
|
||||
Widget _buildSectionHeader(String title, ColorScheme cs) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, left: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: AppStyle.title.copyWith(color: AppColor.accent),
|
||||
style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -322,21 +331,21 @@ class KazanEditorPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildPriceInputRow(
|
||||
String title, String key, String desc, IconData icon) {
|
||||
String title, String key, String desc, IconData icon, ColorScheme cs) {
|
||||
final TextEditingController textController =
|
||||
TextEditingController(text: _getValue(key));
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: AppColor.accent),
|
||||
Icon(icon, size: 20, color: cs.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: AppStyle.body.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(desc, style: AppStyle.caption.copyWith(fontSize: 10)),
|
||||
style: GoogleFonts.inter(fontSize: 14, color: cs.onSurface, fontWeight: FontWeight.bold)),
|
||||
Text(desc, style: GoogleFonts.inter(fontSize: 10, color: cs.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -344,16 +353,15 @@ class KazanEditorPage extends StatelessWidget {
|
||||
width: 100,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surfaceElevated,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
AppStyle.number.copyWith(fontSize: 16, color: AppColor.accent),
|
||||
style: GoogleFonts.jetBrainsMono(fontSize: 16, fontWeight: FontWeight.bold, color: cs.primary),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
@@ -367,7 +375,7 @@ class KazanEditorPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildSliderItem(
|
||||
String title, String key, String desc, IconData icon) {
|
||||
String title, String key, String desc, IconData icon, ColorScheme cs) {
|
||||
double value = double.tryParse(_getValue(key)) ?? 0;
|
||||
|
||||
return Column(
|
||||
@@ -378,25 +386,25 @@ class KazanEditorPage extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: AppColor.accent),
|
||||
Icon(icon, size: 18, color: cs.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(title, style: AppStyle.title),
|
||||
Text(title, style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface)),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'${value.toInt()}%',
|
||||
style: AppStyle.number.copyWith(fontSize: 18),
|
||||
style: GoogleFonts.jetBrainsMono(fontSize: 18, fontWeight: FontWeight.bold, color: cs.onSurface),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(desc, style: AppStyle.caption),
|
||||
Text(desc, style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
Slider(
|
||||
value: value.clamp(0, 100),
|
||||
min: 0,
|
||||
max: 100,
|
||||
activeColor: AppColor.accent,
|
||||
inactiveColor: AppColor.divider,
|
||||
activeColor: cs.primary,
|
||||
inactiveColor: cs.outline,
|
||||
onChanged: (val) {
|
||||
_setValue(key, val.toInt().toString());
|
||||
},
|
||||
@@ -405,14 +413,10 @@ class KazanEditorPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// حفظ البيانات
|
||||
// ====================================================================
|
||||
void _handleSave() async {
|
||||
final data = Map<String, dynamic>.from(controller.kazanData);
|
||||
data['adminId'] = 'admin1';
|
||||
|
||||
// التأكد من وجود country
|
||||
if (!data.containsKey('country') ||
|
||||
data['country'] == null ||
|
||||
data['country'].toString().isEmpty) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/promo_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import '../../widgets/mydialoug.dart';
|
||||
@@ -15,89 +13,110 @@ class PromoManagementPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'إدارة أكواد الخصم'.tr,
|
||||
isleading: true,
|
||||
action: IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline_rounded, color: AppColor.accent),
|
||||
onPressed: () => _showPromoSheet(context),
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
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),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.add_circle_outline_rounded, color: cs.primary),
|
||||
onPressed: () => _showPromoSheet(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: [
|
||||
Obx(() => controller.isLoading.value && controller.promoList.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: controller.promoList.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.confirmation_number_outlined, size: 64, color: AppColor.textMuted),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا يوجد أكواد خصم حالياً', style: AppStyle.subtitle),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () => controller.getPromos(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||
itemCount: controller.promoList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final promo = controller.promoList[index];
|
||||
return _buildPromoCard(context, promo);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
body: Obx(() => controller.isLoading.value && controller.promoList.isEmpty
|
||||
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||
: controller.promoList.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: cs.onSurfaceVariant.withValues(alpha: 0.1)),
|
||||
child: Icon(Icons.confirmation_number_outlined, size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا يوجد أكواد خصم حالياً', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () => controller.getPromos(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
|
||||
itemCount: controller.promoList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final promo = controller.promoList[index];
|
||||
return _buildPromoCard(context, promo);
|
||||
},
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPromoCard(BuildContext context, dynamic promo) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: AppStyle.cardDecoration,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.accentSoft,
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(Icons.local_offer_rounded, color: AppColor.accent),
|
||||
child: Icon(Icons.local_offer_rounded, color: cs.primary),
|
||||
),
|
||||
title: Text(
|
||||
promo['promo_code']?.toString() ?? 'N/A',
|
||||
style: AppStyle.title,
|
||||
style: GoogleFonts.inter(fontWeight: FontWeight.w600, color: cs.onSurface),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(promo['description']?.toString() ?? '', style: AppStyle.caption),
|
||||
Text(promo['description']?.toString() ?? '', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.money_rounded, size: 14, color: AppColor.success),
|
||||
Icon(Icons.money_rounded, size: 14, color: const Color(0xFF10B981)),
|
||||
const SizedBox(width: 4),
|
||||
Text('% ${promo['amount']}', style: AppStyle.number.copyWith(color: AppColor.success)),
|
||||
Text('% ${promo['amount']}', style: GoogleFonts.jetBrainsMono(fontWeight: FontWeight.bold, color: const Color(0xFF10B981))),
|
||||
const SizedBox(width: 12),
|
||||
Icon(Icons.person_rounded, size: 14, color: AppColor.info),
|
||||
Icon(Icons.person_rounded, size: 14, color: cs.tertiary),
|
||||
const SizedBox(width: 4),
|
||||
Text(promo['passengerID'] == 'none' ? 'عام' : 'مخصص', style: AppStyle.caption),
|
||||
Text(promo['passengerID'] == 'none' ? 'عام' : 'مخصص', style: GoogleFonts.inter(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert_rounded, color: AppColor.textSecondary),
|
||||
color: AppColor.surfaceElevated,
|
||||
icon: Icon(Icons.more_vert_rounded, color: cs.onSurfaceVariant),
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit_rounded, size: 18, color: AppColor.info),
|
||||
Icon(Icons.edit_rounded, size: 18, color: cs.tertiary),
|
||||
const SizedBox(width: 8),
|
||||
Text('تعديل'.tr),
|
||||
],
|
||||
@@ -107,9 +126,9 @@ class PromoManagementPage extends StatelessWidget {
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete_outline_rounded, size: 18, color: AppColor.danger),
|
||||
Icon(Icons.delete_outline_rounded, size: 18, color: cs.error),
|
||||
const SizedBox(width: 8),
|
||||
Text('حذف'.tr, style: const TextStyle(color: AppColor.danger)),
|
||||
Text('حذف'.tr, style: TextStyle(color: cs.error)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -131,6 +150,7 @@ class PromoManagementPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _showPromoSheet(BuildContext context, {dynamic promo}) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final TextEditingController codeController = TextEditingController(text: promo?['promo_code']);
|
||||
final TextEditingController amountController = TextEditingController(text: promo?['amount']?.toString());
|
||||
final TextEditingController descController = TextEditingController(text: promo?['description']);
|
||||
@@ -141,9 +161,9 @@ class PromoManagementPage extends StatelessWidget {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColor.surfaceElevated,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
@@ -152,10 +172,10 @@ class PromoManagementPage extends StatelessWidget {
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(color: AppColor.divider, borderRadius: BorderRadius.circular(2)),
|
||||
decoration: BoxDecoration(color: cs.outline, borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(promo == null ? 'إضافة كود خصم جديد' : 'تعديل كود الخصم', style: AppStyle.headTitle),
|
||||
Text(promo == null ? 'إضافة كود خصم جديد' : 'تعديل كود الخصم', style: GoogleFonts.cairo(fontWeight: FontWeight.bold, color: cs.onSurface, fontSize: 18)),
|
||||
const SizedBox(height: 24),
|
||||
MyTextForm(
|
||||
controller: codeController,
|
||||
|
||||
@@ -7,143 +7,324 @@ class BlacklistPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
Get.put(QualityController()).fetchBlacklist();
|
||||
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('إدارة القائمة السوداء (Blacklist)',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: Colors.red[800],
|
||||
bottom: const TabBar(
|
||||
indicatorColor: Colors.white,
|
||||
tabs: [
|
||||
Tab(icon: Icon(Icons.drive_eta), text: 'السائقين المحظورين'),
|
||||
Tab(icon: Icon(Icons.person), text: 'الركاب المحظورين'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: GetBuilder<QualityController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
_buildTabBar(context, cs),
|
||||
Expanded(
|
||||
child: GetBuilder<QualityController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return _buildLoadingState(cs);
|
||||
}
|
||||
|
||||
return TabBarView(
|
||||
children: [
|
||||
_buildDriverList(controller),
|
||||
_buildPassengerList(controller),
|
||||
],
|
||||
);
|
||||
},
|
||||
return TabBarView(
|
||||
controller: DefaultTabController.of(context),
|
||||
children: [
|
||||
_buildDriverList(controller, cs),
|
||||
_buildPassengerList(controller, cs),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
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.error.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.error.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.block_rounded, color: cs.error, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'القائمة السوداء',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: DefaultTabController.of(context),
|
||||
indicator: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
dividerColor: Colors.transparent,
|
||||
labelColor: cs.primary,
|
||||
unselectedLabelColor: cs.onSurfaceVariant,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 13),
|
||||
padding: const EdgeInsets.all(4),
|
||||
labelPadding: EdgeInsets.zero,
|
||||
tabs: const [
|
||||
Tab(text: 'السائقين', icon: Icon(Icons.drive_eta_rounded, size: 16)),
|
||||
Tab(text: 'الركاب', icon: Icon(Icons.person_rounded, size: 16)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('جاري التحميل...',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverList(QualityController controller, ColorScheme cs) {
|
||||
if (controller.driversBlacklist.isEmpty) {
|
||||
return _buildEmptyState('لا يوجد سائقين محظورين', cs);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 80),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: controller.driversBlacklist.length,
|
||||
itemBuilder: (context, index) {
|
||||
var driver = controller.driversBlacklist[index];
|
||||
return _buildBlacklistItem(
|
||||
phone: driver['phone'],
|
||||
reason: driver['reason'] ?? 'غير محدد',
|
||||
date: driver['created_at'],
|
||||
type: 'سائق',
|
||||
icon: Icons.drive_eta_rounded,
|
||||
onUnblock: () => controller.unblockDriver(driver['phone'].toString()),
|
||||
cs: cs,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPassengerList(QualityController controller, ColorScheme cs) {
|
||||
if (controller.passengersBlacklist.isEmpty) {
|
||||
return _buildEmptyState('لا يوجد ركاب محظورين', cs);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 80),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: controller.passengersBlacklist.length,
|
||||
itemBuilder: (context, index) {
|
||||
var passenger = controller.passengersBlacklist[index];
|
||||
return _buildBlacklistItem(
|
||||
phone: passenger['phone'] ?? passenger['phone_normalized'],
|
||||
reason: passenger['reason'] ?? 'غير محدد',
|
||||
date: passenger['created_at'],
|
||||
type: 'راكب',
|
||||
icon: Icons.person_rounded,
|
||||
onUnblock: () => controller.unblockPassenger(
|
||||
passenger['phone_normalized'].toString()),
|
||||
cs: cs,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(String message, ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.block_rounded, size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(message,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBlacklistItem({
|
||||
required String phone,
|
||||
required String reason,
|
||||
required String date,
|
||||
required String type,
|
||||
required IconData icon,
|
||||
required VoidCallback onUnblock,
|
||||
required ColorScheme cs,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.error.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.error.withValues(alpha: 0.12),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: cs.error.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Icon(icon, color: cs.error, size: 22),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
phone,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 15,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.error.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
type,
|
||||
style: TextStyle(
|
||||
color: cs.error,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'السبب: $reason',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () => _showUnblockDialog(Get.context!, type, phone, onUnblock, cs),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFF10B981).withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Icon(Icons.settings_backup_restore_rounded,
|
||||
color: const Color(0xFF10B981), size: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverList(QualityController controller) {
|
||||
if (controller.driversBlacklist.isEmpty) {
|
||||
return const Center(child: Text('لا يوجد سائقين محظورين حالياً'));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.driversBlacklist.length,
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemBuilder: (context, index) {
|
||||
var driver = controller.driversBlacklist[index];
|
||||
return Card(
|
||||
elevation: 3,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.redAccent,
|
||||
child: Icon(Icons.block, color: Colors.white),
|
||||
),
|
||||
title: Text('هاتف: ${driver['phone']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('السبب: ${driver['reason'] ?? "غير محدد"}'),
|
||||
Text('تاريخ الحظر: ${driver['created_at']}'),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.settings_backup_restore,
|
||||
color: Colors.green),
|
||||
onPressed: () {
|
||||
_showUnblockDialog(
|
||||
Get.context!,
|
||||
'سائق',
|
||||
driver['phone'],
|
||||
() => controller.unblockDriver(driver['phone'].toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPassengerList(QualityController controller) {
|
||||
if (controller.passengersBlacklist.isEmpty) {
|
||||
return const Center(child: Text('لا يوجد ركاب محظورين حالياً'));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.passengersBlacklist.length,
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemBuilder: (context, index) {
|
||||
var passenger = controller.passengersBlacklist[index];
|
||||
return Card(
|
||||
elevation: 3,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.orangeAccent,
|
||||
child: Icon(Icons.person_off, color: Colors.white),
|
||||
),
|
||||
title: Text(
|
||||
'هاتف: ${passenger['phone'] ?? passenger['phone_normalized']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('السبب: ${passenger['reason'] ?? "غير محدد"}'),
|
||||
Text('تاريخ الحظر: ${passenger['created_at']}'),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.settings_backup_restore,
|
||||
color: Colors.green),
|
||||
onPressed: () {
|
||||
_showUnblockDialog(
|
||||
Get.context!,
|
||||
'راكب',
|
||||
passenger['phone_normalized'],
|
||||
() => controller.unblockPassenger(
|
||||
passenger['phone_normalized'].toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showUnblockDialog(BuildContext context, String type, String identifier,
|
||||
VoidCallback onConfirm) {
|
||||
void _showUnblockDialog(
|
||||
BuildContext context, String type, String identifier, VoidCallback onConfirm, ColorScheme cs) {
|
||||
Get.defaultDialog(
|
||||
title: "تأكيد فك الحظر",
|
||||
titleStyle: TextStyle(color: cs.onSurface, fontWeight: FontWeight.bold),
|
||||
backgroundColor: cs.surface,
|
||||
middleText:
|
||||
"هل أنت متأكد من فك الحظر عن هذا ال$type ($identifier)؟\nسيتم تسجيل هذه العملية في الـ Audit Log.",
|
||||
textConfirm: "نعم، فك الحظر",
|
||||
textCancel: "تراجع",
|
||||
confirmTextColor: Colors.white,
|
||||
buttonColor: Colors.green,
|
||||
buttonColor: const Color(0xFF10B981),
|
||||
cancelTextColor: cs.onSurfaceVariant,
|
||||
onConfirm: () {
|
||||
Get.back();
|
||||
onConfirm();
|
||||
|
||||
@@ -1,236 +1,435 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/ride_admin_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
|
||||
class Rides extends StatelessWidget {
|
||||
Rides({super.key});
|
||||
final RideAdminController rideAdminController = Get.put(RideAdminController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(title: 'Rides'.tr, isleading: true, body: [
|
||||
GetBuilder<RideAdminController>(
|
||||
builder: (rideAdminController) => rideAdminController.isLoading
|
||||
? const Center(child: MyCircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: Get.height * .4,
|
||||
child: LineChart(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.ease,
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: rideAdminController.chartData,
|
||||
isCurved: true,
|
||||
color: Colors.deepPurpleAccent, // Custom color
|
||||
barWidth: 3, // Thinner line
|
||||
dotData: const FlDotData(
|
||||
show: true), // Show dots on each point
|
||||
belowBarData: BarAreaData(
|
||||
// Add gradient fill below the line
|
||||
show: true,
|
||||
color: AppColor.deepPurpleAccent,
|
||||
),
|
||||
isStrokeJoinRound: true,
|
||||
shadow: const BoxShadow(
|
||||
color: AppColor.yellowColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(2, 2),
|
||||
),
|
||||
),
|
||||
],
|
||||
showingTooltipIndicators: const [],
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
topTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'Days',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
axisNameSize: 30,
|
||||
sideTitles: const SideTitles(
|
||||
reservedSize: 30, showTitles: true)),
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'Total Trips on month'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
axisNameSize: 30,
|
||||
sideTitles: const SideTitles(
|
||||
reservedSize: 30, showTitles: true)),
|
||||
leftTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'Counts of Trips on month'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
axisNameSize: 30,
|
||||
sideTitles: const SideTitles(
|
||||
reservedSize: 30, showTitles: true)),
|
||||
),
|
||||
gridData: const FlGridData(
|
||||
show: true,
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: AppColor.accentColor),
|
||||
left: BorderSide(color: AppColor.accentColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// SizedBox(
|
||||
// height: Get.height * .4,
|
||||
// child: PieChart(
|
||||
// PieChartData(
|
||||
// sectionsSpace: 4, // Adjust spacing between sections
|
||||
// centerSpaceRadius:
|
||||
// 40, // Adjust radius of center space
|
||||
// sections: [
|
||||
// for (final rideData in rideAdminController.rideData)
|
||||
// PieChartSectionData(
|
||||
// value: rideData.ridesCount.toDouble(),
|
||||
// title: '${rideData.day}', showTitle: true,
|
||||
// titleStyle:
|
||||
// AppStyle.subtitle, // Display day as title
|
||||
// radius: 60, // Adjust radius of each section
|
||||
// color:
|
||||
// AppColor.deepPurpleAccent, // Custom color
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
// SizedBox(
|
||||
// // height: 400,
|
||||
// child: SfCartesianChart(
|
||||
// legend: const Legend(
|
||||
// isVisible: true,
|
||||
// position: LegendPosition.bottom,
|
||||
// overflowMode: LegendItemOverflowMode.wrap,
|
||||
// textStyle: TextStyle(
|
||||
// color: Colors.white,
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// borderWidth: 2,
|
||||
// borderColor: AppColor.blueColor,
|
||||
// plotAreaBorderColor: AppColor.deepPurpleAccent,
|
||||
// enableAxisAnimation: true,
|
||||
// primaryXAxis: CategoryAxis(
|
||||
// borderColor: AppColor.accentColor, borderWidth: 2,
|
||||
// title: AxisTitle(
|
||||
// text: 'Total Trips on month'.tr,
|
||||
// textStyle: AppStyle.title,
|
||||
// ),
|
||||
// // labelRotation: 45,
|
||||
// majorGridLines: const MajorGridLines(width: 0),
|
||||
// ),
|
||||
// primaryYAxis: const NumericAxis(isVisible: false),
|
||||
// series: <LineSeries<ChartDataS, String>>[
|
||||
// LineSeries<ChartDataS, String>(
|
||||
// dataSource: rideAdminController.chartDatasync,
|
||||
// xValueMapper: (ChartDataS data, _) => '${data.day}',
|
||||
// yValueMapper: (ChartDataS data, _) =>
|
||||
// data.ridesCount,
|
||||
// dataLabelSettings:
|
||||
// const DataLabelSettings(isVisible: true),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
Expanded(
|
||||
child: GetBuilder<RideAdminController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return _buildLoadingState(cs);
|
||||
}
|
||||
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Card(
|
||||
elevation: 4,
|
||||
color: AppColor.deepPurpleAccent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Total Trips on this Month is ${rideAdminController.jsonResponse['message'][0]['current_month_rides_count']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildChartSection(controller, cs),
|
||||
const SizedBox(height: 16),
|
||||
_buildMonthlyTotal(controller, cs),
|
||||
const SizedBox(height: 16),
|
||||
_buildRideDetails(controller, cs),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
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.map_rounded, color: cs.primary, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'شاشة الرحلات',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('جاري تحميل الرحلات...',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChartSection(RideAdminController controller, ColorScheme cs) {
|
||||
return Container(
|
||||
height: 280,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.primary.withValues(alpha: 0.06),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
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.trending_up_rounded,
|
||||
color: cs.primary, size: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'رحلات الشهر',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: controller.chartData,
|
||||
isCurved: true,
|
||||
color: cs.primary,
|
||||
barWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: cs.primary.withValues(alpha: 0.08),
|
||||
),
|
||||
isStrokeJoinRound: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Card(
|
||||
elevation: 4,
|
||||
color: AppColor.yellowColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Driver Average Duration: ${rideAdminController.ridesDetails[0]['driver_avg_duration']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Number of Drivers: ${rideAdminController.ridesDetails[0]['num_Driver']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Total Rides: ${rideAdminController.ridesDetails[0]['total_rides']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Ongoing Rides: ${rideAdminController.ridesDetails[0]['ongoing_rides']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Completed Rides: ${rideAdminController.ridesDetails[0]['completed_rides']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Cancelled Rides: ${rideAdminController.ridesDetails[0]['cancelled_rides']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Longest Duration: ${rideAdminController.ridesDetails[0]['longest_duration']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Total Distance: ${rideAdminController.ridesDetails[0]['total_distance']} km',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Average Distance: ${rideAdminController.ridesDetails[0]['average_distance']} km',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Longest Distance: ${rideAdminController.ridesDetails[0]['longest_distance']} km',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Total Driver Earnings: \$${rideAdminController.ridesDetails[0]['total_driver_earnings']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Total Company Earnings: \$${rideAdminController.ridesDetails[0]['total_company_earnings']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Company Percentage: ${rideAdminController.ridesDetails[0]['companyPercent']} %',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
))
|
||||
]);
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 24,
|
||||
getTitlesWidget: (v, _) => Text(
|
||||
'${v.toInt()}',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 9),
|
||||
),
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 36,
|
||||
getTitlesWidget: (v, _) => Text(
|
||||
v.toInt().toString(),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (_) => FlLine(
|
||||
color: cs.outline.withValues(alpha: 0.5),
|
||||
strokeWidth: 1),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthlyTotal(RideAdminController controller, ColorScheme cs) {
|
||||
final count = controller.jsonResponse['message']?[0]
|
||||
['current_month_rides_count'] ??
|
||||
'0';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
cs.primary.withValues(alpha: 0.20),
|
||||
cs.primary.withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.calendar_today_rounded,
|
||||
color: cs.primary, size: 24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'إجمالي رحلات الشهر',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$count',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideDetails(RideAdminController controller, ColorScheme cs) {
|
||||
if (controller.ridesDetails.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final details = controller.ridesDetails[0];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.shadow.withValues(alpha: 0.06),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF59E0B).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF59E0B).withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(Icons.analytics_rounded,
|
||||
color: const Color(0xFFF59E0B), size: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'تفاصيل الرحلات',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailGrid(details, cs),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailGrid(dynamic details, ColorScheme cs) {
|
||||
final items = [
|
||||
_DetailItem('متوسط مدة السائق', details['driver_avg_duration'] ?? '',
|
||||
Icons.timer_rounded, cs.tertiary),
|
||||
_DetailItem('عدد السائقين', details['num_Driver'] ?? '',
|
||||
Icons.people_rounded, cs.primary),
|
||||
_DetailItem('إجمالي الرحلات', details['total_rides'] ?? '',
|
||||
Icons.route_rounded, cs.primary),
|
||||
_DetailItem('رحلات نشطة', details['ongoing_rides'] ?? '',
|
||||
Icons.play_circle_rounded, const Color(0xFFF59E0B)),
|
||||
_DetailItem('مكتملة', details['completed_rides'] ?? '',
|
||||
Icons.check_circle_rounded, const Color(0xFF10B981)),
|
||||
_DetailItem('ملغاة', details['cancelled_rides'] ?? '',
|
||||
Icons.cancel_rounded, cs.error),
|
||||
_DetailItem('أطول مدة', details['longest_duration'] ?? '',
|
||||
Icons.hourglass_top_rounded, cs.tertiary),
|
||||
_DetailItem('إجمالي المسافة', '${details['total_distance'] ?? 0} km',
|
||||
Icons.straighten_rounded, cs.primary),
|
||||
_DetailItem('متوسط المسافة', '${details['average_distance'] ?? 0} km',
|
||||
Icons.speed_rounded, const Color(0xFFF59E0B)),
|
||||
_DetailItem('أطول مسافة', '${details['longest_distance'] ?? 0} km',
|
||||
Icons.arrow_forward_rounded, cs.tertiary),
|
||||
_DetailItem('إجمالي أرباح السائقين',
|
||||
'\$${details['total_driver_earnings'] ?? 0}', Icons.payments_rounded,
|
||||
const Color(0xFF10B981)),
|
||||
_DetailItem('إجمالي أرباح الشركة',
|
||||
'\$${details['total_company_earnings'] ?? 0}',
|
||||
Icons.business_center_rounded, cs.primary),
|
||||
_DetailItem('نسبة الشركة', '${details['companyPercent'] ?? 0}%',
|
||||
Icons.pie_chart_rounded, const Color(0xFFF59E0B)),
|
||||
];
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 2.2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: item.color.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(item.icon, color: item.color, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 10,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item.value,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailItem {
|
||||
final String label, value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
_DetailItem(this.label, this.value, this.icon, this.color);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
import 'package:siro_admin/controller/admin/security_v2_controller.dart';
|
||||
|
||||
class AuditLogsPage extends StatelessWidget {
|
||||
@@ -8,21 +7,24 @@ class AuditLogsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('سجل العمليات (Audit Logs)',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: AppColor.surface,
|
||||
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>(
|
||||
init: SecurityV2Controller(),
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColor.accent));
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: cs.primary));
|
||||
}
|
||||
|
||||
if (ctrl.auditLogs.isEmpty) {
|
||||
@@ -31,15 +33,15 @@ class AuditLogsPage extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.security_rounded,
|
||||
size: 64, color: AppColor.textSecondary.withOpacity(0.3)),
|
||||
size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.3)),
|
||||
const SizedBox(height: 16),
|
||||
const Text('لا توجد عمليات مسجلة حالياً',
|
||||
style: TextStyle(color: AppColor.textSecondary)),
|
||||
Text('لا توجد عمليات مسجلة حالياً',
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
Text(
|
||||
'تأكد من إنشاء جدول سجل العمليات في قاعدة البيانات',
|
||||
style: TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 10)),
|
||||
color: cs.onSurfaceVariant, fontSize: 10)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -50,7 +52,7 @@ class AuditLogsPage extends StatelessWidget {
|
||||
itemCount: ctrl.auditLogs.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final log = ctrl.auditLogs[i];
|
||||
return _buildLogItem(log);
|
||||
return _buildLogItem(log, cs);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -58,14 +60,14 @@ class AuditLogsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogItem(Map<String, dynamic> log) {
|
||||
Widget _buildLogItem(Map<String, dynamic> log, ColorScheme cs) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColor.divider),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -74,22 +76,22 @@ class AuditLogsPage extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(log['admin_name'] ?? 'أدمن غير معروف',
|
||||
style: const TextStyle(
|
||||
color: AppColor.accent, fontWeight: FontWeight.bold)),
|
||||
style: TextStyle(
|
||||
color: cs.primary, fontWeight: FontWeight.bold)),
|
||||
Text(log['created_at'] ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 11)),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 11)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(log['action'] ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textPrimary, fontWeight: FontWeight.w600)),
|
||||
style: TextStyle(
|
||||
color: cs.onSurface, fontWeight: FontWeight.w600)),
|
||||
if (log['details'] != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(log['details'],
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 12)),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@@ -97,19 +99,19 @@ class AuditLogsPage extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.info.withOpacity(0.1),
|
||||
color: cs.tertiary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(log['table_name'] ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppColor.info,
|
||||
style: TextStyle(
|
||||
color: cs.tertiary,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('ID: ${log['record_id']}',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 10)),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 10)),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,23 +3,20 @@ import 'package:get/get.dart';
|
||||
import '../../../controller/admin/staff_controller.dart';
|
||||
|
||||
class AddStaffPage extends StatelessWidget {
|
||||
final String role; // 'admin' or 'service'
|
||||
final String role;
|
||||
const AddStaffPage({super.key, required this.role});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(StaffController());
|
||||
controller.selectedRole = role;
|
||||
|
||||
const Color bgColor = Color(0xFF0D1117);
|
||||
const Color inputColor = Color(0xFF161B22);
|
||||
const Color accentColor = Color(0xFF00D4AA);
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: Text(role == 'admin' ? "إضافة مدير جديد" : "إضافة موظف خدمة عملاء"),
|
||||
backgroundColor: bgColor,
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
@@ -29,20 +26,20 @@ class AddStaffPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle("المعلومات الأساسية"),
|
||||
_buildSectionTitle("المعلومات الأساسية", cs),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
controller: controller.nameController,
|
||||
label: "الاسم الكامل",
|
||||
icon: Icons.person_outline,
|
||||
fillColor: inputColor,
|
||||
cs: cs,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
controller: controller.phoneController,
|
||||
label: "رقم الهاتف",
|
||||
icon: Icons.phone_android_outlined,
|
||||
fillColor: inputColor,
|
||||
cs: cs,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -50,7 +47,7 @@ class AddStaffPage extends StatelessWidget {
|
||||
controller: controller.emailController,
|
||||
label: "البريد الإلكتروني",
|
||||
icon: Icons.email_outlined,
|
||||
fillColor: inputColor,
|
||||
cs: cs,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -58,11 +55,11 @@ class AddStaffPage extends StatelessWidget {
|
||||
controller: controller.passwordController,
|
||||
label: "كلمة المرور",
|
||||
icon: Icons.lock_outline,
|
||||
fillColor: inputColor,
|
||||
cs: cs,
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionTitle("معلومات إضافية"),
|
||||
_buildSectionTitle("معلومات إضافية", cs),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
@@ -72,7 +69,7 @@ class AddStaffPage extends StatelessWidget {
|
||||
value: controller.selectedGender,
|
||||
items: ['Male', 'Female'],
|
||||
onChanged: (val) => controller.selectedGender = val!,
|
||||
fillColor: inputColor,
|
||||
cs: cs,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -81,7 +78,7 @@ class AddStaffPage extends StatelessWidget {
|
||||
controller: controller.birthdateController,
|
||||
label: "تاريخ الميلاد",
|
||||
icon: Icons.calendar_today_outlined,
|
||||
fillColor: inputColor,
|
||||
cs: cs,
|
||||
hint: "YYYY-MM-DD",
|
||||
),
|
||||
),
|
||||
@@ -93,7 +90,7 @@ class AddStaffPage extends StatelessWidget {
|
||||
value: controller.selectedCountry,
|
||||
items: const ['Jordan', 'Syria', 'Egypt'],
|
||||
onChanged: (val) => controller.selectedCountry = val!,
|
||||
fillColor: inputColor,
|
||||
cs: cs,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
GetBuilder<StaffController>(
|
||||
@@ -103,17 +100,17 @@ class AddStaffPage extends StatelessWidget {
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading ? null : () => controller.registerStaff(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: accentColor,
|
||||
backgroundColor: cs.primary,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: controller.isLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
? CircularProgressIndicator(color: cs.onPrimary)
|
||||
: Text(
|
||||
"حفظ البيانات",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: bgColor,
|
||||
color: cs.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -126,11 +123,11 @@ class AddStaffPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
Widget _buildSectionTitle(String title, ColorScheme cs) {
|
||||
return Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF7D8590),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
@@ -142,28 +139,28 @@ class AddStaffPage extends StatelessWidget {
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required Color fillColor,
|
||||
required ColorScheme cs,
|
||||
String? hint,
|
||||
bool obscureText = false,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.05)),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
style: TextStyle(color: cs.onSurface),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.white24),
|
||||
labelStyle: const TextStyle(color: Colors.white54),
|
||||
prefixIcon: Icon(icon, color: Colors.white38),
|
||||
hintStyle: TextStyle(color: cs.onSurface.withValues(alpha: 0.24)),
|
||||
labelStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
prefixIcon: Icon(icon, color: cs.onSurfaceVariant),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
@@ -177,22 +174,22 @@ class AddStaffPage extends StatelessWidget {
|
||||
required String value,
|
||||
required List<String> items,
|
||||
required Function(String?) onChanged,
|
||||
required Color fillColor,
|
||||
required ColorScheme cs,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.05)),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: value,
|
||||
dropdownColor: fillColor,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
dropdownColor: cs.surfaceContainerHighest,
|
||||
style: TextStyle(color: cs.onSurface),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: const TextStyle(color: Colors.white54),
|
||||
labelStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
items: items.map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
|
||||
|
||||
@@ -50,7 +50,7 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
|
||||
);
|
||||
if (response != 'failure') {
|
||||
mySnackbarSuccess('تم تنفيذ الإجراء بنجاح');
|
||||
_fetchPendingAdmins(); // تحديث القائمة
|
||||
_fetchPendingAdmins();
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackbarError('حدث خطأ: $e');
|
||||
@@ -59,35 +59,37 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0A0D14),
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
title: const Text('طلبات الانضمام المعلقة', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: const Color(0xFF161D2E),
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
elevation: 0,
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator(color: Color(0xFF00E5FF)))
|
||||
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||
: _pendingAdmins.isEmpty
|
||||
? _buildEmptyState()
|
||||
: _buildList(),
|
||||
? _buildEmptyState(cs)
|
||||
: _buildList(cs),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
Widget _buildEmptyState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.person_search_rounded, size: 80, color: Colors.grey[800]),
|
||||
Icon(Icons.person_search_rounded, size: 80, color: cs.outline),
|
||||
const SizedBox(height: 16),
|
||||
const Text('لا توجد طلبات معلقة حالياً', style: TextStyle(color: Colors.grey)),
|
||||
Text('لا توجد طلبات معلقة حالياً', style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList() {
|
||||
Widget _buildList(ColorScheme cs) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _pendingAdmins.length,
|
||||
@@ -97,40 +99,40 @@ class _PendingAdminsPageState extends State<PendingAdminsPage> {
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161D2E),
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF1F2D4A)),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Color(0xFF1F2D4A),
|
||||
child: Icon(Icons.person, color: Color(0xFF00E5FF)),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: cs.outline,
|
||||
child: Icon(Icons.person, color: cs.primary),
|
||||
),
|
||||
title: Text(admin['name'] ?? 'بدون اسم', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(admin['phone'] ?? 'بدون رقم', style: const TextStyle(color: Colors.grey)),
|
||||
title: Text(admin['name'] ?? 'بدون اسم', style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(admin['phone'] ?? 'بدون رقم', style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
trailing: Text(
|
||||
admin['created_at']?.split(' ')[0] ?? '',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
),
|
||||
const Divider(color: Color(0xFF1F2D4A), height: 24),
|
||||
Divider(color: cs.outline, height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _handleAction(admin['id'], 'rejected'),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.redAccent),
|
||||
style: TextButton.styleFrom(foregroundColor: cs.error),
|
||||
child: const Text('رفض'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => _handleAction(admin['id'], 'approved'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00E5FF),
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: cs.primary,
|
||||
foregroundColor: cs.onPrimary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/colors.dart';
|
||||
import 'package:siro_admin/controller/admin/analytics_v2_controller.dart';
|
||||
|
||||
class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
@@ -10,108 +9,250 @@ class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(AnalyticsV2Controller());
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
appBar: AppBar(
|
||||
title: const Text('التحليلات المتقدمة',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: AppColor.surface,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
onPressed: () => controller.fetchAllAnalytics(),
|
||||
)
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
Expanded(
|
||||
child: GetBuilder<AnalyticsV2Controller>(
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) {
|
||||
return _buildLoadingState(cs);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSummarySection(ctrl.revenueData['summary'], cs),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionTitle('إيرادات آخر 30 يوم', cs),
|
||||
_buildRevenueChart(ctrl.revenueData['daily'] ?? [], cs),
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle('نمو المستخدمين (آخر 30 يوم)', cs),
|
||||
_buildGrowthChart(ctrl.growthData, cs),
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle('أفضل 10 سائقين (حسب الرحلات)', cs),
|
||||
_buildTopDriversList(ctrl.topDrivers, cs),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GetBuilder<AnalyticsV2Controller>(
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColor.accent));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSummarySection(ctrl.revenueData['summary']),
|
||||
const SizedBox(height: 24),
|
||||
_buildSectionTitle('إيرادات آخر 30 يوم'),
|
||||
_buildRevenueChart(ctrl.revenueData['daily'] ?? []),
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle('نمو المستخدمين (آخر 30 يوم)'),
|
||||
_buildGrowthChart(ctrl.growthData),
|
||||
const SizedBox(height: 32),
|
||||
_buildSectionTitle('أفضل 10 سائقين (حسب الرحلات)'),
|
||||
_buildTopDriversList(ctrl.topDrivers),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
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.analytics_rounded,
|
||||
color: cs.primary, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'التحليلات المتقدمة',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => Get.find<AnalyticsV2Controller>().fetchAllAnalytics(),
|
||||
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: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('جاري تحميل التحليلات...',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title, ColorScheme cs) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: AppColor.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummarySection(Map<String, dynamic>? summary) {
|
||||
Widget _buildSummarySection(
|
||||
Map<String, dynamic>? summary, ColorScheme cs) {
|
||||
if (summary == null) return const SizedBox();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
_buildSummaryCard('إجمالي الإيرادات',
|
||||
'${summary['total_revenue_all'] ?? 0}', AppColor.info),
|
||||
_buildSummaryCard(
|
||||
'إجمالي الإيرادات',
|
||||
'${summary['total_revenue_all'] ?? 0}',
|
||||
cs.tertiary,
|
||||
Icons.attach_money_rounded,
|
||||
cs,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildSummaryCard('صافي الربح', '${summary['total_profit_all'] ?? 0}',
|
||||
AppColor.success),
|
||||
_buildSummaryCard(
|
||||
'صافي الربح',
|
||||
'${summary['total_profit_all'] ?? 0}',
|
||||
const Color(0xFF10B981),
|
||||
Icons.trending_up_rounded,
|
||||
cs,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard(String title, String value, Color color) {
|
||||
Widget _buildSummaryCard(
|
||||
String title, String value, Color color, IconData icon, ColorScheme cs) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
border: Border.all(color: color.withValues(alpha: 0.2)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.06),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 8),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
color: color, fontSize: 22, fontWeight: FontWeight.bold)),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: color.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 18),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRevenueChart(List<dynamic> daily) {
|
||||
if (daily.isEmpty) return const Center(child: Text('لا توجد بيانات'));
|
||||
Widget _buildRevenueChart(List<dynamic> daily, ColorScheme cs) {
|
||||
if (daily.isEmpty) {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Center(
|
||||
child: Text('لا توجد بيانات',
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<FlSpot> revenueSpots = [];
|
||||
List<FlSpot> profitSpots = [];
|
||||
@@ -127,71 +268,131 @@ class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
height: 300,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.shadow.withValues(alpha: 0.06),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (v) =>
|
||||
FlLine(color: AppColor.divider, strokeWidth: 1)),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (val, meta) {
|
||||
if (val.toInt() % 7 == 0 && val.toInt() < daily.length) {
|
||||
return Text(
|
||||
daily[val.toInt()]['date'].toString().substring(8),
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 10));
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_LegendDot(color: cs.tertiary, label: 'الإيرادات'),
|
||||
const SizedBox(width: 16),
|
||||
_LegendDot(color: const Color(0xFF10B981), label: 'الربح'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (v) => FlLine(
|
||||
color: cs.outline.withValues(alpha: 0.5),
|
||||
strokeWidth: 1),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: (v, _) => Text(
|
||||
v >= 1000 ? '${(v / 1000).toStringAsFixed(0)}k' : v.toInt().toString(),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 9),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 24,
|
||||
getTitlesWidget: (val, meta) {
|
||||
if (val.toInt() % 7 == 0 &&
|
||||
val.toInt() < daily.length) {
|
||||
return Text(
|
||||
daily[val.toInt()]['date']
|
||||
.toString()
|
||||
.substring(8),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 9),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: revenueSpots,
|
||||
isCurved: true,
|
||||
color: cs.tertiary,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: cs.tertiary.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
LineChartBarData(
|
||||
spots: profitSpots,
|
||||
isCurved: true,
|
||||
color: const Color(0xFF10B981),
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: const Color(0xFF10B981).withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: revenueSpots,
|
||||
isCurved: true,
|
||||
color: AppColor.info,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true, color: AppColor.info.withOpacity(0.1)),
|
||||
),
|
||||
LineChartBarData(
|
||||
spots: profitSpots,
|
||||
isCurved: true,
|
||||
color: AppColor.success,
|
||||
barWidth: 3,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(
|
||||
show: true, color: AppColor.success.withOpacity(0.1)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGrowthChart(Map<String, dynamic> data) {
|
||||
Widget _buildGrowthChart(Map<String, dynamic> data, ColorScheme cs) {
|
||||
final passengers = data['passenger_daily'] as List<dynamic>? ?? [];
|
||||
final drivers = data['driver_daily'] as List<dynamic>? ?? [];
|
||||
|
||||
if (passengers.isEmpty && drivers.isEmpty) {
|
||||
return const Center(child: Text('لا توجد بيانات'));
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Center(
|
||||
child: Text('لا توجد بيانات',
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<BarChartGroupData> barGroups = [];
|
||||
@@ -211,59 +412,118 @@ class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
x: i,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: pCount,
|
||||
color: AppColor.info,
|
||||
width: 8,
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
toY: pCount,
|
||||
color: cs.tertiary.withValues(alpha: 0.7),
|
||||
width: 8,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: dCount,
|
||||
color: AppColor.warning,
|
||||
width: 8,
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
toY: dCount,
|
||||
color: const Color(0xFFF59E0B).withValues(alpha: 0.7),
|
||||
width: 8,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 250,
|
||||
height: 280,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.shadow.withValues(alpha: 0.06),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
barGroups: barGroups,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (val, meta) {
|
||||
if (val.toInt() % 7 == 0 && val.toInt() < passengers.length) {
|
||||
return Text(
|
||||
passengers[val.toInt()]['date'].toString().substring(8),
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 10));
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_LegendDot(color: cs.tertiary, label: 'ركاب جدد'),
|
||||
const SizedBox(width: 16),
|
||||
_LegendDot(
|
||||
color: const Color(0xFFF59E0B), label: 'سائقين جدد'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
barGroups: barGroups,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: (v, _) => Text(
|
||||
v.toInt().toString(),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 9),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 24,
|
||||
getTitlesWidget: (val, meta) {
|
||||
if (val.toInt() % 7 == 0 &&
|
||||
val.toInt() < passengers.length) {
|
||||
return Text(
|
||||
passengers[val.toInt()]['date']
|
||||
.toString()
|
||||
.substring(8),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 9),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopDriversList(List<dynamic> drivers) {
|
||||
if (drivers.isEmpty) return const Center(child: Text('لا توجد بيانات'));
|
||||
Widget _buildTopDriversList(List<dynamic> drivers, ColorScheme cs) {
|
||||
if (drivers.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Center(
|
||||
child: Text('لا توجد بيانات',
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
@@ -271,47 +531,105 @@ class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
itemCount: drivers.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final d = drivers[i];
|
||||
final isTop3 = i < 3;
|
||||
final rankColors = [
|
||||
const Color(0xFFF59E0B),
|
||||
cs.onSurfaceVariant,
|
||||
const Color(0xFFCD7F32),
|
||||
];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isTop3
|
||||
? rankColors[i].withValues(alpha: 0.2)
|
||||
: cs.outline,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppColor.accent.withOpacity(0.1),
|
||||
child: Text('${i + 1}',
|
||||
style: const TextStyle(
|
||||
color: AppColor.accent, fontWeight: FontWeight.bold)),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isTop3
|
||||
? rankColors[i].withValues(alpha: 0.12)
|
||||
: cs.outline,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isTop3
|
||||
? rankColors[i].withValues(alpha: 0.3)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${i + 1}',
|
||||
style: TextStyle(
|
||||
color: isTop3 ? rankColors[i] : cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${d['first_name']} ${d['last_name']}',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textPrimary,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Text(d['phone'] ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppColor.textSecondary, fontSize: 12)),
|
||||
Text(
|
||||
'${d['first_name']} ${d['last_name']}',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
d['phone'] ?? '',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('${d['completed_rides']} رحلة',
|
||||
style: const TextStyle(
|
||||
color: AppColor.info, fontWeight: FontWeight.bold)),
|
||||
Text('${d['total_revenue']} ل.س',
|
||||
style: const TextStyle(
|
||||
color: AppColor.success,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.tertiary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${d['completed_rides']} رحلة',
|
||||
style: TextStyle(
|
||||
color: cs.tertiary,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${d['total_revenue']} ل.س',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF10B981),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -321,3 +639,36 @@ class AdvancedAnalyticsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LegendDot extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
|
||||
const _LegendDot({required this.color, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:siro_admin/constant/theme.dart';
|
||||
import 'package:siro_admin/controller/admin/static_controller.dart';
|
||||
import 'notes_driver_page.dart';
|
||||
|
||||
|
||||
@@ -1,81 +1,255 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/style.dart';
|
||||
import 'package:siro_admin/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_admin/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../controller/admin/wallet_admin_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
|
||||
class Wallet extends StatelessWidget {
|
||||
Wallet({super.key});
|
||||
final WalletAdminController walletAdminController =
|
||||
Get.put(WalletAdminController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Wallet'.tr,
|
||||
body: [
|
||||
GetBuilder<WalletAdminController>(builder: (walletAdminController) {
|
||||
return Center(
|
||||
child: walletAdminController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Column(
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Pay to them to banks'.tr,
|
||||
onPressed: () async {
|
||||
await walletAdminController.payToBankDriverAll();
|
||||
}),
|
||||
SizedBox(
|
||||
height: Get.height * .8,
|
||||
child: ListView.builder(
|
||||
itemCount:
|
||||
walletAdminController.driversWalletPoints.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var res = walletAdminController
|
||||
.driversWalletPoints[index];
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
if (res != null && res['name_arabic'] != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'driver name: ${res['name_arabic'].toString()}'),
|
||||
Text(
|
||||
'Amount: ${res['total_amount'].toString()}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(); // Return an empty container if the data is null
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
return Scaffold(
|
||||
backgroundColor: cs.surface,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
Expanded(
|
||||
child: GetBuilder<WalletAdminController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return _buildLoadingState(cs);
|
||||
}
|
||||
|
||||
if (controller.driversWalletPoints.isEmpty) {
|
||||
return _buildEmptyState(cs);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildPayAllButton(context, controller, cs),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 80),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: controller.driversWalletPoints.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
var res = controller.driversWalletPoints[index];
|
||||
if (res != null && res['name_arabic'] != null) {
|
||||
return _buildWalletCard(res, cs);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context, ColorScheme cs) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
border: Border(bottom: BorderSide(color: cs.outline)),
|
||||
),
|
||||
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.account_balance_wallet_rounded,
|
||||
color: cs.primary, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'المحافظ المالية',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
walletAdminController.getWalletForEachDriverToPay();
|
||||
},
|
||||
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: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.account_balance_wallet_rounded,
|
||||
size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('لا توجد محافظ مالية',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPayAllButton(
|
||||
BuildContext context, WalletAdminController controller, ColorScheme cs) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.send_rounded, color: Colors.white, size: 18),
|
||||
label: Text(
|
||||
'دفع الجميع للبنوك',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: cs.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () async {
|
||||
await controller.payToBankDriverAll();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletCard(dynamic res, ColorScheme cs) {
|
||||
final String name = res['name_arabic']?.toString() ?? 'بدون اسم';
|
||||
final String amount = res['total_amount']?.toString() ?? '0';
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
const Color(0xFF10B981).withValues(alpha: 0.20),
|
||||
const Color(0xFF10B981).withValues(alpha: 0.08),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF10B981).withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Icon(Icons.person_rounded, color: const Color(0xFF10B981), size: 22),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
isleading: true,
|
||||
action: IconButton(
|
||||
onPressed: () async {
|
||||
walletAdminController.getWalletForEachDriverToPay();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.black,
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
'سائق',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
amount,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF10B981),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'ل.س',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF10B981).withValues(alpha: 0.6),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,9 +6,6 @@ import '../../print.dart';
|
||||
import '../widgets/snackbar.dart';
|
||||
import 'add_invoice_page.dart';
|
||||
|
||||
// نفترض أن هذا الموديل موجود في مشروعك، إذا لم يكن موجوداً يرجى إضافته أو تعديل الاستيراد
|
||||
// import '../../model/invoice_model.dart';
|
||||
|
||||
class InvoiceListPage extends StatefulWidget {
|
||||
const InvoiceListPage({super.key});
|
||||
|
||||
@@ -17,17 +14,11 @@ class InvoiceListPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
List<dynamic> invoices = []; // استخدام dynamic لتجنب مشاكل الموديل إذا اختلف
|
||||
List<dynamic> invoices = [];
|
||||
int totalCount = 0;
|
||||
double totalAmount = 0.0;
|
||||
bool isLoading = true;
|
||||
|
||||
// الألوان "الإيجابية" للتصميم الجديد
|
||||
final Color primaryColor = const Color(0xFF4F46E5); // Indigo
|
||||
final Color secondaryColor = const Color(0xFF818CF8); // Lighter Indigo
|
||||
final Color moneyColor = const Color(0xFF059669); // Emerald Green
|
||||
final Color bgColor = const Color(0xFFF3F4F6); // Light Gray Background
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -46,7 +37,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
final data = response;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
invoices = data['data']; // استخدام البيانات مباشرة
|
||||
invoices = data['data'];
|
||||
totalCount = int.tryParse(data['summary']['count'].toString()) ?? 0;
|
||||
totalAmount =
|
||||
double.tryParse(data['summary']['total'].toString()) ?? 0.0;
|
||||
@@ -66,6 +57,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
}
|
||||
|
||||
void _showImageDialog(BuildContext context, String imageUrl) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
@@ -75,8 +67,9 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: ClipRRect(
|
||||
@@ -93,16 +86,16 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
height: 200,
|
||||
width: 200,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(color: primaryColor),
|
||||
child: CircularProgressIndicator(color: cs.primary),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const SizedBox(
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
width: 150,
|
||||
child: Icon(Icons.broken_image,
|
||||
size: 60, color: Colors.grey),
|
||||
child: Icon(Icons.broken_image_rounded,
|
||||
size: 60, color: cs.onSurfaceVariant),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -112,11 +105,17 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Icon(Icons.close_rounded,
|
||||
color: cs.onSurfaceVariant, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -128,17 +127,18 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
// زر عائم بتصميم متدرج
|
||||
backgroundColor: cs.surface,
|
||||
floatingActionButton: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: [primaryColor, secondaryColor]),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
gradient: LinearGradient(colors: [cs.primary, cs.primary.withValues(alpha: 0.7)]),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: primaryColor.withOpacity(0.4),
|
||||
blurRadius: 10,
|
||||
color: cs.primary.withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
@@ -146,32 +146,29 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
onPressed: () => Get.to(() => AddInvoicePage()),
|
||||
label: const Text('إضافة فاتورة',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
icon: const Icon(Icons.add),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 1. رأس الصفحة (Header & Summary)
|
||||
_buildHeader(),
|
||||
|
||||
// 2. قائمة الفواتير
|
||||
_buildHeader(cs),
|
||||
Expanded(
|
||||
child: isLoading
|
||||
? Center(child: CircularProgressIndicator(color: primaryColor))
|
||||
? _buildLoadingState(cs)
|
||||
: invoices.isEmpty
|
||||
? _buildEmptyState()
|
||||
? _buildEmptyState(cs)
|
||||
: RefreshIndicator(
|
||||
onRefresh: fetchInvoices,
|
||||
color: primaryColor,
|
||||
color: cs.primary,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
|
||||
itemCount: invoices.length,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final invoice = invoices[index];
|
||||
return _buildInvoiceCard(invoice);
|
||||
return _buildInvoiceCard(invoice, cs);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -181,13 +178,12 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// === تصميم الهيدر (رأس الصفحة) ===
|
||||
Widget _buildHeader() {
|
||||
Widget _buildHeader(ColorScheme cs) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 20,
|
||||
bottom: 30,
|
||||
bottom: 24,
|
||||
left: 20,
|
||||
right: 20,
|
||||
),
|
||||
@@ -195,15 +191,15 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [primaryColor, const Color(0xFF6366F1)],
|
||||
colors: [cs.primary, cs.primary.withValues(alpha: 0.7)],
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(30),
|
||||
bottomRight: Radius.circular(30),
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: primaryColor.withOpacity(0.3),
|
||||
color: cs.primary.withValues(alpha: 0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
@@ -211,38 +207,41 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// العنوان وزر الرجوع
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios,
|
||||
color: Colors.white, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
"سجل الفواتير",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: Colors.white, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 40), // للمحاذاة
|
||||
const Spacer(),
|
||||
const Text(
|
||||
"سجل الفواتير",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(width: 34),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// بطاقات الملخص
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSummaryItem(
|
||||
title: "الإجمالي",
|
||||
value: "${totalAmount.toStringAsFixed(1)} د.أ",
|
||||
icon: Icons.attach_money,
|
||||
icon: Icons.attach_money_rounded,
|
||||
isMoney: true,
|
||||
),
|
||||
),
|
||||
@@ -251,7 +250,7 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
child: _buildSummaryItem(
|
||||
title: "عدد الفواتير",
|
||||
value: "$totalCount",
|
||||
icon: Icons.receipt_long,
|
||||
icon: Icons.receipt_long_rounded,
|
||||
isMoney: false,
|
||||
),
|
||||
),
|
||||
@@ -262,17 +261,18 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryItem(
|
||||
{required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required bool isMoney}) {
|
||||
Widget _buildSummaryItem({
|
||||
required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required bool isMoney,
|
||||
}) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 20),
|
||||
@@ -282,21 +282,70 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: isMoney ? const Color(0xFFD1FAE5) : Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12),
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 11, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// === تصميم بطاقة الفاتورة ===
|
||||
Widget _buildInvoiceCard(dynamic invoice) {
|
||||
// استخراج البيانات بأمان
|
||||
Widget _buildLoadingState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
color: cs.primary,
|
||||
strokeWidth: 2,
|
||||
backgroundColor: cs.primary.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('جاري تحميل الفواتير...',
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(ColorScheme cs) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.receipt_long_rounded,
|
||||
size: 48, color: cs.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text("لا توجد فواتير حالياً",
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
TextButton.icon(
|
||||
onPressed: fetchInvoices,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text("تحديث"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInvoiceCard(dynamic invoice, ColorScheme cs) {
|
||||
String name = invoice['name'] ?? 'بدون اسم';
|
||||
String amount = invoice['amount']?.toString() ?? '0';
|
||||
String date = invoice['date'] ?? '';
|
||||
@@ -304,22 +353,16 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
String? imageUrl = invoice['imageLink'];
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
_showImageDialog(context, imageUrl);
|
||||
@@ -328,96 +371,93 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
// 1. أيقونة أو صورة مصغرة
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: cs.primary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: imageUrl != null && imageUrl.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
Icon(Icons.receipt, color: primaryColor),
|
||||
Icon(Icons.receipt_rounded, color: cs.primary, size: 22),
|
||||
),
|
||||
)
|
||||
: Icon(Icons.receipt_outlined, color: primaryColor),
|
||||
: Icon(Icons.receipt_outlined, color: cs.primary, size: 22),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 2. التفاصيل
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Color(0xFF1F2937),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.outline),
|
||||
),
|
||||
child: Text(
|
||||
"#$invNumber",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.bold),
|
||||
color: cs.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 3. المبلغ
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
amount,
|
||||
style: TextStyle(
|
||||
color: moneyColor,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: const Color(0xFF10B981),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"د.أ",
|
||||
style: TextStyle(
|
||||
color: moneyColor.withOpacity(0.7),
|
||||
color: const Color(0xFF10B981).withValues(alpha: 0.6),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -429,26 +469,4 @@ class _InvoiceListPageState extends State<InvoiceListPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long_rounded, size: 80, color: Colors.grey[300]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"لا توجد فواتير حالياً",
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton.icon(
|
||||
onPressed: fetchInvoices,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text("تحديث"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// org_list_page.dart — قائمة مؤسسات مواصلاتي (لوحة إدارة سيرو)
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../controller/transit/transit_admin_controller.dart';
|
||||
import '../../controller/transit/transit_admin_models.dart';
|
||||
import 'org_create_page.dart';
|
||||
@@ -27,22 +25,25 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return GetBuilder<TransitAdminController>(
|
||||
builder: (c) => Scaffold(
|
||||
backgroundColor: AppColor.bg,
|
||||
backgroundColor: cs.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColor.bg,
|
||||
backgroundColor: cs.surface,
|
||||
elevation: 0,
|
||||
title: const Text('مواصلاتي — المؤسسات', style: TextStyle(color: AppColor.textPrimary)),
|
||||
title: Text('مواصلاتي — المؤسسات',
|
||||
style: TextStyle(color: cs.onSurface)),
|
||||
iconTheme: IconThemeData(color: cs.onSurface),
|
||||
actions: [
|
||||
// زر اعتماد الخطوط
|
||||
IconButton(
|
||||
icon: const Icon(Icons.route_rounded, color: AppColor.warning),
|
||||
icon: Icon(Icons.route_rounded, color: cs.tertiary),
|
||||
tooltip: 'اعتماد الخطوط',
|
||||
onPressed: () => Get.to(() => const RouteApprovalPage()),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: AppColor.accent),
|
||||
icon: Icon(Icons.add, color: cs.primary),
|
||||
onPressed: () async {
|
||||
final created = await Get.to(() => const TransitOrgCreatePage());
|
||||
if (created == true) c.fetchOrgs();
|
||||
@@ -56,17 +57,17 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
style: const TextStyle(color: AppColor.textPrimary),
|
||||
style: TextStyle(color: cs.onSurface),
|
||||
onSubmitted: (v) {
|
||||
c.searchQuery = v;
|
||||
c.fetchOrgs();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'ابحث عن مؤسسة...',
|
||||
hintStyle: const TextStyle(color: AppColor.textSecondary),
|
||||
prefixIcon: const Icon(Icons.search, color: AppColor.textSecondary),
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
prefixIcon: Icon(Icons.search, color: cs.onSurfaceVariant),
|
||||
filled: true,
|
||||
fillColor: AppColor.surface,
|
||||
fillColor: cs.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
@@ -76,17 +77,18 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
|
||||
),
|
||||
Expanded(
|
||||
child: c.isLoadingList
|
||||
? const Center(child: CircularProgressIndicator(color: AppColor.accent))
|
||||
? Center(child: CircularProgressIndicator(color: cs.primary))
|
||||
: c.orgs.isEmpty
|
||||
? const Center(
|
||||
child: Text('لا توجد مؤسسات', style: TextStyle(color: AppColor.textSecondary)),
|
||||
? Center(
|
||||
child: Text('لا توجد مؤسسات',
|
||||
style: TextStyle(color: cs.onSurfaceVariant)),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: c.fetchOrgs,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemCount: c.orgs.length,
|
||||
itemBuilder: (_, i) => _orgCard(c.orgs[i]),
|
||||
itemBuilder: (_, i) => _orgCard(c.orgs[i], cs),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -96,30 +98,30 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _orgCard(TransitOrgSummary org) {
|
||||
Widget _orgCard(TransitOrgSummary org, ColorScheme cs) {
|
||||
Color statusColor;
|
||||
switch (org.contractStatus) {
|
||||
case 'active':
|
||||
statusColor = AppColor.success;
|
||||
statusColor = const Color(0xFF10B981);
|
||||
break;
|
||||
case 'trial':
|
||||
statusColor = AppColor.info;
|
||||
statusColor = cs.tertiary;
|
||||
break;
|
||||
case 'suspended':
|
||||
statusColor = AppColor.warning;
|
||||
statusColor = cs.tertiary;
|
||||
break;
|
||||
default:
|
||||
statusColor = AppColor.danger;
|
||||
statusColor = cs.error;
|
||||
}
|
||||
|
||||
return Card(
|
||||
color: AppColor.surface,
|
||||
color: cs.surfaceContainerHighest,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
title: Text(org.nameAr,
|
||||
style: const TextStyle(color: AppColor.textPrimary, fontWeight: FontWeight.bold)),
|
||||
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.bold)),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Row(
|
||||
@@ -127,7 +129,7 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.15),
|
||||
color: statusColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(org.contractStatus,
|
||||
@@ -135,21 +137,21 @@ class _TransitOrgListPageState extends State<TransitOrgListPage> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text('${org.city} · ${org.country}',
|
||||
style: const TextStyle(color: AppColor.textSecondary, fontSize: 12)),
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
const Spacer(),
|
||||
Icon(Icons.directions_bus, size: 14, color: AppColor.textSecondary),
|
||||
Icon(Icons.directions_bus, size: 14, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 2),
|
||||
Text('${org.vehiclesCount}',
|
||||
style: const TextStyle(color: AppColor.textSecondary, fontSize: 12)),
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
const SizedBox(width: 10),
|
||||
Icon(Icons.people, size: 14, color: AppColor.textSecondary),
|
||||
Icon(Icons.people, size: 14, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 2),
|
||||
Text('${org.activeEnrollments}',
|
||||
style: const TextStyle(color: AppColor.textSecondary, fontSize: 12)),
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 14, color: AppColor.textSecondary),
|
||||
trailing: Icon(Icons.arrow_forward_ios, size: 14, color: cs.onSurfaceVariant),
|
||||
onTap: () => Get.to(() => TransitOrgDetailsPage(orgId: org.id, orgName: org.nameAr)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_admin/constant/theme.dart';
|
||||
import 'package:siro_admin/main.dart';
|
||||
import 'package:siro_admin/constant/box_name.dart';
|
||||
import 'package:siro_admin/views/admin/captain/captain.dart';
|
||||
import 'package:siro_admin/views/admin/passenger/passenger.dart';
|
||||
import 'package:siro_admin/views/admin/drivers/driver_tracker_screen.dart';
|
||||
import 'package:siro_admin/views/transit/org_list_page.dart';
|
||||
import 'package:siro_admin/views/admin/rides/rides.dart';
|
||||
import 'package:siro_admin/views/admin/static/static.dart';
|
||||
import 'package:siro_admin/controller/admin/static_controller.dart';
|
||||
import 'package:siro_admin/views/admin/drivers/monitor_ride.dart';
|
||||
|
||||
Reference in New Issue
Block a user