first commit
This commit is contained in:
144
siro_driver/lib/views/Rate/rate_app_page.dart
Executable file
144
siro_driver/lib/views/Rate/rate_app_page.dart
Executable file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../controller/rate/rate_app_controller.dart';
|
||||
|
||||
class RatingScreen extends StatelessWidget {
|
||||
final RatingController ratingController = Get.put(RatingController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(
|
||||
"Rate Our App".tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Obx(() {
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// App logo or visual
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image.asset(
|
||||
'assets/images/logo.gif',
|
||||
height: 120,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Rating Prompt
|
||||
Text(
|
||||
"How would you rate our app?".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Comment Box
|
||||
CupertinoTextField(
|
||||
controller: ratingController.comment,
|
||||
placeholder: 'Write your comment here'.tr,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: CupertinoColors.systemGrey4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
prefix: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(CupertinoIcons.pencil_ellipsis_rectangle,
|
||||
color: CupertinoColors.systemGrey),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Star Rating Section
|
||||
_buildRatingStars(),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Submit Button
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 14),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: CupertinoColors.activeGreen,
|
||||
onPressed: () {
|
||||
if (ratingController.userRating.value > 0) {
|
||||
ratingController
|
||||
.submitRating(ratingController.userRating.value);
|
||||
Get.snackbar(
|
||||
"Thank You!".tr,
|
||||
"Your rating has been submitted.".tr,
|
||||
backgroundColor: CupertinoColors.systemGrey6,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16),
|
||||
borderRadius: 12,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Error".tr,
|
||||
"Please select a rating before submitting.".tr,
|
||||
backgroundColor: CupertinoColors.systemRed,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(16),
|
||||
borderRadius: 12,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
"Submit Rating".tr,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget for building rating stars with animations
|
||||
Widget _buildRatingStars() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(5, (index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
ratingController.userRating.value = index + 1;
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(
|
||||
CupertinoIcons.star_fill,
|
||||
size: 40,
|
||||
color: index < ratingController.userRating.value
|
||||
? CupertinoColors.systemYellow
|
||||
: CupertinoColors.systemGrey3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
331
siro_driver/lib/views/Rate/rate_passenger.dart
Executable file
331
siro_driver/lib/views/Rate/rate_passenger.dart
Executable file
@@ -0,0 +1,331 @@
|
||||
import 'package:siro_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart'; // Import this for formatting
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/rate/rate_conroller.dart';
|
||||
|
||||
class RatePassenger extends StatelessWidget {
|
||||
final RateController controller = Get.put(RateController());
|
||||
|
||||
// Format: 1,234.5
|
||||
final NumberFormat currencyFormatter = NumberFormat("#,##0.0", "en_US");
|
||||
|
||||
RatePassenger({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Trip Completed'.tr),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: 0,
|
||||
),
|
||||
body: GetBuilder<RateController>(
|
||||
builder: (controller) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1. The HERO Section: Big Price Display
|
||||
_buildHeroPriceDisplay(context),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 2. Wallet Section (Conditional)
|
||||
if (controller.walletChecked != 'true')
|
||||
_buildWalletSection(context, controller),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 3. Rating Section
|
||||
_buildRatingSection(context, controller),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// 4. Submit Button
|
||||
SizedBox(
|
||||
height: 55,
|
||||
child: MyElevatedButton(
|
||||
title: 'Finish & Submit'.tr,
|
||||
onPressed: () => controller.addRateToPassenger(),
|
||||
// isFullWidth: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- WIDGETS ---
|
||||
|
||||
Widget _buildHeroPriceDisplay(BuildContext context) {
|
||||
final MapDriverController mapController = Get.find<MapDriverController>();
|
||||
|
||||
// Parse the string to double to format it correctly
|
||||
double amount =
|
||||
double.tryParse(mapController.paymentAmount.toString()) ?? 0.0;
|
||||
String formattedAmount = currencyFormatter.format(amount);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor, // Use your brand color or a dark color
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Collect Cash'.tr.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Currency Symbol (Small)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'SYP'.tr, // Replace with your local currency symbol if needed
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// The Price (Huge)
|
||||
Text(
|
||||
formattedAmount,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 56, // Very Large Font
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'Passenger: ${mapController.passengerName}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletSection(BuildContext context, RateController controller) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: controller.ispassengerWantWalletFromDriver
|
||||
? _buildAmountInput(context, controller)
|
||||
: _buildWalletQuery(context, controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWalletQuery(BuildContext context, RateController controller) {
|
||||
return Column(
|
||||
key: const ValueKey('walletQuery'),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child:
|
||||
const Icon(Icons.account_balance_wallet, color: Colors.amber),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Pay remaining to Wallet?".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {}, // Optional logic
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).hintColor,
|
||||
side: BorderSide(color: Theme.of(context).dividerColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Text('No'.tr),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => controller.passengerWantPay(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Text('Yes, Pay'.tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountInput(BuildContext context, RateController controller) {
|
||||
return Column(
|
||||
key: const ValueKey('amountInput'),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Enter Amount Paid".tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.passengerPayAmount,
|
||||
label: "Amount".tr,
|
||||
hint: "0.00",
|
||||
type: const TextInputType.numberWithOptions(decimal: true),
|
||||
// Suggestion: Add a suffix icon for currency if available in your widget
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => controller.addPassengerWallet(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Text("Confirm Payment".tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRatingSection(BuildContext context, RateController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Rate Passenger'.tr,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
RatingBar.builder(
|
||||
initialRating: 0,
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
allowHalfRating: false,
|
||||
itemCount: 5,
|
||||
itemSize: 45, // Large stars
|
||||
itemPadding: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||
itemBuilder: (context, _) => const Icon(
|
||||
Icons.star_rounded,
|
||||
color: Colors.amber,
|
||||
),
|
||||
onRatingUpdate: (rating) {
|
||||
controller.selectRateItem(rating);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Simplified comment box
|
||||
TextField(
|
||||
controller: controller.comment,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Any comments about the passenger?'.tr,
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
448
siro_driver/lib/views/Rate/ride_calculate_driver.dart
Executable file
448
siro_driver/lib/views/Rate/ride_calculate_driver.dart
Executable file
@@ -0,0 +1,448 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/info.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../controller/home/captin/duration_controller .dart';
|
||||
|
||||
class RideCalculateDriver extends StatelessWidget {
|
||||
RideCalculateDriver({super.key});
|
||||
// DurationController durationController = Get.put(DurationController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(DurationController());
|
||||
return MyScafolld(
|
||||
title: 'Ride Summaries'.tr,
|
||||
body: [
|
||||
GetBuilder<DurationController>(
|
||||
builder: (durationController) {
|
||||
if (durationController.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
bool hasDurations = durationController.jsonData1.isNotEmpty &&
|
||||
durationController.jsonData1['message'] != null &&
|
||||
(durationController.jsonData1['message'] as List).isNotEmpty;
|
||||
|
||||
bool hasRides = durationController.jsonData2.isNotEmpty &&
|
||||
durationController.jsonData2['message'] != null &&
|
||||
(durationController.jsonData2['message'] as List).isNotEmpty;
|
||||
|
||||
bool hasStats = durationController.monthlyList.isNotEmpty;
|
||||
|
||||
if (!hasDurations && !hasRides && !hasStats) {
|
||||
return Center(
|
||||
child: Text('No data yet!'.tr),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
if (hasDurations) ...[
|
||||
Text(
|
||||
'${'Average of Hours of'.tr} ${AppInformation.appName}${' is ON for this month'.tr}${' ${durationController.jsonData1['message'][0]['day'].toString().split('-')[1]}'.tr}',
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .4,
|
||||
child: LineChart(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.ease,
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isStepLineChart: true,
|
||||
spots: durationController.chartData,
|
||||
isCurved: true,
|
||||
color: Colors.deepPurpleAccent,
|
||||
barWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
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'.tr, style: AppStyle.title),
|
||||
axisNameSize: 30,
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'Total Hours on month'.tr,
|
||||
style: AppStyle.title),
|
||||
axisNameSize: 30,
|
||||
sideTitles: const SideTitles(
|
||||
reservedSize: 30, showTitles: true)),
|
||||
leftTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'Counts of Hours on days'.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (hasRides) ...[
|
||||
const SizedBox(height: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .4,
|
||||
child: LineChart(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.ease,
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: durationController.chartRideCount,
|
||||
color: Colors.deepPurpleAccent,
|
||||
barWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
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'.tr, style: AppStyle.title),
|
||||
axisNameSize: 30,
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'${"Total rides on month".tr} = ${durationController.jsonData2['message'][0]['totalCount'] ?? 0}'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
axisNameSize: 30,
|
||||
sideTitles: const SideTitles(
|
||||
reservedSize: 30, showTitles: true)),
|
||||
leftTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'Counts of rides on days'.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .4,
|
||||
child: LineChart(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.ease,
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isStepLineChart: true,
|
||||
spots: durationController.chartRidePriceDriver,
|
||||
isCurved: true,
|
||||
isStrokeCapRound: true,
|
||||
preventCurveOverShooting: true,
|
||||
color: Colors.deepPurpleAccent,
|
||||
barWidth: 3,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
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'.tr, style: AppStyle.title),
|
||||
axisNameSize: 30,
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'${"Total budgets on month".tr} = ${durationController.jsonData2['message'][0]['totalPrice'] ?? 0}'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
axisNameSize: 30,
|
||||
sideTitles: const SideTitles(
|
||||
reservedSize: 30, showTitles: true)),
|
||||
leftTitles: AxisTitles(
|
||||
axisNameWidget: Text(
|
||||
'Counts of budgets on days'.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: !hasStats
|
||||
? SizedBox(
|
||||
height: Get.height * .2,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No statistics yet".tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
)
|
||||
: DriverStatsTable(
|
||||
monthlyList: durationController.monthlyList,
|
||||
)))
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
// BarChartWidget(),
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
|
||||
class DriverStatsTable extends StatelessWidget {
|
||||
final List monthlyList;
|
||||
|
||||
const DriverStatsTable({Key? key, required this.monthlyList})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: DataTable(
|
||||
columnSpacing: 12,
|
||||
columns: [
|
||||
DataColumn(label: Text('Statistic'.tr)),
|
||||
DataColumn(label: Text('Value'.tr)),
|
||||
],
|
||||
rows: _buildRows(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DataRow> _buildRows() {
|
||||
return [
|
||||
DataRow(cells: [
|
||||
DataCell(Text(
|
||||
'Total Orders'.tr,
|
||||
style: AppStyle.title,
|
||||
)),
|
||||
DataCell(Text(
|
||||
monthlyList[0]['total_orders'].toString(),
|
||||
style: AppStyle.number,
|
||||
)),
|
||||
]),
|
||||
DataRow(cells: [
|
||||
DataCell(Text(
|
||||
'Completed'.tr,
|
||||
style: AppStyle.title,
|
||||
)),
|
||||
DataCell(Text(
|
||||
monthlyList[0]['completed_orders'].toString(),
|
||||
style: AppStyle.number,
|
||||
)),
|
||||
]),
|
||||
DataRow(cells: [
|
||||
DataCell(Text(
|
||||
'Canceled Orders'.tr,
|
||||
style: AppStyle.title,
|
||||
)),
|
||||
DataCell(Text(
|
||||
monthlyList[0]['canceled_orders'].toString(),
|
||||
style: AppStyle.number,
|
||||
)),
|
||||
]),
|
||||
DataRow(cells: [
|
||||
DataCell(Text(
|
||||
'Rejected Orders'.tr,
|
||||
style: AppStyle.title,
|
||||
)),
|
||||
DataCell(Text(
|
||||
monthlyList[0]['rejected_orders'].toString(),
|
||||
style: AppStyle.number,
|
||||
)),
|
||||
]),
|
||||
DataRow(cells: [
|
||||
DataCell(Text(
|
||||
'Percent Rejected'.tr,
|
||||
style: AppStyle.title,
|
||||
)),
|
||||
DataCell(Text(
|
||||
'${(monthlyList[0]['percent_rejected']).toString()}%',
|
||||
style: AppStyle.number.copyWith(
|
||||
color: double.parse(monthlyList[0]['percent_canceled']) < .3
|
||||
? AppColor.greenColor
|
||||
: AppColor.redColor),
|
||||
)),
|
||||
]),
|
||||
DataRow(cells: [
|
||||
DataCell(Text(
|
||||
'Percent Canceled'.tr,
|
||||
style: AppStyle.title,
|
||||
)),
|
||||
DataCell(Text(
|
||||
'${(monthlyList[0]['percent_canceled']).toString()}%',
|
||||
style: AppStyle.number.copyWith(
|
||||
color: double.parse(monthlyList[0]['percent_canceled']) < .3
|
||||
? AppColor.greenColor
|
||||
: AppColor.redColor),
|
||||
)),
|
||||
]),
|
||||
DataRow(cells: [
|
||||
DataCell(Text(
|
||||
'Percent Completed'.tr,
|
||||
style: AppStyle.title,
|
||||
)),
|
||||
DataCell(Text(
|
||||
'${(monthlyList[0]['percent_completed']).toString()}%',
|
||||
style: AppStyle.number.copyWith(
|
||||
color: double.parse(monthlyList[0]['percent_completed']) > .7
|
||||
? AppColor.greenColor
|
||||
: AppColor.redColor),
|
||||
)),
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class StaticDriverOrder extends StatelessWidget {
|
||||
const StaticDriverOrder({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.jsonTitle,
|
||||
}) : super(key: key);
|
||||
|
||||
final String title, jsonTitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
jsonTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
977
siro_driver/lib/views/auth/captin/ai_page.dart
Executable file
977
siro_driver/lib/views/auth/captin/ai_page.dart
Executable file
@@ -0,0 +1,977 @@
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/constant/table_names.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/register_captin_controller.dart';
|
||||
import 'package:siro_driver/controller/functions/ocr_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../controller/functions/gemeni.dart';
|
||||
|
||||
class AiPage extends StatelessWidget {
|
||||
ScanDocumentsByApi scanDocumentsByApi = Get.put(ScanDocumentsByApi());
|
||||
RegisterCaptainController registerCaptainController =
|
||||
Get.put(RegisterCaptainController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(AI());
|
||||
String text = '';
|
||||
return MyScafolld(
|
||||
title: 'Documents check'.tr,
|
||||
body: [
|
||||
GetBuilder<AI>(builder: (controller) {
|
||||
return controller.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
// egyptDriverLicense(),
|
||||
// egyptCarLicenceFront(),
|
||||
// egyptCarLicenceBack(),
|
||||
// egyptDriverIDFront(),
|
||||
// egyptDriverIDBack(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
|
||||
GetBuilder<AI> egyptDriverLicenseWidget() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (contentController) => contentController.responseMap.isNotEmpty
|
||||
? contentController.isloading
|
||||
? Column(
|
||||
children: [
|
||||
const MyCircularProgressIndicator(),
|
||||
Text(
|
||||
'We are process picture please wait '.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
)
|
||||
: SizedBox(
|
||||
height: Get.height * .7,
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
// height: Get.height * .4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: contentController.responseMap.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Capture an Image of Your Driver’s License'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${'Name'.tr} :${(contentController.responseMap['first_name'])}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
' ${(contentController.responseMap['last_name'])}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'${'Name in arabic'.tr}: ${(contentController.responseMap['name_in_arabic'])}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Drivers License Class'.tr}: ${contentController.responseMap['class']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'National Number'.tr}: ${(contentController.responseMap['id'])}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
// Image.memory(
|
||||
// scanDocumentsByApi
|
||||
// .imagePortrait,
|
||||
// width: 60,
|
||||
// ),
|
||||
]),
|
||||
Text(
|
||||
'${'Address'.tr}: ${(contentController.responseMap['address'])}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Date of Birth'.tr}: ${contentController.responseMap['dob']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Age'.tr} : ${contentController.responseMap['age_in_years']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Expiry Date'.tr}: ${contentController.responseMap['expiration_date']}',
|
||||
style: DateTime.parse(
|
||||
contentController
|
||||
.responseMap[
|
||||
'expiration_date']
|
||||
.toString())
|
||||
.isBefore(
|
||||
contentController.now)
|
||||
? AppStyle.title.copyWith(
|
||||
color: AppColor.redColor)
|
||||
: AppStyle.title.copyWith(
|
||||
color: AppColor.greenColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
DateTime.parse(contentController
|
||||
.responseMap['expiration_date']
|
||||
.toString())
|
||||
.isBefore(contentController.now)
|
||||
? Text(
|
||||
'You can\'t continue with us .\nYou should renew Driver license'
|
||||
.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.redColor),
|
||||
)
|
||||
: MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Lets check Car license '.tr,
|
||||
onPressed: () => contentController
|
||||
.getCarLicenseJordanContent()),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
contentController.responseCarLicenseMapJordan.isNotEmpty
|
||||
? Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
// height: Get.height * .3,
|
||||
width: Get.width * .9,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'${'Name'.tr} ${contentController.responseCarLicenseMapJordan['name']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Address'.tr} ${contentController.responseCarLicenseMapJordan['address']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Car Kind'.tr} ${contentController.responseCarLicenseMapJordan['car_kind']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Color'.tr} ${contentController.responseCarLicenseMapJordan['car_color']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Year'.tr} ${contentController.responseCarLicenseMapJordan['car_year']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Car Plate'.tr} ${contentController.responseCarLicenseMapJordan['car_plate']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Car Expire'.tr} ${contentController.responseCarLicenseMapJordan['expire_date_of_license']}',
|
||||
style: contentController
|
||||
.responseCarLicenseMapJordan
|
||||
.isNotEmpty
|
||||
? DateTime.parse(contentController
|
||||
.responseCarLicenseMapJordan[
|
||||
'expire_date_of_license']
|
||||
.toString())
|
||||
.isBefore(contentController.now)
|
||||
? AppStyle.title.copyWith(
|
||||
color: AppColor.redColor)
|
||||
: AppStyle.title.copyWith(
|
||||
color: AppColor.greenColor)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
// DateTime.parse(contentController
|
||||
// .responseCarLicenseMap[
|
||||
// 'expire_date_of_license']
|
||||
// .toString())
|
||||
// .isBefore(contentController.now)
|
||||
// ? Text(
|
||||
// 'You can\'t continue with us .\nYou should renew Car license'
|
||||
// .tr,
|
||||
// style: AppStyle.title.copyWith(
|
||||
// color: AppColor.redColor),
|
||||
// )
|
||||
// :
|
||||
MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Lets check License Back Face'.tr,
|
||||
onPressed: () => contentController
|
||||
.generateBackCarLicenseJordanContent()),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
contentController.responseBackCarLicenseMap.isNotEmpty
|
||||
? Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
// height: 300,
|
||||
child: Column(children: [
|
||||
Text(
|
||||
'VIN ${contentController.responseBackCarLicenseMap['vin']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Fuel Type ${contentController.responseBackCarLicenseMap['fuelType']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Insurance Company ${contentController.responseBackCarLicenseMap['insuranceCompany']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Policy Number ${contentController.responseBackCarLicenseMap['policyNumber']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Insurance Type ${contentController.responseBackCarLicenseMap['insuranceType']}',
|
||||
style: AppStyle.title,
|
||||
)
|
||||
]))
|
||||
: const SizedBox()
|
||||
],
|
||||
),
|
||||
)
|
||||
: Positioned(
|
||||
top: Get.height * .06,
|
||||
left: Get.width * .051,
|
||||
right: Get.width * .051,
|
||||
child: scanDocumentsByApi.isLoading
|
||||
? Column(
|
||||
children: [
|
||||
const MyCircularProgressIndicator(),
|
||||
Text(
|
||||
'We are process picture please wait '.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .35,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await CRUD().allMethodForAI(
|
||||
'name,address,dob,nationalNo,',
|
||||
AppLink.uploadEgypt,
|
||||
'idFront'); //egypt
|
||||
},
|
||||
child: Text(
|
||||
'Take Picture Of ID Card'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> jordanDriverLicenseWidget() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (contentController) => contentController.responseMap.isNotEmpty
|
||||
? Positioned(
|
||||
top: Get.height * .09,
|
||||
left: Get.width * .051,
|
||||
right: Get.width * .051,
|
||||
child: contentController.isloading
|
||||
? Column(
|
||||
children: [
|
||||
const MyCircularProgressIndicator(),
|
||||
Text(
|
||||
'We are process picture please wait '.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
)
|
||||
: SizedBox(
|
||||
height: Get.height * .7,
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
// height: Get.height * .4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: contentController.responseMap.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'There is no data yet.'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${'Name'.tr} :${contentController.responseMap['first_name']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
' ${contentController.responseMap['last_name']}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'${'Name in arabic'.tr}: ${contentController.responseMap['name_in_arabic']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Drivers License Class'.tr}: ${contentController.responseMap['class']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'National Number'.tr}: ${contentController.responseMap['id']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
// Image.memory(
|
||||
// scanDocumentsByApi
|
||||
// .imagePortrait,
|
||||
// width: 60,
|
||||
// ),
|
||||
]),
|
||||
Text(
|
||||
'${'Address'.tr}: ${contentController.responseMap['address']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Date of Birth'.tr}: ${contentController.responseMap['dob']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Age'.tr} : ${contentController.responseMap['age_in_years']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Expiry Date'.tr}: ${contentController.responseMap['expiration_date']}',
|
||||
style: DateTime.parse(
|
||||
contentController
|
||||
.responseMap[
|
||||
'expiration_date']
|
||||
.toString())
|
||||
.isBefore(
|
||||
contentController.now)
|
||||
? AppStyle.title.copyWith(
|
||||
color: AppColor.redColor)
|
||||
: AppStyle.title.copyWith(
|
||||
color:
|
||||
AppColor.greenColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
DateTime.parse(contentController
|
||||
.responseMap['expiration_date']
|
||||
.toString())
|
||||
.isBefore(contentController.now)
|
||||
? Text(
|
||||
'You can\'t continue with us .\nYou should renew Driver license'
|
||||
.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.redColor),
|
||||
)
|
||||
: MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Lets check Car license '.tr,
|
||||
onPressed: () => contentController
|
||||
.getTextFromCard(
|
||||
'''Extract the following information from the front face of the car license card in Jordan:
|
||||
|
||||
* name
|
||||
* Address
|
||||
* Vehicle type
|
||||
* car_kind
|
||||
* car_color
|
||||
* Vehicle category
|
||||
* car_year
|
||||
* car_plate
|
||||
* Registration type
|
||||
* Usage type
|
||||
* expire_date_of_license
|
||||
|
||||
Output the extracted information in the following JSON formate and make date format like YYYY-MM-DD''')),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
contentController
|
||||
.responseCarLicenseMapJordan.isNotEmpty
|
||||
? Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
// height: Get.height * .3,
|
||||
width: Get.width * .9,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'${'Name'.tr} ${contentController.responseCarLicenseMapJordan['name']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Address'.tr} ${contentController.responseCarLicenseMapJordan['address']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Car Kind'.tr} ${contentController.responseCarLicenseMapJordan['car_kind']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Color'.tr} ${contentController.responseCarLicenseMapJordan['car_color']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Year'.tr} ${contentController.responseCarLicenseMapJordan['car_year']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'Car Plate'.tr} ${contentController.responseCarLicenseMapJordan['car_plate']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'${'ُExpire Date'.tr} ${contentController.responseCarLicenseMapJordan['expire_date_of_license']}',
|
||||
style: contentController
|
||||
.responseCarLicenseMapJordan
|
||||
.isNotEmpty
|
||||
? DateTime.parse(contentController
|
||||
.responseCarLicenseMapJordan[
|
||||
'expire_date_of_license']
|
||||
.toString())
|
||||
.isBefore(
|
||||
contentController.now)
|
||||
? AppStyle.title.copyWith(
|
||||
color: AppColor.redColor)
|
||||
: AppStyle.title.copyWith(
|
||||
color: AppColor.greenColor)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
//todo temporary
|
||||
// DateTime.parse(contentController
|
||||
// .responseCarLicenseMap[
|
||||
// 'expire_date_of_license']
|
||||
// .toString())
|
||||
// .isBefore(contentController.now)
|
||||
// ? Text(
|
||||
// 'You can\'t continue with us .\nYou should renew Car license'
|
||||
// .tr,
|
||||
// style: AppStyle.title.copyWith(
|
||||
// color: AppColor.redColor),
|
||||
// )
|
||||
// :
|
||||
MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Lets check License Back Face'.tr,
|
||||
onPressed: () =>
|
||||
contentController.getTextFromCard(
|
||||
'write json output from extracting car license back face for these key ,vin,fuelType,passengerType,curbWeight,insuranceCompany,policyNumber,notes,insuranceType and output it json .dont add data else this image',
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
contentController.responseBackCarLicenseMap.isNotEmpty
|
||||
? Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
// height: 300,
|
||||
child: Column(children: [
|
||||
Text(
|
||||
'VIN ${contentController.responseBackCarLicenseMap['vin']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Fuel Type ${contentController.responseBackCarLicenseMap['fuelType']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Insurance Company ${contentController.responseBackCarLicenseMap['insuranceCompany']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Policy Number ${contentController.responseBackCarLicenseMap['policyNumber']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Insurance Type ${contentController.responseBackCarLicenseMap['insuranceType']}',
|
||||
style: AppStyle.title,
|
||||
)
|
||||
]))
|
||||
: const SizedBox()
|
||||
// MyElevatedButton(
|
||||
// title: 'Detect Your Face '.tr,
|
||||
// onPressed: () => scanDocumentsByApi
|
||||
// .checkMatchFaceApi(),
|
||||
// ),
|
||||
// scanDocumentsByApi.res.isEmpty
|
||||
// ? const SizedBox()
|
||||
// : scanDocumentsByApi.res['data']
|
||||
// ['result']
|
||||
// .toString() ==
|
||||
// 'Same'
|
||||
// ? MyElevatedButton(
|
||||
// onPressed: () async {
|
||||
// await registerCaptainController
|
||||
// .register();
|
||||
// await registerCaptainController
|
||||
// .addLisence();
|
||||
// // await scanDocumentsByApi
|
||||
// // .uploadImagePortrate();
|
||||
// },
|
||||
// title:
|
||||
// 'Go to next step\nscan Car License.'
|
||||
// .tr,
|
||||
// kolor: AppColor.greenColor,
|
||||
// )
|
||||
// : const SizedBox(),
|
||||
// MyElevatedButton(
|
||||
// title: 'get sql data',
|
||||
// kolor: AppColor.yellowColor,
|
||||
// onPressed: () {
|
||||
// sql.deleteAllData(
|
||||
// TableName.faceDetectTimes);
|
||||
// sql
|
||||
// .getAllData(
|
||||
// TableName.faceDetectTimes)
|
||||
// value[0]['faceDetectTimes']));
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Positioned(
|
||||
top: Get.height * .06,
|
||||
left: Get.width * .051,
|
||||
right: Get.width * .051,
|
||||
child: scanDocumentsByApi.isLoading
|
||||
? Column(
|
||||
children: [
|
||||
const MyCircularProgressIndicator(),
|
||||
Text(
|
||||
'We are process picture please wait '.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
height: Get.height * .35,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'There is no data yet.'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<ScanDocumentsByApi> usaDriverLicensWidget() {
|
||||
return GetBuilder<ScanDocumentsByApi>(
|
||||
builder: (scanDocumentsByApi) => scanDocumentsByApi.responseMap.isNotEmpty
|
||||
? Positioned(
|
||||
top: Get.height * .06,
|
||||
left: Get.width * .051,
|
||||
right: Get.width * .051,
|
||||
child: scanDocumentsByApi.isLoading
|
||||
? Column(
|
||||
children: [
|
||||
const MyCircularProgressIndicator(),
|
||||
Text(
|
||||
'We are process picture please wait '.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
height: Get.height * .4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: scanDocumentsByApi.responseMap.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'There is no data yet.'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${'Name :'.tr}${scanDocumentsByApi.name}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Drivers License Class: '.tr}${scanDocumentsByApi.licenseClass}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Image.memory(
|
||||
scanDocumentsByApi
|
||||
.imageSignature,
|
||||
width: 100,
|
||||
height: 30,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Document Number: '.tr}${scanDocumentsByApi.documentNo}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Image.memory(
|
||||
scanDocumentsByApi.imagePortrait,
|
||||
width: 60,
|
||||
),
|
||||
]),
|
||||
Text(
|
||||
'${'Address: '.tr}${scanDocumentsByApi.address}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Height: '.tr}${scanDocumentsByApi.height}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Postal Code: ${scanDocumentsByApi.postalCode}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
'Sex: ${scanDocumentsByApi.sex}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Territorial Code: ${scanDocumentsByApi.stateCode}',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Expiry Date: '.tr}${scanDocumentsByApi.expireDate}',
|
||||
style: DateTime.parse(
|
||||
scanDocumentsByApi
|
||||
.responseMap['data']
|
||||
['ocr']
|
||||
['dateOfExpiry']
|
||||
.toString())
|
||||
.isBefore(
|
||||
scanDocumentsByApi.now)
|
||||
? AppStyle.title.copyWith(
|
||||
color: AppColor.redColor)
|
||||
: AppStyle.title.copyWith(
|
||||
color: AppColor.greenColor),
|
||||
),
|
||||
Text(
|
||||
'${'Date of Birth: '.tr}${scanDocumentsByApi.dob}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
DateTime.parse(scanDocumentsByApi.responseMap['data']
|
||||
['ocr']['dateOfExpiry']
|
||||
.toString())
|
||||
.isBefore(scanDocumentsByApi.now)
|
||||
? Text(
|
||||
'You can\'t continue with us .\nYou should renew Driver license',
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.redColor),
|
||||
)
|
||||
: MyElevatedButton(
|
||||
title: 'Detect Your Face '.tr,
|
||||
onPressed: () =>
|
||||
scanDocumentsByApi.checkMatchFaceApi(),
|
||||
),
|
||||
scanDocumentsByApi.res.isEmpty
|
||||
? const SizedBox()
|
||||
: scanDocumentsByApi.res['data']['result']
|
||||
.toString() ==
|
||||
'Same'
|
||||
? MyElevatedButton(
|
||||
onPressed: () async {
|
||||
await registerCaptainController
|
||||
.register();
|
||||
await registerCaptainController
|
||||
.addLisence();
|
||||
// await scanDocumentsByApi
|
||||
// .uploadImagePortrate();
|
||||
},
|
||||
title:
|
||||
'Go to next step\nscan Car License.'.tr,
|
||||
kolor: AppColor.greenColor,
|
||||
)
|
||||
: const SizedBox(),
|
||||
MyElevatedButton(
|
||||
title: 'get sql data',
|
||||
kolor: AppColor.yellowColor,
|
||||
onPressed: () {
|
||||
sql.deleteAllData(TableName.faceDetectTimes);
|
||||
sql.getAllData(TableName.faceDetectTimes);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: Positioned(
|
||||
top: Get.height * .06,
|
||||
left: Get.width * .051,
|
||||
right: Get.width * .051,
|
||||
child: scanDocumentsByApi.isLoading
|
||||
? Column(
|
||||
children: [
|
||||
const MyCircularProgressIndicator(),
|
||||
Text(
|
||||
'We are process picture please wait '.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
height: Get.height * .35,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'There is no data yet.'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EgyptDocuments extends StatelessWidget {
|
||||
const EgyptDocuments({
|
||||
super.key,
|
||||
required this.contentController,
|
||||
});
|
||||
|
||||
final AI contentController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 3,
|
||||
left: Get.width * .1,
|
||||
right: Get.width * .1,
|
||||
child: MyElevatedButton(
|
||||
title: 'Take Picture Of ID Card'.tr, //egypt
|
||||
onPressed: () async {
|
||||
await CRUD().allMethodForAI('name,address,dob,nationalNo,',
|
||||
AppLink.uploadEgypt, 'idFront'); //egypt
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class JordanDocumants extends StatelessWidget {
|
||||
const JordanDocumants({
|
||||
super.key,
|
||||
required this.contentController,
|
||||
});
|
||||
|
||||
final AI contentController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 3,
|
||||
left: Get.width * .1,
|
||||
right: Get.width * .1,
|
||||
child: MyElevatedButton(
|
||||
title: 'Take Picture Of Driver License Card'.tr,
|
||||
onPressed: () {
|
||||
contentController.getDriverLicenseJordanContent();
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class UsaAiDocuments extends StatelessWidget {
|
||||
const UsaAiDocuments({
|
||||
super.key,
|
||||
required this.scanDocumentsByApi,
|
||||
});
|
||||
|
||||
final ScanDocumentsByApi scanDocumentsByApi;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 3,
|
||||
left: Get.width * .2,
|
||||
right: Get.width * .2,
|
||||
child: MyElevatedButton(
|
||||
title: 'Take Picture Of ID Card'.tr,
|
||||
onPressed: () {
|
||||
scanDocumentsByApi.scanDocumentsByApi();
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
83
siro_driver/lib/views/auth/captin/camera_widgets/camera_lisence_page.dart
Executable file
83
siro_driver/lib/views/auth/captin/camera_widgets/camera_lisence_page.dart
Executable file
@@ -0,0 +1,83 @@
|
||||
// import 'package:camera/camera.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
// import '../../../../constant/colors.dart';
|
||||
// import '../../../../constant/style.dart';
|
||||
// import '../../../../controller/functions/camer_controller.dart';
|
||||
// import '../../../../controller/functions/ocr_controller.dart';
|
||||
// import '../../../widgets/my_scafold.dart';
|
||||
|
||||
// class CameraLisencePage extends StatelessWidget {
|
||||
// CameraLisencePage.CameraLicensePage({super.key});
|
||||
// final CameraClassController cameraClassController =
|
||||
// Get.put(CameraClassController());
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: 'Scan Driver License'.tr,
|
||||
// body: [
|
||||
// Column(children: [
|
||||
// Text(
|
||||
// 'Please put your licence in these border'.tr,
|
||||
// style: AppStyle.title.copyWith(color: AppColor.greenColor),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
// child: GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) =>
|
||||
// cameraClassController.isCameraInitialized
|
||||
// ? Stack(
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: CameraPreview(
|
||||
// cameraClassController.cameraController,
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: Get.height * .1,
|
||||
// right: 5,
|
||||
// left: 5,
|
||||
// child: Container(
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width * .9,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.yellowColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// : Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Camera not initialized yet',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 20,
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Take Image'.tr,
|
||||
// onPressed: () {
|
||||
// ScanDocumentsByApi().scanDocumentsByApi();
|
||||
// },
|
||||
// )
|
||||
// ]),
|
||||
// ],
|
||||
// isleading: true,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
169
siro_driver/lib/views/auth/captin/car_license_page.dart
Executable file
169
siro_driver/lib/views/auth/captin/car_license_page.dart
Executable file
@@ -0,0 +1,169 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/functions/ocr_controller.dart';
|
||||
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/auth/captin/ml_google_doc.dart';
|
||||
import '../../../controller/auth/captin/register_captin_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
|
||||
// class CarLicensePage extends StatelessWidget {
|
||||
// CarLicensePage({super.key});
|
||||
// // CarRegistrationRecognizerController carRegistrationRecognizerController =
|
||||
// // Get.put(CarRegistrationRecognizerController());
|
||||
// RegisterCaptainController registerCaptainController =
|
||||
// Get.put(RegisterCaptainController());
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// Get.find<ScanDocumentsByApi>().uploadImagePortrate();
|
||||
// return MyScafolld(
|
||||
// title: 'Car License Card'.tr,
|
||||
// body: [
|
||||
// Positioned(
|
||||
// top: 3,
|
||||
// left: Get.width * .2,
|
||||
// right: Get.width * .2,
|
||||
// child: MyElevatedButton(
|
||||
// title: 'Take Picture Of ID Card'.tr,
|
||||
// onPressed: () async {
|
||||
// //0vQRyaYYDWpsv73A5CZOknseK7S2sgwE
|
||||
// //3vQRyaYYSWpmv69A58ZOkxmeK6M1mgwEDlXrXlBl
|
||||
// //0pALdqDDYHvzp73Q59SIgbzjG7Z2zkhJXr
|
||||
// // String? visionApi = AK.serverPHP;
|
||||
// await carRegistrationRecognizerController.scanText();
|
||||
// },
|
||||
// )),
|
||||
// Positioned(
|
||||
// top: 50,
|
||||
// child: SizedBox(
|
||||
// height: Get.height * .6,
|
||||
// width: Get.width,
|
||||
// child: buildImageWithBoundingBoxes(),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// bottom: Get.height * .2,
|
||||
// left: Get.width * .2,
|
||||
// right: Get.width * .2,
|
||||
// child: MyElevatedButton(
|
||||
// title: 'Register'.tr,
|
||||
// onPressed: () async {
|
||||
// // registerCaptainController.addLisence();
|
||||
// // registerCaptainController.register();
|
||||
// registerCaptainController.addRegisrationCarForDriver(
|
||||
// carRegistrationRecognizerController.extracted['vin'],
|
||||
// carRegistrationRecognizerController.extracted['make'],
|
||||
// carRegistrationRecognizerController.extracted['model'],
|
||||
// carRegistrationRecognizerController.extracted['year'],
|
||||
// carRegistrationRecognizerController.extracted['color'],
|
||||
// carRegistrationRecognizerController.extracted['owner'],
|
||||
// carRegistrationRecognizerController
|
||||
// .extracted['expiration_date'],
|
||||
// carRegistrationRecognizerController
|
||||
// .extracted['registration_date'],
|
||||
// );
|
||||
// },
|
||||
// )),
|
||||
// ],
|
||||
// isleading: true);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Widget buildImageWithBoundingBoxes() {
|
||||
// Get.put(CarRegistrationRecognizerController());
|
||||
// return GetBuilder<CarRegistrationRecognizerController>(
|
||||
// builder: (carRegistrationRecognizerController) =>
|
||||
// carRegistrationRecognizerController.image == null ||
|
||||
// carRegistrationRecognizerController.extracted.isEmpty
|
||||
// ? Center(
|
||||
// child: Text(
|
||||
// 'No image selected yet'.tr,
|
||||
// style: AppStyle.headTitle2,
|
||||
// ))
|
||||
// : Column(
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: Get.width * .8,
|
||||
// height: Get.width * .5,
|
||||
// child: Image.file(
|
||||
// File(carRegistrationRecognizerController
|
||||
// .croppedFile!.path),
|
||||
// // fit: BoxFit.fill,
|
||||
// )),
|
||||
// const SizedBox(
|
||||
// height: 20,
|
||||
// ),
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.width * .5,
|
||||
// width: Get.width * .9,
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// Text(
|
||||
// '${'Made :'.tr}${carRegistrationRecognizerController.extracted['make']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// Text(
|
||||
// '${'model :'.tr}${carRegistrationRecognizerController.extracted['model']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// Text(
|
||||
// '${'VIN :'.tr}${carRegistrationRecognizerController.extracted['vin']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// Text(
|
||||
// '${'year :'.tr}${carRegistrationRecognizerController.extracted['year']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// Column(
|
||||
// children: [
|
||||
// Text(
|
||||
// 'expiration date :${carRegistrationRecognizerController.extracted['expiration_date']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// Text(
|
||||
// 'registration date :${carRegistrationRecognizerController.extracted['registration_date']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Text(
|
||||
// 'color :${carRegistrationRecognizerController.extracted['color']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// Text(
|
||||
// 'owner :${carRegistrationRecognizerController.extracted['owner']}',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// )
|
||||
// ],
|
||||
// ));
|
||||
// }
|
||||
80
siro_driver/lib/views/auth/captin/cards/gemini_egypt.dart
Executable file
80
siro_driver/lib/views/auth/captin/cards/gemini_egypt.dart
Executable file
@@ -0,0 +1,80 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class GeminiEgypt extends GetxController {
|
||||
Map<String, dynamic> responseIdCardDriverEgypt = {};
|
||||
String? responseIdCardDriverEgypt1;
|
||||
|
||||
Future geminiAiExtraction(String prompt, payload) async {
|
||||
var requestBody = jsonEncode({
|
||||
'contents': [
|
||||
{
|
||||
'parts': [
|
||||
// {
|
||||
// 'inlineData': {
|
||||
// 'mimeType': 'image/jpeg',
|
||||
// 'data': imageData,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
'text': """
|
||||
$payload
|
||||
|
||||
$prompt ,and make dates format like year-month-day"""
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'generationConfig': {
|
||||
'temperature': 0.4,
|
||||
'topK': 32,
|
||||
'topP': 1,
|
||||
'maxOutputTokens': 4096,
|
||||
'stopSequences': [],
|
||||
},
|
||||
'safety_settings': [
|
||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
||||
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "BLOCK_NONE"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "BLOCK_NONE"
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(
|
||||
// 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${AK.geminiApi}'),
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent?key=AIzaSyCyoLcSkDzK5_SMe00nhut56SSXWPR074w'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var responseData = jsonDecode(response.body);
|
||||
// Process the responseData as needed
|
||||
|
||||
var result = responseData['candidates'][0]['content']['parts'][0]['text'];
|
||||
RegExp regex = RegExp(r"```json([^`]*)```");
|
||||
String? jsonString =
|
||||
regex.firstMatch(responseData.toString())?.group(1)?.trim();
|
||||
|
||||
if (jsonString != null) {
|
||||
// Convert the JSON object to a String
|
||||
jsonString = jsonEncode(json.decode(jsonString));
|
||||
responseIdCardDriverEgypt1 = jsonString;
|
||||
|
||||
responseIdCardDriverEgypt = jsonDecode(responseIdCardDriverEgypt1!);
|
||||
update();
|
||||
return responseIdCardDriverEgypt;
|
||||
} else {}
|
||||
|
||||
// Rest of your code...
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
221
siro_driver/lib/views/auth/captin/cards/sms_signup.dart
Executable file
221
siro_driver/lib/views/auth/captin/cards/sms_signup.dart
Executable file
@@ -0,0 +1,221 @@
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/register_captin_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../Rate/rate_app_page.dart';
|
||||
|
||||
class SmsSignupEgypt extends StatelessWidget {
|
||||
SmsSignupEgypt({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(RegisterCaptainController());
|
||||
return MyScafolld(
|
||||
title: 'Phone Check'.tr,
|
||||
body: [
|
||||
GetBuilder<RegisterCaptainController>(
|
||||
builder: (registerCaptainController) {
|
||||
return ListView(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Logo at the top
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20.0),
|
||||
child: Image.asset(
|
||||
'assets/images/logo.gif', // Make sure you have a logo image in your assets folder
|
||||
height: 100,
|
||||
),
|
||||
),
|
||||
// Message to the driver
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Text(
|
||||
'We need your phone number to contact you and to help you receive orders.'
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
// Enter phone number text
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Text(
|
||||
'Enter your phone number'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
// Phone number input field
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: !registerCaptainController.isSent
|
||||
? Form(
|
||||
key: registerCaptainController.formKey3,
|
||||
child: MyTextForm(
|
||||
controller:
|
||||
registerCaptainController.phoneController,
|
||||
label: 'Enter your phone number'.tr,
|
||||
hint: 'Enter your phone number'.tr,
|
||||
type: TextInputType.phone),
|
||||
)
|
||||
: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
registerCaptainController.phoneController.text,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: registerCaptainController.isSent
|
||||
? Form(
|
||||
key: registerCaptainController.formKey3,
|
||||
child: MyTextForm(
|
||||
controller: registerCaptainController.verifyCode,
|
||||
label: '5 digit'.tr,
|
||||
hint: '5 digit'.tr,
|
||||
type: TextInputType.number),
|
||||
)
|
||||
: const SizedBox()),
|
||||
// Submit button
|
||||
registerCaptainController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: MyElevatedButton(
|
||||
onPressed: () async {
|
||||
!registerCaptainController.isSent
|
||||
? await registerCaptainController.sendOtpMessage()
|
||||
: await registerCaptainController.verifySMSCode();
|
||||
},
|
||||
title: 'Submit'.tr,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: MyElevatedButton(
|
||||
kolor: AppColor.yellowColor,
|
||||
title: "Rate Our App".tr,
|
||||
onPressed: () {
|
||||
Get.to(RatingScreen());
|
||||
}),
|
||||
),
|
||||
|
||||
// IconButton(
|
||||
// onPressed: () async {
|
||||
// // final plainText =
|
||||
// // 'https://server.sefer.live/sefer.click/sefer';
|
||||
// // debugPrint('Plain Text: $plainText');
|
||||
|
||||
// // Encrypt the data
|
||||
// // final encryptedData = encryptionHelper.encryptData(plainText);
|
||||
// // debugPrint('Encrypted: $encryptedData');
|
||||
|
||||
// // Decrypt the data
|
||||
// // final decryptedData = encryptionHelper.decryptData(
|
||||
// // '2unGmj8jSMFBfxqH8+GN'); // Use the encryptedData variable
|
||||
// // debugPrint('Decrypted: $decryptedData');
|
||||
// // box.remove('DriversSecure');
|
||||
// var drivers0 = await CRUD().get(
|
||||
// link:
|
||||
// 'https://server.sefer.live/sefer.click/sefer/auth/captin/getAllDriverSecure.php',
|
||||
// payload: {});
|
||||
// var decodedDriver;
|
||||
// if (drivers0 != 'failure') {
|
||||
// decodedDriver = jsonDecode(drivers0);
|
||||
|
||||
// // // // box.write('DriversSecure', decodedDriver['message']);
|
||||
// }
|
||||
// var drivers = decodedDriver['message'];
|
||||
// Log.print('drivers.length: ${drivers.length}');
|
||||
// for (var i = 0; i < drivers.length; i++) {
|
||||
// Log.print('id: ${drivers[i]['id']}');
|
||||
// var payload = {
|
||||
// "phone": encryptionHelper
|
||||
// .encryptData(drivers[i]['phone'].toString()),
|
||||
// "email": encryptionHelper
|
||||
// .encryptData(drivers[i]['email'].toString()),
|
||||
// "gender": encryptionHelper
|
||||
// .encryptData(drivers[i]['gender'] ?? 'unknown'),
|
||||
// "birthdate": encryptionHelper
|
||||
// .encryptData(drivers[i]['birthdate'].toString()),
|
||||
// "first_name": encryptionHelper
|
||||
// .encryptData(drivers[i]['first_name'].toString()),
|
||||
// "last_name": encryptionHelper
|
||||
// .encryptData(drivers[i]['last_name'].toString()),
|
||||
// "sosPhone": encryptionHelper
|
||||
// .encryptData(drivers[i]['sosPhone'].toString()),
|
||||
// // "name_english": encryptionHelper
|
||||
// // .encryptData(drivers[i]['name_english'].toString()),
|
||||
// // "last_name": encryptionHelper
|
||||
// // .encryptData(drivers[i]['last_name'].toString()),
|
||||
// // "sosPhone": encryptionHelper
|
||||
// // .encryptData(drivers[i]['sosPhone'].toString()),
|
||||
// // "address": encryptionHelper
|
||||
// // .encryptData(drivers[i]['address'].toString()),
|
||||
// // "card_id": encryptionHelper
|
||||
// // .encryptData(drivers[i]['card_id'].toString()),
|
||||
// // "occupation": encryptionHelper
|
||||
// // .encryptData(drivers[i]['occupation'].toString()),
|
||||
// // "religion": encryptionHelper
|
||||
// // .encryptData(drivers[i]['religion'].toString()),
|
||||
// // "site": encryptionHelper
|
||||
// // .encryptData(drivers[i]['site'].toString()),
|
||||
// // "education": encryptionHelper
|
||||
// // .encryptData(drivers[i]['education'].toString()),
|
||||
// // "accountBank": encryptionHelper
|
||||
// // .encryptData(drivers[i]['accountBank'].toString()),
|
||||
// // "employmentType": encryptionHelper
|
||||
// // .encryptData(drivers[i]['employmentType'].toString()),
|
||||
// // "maritalStatus": (drivers[i]['maritalStatus'].toString()),
|
||||
// // "fullNameMaritial": encryptionHelper.encryptData(
|
||||
// // drivers[i]['fullNameMaritial'].toString()),
|
||||
// 'id': drivers[i]['id'].toString()
|
||||
// };
|
||||
// // print(drivers[i]['idn']);
|
||||
// // if (drivers[i]['id'].toString() !=
|
||||
// // '01002165502a9sHC1tbrUrUw') {
|
||||
// var result = await CRUD().post(
|
||||
// link:
|
||||
// 'https://server.sefer.live/sefer.click/sefer/auth/captin/updateDriverSecure.php',
|
||||
// payload: payload);
|
||||
// if (result != 'failure') {
|
||||
// print(result);
|
||||
// } else {
|
||||
// print('failure');
|
||||
// }
|
||||
// // Future.delayed(Duration(microseconds: 200));
|
||||
// // }
|
||||
// }
|
||||
// MyDialog().getDialog('title', 'midTitle', () {
|
||||
// Get.back();
|
||||
// });
|
||||
// },
|
||||
// icon: const Icon(
|
||||
// FontAwesome5.grin_tears,
|
||||
// size: 29,
|
||||
// color: AppColor.blueColor,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
isleading: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
987
siro_driver/lib/views/auth/captin/cards/syrian_card_a_i.dart
Executable file
987
siro_driver/lib/views/auth/captin/cards/syrian_card_a_i.dart
Executable file
@@ -0,0 +1,987 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/functions/audio_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/links.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/functions/gemeni.dart';
|
||||
import '../../../../controller/functions/package_info.dart';
|
||||
import '../../../../controller/functions/tts.dart';
|
||||
import '../../../../print.dart';
|
||||
import '../../../widgets/elevated_btn.dart';
|
||||
import '../../../widgets/my_circular_indicator_timer.dart';
|
||||
import '../../../widgets/my_scafold.dart';
|
||||
import '../../../widgets/mydialoug.dart';
|
||||
|
||||
// --- اقتراحات الألوان الجديدة ---
|
||||
// يمكنك تعريف هذه الألوان في ملف AppColor.dart الخاص بك
|
||||
class NewAppColor {
|
||||
static const Color primaryColor = Color(0xFF0D47A1); // أزرق داكن
|
||||
static const Color accentColor = Color(0xFF1976D2); // أزرق أفتح
|
||||
static const Color backgroundColor = Color(0xFFF5F7FA); // رمادي فاتح للخلفية
|
||||
static const Color cardColor = Colors.white;
|
||||
static const Color textColor = Color(0xFF333333); // أسود ناعم للنصوص
|
||||
static const Color subTextColor = Color(0xFF757575); // رمادي للنصوص الفرعية
|
||||
static const Color successColor = Color(0xFF2E7D32); // أخضر للنجاح
|
||||
static const Color errorColor = Color(0xFFC62828); // أحمر للخطأ
|
||||
static const Color borderColor = Color(0xFFE0E0E0); // لون الحدود
|
||||
}
|
||||
|
||||
// --- اقتراحات للخطوط ---
|
||||
// يمكنك استخدام حزمة google_fonts وتعيين الخط 'Cairo' أو 'Tajawal' للتطبيق
|
||||
class NewAppStyle {
|
||||
static TextStyle get headlineStyle {
|
||||
return const TextStyle(
|
||||
fontFamily: 'Cairo', // اسم الخط المقترح
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: NewAppColor.primaryColor,
|
||||
);
|
||||
}
|
||||
|
||||
static TextStyle get titleStyle {
|
||||
return const TextStyle(
|
||||
fontFamily: 'Cairo',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: NewAppColor.textColor,
|
||||
);
|
||||
}
|
||||
|
||||
static TextStyle get bodyStyle {
|
||||
return const TextStyle(
|
||||
fontFamily: 'Cairo',
|
||||
fontSize: 14,
|
||||
color: NewAppColor.subTextColor,
|
||||
height: 1.5,
|
||||
);
|
||||
}
|
||||
|
||||
static TextStyle get valueStyle {
|
||||
return const TextStyle(
|
||||
fontFamily: 'Cairo',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: NewAppColor.textColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SyrianCardAI extends StatelessWidget {
|
||||
SyrianCardAI({super.key});
|
||||
final TextToSpeechController textToSpeechController =
|
||||
Get.put(TextToSpeechController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(AI());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
checkForUpdate(context);
|
||||
});
|
||||
return MyScafolld(
|
||||
// تم تغيير لون الخلفية للتصميم الجديد
|
||||
// backgroundColor: NewAppColor.backgroundColor,
|
||||
title: "Approve Driver Documents".tr,
|
||||
action: GetBuilder<AI>(builder: (cont) {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
cont.isLoading = false;
|
||||
cont.update();
|
||||
},
|
||||
icon: const Icon(Icons.refresh, color: NewAppColor.primaryColor),
|
||||
);
|
||||
}),
|
||||
body: [
|
||||
GetBuilder<AI>(builder: (controller) {
|
||||
return controller.isLoading
|
||||
? MyCircularProgressIndicatorWithTimer(
|
||||
isLoading: controller.isLoading,
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0, vertical: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- زر "التالي" بتصميم جديد ---
|
||||
if (controller.licenceFrontSy.isNotEmpty &&
|
||||
controller.licenceBackSy.isNotEmpty &&
|
||||
(controller.idFrontSy.isNotEmpty) &&
|
||||
(controller.idBackSy.isNotEmpty) &&
|
||||
controller.vehicleFrontSy.isNotEmpty &&
|
||||
controller.vehicleBackSy.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: MyElevatedButton(
|
||||
title: 'التالي'.tr,
|
||||
// استخدام اللون الجديد للنجاح
|
||||
kolor: NewAppColor.successColor,
|
||||
onPressed: () {
|
||||
controller.addDriverAndCarEgypt();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
// --- فيديو الشرح بتصميم جديد ---
|
||||
VideoButton(),
|
||||
const SizedBox(height: 16),
|
||||
egyptDriverLicense(),
|
||||
const SizedBox(height: 16),
|
||||
syriaDriverLicenseBack(),
|
||||
const SizedBox(height: 16),
|
||||
syriaVehicleCardFront(),
|
||||
const SizedBox(height: 16),
|
||||
syriaVehicleCardBack(),
|
||||
const SizedBox(height: 16),
|
||||
syriaIdCardFront(),
|
||||
const SizedBox(height: 16),
|
||||
syriaDriverIDBack(),
|
||||
const SizedBox(height: 16),
|
||||
// egyptCriminalRecord(),
|
||||
// const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
// --- شاشة الموافقة بتصميم جديد ---
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: GetBuilder<AI>(builder: (controller) {
|
||||
return controller.approved == false
|
||||
// --- إضافة خلفية معتمة ---
|
||||
? Container(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
spreadRadius: 5,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Approve Driver Documents".tr,
|
||||
style: NewAppStyle.headlineStyle,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"To become a ride-sharing driver on the Intaleq app, you need to upload your driver's license, ID document, and car registration document. Our AI system will instantly review and verify their authenticity in just 2-3 minutes. If your documents are approved, you can start working as a driver on the Intaleq app. Please note, submitting fraudulent documents is a serious offense and may result in immediate termination and legal consequences."
|
||||
.tr,
|
||||
style: NewAppStyle.bodyStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// --- زر الاستماع بتصميم جديد ---
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: NewAppColor.accentColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 10),
|
||||
),
|
||||
onPressed: () async {
|
||||
controller.startTimer();
|
||||
if (box.read(BoxName.lang) == 'ar') {
|
||||
await Get.put(AudioController())
|
||||
.playAudio1('assets/aggrement.wav');
|
||||
} else {
|
||||
await textToSpeechController.speakText(
|
||||
'To become a ride-sharing driver on the Intaleq app...'
|
||||
.tr);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.volume_up_outlined,
|
||||
size: 30),
|
||||
label: Text('اضغط للاستماع'.tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: NewAppColor.accentColor)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// --- أزرار الموافقة والرفض بتصميم جديد ---
|
||||
controller.isTimerComplete
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: 'إلغاء'.tr,
|
||||
kolor: NewAppColor.errorColor,
|
||||
onPressed: () {
|
||||
MyDialog().getDialog(
|
||||
'سيتم إلغاء التسجيل'.tr, '',
|
||||
() async {
|
||||
Get.back();
|
||||
Get.back();
|
||||
});
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: 'أوافق'.tr,
|
||||
kolor: NewAppColor.successColor,
|
||||
onPressed: () {
|
||||
controller.setApproved();
|
||||
}),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: controller.progressValue,
|
||||
color: NewAppColor.primaryColor,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${'الوقت المتبقي'.tr}: ${controller.remainingSeconds} ${"ثانية".tr}',
|
||||
style: NewAppStyle.bodyStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
}),
|
||||
)
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
|
||||
// --- واجهة عرض بيانات الوثيقة ---
|
||||
Widget _buildDocumentDataCard({
|
||||
required String title,
|
||||
required VoidCallback onRefresh,
|
||||
required List<Widget> children,
|
||||
}) {
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
color: NewAppColor.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
side: BorderSide(color: NewAppColor.borderColor, width: 1),
|
||||
),
|
||||
shadowColor: NewAppColor.primaryColor.withOpacity(0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: NewAppStyle.headlineStyle),
|
||||
IconButton(
|
||||
onPressed: onRefresh,
|
||||
icon:
|
||||
const Icon(Icons.refresh, color: NewAppColor.accentColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 24, thickness: 1, color: NewAppColor.borderColor),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- واجهة التقاط صورة الوثيقة ---
|
||||
Widget _buildCaptureCard({
|
||||
required String title,
|
||||
required String imagePath,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
elevation: 2.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Image.asset(
|
||||
imagePath,
|
||||
height: Get.height * .20,
|
||||
fit: BoxFit.cover,
|
||||
// --- في حال لم يتم العثور على الصورة ---
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: Get.height * .20,
|
||||
color: NewAppColor.borderColor,
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 50,
|
||||
color: NewAppColor.subTextColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
color: NewAppColor.cardColor,
|
||||
child: Text(
|
||||
title,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت لعرض معلومة (سطر) ---
|
||||
Widget _infoRow(String label, String? value, {Color? valueColor}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$label: ', style: NewAppStyle.bodyStyle),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value ?? 'غير متوفر',
|
||||
style: NewAppStyle.valueStyle.copyWith(color: valueColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> egyptDriverLicense() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.licenceFrontSy.isNotEmpty) {
|
||||
final data = ai.licenceFrontSy;
|
||||
|
||||
DateTime? expiryDateTime;
|
||||
bool isExpired = false;
|
||||
if (data['expiry_date'] != null) {
|
||||
expiryDateTime = DateTime.tryParse(data['expiry_date']);
|
||||
isExpired = expiryDateTime != null &&
|
||||
expiryDateTime.isBefore(DateTime.now());
|
||||
}
|
||||
|
||||
// بطاقة «رخصة القيادة – الوجه الأمامي» بتنسيق مضغوط وأيقونات
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: NewAppColor.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: NewAppColor.borderColor, width: .8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// العنوان + زر التحديث
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('رخصة القيادة – الوجه الأمامي'.tr,
|
||||
style: NewAppStyle.headlineStyle),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh,
|
||||
size: 20, color: NewAppColor.accentColor),
|
||||
splashRadius: 18,
|
||||
onPressed: () async => await ai
|
||||
.pickAndSendImage('driving_license_sy_front'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// سطر الاسم الكامل
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['name_arabic'] != null)
|
||||
_iconInfo(Icons.person, data['name_arabic']!),
|
||||
if (data['birth_place'] != null)
|
||||
_iconInfo(Icons.location_city, data['birth_place']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// بقية الحقول داخل Wrap لتقليل الطول
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
if (data['national_number'] != null)
|
||||
_iconInfo(Icons.badge, data['national_number']!),
|
||||
if (data['civil_registry'] != null)
|
||||
_iconInfo(Icons.location_on, data['civil_registry']!),
|
||||
if (data['blood_type'] != null)
|
||||
_iconInfo(Icons.water_drop, data['blood_type']!),
|
||||
if (data['birth_year'] != null)
|
||||
_iconInfo(Icons.calendar_today, data['birth_year']!),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _buildCaptureCard(
|
||||
title: 'التقط صورة لرخصة القيادة'.tr,
|
||||
imagePath: 'assets/images/1.png',
|
||||
onTap: () async {
|
||||
await ai.pickAndSendImage('driving_license_sy_front');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaDriverLicenseBack() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.licenceBackSy.isNotEmpty) {
|
||||
final data = ai.licenceBackSy;
|
||||
|
||||
// صلاحية الرخصة
|
||||
final DateTime? expDate =
|
||||
DateTime.tryParse(data['expiry_date'] ?? '');
|
||||
final bool expired =
|
||||
expDate != null && expDate.isBefore(DateTime.now());
|
||||
final Color expColor = expired
|
||||
? NewAppColor.errorColor // أحمر إن انتهت
|
||||
: NewAppColor.successColor; // أخضر إن صالحة
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: NewAppColor.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: NewAppColor.borderColor, width: .8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// عنوان البطاقة + زر الإنعاش
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('رخصة القيادة – الوجه الخلفي'.tr,
|
||||
style: NewAppStyle.headlineStyle),
|
||||
IconButton(
|
||||
splashRadius: 18,
|
||||
icon: const Icon(Icons.refresh,
|
||||
size: 20, color: NewAppColor.accentColor),
|
||||
onPressed: () async => await ai
|
||||
.pickAndSendImage('driving_license_sy_back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// صفّ أول (الفئة + الرقم)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['license_category'] != null)
|
||||
_iconInfo(
|
||||
Icons.star, data['license_category']!), // D1 / D2…
|
||||
if (data['license_number'] != null)
|
||||
_iconInfo(
|
||||
Icons.confirmation_number, data['license_number']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// صفّ ثانٍ (التواريخ) مع تلوين تاريخ الصلاحية
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['issue_date'] != null)
|
||||
_iconInfo(Icons.event, data['issue_date']!),
|
||||
if (data['expiry_date'] != null)
|
||||
_iconInfo(Icons.event_busy, data['expiry_date']!,
|
||||
valueColor: expColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// بطاقة الالتقاط الافتراضية
|
||||
return _buildCaptureCard(
|
||||
title: 'التقط صورة الوجه الخلفي للرخصة'.tr,
|
||||
imagePath: 'assets/images/5.png',
|
||||
onTap: () async =>
|
||||
await ai.pickAndSendImage('driving_license_sy_back'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaDriverIDBack() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
// استلمنا الحقول الأربعة فقط (governorate-address-gender-issue_date)
|
||||
if (ai.idBackSy.isNotEmpty) {
|
||||
final data = ai.idBackSy;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: NewAppColor.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: NewAppColor.borderColor, width: .8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// العنوان + زر التحديث
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('بطاقة الهوية – الوجه الخلفي'.tr,
|
||||
style: NewAppStyle.headlineStyle),
|
||||
IconButton(
|
||||
splashRadius: 18,
|
||||
icon: const Icon(Icons.refresh,
|
||||
size: 20, color: NewAppColor.accentColor),
|
||||
onPressed: () async =>
|
||||
await ai.pickAndSendImage('id_back_sy'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// المحافظة + العنوان
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['governorate'] != null)
|
||||
_iconInfo(Icons.location_city, data['governorate']!),
|
||||
if (data['address'] != null)
|
||||
_iconInfo(Icons.home, data['address']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// الجنس + تاريخ الإصدار
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['gender'] != null)
|
||||
_iconInfo(Icons.person, data['gender']!),
|
||||
if (data['issue_date'] != null)
|
||||
_iconInfo(Icons.event, data['issue_date']!),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// البطاقة الافتراضية لالتقاط الصورة
|
||||
return _buildCaptureCard(
|
||||
title: 'التقط صورة للوجه الخلفي للهوية'.tr,
|
||||
imagePath: 'assets/images/4.png',
|
||||
onTap: () async => await ai.pickAndSendImage('id_back_sy'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// عنصر (أيقونة + قيمة) مع لون نص مخصّص عند الحاجة
|
||||
Widget _iconInfo(IconData icon, String value, {Color? valueColor}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: NewAppColor.accentColor),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value.tr,
|
||||
style: NewAppStyle.bodyStyle.copyWith(color: valueColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaIdCardFront() {
|
||||
// أبقِ الاسم القديم إذا أردت
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.idFrontSy.isNotEmpty) {
|
||||
// غيّر المفتاح حسب متغيرك
|
||||
final data = ai.idFrontSy;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: NewAppColor.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: NewAppColor.borderColor, width: .8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// العنوان + زر التحديث
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('بطاقة الهوية – الوجه الأمامي'.tr,
|
||||
style: NewAppStyle.headlineStyle),
|
||||
IconButton(
|
||||
splashRadius: 18,
|
||||
icon: const Icon(Icons.refresh,
|
||||
size: 20, color: NewAppColor.accentColor),
|
||||
onPressed: () async => await ai.pickAndSendImage(
|
||||
'id_front_sy', // أو id_front حسب تسمية end-point
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// سطر الاسم الكامل
|
||||
if (data['full_name'] != null)
|
||||
_iconInfo(Icons.person, data['full_name']!),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// صفّ (الرقم الوطني + تاريخ الميلاد)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['national_number'] != null)
|
||||
_iconInfo(Icons.badge, data['national_number']!),
|
||||
if (data['dob'] != null)
|
||||
_iconInfo(Icons.cake, data['dob']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// العنوان كامل بمفرده
|
||||
if (data['address'] != null)
|
||||
_iconInfo(Icons.home, data['address']!),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// كارد الالتقاط الافتراضية
|
||||
return _buildCaptureCard(
|
||||
title: 'التقط صورة للوجه الأمامي للهوية'.tr,
|
||||
imagePath: 'assets/images/2.png',
|
||||
onTap: () async => await ai.pickAndSendImage('id_front_sy'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaVehicleCardFront() {
|
||||
// يمكنك إبقاء الاسم القديم إن شئت
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.vehicleFrontSy.isNotEmpty) {
|
||||
final data = ai.vehicleFrontSy;
|
||||
|
||||
// تاريخ الفحص القادم للفحص الدوري (inspection_date)
|
||||
final DateTime? nextCheck =
|
||||
DateTime.tryParse(data['inspection_date'] ?? '');
|
||||
final bool overdue =
|
||||
nextCheck != null && nextCheck.isBefore(DateTime.now());
|
||||
final Color checkColor =
|
||||
overdue ? NewAppColor.errorColor : NewAppColor.successColor;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: NewAppColor.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: NewAppColor.borderColor, width: .8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// العنوان + تحديث
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('رخصة المركبة – الوجه الأمامي'.tr,
|
||||
style: NewAppStyle.headlineStyle),
|
||||
IconButton(
|
||||
splashRadius: 18,
|
||||
icon: const Icon(Icons.refresh,
|
||||
size: 20, color: NewAppColor.accentColor),
|
||||
onPressed: () async => await ai.pickAndSendImage(
|
||||
'vehicle_license_sy_front',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// الصف الأوّل (لوحة + مالك)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['car_plate'] != null)
|
||||
_iconInfo(Icons.directions_car, data['car_plate']!),
|
||||
if (data['owner'] != null)
|
||||
_iconInfo(Icons.person, data['owner']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// الصف الثاني (VIN + اللون)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['vin'] != null)
|
||||
_iconInfo(Icons.confirmation_num, data['vin']!),
|
||||
if (data['color'] != null)
|
||||
_iconInfo(Icons.palette, data['color']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// الصف الثالث (تاريخ المنح + الفحص القادم)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['issue_date'] != null)
|
||||
_iconInfo(Icons.event, data['issue_date']!),
|
||||
if (data['inspection_date'] != null)
|
||||
_iconInfo(
|
||||
Icons.event_available, data['inspection_date']!,
|
||||
valueColor: checkColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// لو لم تُرفع صورة بعد
|
||||
return _buildCaptureCard(
|
||||
title: 'التقط صورة لوجه رخصة المركبة'.tr,
|
||||
imagePath: 'assets/images/6.png',
|
||||
onTap: () async =>
|
||||
await ai.pickAndSendImage('vehicle_license_sy_front'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaVehicleCardBack() {
|
||||
// أبقِ الاسم القديم إن أردت
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.vehicleBackSy.isNotEmpty) {
|
||||
final data = ai.vehicleBackSy;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: NewAppColor.cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: NewAppColor.borderColor, width: .8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// العنوان + زر تحديث
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('رخصة المركبة – الوجه الخلفي'.tr,
|
||||
style: NewAppStyle.headlineStyle),
|
||||
IconButton(
|
||||
splashRadius: 18,
|
||||
icon: const Icon(Icons.refresh,
|
||||
size: 20, color: NewAppColor.accentColor),
|
||||
onPressed: () async => await ai.pickAndSendImage(
|
||||
'vehicle_license_sy_back',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// صفّ (الشركة + الطراز)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['make'] != null)
|
||||
_iconInfo(Icons.factory, data['make']!),
|
||||
if (data['model'] != null)
|
||||
_iconInfo(Icons.directions_car_filled, data['model']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// صفّ (سنة الصنع + اللون)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (data['year'] != null)
|
||||
_iconInfo(Icons.calendar_today, data['year']!),
|
||||
if (data['fuel'] != null)
|
||||
_iconInfo(Icons.local_gas_station, data['fuel']!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// رقم الهيكل بمفرده (قد يكون طويلًا)
|
||||
if (data['chassis'] != null)
|
||||
_iconInfo(Icons.confirmation_num, data['chassis']!),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// بطاقة الالتقاط الافتراضية
|
||||
return _buildCaptureCard(
|
||||
title: 'التقط صورة لخلفية رخصة المركبة'.tr,
|
||||
imagePath: 'assets/images/3.png',
|
||||
onTap: () async =>
|
||||
await ai.pickAndSendImage('vehicle_license_sy_back'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> egyptCriminalRecord() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseCriminalRecordEgypt.isNotEmpty) {
|
||||
return _buildDocumentDataCard(
|
||||
title: 'صحيفة الحالة الجنائية'.tr,
|
||||
onRefresh: () async {
|
||||
await ai.allMethodForAI(
|
||||
(ai.prompts[5]['prompt'].toString()),
|
||||
AppLink.uploadEgypt,
|
||||
'criminalRecord',
|
||||
);
|
||||
},
|
||||
children: [
|
||||
_infoRow('نتيجة الفحص'.tr,
|
||||
ai.responseCriminalRecordEgypt['InspectionResult']),
|
||||
_infoRow(
|
||||
'الاسم الكامل'.tr,
|
||||
ai.responseCriminalRecordEgypt['FullName'],
|
||||
valueColor: (ai.responseCriminalRecordEgypt['FullName']) ==
|
||||
(ai.responseIdEgyptDriverLicense['name_arabic'])
|
||||
? NewAppColor.successColor
|
||||
: NewAppColor.errorColor,
|
||||
),
|
||||
_infoRow('الرقم القومي'.tr,
|
||||
ai.responseCriminalRecordEgypt['NationalID']),
|
||||
],
|
||||
);
|
||||
}
|
||||
return _buildCaptureCard(
|
||||
title: 'التقط صورة لصحيفة الحالة الجنائية'.tr,
|
||||
imagePath: 'assets/images/6.png',
|
||||
onTap: () async {
|
||||
await ai.allMethodForAI(
|
||||
(ai.prompts[5]['prompt'].toString()),
|
||||
AppLink.uploadEgypt,
|
||||
'criminalRecord',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- واجهة عرض الأقسام غير المصرية (بدون تغيير) ---
|
||||
|
||||
// --- زر الفيديو بتصميم جديد ---
|
||||
class VideoButton extends StatelessWidget {
|
||||
final String videoUrl =
|
||||
"https://youtube.com/shorts/fC0RmYH5B_0?feature=share";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (await canLaunchUrl(Uri.parse(videoUrl))) {
|
||||
await launchUrl(Uri.parse(videoUrl));
|
||||
} else {
|
||||
Get.snackbar('خطأ', 'لا يمكن فتح الفيديو');
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.play_circle_outline,
|
||||
color: NewAppColor.accentColor, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"شاهد فيديو الشرح".tr,
|
||||
style: AppStyle.title.copyWith(color: NewAppColor.accentColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
100
siro_driver/lib/views/auth/captin/contact_us_page.dart
Executable file
100
siro_driver/lib/views/auth/captin/contact_us_page.dart
Executable file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/captin/contact_us_controller.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
|
||||
class ContactUsPage extends StatelessWidget {
|
||||
ContactUsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(ContactUsController());
|
||||
return GetBuilder<ContactUsController>(builder: (controller) {
|
||||
return MyScafolld(
|
||||
title: "Contact Us".tr,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: Image.asset('assets/images/logo.gif')),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Get.put(TextToSpeechController()).speakText(
|
||||
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
||||
.tr);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"You can contact us during working hours from 10:00 - 17:00."
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => controller.showContactDialog(context),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.phone,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
Icon(
|
||||
FontAwesome.whatsapp,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
Icon(
|
||||
Icons.email,
|
||||
color: AppColor.redColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
211
siro_driver/lib/views/auth/captin/criminal_documents_page.dart
Executable file
211
siro_driver/lib/views/auth/captin/criminal_documents_page.dart
Executable file
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../controller/functions/gemeni.dart';
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
|
||||
class CriminalDocumemtPage extends StatelessWidget {
|
||||
const CriminalDocumemtPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(AI());
|
||||
return MyScafolld(
|
||||
title: "Criminal Document".tr,
|
||||
isleading: false,
|
||||
body: [
|
||||
GetBuilder<AI>(builder: (controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('You have upload Criminal documents'.tr),
|
||||
)),
|
||||
egyptCriminalRecord(),
|
||||
controller.responseCriminalRecordEgypt.isNotEmpty
|
||||
? MyElevatedButton(
|
||||
title: 'Next'.tr,
|
||||
onPressed: () async {
|
||||
if ((controller
|
||||
.responseCriminalRecordEgypt['FullName']) !=
|
||||
box.read(BoxName.nameArabic)) //todo get from server
|
||||
{
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Criminal Record Mismatch',
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.warning,
|
||||
size: 48, color: Colors.red),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'The full name on your criminal record does not match the one on your driver’s license. Please verify and provide the correct documents.'
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await Get.find<TextToSpeechController>()
|
||||
.speakText(
|
||||
'The full name on your criminal record does not match the one on your driver’s license. Please verify and provide the correct documents.'
|
||||
.tr,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.volume_up),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
await controller.addCriminalDocuments();
|
||||
}
|
||||
})
|
||||
: const SizedBox(),
|
||||
],
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> egyptCriminalRecord() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseCriminalRecordEgypt.isNotEmpty) {
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Criminal Record'.tr, style: AppStyle.headTitle2),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await ai.allMethodForAI(
|
||||
"""
|
||||
Write a JSON object from the following information extracted from the provided Arabic text:
|
||||
|
||||
{
|
||||
"InspectionResult": "",
|
||||
"NationalID": "",
|
||||
"FullName": "",
|
||||
"IssueDate": "" // Format: YYYY-MM-DD
|
||||
}
|
||||
|
||||
Important notes:
|
||||
1. For the IssueDate, ensure the date is in YYYY-MM-DD format using Latin numerals (0-9).
|
||||
2. Add appropriate spaces in all text fields to ensure readability.
|
||||
3. If any information is missing, leave the corresponding field as an empty string.
|
||||
4. Ensure all text is properly formatted and spaces are used correctly.
|
||||
5. Convert any Arabic numerals to Latin numerals (0-9) where applicable.
|
||||
|
||||
Please fill in the JSON object with the extracted information, following these guidelines.
|
||||
""",
|
||||
AppLink.uploadEgypt,
|
||||
'criminalRecord',
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(color: AppColor.accentColor),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'InspectionResult'.tr}: ${(ai.responseCriminalRecordEgypt['InspectionResult'])}'),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'FullName'.tr}: ${(ai.responseCriminalRecordEgypt['FullName'])}',
|
||||
style: AppStyle.title.copyWith(
|
||||
color: (ai.responseCriminalRecordEgypt['FullName']) ==
|
||||
(ai.responseIdEgyptDriverLicense['name_arabic'])
|
||||
? AppColor.greenColor
|
||||
: AppColor.redColor),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'NationalID'.tr}: ${(ai.responseCriminalRecordEgypt['NationalID'])}'),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'IssueDate'.tr}: ${ai.responseCriminalRecordEgypt['IssueDate']}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await ai.allMethodForAI(
|
||||
"""
|
||||
Write a JSON object from the following information extracted from the provided Arabic text:
|
||||
|
||||
{
|
||||
"InspectionResult": "",
|
||||
"NationalID": "",
|
||||
"FullName": "",
|
||||
"IssueDate": "" // Format: YYYY-MM-DD
|
||||
}
|
||||
|
||||
Important notes:
|
||||
1. For the IssueDate, ensure the date is in YYYY-MM-DD format using Latin numerals (0-9).
|
||||
2. Add appropriate spaces in all text fields to ensure readability.
|
||||
3. If any information is missing, leave the corresponding field as an empty string.
|
||||
4. Ensure all text is properly formatted and spaces are used correctly.
|
||||
5. Convert any Arabic numerals to Latin numerals (0-9) where applicable.
|
||||
|
||||
Please fill in the JSON object with the extracted information, following these guidelines.
|
||||
""",
|
||||
AppLink.uploadEgypt,
|
||||
'criminalRecord',
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/6.png',
|
||||
height: Get.height * .25,
|
||||
width: double.maxFinite,
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
Text(
|
||||
'Capture an Image of Your Criminal Record'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
110
siro_driver/lib/views/auth/captin/driver_car_controller.dart
Executable file
110
siro_driver/lib/views/auth/captin/driver_car_controller.dart
Executable file
@@ -0,0 +1,110 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/error_snakbar.dart';
|
||||
|
||||
class DriverCarController extends GetxController {
|
||||
bool isLoading = false;
|
||||
List cars = [];
|
||||
// int? carId;
|
||||
fetchCatrsForDrivers() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().get(link: AppLink.getNewCarsDrivers, payload: {
|
||||
"driverID": box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res)['message'];
|
||||
cars = d;
|
||||
// carId = cars.isEmpty ? 1 : cars.length + 1;
|
||||
}
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
addCarsForDrivers(
|
||||
String vin,
|
||||
String carPlate,
|
||||
String make,
|
||||
String model,
|
||||
String year,
|
||||
String expirationDate,
|
||||
String color,
|
||||
String colorHex,
|
||||
String address,
|
||||
String owner,
|
||||
String registrationDate,
|
||||
String displacement,
|
||||
String fuel) async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.addRegisrationCar,
|
||||
payload: {
|
||||
"driverID": box.read(BoxName.driverID).toString(),
|
||||
"vin": vin ?? 'unknown',
|
||||
"car_plate": carPlate.toString(),
|
||||
"make": make ?? 'unknown',
|
||||
"model": model ?? 'unknown',
|
||||
"year": year ?? 'unknown',
|
||||
"expiration_date": expirationDate ?? 'unknown',
|
||||
"color": color ?? 'unknown',
|
||||
"owner": owner ?? 'unknown',
|
||||
"color_hex": colorHex ?? '#000000',
|
||||
"address": address ?? 'unknown',
|
||||
"displacement": displacement ?? 'unknown',
|
||||
"fuel": fuel ?? 'unknown',
|
||||
"registration_date": registrationDate ?? 'unknown',
|
||||
},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
mySnackbarSuccess('');
|
||||
|
||||
fetchCatrsForDrivers();
|
||||
} else {
|
||||
mySnackeBarError('');
|
||||
}
|
||||
}
|
||||
|
||||
// update carRegistration only and insert on it without tow column
|
||||
Future<void> updateCarRegistration(String id, String driverID) async {
|
||||
final body = {
|
||||
'id': id,
|
||||
'driverID': driverID,
|
||||
};
|
||||
// remove default before update
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.makeDefaultCar,
|
||||
payload: body,
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
mySnackbarSuccess('Updated'.tr);
|
||||
} else {
|
||||
mySnackeBarError('Not updated'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
//todo need review
|
||||
removeCar(String carId) async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().post(link: AppLink.deleteNewCarsDrivers, payload: {
|
||||
"id": carId.toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
mySnackbarSuccess('deleted'.tr);
|
||||
}
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
fetchCatrsForDrivers();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
0
siro_driver/lib/views/auth/captin/forget.dart
Executable file
0
siro_driver/lib/views/auth/captin/forget.dart
Executable file
639
siro_driver/lib/views/auth/captin/invite_driver_screen.dart
Executable file
639
siro_driver/lib/views/auth/captin/invite_driver_screen.dart
Executable file
@@ -0,0 +1,639 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/auth/captin/invit_controller.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class InviteScreen extends StatelessWidget {
|
||||
final InviteController controller = Get.put(InviteController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: CupertinoColors.systemBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: CupertinoColors.systemBackground,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
'Invite'.tr,
|
||||
style: const TextStyle(color: CupertinoColors.label),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: AppColor.blueColor),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: GetBuilder<InviteController>(
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemGrey6,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SegmentedButton<int>(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
? CupertinoColors.white
|
||||
: Colors.transparent,
|
||||
),
|
||||
foregroundColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(states) => states.contains(WidgetState.selected)
|
||||
? AppColor.blueColor
|
||||
: CupertinoColors.label,
|
||||
),
|
||||
),
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: 0,
|
||||
label: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('Drivers'.tr),
|
||||
),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: 1,
|
||||
label: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text('Passengers'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
selected: {controller.selectedTab},
|
||||
onSelectionChanged: (Set<int> newSelection) {
|
||||
controller.updateSelectedTab(newSelection.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: controller.selectedTab == 0
|
||||
? _buildDriverTab(context)
|
||||
: _buildPassengerTab(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverTab(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Invite another driver and both get a gift after he completes 100 trips!"
|
||||
.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CupertinoColors.label,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPhoneInput(),
|
||||
const SizedBox(height: 20),
|
||||
_buildActionButtons(),
|
||||
const SizedBox(height: 20),
|
||||
_buildInvitationsList(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPassengerTab(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemGrey6,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Share this code with passengers and earn rewards when they use it!"
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPhoneInput(),
|
||||
const SizedBox(height: 20),
|
||||
_buildActionButtonsPassengers(),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 20),
|
||||
_buildInvitationsListPassengers(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneInput() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemGrey6,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CupertinoTextField.borderless(
|
||||
controller: controller.invitePhoneController,
|
||||
placeholder: 'Enter phone'.tr,
|
||||
padding: const EdgeInsets.all(12),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
child: const Icon(CupertinoIcons.person_badge_plus,
|
||||
color: AppColor.blueColor),
|
||||
onPressed: () async {
|
||||
await controller.pickContactFromNativeApp();
|
||||
if (controller.contacts.isNotEmpty) {
|
||||
if (box.read(BoxName.isSavedPhones) == null) {
|
||||
// controller.savePhoneToServer();
|
||||
box.write(BoxName.isSavedPhones, true);
|
||||
}
|
||||
_showContactsDialog(Get.context!);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CupertinoButton(
|
||||
color: AppColor.blueColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
onPressed: controller.sendInvite,
|
||||
child: Text(
|
||||
'Send Invite'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CupertinoButton(
|
||||
color: AppColor.blueColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Text(
|
||||
'Show Invitations'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
controller.fetchDriverStats();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtonsPassengers() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CupertinoButton(
|
||||
color: AppColor.blueColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
onPressed: controller.sendInviteToPassenger,
|
||||
child: Text(
|
||||
'Send Invite'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CupertinoButton(
|
||||
color: AppColor.blueColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Text(
|
||||
'Show Invitations'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
controller.fetchDriverStatsPassengers();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInvitationsList(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: Get.height * .4,
|
||||
child: controller.driverInvitationData.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No invitation found yet!".tr,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: controller.driverInvitationData.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildInvitationItem(context, index);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInvitationsListPassengers(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: Get.height * .4,
|
||||
child: controller.driverInvitationDataToPassengers.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No invitation found yet!".tr,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: controller.driverInvitationDataToPassengers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildInvitationItemPassengers(context, index);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInvitationItem(BuildContext context, int index) {
|
||||
int countOfInvitDriver = int.tryParse(
|
||||
(controller.driverInvitationData[index]['countOfInvitDriver'])
|
||||
?.toString() ??
|
||||
'0') ??
|
||||
0;
|
||||
double progressValue = (countOfInvitDriver / 100.0).clamp(0.0, 1.0);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.onSelectDriverInvitation(index);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemGrey6,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
(controller.driverInvitationData[index]['invitorName']),
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CupertinoColors.label,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: progressValue,
|
||||
backgroundColor: CupertinoColors.systemGrey4,
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation<Color>(AppColor.blueColor),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$countOfInvitDriver / 100 ${'Trip'.tr}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInvitationItemPassengers(BuildContext context, int index) {
|
||||
// Extracting the data from the sample JSON-like structure
|
||||
var invitation = controller.driverInvitationDataToPassengers[index];
|
||||
|
||||
int countOfInvitDriver =
|
||||
int.tryParse((invitation['countOfInvitDriver'])?.toString() ?? '0') ??
|
||||
0;
|
||||
double progressValue = (countOfInvitDriver / 10.0).clamp(0.0, 1.0);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.onSelectPassengerInvitation(index);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemGrey6,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
(invitation['passengerName'])
|
||||
.toString(), // Handle null or missing data
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CupertinoColors.label,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: progressValue,
|
||||
backgroundColor: CupertinoColors.systemGrey4,
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation<Color>(AppColor.blueColor),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$countOfInvitDriver / 3 ${'Trip'.tr}', // Show trips completed
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPassengerStats(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.systemGrey6,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Your Rewards".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CupertinoColors.label,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatItem(
|
||||
context,
|
||||
"Total Invites".tr,
|
||||
(controller.driverInvitationDataToPassengers[0]
|
||||
['countOfInvitDriver']
|
||||
.toString()),
|
||||
),
|
||||
_buildStatItem(
|
||||
context,
|
||||
"Active Users".tr,
|
||||
(controller.driverInvitationDataToPassengers[0]['passengerName'])
|
||||
.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.label,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.blueColor,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContactsDialog(BuildContext context) {
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (BuildContext context) => Container(
|
||||
height: 400,
|
||||
color: CupertinoColors.systemBackground,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: CupertinoColors.separator.withOpacity(0.5)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text('Cancel'.tr),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
Text(
|
||||
'Choose from contact'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 60), // For balance
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.contactMaps.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = controller.contactMaps[index];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: CupertinoColors.separator.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
contact['name'],
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.label,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(contact['phones'][0].toString()),
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
CupertinoIcons.chevron_right,
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
controller.selectPhone(contact['phones'].toString());
|
||||
// Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
510
siro_driver/lib/views/auth/captin/login_captin.dart
Executable file
510
siro_driver/lib/views/auth/captin/login_captin.dart
Executable file
@@ -0,0 +1,510 @@
|
||||
import 'dart:io';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/auth/apple_sigin.dart';
|
||||
import '../../../controller/auth/captin/login_captin_controller.dart';
|
||||
import '../../../controller/auth/google_sign.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import 'contact_us_page.dart';
|
||||
import 'otp_page.dart'; // تأكد من وجود هذا الملف لديك
|
||||
|
||||
class LoginCaptin extends StatefulWidget {
|
||||
const LoginCaptin({super.key});
|
||||
|
||||
@override
|
||||
State<LoginCaptin> createState() => _LoginCaptinState();
|
||||
}
|
||||
|
||||
class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||
final AuthController authController = Get.put(AuthController());
|
||||
final LoginDriverController controller = Get.put(LoginDriverController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// فحص الإذن عند فتح الصفحة مباشرة
|
||||
controller.getLocationPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
// التحقق عند العودة من الإعدادات
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
controller.getLocationPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<LoginDriverController>(
|
||||
builder: (controller) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: _buildBodyContent(context, controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildBodyContent(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
// 1. صفحة الموافقة على الشروط
|
||||
if (box.read(BoxName.agreeTerms) != 'agreed') {
|
||||
return _buildAgreementPage(context, controller);
|
||||
}
|
||||
|
||||
// 2. صفحة إذن الموقع
|
||||
if (box.read(BoxName.locationPermission) != 'true') {
|
||||
return _buildLocationPermissionPage(context, controller);
|
||||
}
|
||||
|
||||
// 3. صفحة تسجيل الدخول (رقم الهاتف)
|
||||
return PhoneNumberScreen();
|
||||
}
|
||||
|
||||
// --- صفحة الشروط والأحكام ---
|
||||
Widget _buildAgreementPage(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.policy_outlined,
|
||||
size: 80, color: AppColor.primaryColor),
|
||||
const SizedBox(height: 20),
|
||||
Text('Driver Agreement'.tr,
|
||||
textAlign: TextAlign.center, style: AppStyle.headTitle2),
|
||||
const SizedBox(height: 30),
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: AppStyle.title.copyWith(height: 1.5),
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
"To become a driver, you must review and agree to the "
|
||||
.tr),
|
||||
TextSpan(
|
||||
text: 'Terms of Use'.tr,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: AppColor.blueColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrl(
|
||||
Uri.parse('${AppLink.server}/privacy_policy.php'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
}),
|
||||
TextSpan(text: " and acknowledge our Privacy Policy.".tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).cardColor.withOpacity(0.5),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: HtmlWidget(
|
||||
box.read(BoxName.lang).toString() == 'ar'
|
||||
? AppInformation.privacyPolicyArabic
|
||||
: AppInformation.privacyPolicy,
|
||||
textStyle: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
CheckboxListTile(
|
||||
title: Text('I Agree'.tr, style: AppStyle.title),
|
||||
value: controller.isAgreeTerms,
|
||||
onChanged: (value) => controller.changeAgreeTerm(),
|
||||
activeColor: AppColor.primaryColor,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: MyElevatedButton(
|
||||
title: 'Continue'.tr,
|
||||
onPressed: controller.isAgreeTerms
|
||||
? () => controller.saveAgreementTerms()
|
||||
: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- صفحة إذن الموقع ---
|
||||
Widget _buildLocationPermissionPage(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Icon(Icons.location_on_outlined,
|
||||
size: 80, color: AppColor.primaryColor),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Location Access Required'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'We need access to your location to match you with nearby passengers and provide accurate navigation.'
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Please allow location access "all the time" to receive ride requests even when the app is in the background.'
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: AppColor.greenColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
MyElevatedButton(
|
||||
title: "Allow Location Access".tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () async {
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
controller.update();
|
||||
// 1. طلب إذن الموقع العادي (أثناء الاستخدام) أولاً
|
||||
var status = await Permission.location.request();
|
||||
|
||||
if (status.isGranted) {
|
||||
// 2. إذا كنت تحتاج "طوال الوقت" (Background)، اطلبه الآن بشكل منفصل
|
||||
// ملاحظة: في أندرويد 11+ سينقلك هذا لصفحة إعدادات خاصة
|
||||
var backgroundStatus =
|
||||
await Permission.locationAlways.request();
|
||||
|
||||
if (backgroundStatus.isGranted) {
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
controller.update();
|
||||
} else {
|
||||
// المستخدم وافق على "أثناء الاستخدام" فقط، يمكنك المشي في الحال
|
||||
// أو إجباره، حسب منطق تطبيقك (تطبيق سائق يفضل Always)
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
controller.update();
|
||||
}
|
||||
} else if (status.isPermanentlyDenied) {
|
||||
// إذا كانت الحالة مرفوضة نهائياً، يجب فتح الإعدادات
|
||||
openAppSettings();
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => openAppSettings(),
|
||||
child: Text("Open Settings".tr,
|
||||
style: const TextStyle(color: AppColor.blueColor)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- واجهة تسجيل الدخول اليدوي/الاجتماعي (للاستخدام المستقبلي إذا لزم الأمر) ---
|
||||
Widget _buildLoginUI(BuildContext context, LoginDriverController controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Image.asset('assets/images/logo.gif', height: 120, width: 120),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Driver Portal'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2.copyWith(fontSize: 28),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Sign in to start your journey'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
if (controller.isGoogleDashOpen)
|
||||
_buildManualLoginForm(context, controller, isRegistration: true)
|
||||
else if (Platform.isIOS && controller.isTest == 0)
|
||||
_buildManualLoginForm(context, controller, isRegistration: false)
|
||||
else
|
||||
_buildSocialLoginOptions(context, controller),
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.to(() => ContactUsPage()),
|
||||
child: Text(
|
||||
'Need help? Contact Us'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.blueColor,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSocialLoginOptions(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Sign in with a provider for easy access'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSocialButton(
|
||||
text: 'Sign In with Google'.tr,
|
||||
icon: FontAwesome.google,
|
||||
backgroundColor: AppColor.redColor,
|
||||
onPressed: () async {
|
||||
GoogleSignInHelper().signInFromLogin();
|
||||
},
|
||||
),
|
||||
if (Platform.isIOS) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSocialButton(
|
||||
text: 'Sign in with Apple'.tr,
|
||||
icon: Icons.apple,
|
||||
backgroundColor: Colors.black,
|
||||
onPressed: () async {
|
||||
User? user = await authController.signInWithApple();
|
||||
if (user != null) {
|
||||
box.write(BoxName.emailDriver, user.email.toString());
|
||||
box.write(BoxName.driverID, user.uid);
|
||||
controller.loginWithGoogleCredential(
|
||||
user.uid,
|
||||
user.email.toString(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: Theme.of(context).dividerColor)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Or'.tr, style: AppStyle.subtitle),
|
||||
),
|
||||
Expanded(child: Divider(color: Theme.of(context).dividerColor)),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
MyElevatedButton(
|
||||
title: 'Create Account with Email'.tr,
|
||||
onPressed: () => controller.changeGoogleDashOpen(),
|
||||
kolor: AppColor.blueColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildManualLoginForm(
|
||||
BuildContext context, LoginDriverController controller,
|
||||
{required bool isRegistration}) {
|
||||
return Card(
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
isRegistration ? 'Create Driver Account'.tr : 'Driver Login'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTextFormField(
|
||||
controller: controller.emailController,
|
||||
labelText: 'Email'.tr,
|
||||
hintText: 'Enter your email'.tr,
|
||||
prefixIcon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || !GetUtils.isEmail(value)) {
|
||||
return 'Please enter a valid email'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
GetBuilder<LoginDriverController>(
|
||||
id: 'passwordVisibility',
|
||||
builder: (_) => _buildTextFormField(
|
||||
controller: controller.passwordController,
|
||||
labelText: 'Password'.tr,
|
||||
hintText: 'Enter your password'.tr,
|
||||
prefixIcon: Icons.lock_outline,
|
||||
obscureText: controller.isPasswordHidden,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordHidden
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
onPressed: () => controller.togglePasswordVisibility(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 6) {
|
||||
return 'Password must be at least 6 characters'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
controller.isloading
|
||||
? const Center(child: MyCircularProgressIndicator())
|
||||
: MyElevatedButton(
|
||||
title:
|
||||
(isRegistration ? 'Create Account'.tr : 'Login'.tr),
|
||||
onPressed: () {
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
if (isRegistration) {
|
||||
String email = controller.emailController.text;
|
||||
String uniqueId =
|
||||
controller.generateUniqueIdFromEmail(email);
|
||||
box.write(BoxName.driverID, uniqueId);
|
||||
box.write(BoxName.emailDriver, email);
|
||||
controller.loginUsingCredentialsWithoutGoogle(
|
||||
controller.passwordController.text,
|
||||
email,
|
||||
);
|
||||
} else {
|
||||
controller.loginWithGoogleCredential(
|
||||
controller.passwordController.text,
|
||||
controller.emailController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
if (isRegistration)
|
||||
TextButton(
|
||||
onPressed: () => controller.changeGoogleDashOpen(),
|
||||
child: Text(
|
||||
'Back to other sign-in options'.tr,
|
||||
style: TextStyle(color: AppColor.primaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextFormField _buildTextFormField({
|
||||
required TextEditingController controller,
|
||||
required String labelText,
|
||||
required String hintText,
|
||||
required IconData prefixIcon,
|
||||
required String? Function(String?) validator,
|
||||
bool obscureText = false,
|
||||
Widget? suffixIcon,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
labelStyle: TextStyle(color: Theme.of(context).hintColor),
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(color: Theme.of(context).hintColor.withOpacity(0.5)),
|
||||
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide: const BorderSide(color: AppColor.primaryColor, width: 2.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
Widget _buildSocialButton({
|
||||
required String text,
|
||||
required IconData icon,
|
||||
required Color backgroundColor,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return ElevatedButton.icon(
|
||||
icon: Icon(icon, color: Colors.white),
|
||||
label:
|
||||
Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
42
siro_driver/lib/views/auth/captin/logout_captain.dart
Executable file
42
siro_driver/lib/views/auth/captin/logout_captain.dart
Executable file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/functions/log_out.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
class LogoutCaptain extends StatelessWidget {
|
||||
const LogoutCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Log Out Page'.tr,
|
||||
body: [
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Log Off'.tr,
|
||||
onPressed: () {
|
||||
LogOutController().logOutCaptain();
|
||||
}),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Delete My Account'.tr,
|
||||
onPressed: () {
|
||||
LogOutController().deletecaptainAccount();
|
||||
},
|
||||
kolor: AppColor.redColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
600
siro_driver/lib/views/auth/captin/otp_page.dart
Normal file
600
siro_driver/lib/views/auth/captin/otp_page.dart
Normal file
@@ -0,0 +1,600 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
|
||||
// --- Placeholder Imports ---
|
||||
// Assuming these files exist in your project structure.
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/auth/captin/phone_helper_controller.dart';
|
||||
import '../../../controller/local/phone_intel/intl_phone_field.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
// Assuming you have an AppColor class defined in your project.
|
||||
// import 'path/to/your/app_color.dart';
|
||||
|
||||
/// A visually revamped authentication screen with a light, glassy effect,
|
||||
/// themed for the driver application using a green primary color.
|
||||
class AuthScreen extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget form;
|
||||
|
||||
const AuthScreen({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.form,
|
||||
});
|
||||
|
||||
/// Shows a dialog for testers to log in using email and password.
|
||||
void _showTesterLoginDialog(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
final testerEmailController = TextEditingController();
|
||||
final testerPasswordController = TextEditingController();
|
||||
final testerFormKey = GlobalKey<FormState>();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: AlertDialog(
|
||||
backgroundColor: Colors.white.withOpacity(0.85),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: const Text(
|
||||
'App Tester Login',
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
),
|
||||
content: Form(
|
||||
key: testerFormKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: testerEmailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColor.greenColor, width: 2),
|
||||
),
|
||||
),
|
||||
validator: (value) => value == null || !value.contains('@')
|
||||
? 'Enter a valid email'.tr
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: testerPasswordController,
|
||||
obscureText: true,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColor.greenColor, width: 2),
|
||||
),
|
||||
),
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
? 'Enter a password'
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child:
|
||||
const Text('Login', style: TextStyle(color: Colors.white)),
|
||||
onPressed: () {
|
||||
if (testerFormKey.currentState!.validate()) {
|
||||
controller.logintest(
|
||||
testerPasswordController.text.trim(),
|
||||
testerEmailController.text.trim(),
|
||||
);
|
||||
Navigator.of(dialogContext).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Controller for the driver's login logic
|
||||
final loginController = Get.put(LoginDriverController());
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
// NEW: Light and airy gradient with green accents
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.white,
|
||||
Colors.green.shade50,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Subtle background shapes with the new primary color
|
||||
Positioned(
|
||||
top: -80,
|
||||
left: -100,
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColor.greenColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -120,
|
||||
right: -150,
|
||||
child: Container(
|
||||
width: 350,
|
||||
height: 350,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColor.greenColor.withOpacity(0.15),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// GestureDetector for tester login
|
||||
GestureDetector(
|
||||
onLongPress: () {
|
||||
_showTesterLoginDialog(context, loginController);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.8), width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 5)
|
||||
]),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Image.asset('assets/images/logo.gif',
|
||||
height: 100)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Glassmorphism Container for the form with a whiter look
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: form,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Button for app testers, adapted to the light theme
|
||||
Material(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_showTesterLoginDialog(context, loginController),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.admin_panel_settings_outlined,
|
||||
color: Colors.black.withOpacity(0.6)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'For App Reviewers / Testers',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI Screens ---
|
||||
|
||||
class PhoneNumberScreen extends StatefulWidget {
|
||||
const PhoneNumberScreen({super.key});
|
||||
@override
|
||||
State<PhoneNumberScreen> createState() => _PhoneNumberScreenState();
|
||||
}
|
||||
|
||||
class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
String _completePhone = '';
|
||||
|
||||
void _submit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() => _isLoading = true);
|
||||
final rawPhone = _completePhone.replaceFirst('+', '');
|
||||
Log.print('📱 _submit rawPhone: "$rawPhone" (from _completePhone: "$_completePhone")');
|
||||
final success = await PhoneAuthHelper.sendOtp(rawPhone);
|
||||
if (success && mounted) {
|
||||
Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone));
|
||||
}
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AuthScreen(
|
||||
title: 'welcome to intaleq'.tr,
|
||||
subtitle: 'login or register subtitle'.tr,
|
||||
form: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Enter your phone number'.tr,
|
||||
style: TextStyle(color: Colors.black87, fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
IntlPhoneField(
|
||||
showCountryFlag: false,
|
||||
searchText: 'Search country'.tr,
|
||||
languageCode: 'ar',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
dropdownTextStyle: const TextStyle(color: Colors.black87),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Phone Number'.tr,
|
||||
hintText: 'witout zero'.tr,
|
||||
labelStyle: const TextStyle(color: Colors.black54),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.black.withOpacity(0.1)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide:
|
||||
const BorderSide(color: AppColor.greenColor, width: 2),
|
||||
),
|
||||
),
|
||||
initialCountryCode: 'SY',
|
||||
onChanged: (phone) {
|
||||
_completePhone = phone.completeNumber;
|
||||
},
|
||||
validator: (phone) {
|
||||
if (phone == null || phone.number.isEmpty) {
|
||||
return 'Please enter your phone number'.tr;
|
||||
} // Check if the number is a Syrian number
|
||||
if (phone.countryISOCode != 'SY') {
|
||||
return 'Only Syrian phone numbers are allowed'.tr;
|
||||
}
|
||||
// Check if the national number part starts with '0'
|
||||
if (phone.completeNumber.startsWith('96309') ||
|
||||
phone.completeNumber.startsWith('+9630') ||
|
||||
phone.completeNumber.startsWith('09')) {
|
||||
return 'Please enter the number without the leading 0'.tr;
|
||||
}
|
||||
if (phone.completeNumber.length < 10) {
|
||||
return 'Phone number seems too short'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_isLoading
|
||||
? const CircularProgressIndicator(color: AppColor.greenColor)
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(
|
||||
'send otp button'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OtpVerificationScreen extends StatefulWidget {
|
||||
final String phoneNumber;
|
||||
const OtpVerificationScreen({super.key, required this.phoneNumber});
|
||||
@override
|
||||
State<OtpVerificationScreen> createState() => _OtpVerificationScreenState();
|
||||
}
|
||||
|
||||
class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _otpController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
void _submit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() => _isLoading = true);
|
||||
await PhoneAuthHelper.verifyOtp(widget.phoneNumber, _otpController.text.trim());
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AuthScreen(
|
||||
title: 'verify your number title'.tr,
|
||||
subtitle:
|
||||
'otp sent subtitle'.trParams({'phoneNumber': widget.phoneNumber}),
|
||||
form: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Enter the 3-digit code',
|
||||
style: TextStyle(color: Colors.black87, fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: TextFormField(
|
||||
controller: _otpController,
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 3,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
letterSpacing: 18,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
hintText: '---',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
letterSpacing: 18,
|
||||
fontSize: 28),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
validator: (v) => v == null || v.length < 3 ? '' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
_isLoading
|
||||
? const CircularProgressIndicator(color: AppColor.greenColor)
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(
|
||||
'verify and continue button'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RegistrationScreen extends StatefulWidget {
|
||||
final String phoneNumber;
|
||||
const RegistrationScreen({super.key, required this.phoneNumber});
|
||||
@override
|
||||
State<RegistrationScreen> createState() => _RegistrationScreenState();
|
||||
}
|
||||
|
||||
class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
void _submit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() => _isLoading = true);
|
||||
await PhoneAuthHelper.registerUser(
|
||||
phoneNumber: widget.phoneNumber,
|
||||
firstName: _firstNameController.text.trim(),
|
||||
lastName: _lastNameController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
);
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTextFormField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
style: const TextStyle(color: Colors.black87),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: const TextStyle(color: Colors.black54),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.black.withOpacity(0.1)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColor.greenColor, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AuthScreen(
|
||||
title: 'one last step title'.tr,
|
||||
subtitle: 'complete profile subtitle'.tr,
|
||||
form: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildTextFormField(
|
||||
controller: _firstNameController,
|
||||
label: 'first name label'.tr,
|
||||
validator: (v) => v!.isEmpty ? 'first name required'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextFormField(
|
||||
controller: _lastNameController,
|
||||
label: 'last name label'.tr,
|
||||
validator: (v) => v!.isEmpty ? 'last name required'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextFormField(
|
||||
controller: _emailController,
|
||||
label: 'email optional label'.tr,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_isLoading
|
||||
? const CircularProgressIndicator(color: AppColor.greenColor)
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(
|
||||
'complete registration button'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
176
siro_driver/lib/views/auth/captin/otp_token_page.dart
Normal file
176
siro_driver/lib/views/auth/captin/otp_token_page.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../controller/auth/captin/opt_token_controller.dart';
|
||||
|
||||
class OtpVerificationPage extends StatefulWidget {
|
||||
final String phone;
|
||||
final String deviceToken;
|
||||
final String token;
|
||||
final String ptoken;
|
||||
|
||||
const OtpVerificationPage({
|
||||
super.key,
|
||||
required this.phone,
|
||||
required this.deviceToken,
|
||||
required this.token,
|
||||
required this.ptoken,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OtpVerificationPage> createState() => _OtpVerificationPageState();
|
||||
}
|
||||
|
||||
class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
||||
late final OtpVerificationController controller;
|
||||
final List<FocusNode> _focusNodes = List.generate(3, (index) => FocusNode());
|
||||
final List<TextEditingController> _textControllers =
|
||||
List.generate(3, (index) => TextEditingController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = Get.put(OtpVerificationController(
|
||||
phone: widget.phone,
|
||||
deviceToken: widget.deviceToken,
|
||||
token: widget.token,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
for (var controller in _textControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onOtpChanged(String value, int index) {
|
||||
if (value.isNotEmpty) {
|
||||
if (index < 2) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
} else {
|
||||
_focusNodes[index].unfocus(); // إلغاء التركيز بعد آخر حقل
|
||||
}
|
||||
} else if (index > 0) {
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
}
|
||||
// تجميع نصوص كل الحقول لتكوين الرمز النهائي
|
||||
controller.otpCode.value = _textControllers.map((c) => c.text).join();
|
||||
}
|
||||
|
||||
Widget _buildOtpInputFields() {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr, // لضمان ترتيب الحقول من اليسار لليمين
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(3, (index) {
|
||||
return SizedBox(
|
||||
width: 45,
|
||||
height: 55,
|
||||
child: TextFormField(
|
||||
controller: _textControllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 1,
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).primaryColor, width: 2),
|
||||
),
|
||||
),
|
||||
onChanged: (value) => _onOtpChanged(value, index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Verify OTP'.tr),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
),
|
||||
backgroundColor: Colors.grey[50],
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Icon(Icons.phonelink_lock_rounded,
|
||||
size: 80, color: Theme.of(context).primaryColor),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Verification Code'.tr,
|
||||
style:
|
||||
const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
'${'We have sent a verification code to your mobile number:'.tr} ${widget.phone}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600, fontSize: 16, height: 1.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
_buildOtpInputFields(),
|
||||
const SizedBox(height: 40),
|
||||
Obx(() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: controller.isVerifying.value
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12))),
|
||||
onPressed: () =>
|
||||
controller.verifyOtp(widget.ptoken),
|
||||
child: Text('Verify'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 24),
|
||||
Obx(
|
||||
() => controller.canResend.value
|
||||
? TextButton(
|
||||
onPressed: controller.sendOtp,
|
||||
child: Text('Resend Code'.tr),
|
||||
)
|
||||
: Text(
|
||||
'${'You can resend in'.tr} ${controller.countdown.value} ${'seconds'.tr}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
202
siro_driver/lib/views/auth/captin/register_captin.dart
Executable file
202
siro_driver/lib/views/auth/captin/register_captin.dart
Executable file
@@ -0,0 +1,202 @@
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/register_captin_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/auth/google_sign.dart';
|
||||
|
||||
class RegisterCaptin extends StatelessWidget {
|
||||
const RegisterCaptin({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(RegisterCaptainController());
|
||||
return MyScafolld(
|
||||
title: 'Register Driver'.tr,
|
||||
body: [
|
||||
// GetBuilder<RegisterCaptainController>(
|
||||
// builder: (controller) => Form(
|
||||
// key: controller.formKey,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: SingleChildScrollView(
|
||||
// child: Container(
|
||||
// decoration: const BoxDecoration(
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// offset: Offset(3, 3),
|
||||
// color: AppColor.accentColor,
|
||||
// blurRadius: 3)
|
||||
// ],
|
||||
// color: AppColor.secondaryColor,
|
||||
// ),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(16),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: Get.width * .8,
|
||||
// child: TextFormField(
|
||||
// keyboardType: TextInputType.emailAddress,
|
||||
// controller: controller.emailController,
|
||||
// decoration: InputDecoration(
|
||||
// focusedBorder: OutlineInputBorder(
|
||||
// borderSide: const BorderSide(
|
||||
// color: AppColor.primaryColor,
|
||||
// width: 2.0,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// fillColor: AppColor.accentColor,
|
||||
// hoverColor: AppColor.accentColor,
|
||||
// focusColor: AppColor.accentColor,
|
||||
// border: const OutlineInputBorder(
|
||||
// borderRadius:
|
||||
// BorderRadius.all(Radius.circular(12))),
|
||||
// labelText: 'Email'.tr,
|
||||
// hintText: 'Enter your email address'.tr,
|
||||
// ),
|
||||
// validator: (value) {
|
||||
// if (value!.isEmpty ||
|
||||
// (!value.contains('@') ||
|
||||
// !value.contains('.'))) {
|
||||
// return 'Please enter Your Email.'.tr;
|
||||
// }
|
||||
// return null;
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 15,
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: Get.width * .8,
|
||||
// child: TextFormField(
|
||||
// obscureText: true,
|
||||
// keyboardType: TextInputType.emailAddress,
|
||||
// controller: controller.passwordController,
|
||||
// decoration: InputDecoration(
|
||||
// focusedBorder: OutlineInputBorder(
|
||||
// borderSide: const BorderSide(
|
||||
// color: AppColor.primaryColor,
|
||||
// width: 2.0,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// fillColor: AppColor.accentColor,
|
||||
// hoverColor: AppColor.accentColor,
|
||||
// focusColor: AppColor.accentColor,
|
||||
// border: const OutlineInputBorder(
|
||||
// borderRadius:
|
||||
// BorderRadius.all(Radius.circular(12))),
|
||||
// labelText: 'Password'.tr,
|
||||
// hintText: 'Enter your Password'.tr,
|
||||
// ),
|
||||
// validator: (value) {
|
||||
// if (value!.isEmpty) {
|
||||
// return 'Please enter Your Password.'.tr;
|
||||
// }
|
||||
// if (value.length < 6) {
|
||||
// return 'Password must br at least 6 character.'
|
||||
// .tr;
|
||||
// }
|
||||
// return null;
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 15,
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: Get.width * .8,
|
||||
// child: TextFormField(
|
||||
// keyboardType: TextInputType.phone,
|
||||
// cursorColor: AppColor.accentColor,
|
||||
// controller: controller.phoneController,
|
||||
// decoration: InputDecoration(
|
||||
// focusedBorder: OutlineInputBorder(
|
||||
// borderSide: const BorderSide(
|
||||
// color: AppColor.primaryColor,
|
||||
// width: 2.0,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// focusColor: AppColor.accentColor,
|
||||
// fillColor: AppColor.accentColor,
|
||||
// border: const OutlineInputBorder(
|
||||
// borderRadius:
|
||||
// BorderRadius.all(Radius.circular(12))),
|
||||
// labelText: 'Phone'.tr,
|
||||
// hintText: 'Enter your phone number'.tr,
|
||||
// ),
|
||||
// validator: (value) {
|
||||
// if (value!.isEmpty || value.length != 10) {
|
||||
// return 'Please enter your phone number.'.tr;
|
||||
// }
|
||||
// return null;
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 15,
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Next'.tr,
|
||||
// onPressed: () => controller.nextToAIDetection()),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
Image.asset(
|
||||
'assets/images/on1.png',
|
||||
fit: BoxFit.cover,
|
||||
height: double.maxFinite,
|
||||
width: double.maxFinite,
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .7,
|
||||
width: Get.width * .9,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/logo.gif',
|
||||
height: Get.width * .3,
|
||||
width: Get.width * .3,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .3,
|
||||
width: Get.width * .8,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Sign in with Google for easier email and name entry'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Sign In by Google'.tr,
|
||||
onPressed: () async {
|
||||
// await GoogleSignInHelper.signIn();
|
||||
},
|
||||
kolor: AppColor.blueColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
91
siro_driver/lib/views/auth/captin/verify_email_captain.dart
Executable file
91
siro_driver/lib/views/auth/captin/verify_email_captain.dart
Executable file
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/register_captin_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
class VerifyEmailCaptainPage extends StatelessWidget {
|
||||
VerifyEmailCaptainPage({super.key});
|
||||
RegisterCaptainController registerCaptinController =
|
||||
Get.put(RegisterCaptainController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Verify Email For Driver'.tr,
|
||||
body: [
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: 20,
|
||||
child: Text(
|
||||
'We sent 5 digit to your Email provided'.tr,
|
||||
style: AppStyle.title.copyWith(fontSize: 20),
|
||||
)),
|
||||
GetBuilder<RegisterCaptainController>(
|
||||
builder: (controller) => Positioned(
|
||||
top: 100,
|
||||
left: 80,
|
||||
right: 80,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: registerCaptinController.verifyCode,
|
||||
decoration: InputDecoration(
|
||||
labelStyle: AppStyle.title,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: '5 digit'.tr,
|
||||
counterStyle: AppStyle.number,
|
||||
hintStyle: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.accentColor),
|
||||
),
|
||||
maxLength: 5,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Send Verfication Code'.tr,
|
||||
onPressed: () {
|
||||
registerCaptinController.sendVerifications();
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
|
||||
Padding verifyEmail() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColor.accentColor,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
child: TextField(
|
||||
maxLength: 1,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
185
siro_driver/lib/views/auth/country_widget.dart
Executable file
185
siro_driver/lib/views/auth/country_widget.dart
Executable file
@@ -0,0 +1,185 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
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/auth/captin/login_captin_controller.dart';
|
||||
import '../../controller/profile/profile_controller.dart';
|
||||
import '../../main.dart';
|
||||
import '../widgets/elevated_btn.dart';
|
||||
import 'captin/login_captin.dart';
|
||||
|
||||
class CountryPicker extends StatelessWidget {
|
||||
final ProfileController controller = Get.put(ProfileController());
|
||||
|
||||
final List<String> countryOptions = [
|
||||
'Jordan',
|
||||
'USA',
|
||||
'Egypt',
|
||||
'Turkey',
|
||||
'Saudi Arabia',
|
||||
'Qatar',
|
||||
'Bahrain',
|
||||
'Kuwait',
|
||||
];
|
||||
|
||||
CountryPicker({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<ProfileController>(builder: (controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
"Select Your Country".tr,
|
||||
style: AppStyle.headTitle2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// const SizedBox(
|
||||
// height: 20,
|
||||
// ),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
"To ensure you receive the most accurate information for your location, please select your country below. This will help tailor the app experience and content to your country."
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CupertinoPicker(
|
||||
itemExtent: 32,
|
||||
onSelectedItemChanged: (int index) {
|
||||
controller.setCountry(countryOptions[index]);
|
||||
box.write(BoxName.countryCode,
|
||||
countryOptions[index]); // Save in English
|
||||
},
|
||||
children: List.generate(
|
||||
countryOptions.length,
|
||||
(index) => Center(
|
||||
child: Text(
|
||||
countryOptions[index]
|
||||
.tr, // Display translated if not English
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
MyElevatedButton(
|
||||
title: 'Select Country'.tr, // Use translated text for button
|
||||
onPressed: () {
|
||||
Get.find<LoginDriverController>().saveCountryCode(controller
|
||||
.selectedCountry
|
||||
.toString()); // No conversion needed
|
||||
box.write(
|
||||
BoxName.countryCode, //
|
||||
controller.selectedCountry); // Already saved in English
|
||||
Get.snackbar(controller.selectedCountry.toString().tr, '');
|
||||
Get.off(LoginCaptin());
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CountryPickerFromSetting extends StatelessWidget {
|
||||
final ProfileController controller = Get.put(ProfileController());
|
||||
final LoginDriverController loginController =
|
||||
Get.put(LoginDriverController());
|
||||
|
||||
final List<String> countryOptions = [
|
||||
'Jordan',
|
||||
'Egypt',
|
||||
'Turkey',
|
||||
'Saudi Arabia',
|
||||
'Qatar',
|
||||
'Bahrain',
|
||||
'Kuwait',
|
||||
'USA',
|
||||
];
|
||||
|
||||
CountryPickerFromSetting({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<ProfileController>(builder: (controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
"Select Your Country".tr,
|
||||
style: AppStyle.headTitle2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// const SizedBox(
|
||||
// height: 20,
|
||||
// ),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
"To ensure you receive the most accurate information for your location, please select your country below. This will help tailor the app experience and content to your country."
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CupertinoPicker(
|
||||
itemExtent: 32,
|
||||
onSelectedItemChanged: (int index) {
|
||||
controller.setCountry(countryOptions[index]);
|
||||
box.write(BoxName.countryCode,
|
||||
countryOptions[index]); // Save in English
|
||||
},
|
||||
children: List.generate(
|
||||
countryOptions.length,
|
||||
(index) => Center(
|
||||
child: Text(
|
||||
countryOptions[index]
|
||||
.tr, // Display translated if not English
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
MyElevatedButton(
|
||||
title: 'Select Country'.tr, // Use translated text for button
|
||||
onPressed: () async {
|
||||
loginController.saveCountryCode(controller.selectedCountry
|
||||
.toString()); // No conversion needed
|
||||
box.write(
|
||||
BoxName.countryCode, //
|
||||
controller.selectedCountry); // Already saved in English
|
||||
Get.snackbar(controller.selectedCountry.toString().tr, '',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
// Get.back();//
|
||||
// Get.back();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
295
siro_driver/lib/views/auth/register_page.dart
Executable file
295
siro_driver/lib/views/auth/register_page.dart
Executable file
@@ -0,0 +1,295 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/auth/register_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
|
||||
class RegisterPage extends StatelessWidget {
|
||||
const RegisterPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(RegisterController());
|
||||
return MyScafolld(
|
||||
title: 'Register'.tr,
|
||||
body: [
|
||||
GetBuilder<RegisterController>(
|
||||
builder: (controller) => Form(
|
||||
key: controller.formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: const Offset(3, 3),
|
||||
color: AppColor.accentColor,
|
||||
blurRadius: 3)
|
||||
],
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Get.width * .4,
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
controller: controller.firstNameController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
fillColor: AppColor.accentColor,
|
||||
hoverColor: AppColor.accentColor,
|
||||
focusColor: AppColor.accentColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'First name'.tr,
|
||||
hintText: 'Enter your first name'.tr,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Please enter your first name.'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .4,
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
controller: controller.lastNameController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusColor: AppColor.accentColor,
|
||||
fillColor: AppColor.accentColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'Last name'.tr,
|
||||
hintText: 'Enter your last name'.tr,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Please enter your last name.'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
controller: controller.emailController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
fillColor: AppColor.accentColor,
|
||||
hoverColor: AppColor.accentColor,
|
||||
focusColor: AppColor.accentColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(12))),
|
||||
labelText: 'Email'.tr,
|
||||
hintText: 'Enter your email address'.tr,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty ||
|
||||
(!value.contains('@') ||
|
||||
!value.contains('.'))) {
|
||||
return 'Please enter Your Email.'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
controller: controller.passwordController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
fillColor: AppColor.accentColor,
|
||||
hoverColor: AppColor.accentColor,
|
||||
focusColor: AppColor.accentColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(12))),
|
||||
labelText: 'Password'.tr,
|
||||
hintText: 'Enter your Password'.tr,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Please enter Your Password.'.tr;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return 'Password must br at least 6 character.'
|
||||
.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Get.width * .4,
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.phone,
|
||||
cursorColor: AppColor.accentColor,
|
||||
controller: controller.phoneController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusColor: AppColor.accentColor,
|
||||
fillColor: AppColor.accentColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'Phone'.tr,
|
||||
hintText: 'Enter your phone number'.tr,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty || value.length != 10) {
|
||||
return 'Please enter your phone number.'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: Get.width * .4,
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
controller: controller.siteController,
|
||||
decoration: InputDecoration(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
color: AppColor.primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
focusColor: AppColor.accentColor,
|
||||
fillColor: AppColor.accentColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12))),
|
||||
labelText: 'City'.tr,
|
||||
hintText: 'Enter your City'.tr,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Please enter your City.'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => controller.getBirthDate(),
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: Get.width * .4,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(),
|
||||
borderRadius: BorderRadius.circular(13)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20),
|
||||
child: Text(
|
||||
controller.birthDate,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// DropdownButton(
|
||||
// value: controller.gender,
|
||||
// items: [
|
||||
// DropdownMenuItem(
|
||||
// value: 'Male'.tr,
|
||||
// child: Text('Male'.tr),
|
||||
// ),
|
||||
// DropdownMenuItem(
|
||||
// value: 'Female'.tr,
|
||||
// child: Text('Female'.tr),
|
||||
// ),
|
||||
// DropdownMenuItem(
|
||||
// value: '--'.tr,
|
||||
// child: Text('--'.tr),
|
||||
// ),
|
||||
// ],
|
||||
// onChanged: (value) {
|
||||
// controller.changeGender(value!);
|
||||
// },
|
||||
// )
|
||||
],
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Register'.tr,
|
||||
onPressed: () => controller.register())
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
167
siro_driver/lib/views/auth/syria/pending_driver_page.dart
Normal file
167
siro_driver/lib/views/auth/syria/pending_driver_page.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class DriverVerificationScreen extends StatefulWidget {
|
||||
const DriverVerificationScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DriverVerificationScreen> createState() =>
|
||||
_DriverVerificationScreenState();
|
||||
}
|
||||
|
||||
class _DriverVerificationScreenState extends State<DriverVerificationScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Animated Icon
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (_, child) {
|
||||
return Transform.rotate(
|
||||
angle: _controller.value * 2 * math.pi,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.sync,
|
||||
size: 80,
|
||||
color: theme.primaryColor.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
"Your Application is Under Review".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.textTheme.titleLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Main Message
|
||||
Text(
|
||||
"We have received your application to join us as a driver. Our team is currently reviewing it. Thank you for your patience."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black54,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Notification Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: theme.primaryColor.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.notifications_active_outlined,
|
||||
color: theme.primaryColor, size: 30),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"You Will Be Notified".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"We will send you a notification as soon as your account is approved. You can safely close this page, and we'll let you know when the review is complete."
|
||||
.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Refresh Button
|
||||
// ElevatedButton.icon(
|
||||
// onPressed: () {
|
||||
// // TODO: Add logic to check status from your API
|
||||
// Get.snackbar(
|
||||
// "Status", // This can also be a key if you want
|
||||
// "Checking for updates...".tr,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// );
|
||||
// },
|
||||
// icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
// label: Text(
|
||||
// "Refresh Status".tr,
|
||||
// style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||
// ),
|
||||
// style: ElevatedButton.styleFrom(
|
||||
// backgroundColor: theme.primaryColor,
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 40, vertical: 15),
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(30.0),
|
||||
// ),
|
||||
// elevation: 3,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
500
siro_driver/lib/views/auth/syria/registration_view.dart
Normal file
500
siro_driver/lib/views/auth/syria/registration_view.dart
Normal file
@@ -0,0 +1,500 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../controller/auth/syria/registration_controller.dart';
|
||||
|
||||
class RegistrationView extends StatelessWidget {
|
||||
const RegistrationView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final RegistrationController controller = Get.put(RegistrationController());
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Driver Registration'.tr),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 90,
|
||||
child: Obx(
|
||||
() => Stepper(
|
||||
currentStep: controller.currentPage.value,
|
||||
type: StepperType.horizontal,
|
||||
controlsBuilder: (_, __) => const SizedBox.shrink(),
|
||||
steps: [
|
||||
Step(
|
||||
title: Text('Driver'.tr),
|
||||
content: const SizedBox.shrink()),
|
||||
Step(
|
||||
title: Text('Vehicle'.tr),
|
||||
content: const SizedBox.shrink()),
|
||||
Step(
|
||||
title: Text('Docs'.tr), content: const SizedBox.shrink()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: controller.pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onPageChanged: (i) => controller.currentPage.value = i,
|
||||
children: [
|
||||
_buildDriverInfoStep(context, controller),
|
||||
_buildCarInfoStep(context, controller),
|
||||
_buildDocumentUploadStep(context, controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _buildBottomNavBar(controller),
|
||||
);
|
||||
}
|
||||
|
||||
// STEP 1
|
||||
Widget _buildDriverInfoStep(BuildContext ctx, RegistrationController c) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: c.driverInfoFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Driver's Personal Information".tr,
|
||||
style:
|
||||
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: c.firstNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'First Name'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Required field'.tr;
|
||||
}
|
||||
if (v.length < 2) {
|
||||
return 'Name must be at least 2 characters'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.lastNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Last Name'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Required field'.tr;
|
||||
}
|
||||
if (v.length < 2) {
|
||||
return 'Name must be at least 2 characters'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.nationalIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'National ID Number'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Required field'.tr;
|
||||
}
|
||||
if (v.length != 11) {
|
||||
return 'National ID must be 11 digits'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.bithdateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'سنة الميلاد'.tr,
|
||||
hintText: '1999'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Required field'.tr;
|
||||
}
|
||||
if (v.length != 4) {
|
||||
return 'Birth year must be 4 digits'.tr;
|
||||
}
|
||||
// Optional: check if it’s a valid number
|
||||
if (int.tryParse(v) == null) {
|
||||
return 'Enter a valid year'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.driverLicenseExpiryController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'License Expiry Date'.tr,
|
||||
hintText: 'YYYY-MM-DD'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
DateTime? d = await showDatePicker(
|
||||
context: ctx,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
);
|
||||
if (d != null) {
|
||||
c.driverLicenseExpiryDate = d;
|
||||
c.driverLicenseExpiryController.text =
|
||||
d.toLocal().toString().split(' ')[0];
|
||||
}
|
||||
},
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Please select a date'.tr : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// STEP 2
|
||||
Widget _buildCarInfoStep(BuildContext ctx, RegistrationController c) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: c.carInfoFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Vehicle Information'.tr,
|
||||
style:
|
||||
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
// ============================================================
|
||||
// 1. القائمة المنسدلة لتصنيف المركبة (سيارة، دراجة، فان)
|
||||
// ============================================================
|
||||
DropdownButtonFormField<int>(
|
||||
value: c.selectedVehicleCategoryId,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Vehicle Category'.tr, // ترجمة: تصنيف المركبة
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.directions_car),
|
||||
),
|
||||
items: c.vehicleCategoryOptions.map((item) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: item['id'],
|
||||
child: Text(item['name']),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
c.selectedVehicleCategoryId = val;
|
||||
c.update(); // تحديث الواجهة إذا لزم الأمر
|
||||
},
|
||||
validator: (v) => (v == null) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// ============================================================
|
||||
// 2. القائمة المنسدلة لنوع الوقود (بنزين، كهرباء...)
|
||||
// ============================================================
|
||||
DropdownButtonFormField<int>(
|
||||
value: c.selectedFuelTypeId,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Fuel Type'.tr, // ترجمة: نوع الوقود
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.local_gas_station),
|
||||
),
|
||||
items: c.fuelTypeOptions.map((item) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: item['id'],
|
||||
child: Text(item['name']),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
c.selectedFuelTypeId = val;
|
||||
c.update();
|
||||
},
|
||||
validator: (v) => (v == null) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.carPlateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Car Plate Number'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.carMakeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Car Make (e.g., Toyota)'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.carModelController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Car Model (e.g., Corolla)'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.carYearController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Year of Manufacture'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// حقل اسم اللون (يبقى اختياري أو نملؤه تلقائيًا عند اختيار الهكس)
|
||||
// TextFormField(
|
||||
// controller: c.carColorController,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: 'Car Color (Name)'.tr,
|
||||
// border: const OutlineInputBorder(),
|
||||
// ),
|
||||
// validator: (v) =>
|
||||
// (v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
|
||||
// الدروب داون للهكس + دائرة اللون
|
||||
GetBuilder<RegistrationController>(
|
||||
id: 'carColor', // اختياري لتحديث انتقائي
|
||||
builder: (c) {
|
||||
return DropdownButtonFormField<String>(
|
||||
value: (c.colorHex != null && c.colorHex!.isNotEmpty)
|
||||
? c.colorHex
|
||||
: null,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Car Color (Hex)'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
// prefixIcon: Padding(
|
||||
// padding:
|
||||
// const EdgeInsetsDirectional.only(start: 12, end: 8),
|
||||
// child: Container(
|
||||
// width: 18,
|
||||
// height: 18,
|
||||
// decoration: BoxDecoration(
|
||||
// color: (c.colorHex?.isNotEmpty ?? false)
|
||||
// ? c.hexToColor(c.colorHex!)
|
||||
// : Colors.transparent,
|
||||
// shape: BoxShape.circle,
|
||||
// border: Border.all(color: Colors.black26),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
),
|
||||
items: RegistrationController.kCarColorOptions.map((opt) {
|
||||
final hex = opt['hex']!;
|
||||
final key = opt['key']!;
|
||||
return DropdownMenuItem<String>(
|
||||
value: hex,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: c.hexToColor(hex),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.black12),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(key.tr)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (hex) {
|
||||
c.colorHex = hex; // خزّن الهكس
|
||||
final key = RegistrationController.kCarColorOptions
|
||||
.firstWhere((o) => o['hex'] == hex)['key']!;
|
||||
c.carColorController.text = key.tr;
|
||||
c.update([
|
||||
'carColor'
|
||||
]); // <-- مهم: يعيد بناء الودجت ويحدّث الدائرة
|
||||
},
|
||||
validator: (v) =>
|
||||
(v == null || v.isEmpty) ? 'Required field'.tr : null,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// STEP 3
|
||||
Widget _buildDocumentUploadStep(BuildContext ctx, RegistrationController c) {
|
||||
final String linkUpload =
|
||||
'https://syria.intaleq.xyz/intaleq/auth/syria/uploadImage.php';
|
||||
|
||||
return GetBuilder<RegistrationController>(
|
||||
builder: (ctrl) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Upload Documents'.tr,
|
||||
style:
|
||||
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
_buildImagePickerBox(
|
||||
'Driver License (Front)'.tr,
|
||||
ctrl.docUrls['driver_license_front'],
|
||||
// () => ctrl.pickImage(ImageType.driverLicenseFront),
|
||||
|
||||
() async =>
|
||||
await ctrl.choosImage(linkUpload, 'driver_license_front'),
|
||||
),
|
||||
_buildImagePickerBox(
|
||||
'Driver License (Back)'.tr,
|
||||
ctrl.docUrls['driver_license_back'],
|
||||
() async =>
|
||||
await ctrl.choosImage(linkUpload, 'driver_license_back'),
|
||||
// () => ctrl.pickImage(ImageType.driverLicenseBack),
|
||||
),
|
||||
_buildImagePickerBox(
|
||||
'Car Registration (Front)'.tr,
|
||||
ctrl.docUrls['car_license_front'],
|
||||
() async =>
|
||||
await ctrl.choosImage(linkUpload, 'car_license_front'),
|
||||
// () => ctrl.pickImage(ImageType.carLicenseFront),
|
||||
),
|
||||
_buildImagePickerBox(
|
||||
'Car Registration (Back)'.tr,
|
||||
ctrl.docUrls['car_license_back'],
|
||||
() async => await ctrl.choosImage(linkUpload, 'car_license_back'),
|
||||
// () => ctrl.pickImage(ImageType.carLicenseBack),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget signedImageWithAuth(String fileUrl, String bearerToken) {
|
||||
return Image.network(
|
||||
fileUrl,
|
||||
headers: {'Authorization': 'Bearer $bearerToken'},
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Text('Image expired or unauthorized'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImagePickerBox(String title, String? img, VoidCallback onTap) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
child: (img != null && img.isNotEmpty)
|
||||
? Image.network(
|
||||
img,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.broken_image, size: 40, color: Colors.red),
|
||||
const SizedBox(height: 8),
|
||||
Text('Image not available',
|
||||
style: TextStyle(color: Colors.red[700])),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.camera_alt_outlined,
|
||||
size: 40, color: Colors.grey[600]),
|
||||
const SizedBox(height: 8),
|
||||
Text(title, style: TextStyle(color: Colors.grey[700])),
|
||||
Text('Tap to upload'.tr,
|
||||
style:
|
||||
const TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomNavBar(RegistrationController c) {
|
||||
return Obx(() => Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (c.currentPage.value > 0)
|
||||
TextButton(
|
||||
onPressed: c.goToPreviousStep,
|
||||
child: Text('<< BACK'.tr),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 40, vertical: 12),
|
||||
backgroundColor: c.currentPage.value == 2
|
||||
? Colors.green
|
||||
: Theme.of(Get.context!).primaryColor,
|
||||
),
|
||||
onPressed: c.isLoading.value
|
||||
? null
|
||||
: () {
|
||||
if (c.currentPage.value == 2) {
|
||||
c.submitRegistration();
|
||||
} else {
|
||||
c.goToNextStep();
|
||||
}
|
||||
},
|
||||
child: c.isLoading.value
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white, strokeWidth: 2))
|
||||
: Text(
|
||||
c.currentPage.value == 2 ? 'SUBMIT'.tr : 'NEXT >>'.tr,
|
||||
style:
|
||||
const TextStyle(fontSize: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
90
siro_driver/lib/views/auth/verify_email_page.dart
Executable file
90
siro_driver/lib/views/auth/verify_email_page.dart
Executable file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/auth/register_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
class VerifyEmailPage extends StatelessWidget {
|
||||
const VerifyEmailPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(RegisterController());
|
||||
return MyScafolld(
|
||||
title: 'Verify Email'.tr,
|
||||
body: [
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Text(
|
||||
'We sent 5 digit to your Email provided'.tr,
|
||||
style: AppStyle.title.copyWith(fontSize: 20),
|
||||
)),
|
||||
GetBuilder<RegisterController>(
|
||||
builder: (controller) => Positioned(
|
||||
top: 100,
|
||||
left: 80,
|
||||
right: 80,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: controller.verfyCode,
|
||||
decoration: InputDecoration(
|
||||
labelStyle: AppStyle.title,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: '5 digit'.tr,
|
||||
counterStyle: AppStyle.number,
|
||||
hintStyle: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.accentColor),
|
||||
),
|
||||
maxLength: 5,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Send Verfication Code'.tr,
|
||||
onPressed: () => controller.sendVerifications())
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
|
||||
Padding verifyEmail() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColor.accentColor,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
child: TextField(
|
||||
maxLength: 1,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
229
siro_driver/lib/views/gamification/challenges_page.dart
Normal file
229
siro_driver/lib/views/gamification/challenges_page.dart
Normal file
@@ -0,0 +1,229 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../controller/gamification/challenges_controller.dart';
|
||||
|
||||
class ChallengesPage extends StatelessWidget {
|
||||
ChallengesPage({super.key});
|
||||
final ChallengesController controller = Get.put(ChallengesController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
body: GetBuilder<ChallengesController>(builder: (cc) {
|
||||
if (cc.isLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark));
|
||||
}
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 160,
|
||||
pinned: true,
|
||||
backgroundColor: Get.isDarkMode ? FinanceDesignSystem.primaryDark : const Color(0xFF1A237E),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: Colors.white, size: 20),
|
||||
onPressed: () => Get.back()),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded,
|
||||
color: Colors.white),
|
||||
onPressed: () => cc.fetchChallengeProgress())
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text('Challenges'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18)),
|
||||
background: Stack(fit: StackFit.expand, children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: Get.isDarkMode
|
||||
? [FinanceDesignSystem.primaryDark, const Color(0xFF1E1E2E)]
|
||||
: [const Color(0xFF0A0E21), const Color(0xFF1A237E)]
|
||||
))),
|
||||
Positioned(
|
||||
right: -30,
|
||||
top: -10,
|
||||
child: Icon(Icons.bolt_rounded,
|
||||
size: 160, color: Colors.white.withValues(alpha: 0.04))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Daily Challenges
|
||||
_sectionHeader(
|
||||
'Daily Challenges'.tr,
|
||||
'🔥',
|
||||
cc.dailyChallenges.where((c) => c.isCompleted).length,
|
||||
cc.dailyChallenges.length),
|
||||
const SizedBox(height: 12),
|
||||
...cc.dailyChallenges.map((c) => _challengeCard(c, cc)),
|
||||
const SizedBox(height: 28),
|
||||
// Weekly Challenges
|
||||
_sectionHeader(
|
||||
'Weekly Challenges'.tr,
|
||||
'🏆',
|
||||
cc.weeklyChallenges.where((c) => c.isCompleted).length,
|
||||
cc.weeklyChallenges.length),
|
||||
const SizedBox(height: 12),
|
||||
...cc.weeklyChallenges.map((c) => _challengeCard(c, cc)),
|
||||
]))),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionHeader(String title, String emoji, int done, int total) {
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Row(children: [
|
||||
Text(emoji, style: const TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: 8),
|
||||
Text(title, style: FinanceDesignSystem.headingStyle),
|
||||
]),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.accentBlue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20)),
|
||||
child: Text('$done/$total',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.accentBlue)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _challengeCard(Challenge c, ChallengesController cc) {
|
||||
final isAr = Get.locale?.languageCode == 'ar';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
border: c.isCompleted
|
||||
? Border.all(color: c.color.withValues(alpha: 0.3), width: 1.5)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: c.color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(c.icon, color: c.color, size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(isAr ? c.titleAr : c.titleEn,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
Text(isAr ? c.descriptionAr : c.descriptionEn,
|
||||
style:
|
||||
TextStyle(fontSize: 11, color: FinanceDesignSystem.textSecondary)),
|
||||
])),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFD700).withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Text('+${c.reward}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFFF8F00))),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
// Progress bar
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text('${c.currentProgress}/${c.target}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: FinanceDesignSystem.textSecondary)),
|
||||
Text('${(c.progress * 100).toStringAsFixed(0)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 11, fontWeight: FontWeight.bold, color: c.color)),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
Stack(children: [
|
||||
Container(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(4))),
|
||||
LayoutBuilder(
|
||||
builder: (ctx, cons) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.easeOutCubic,
|
||||
height: 8,
|
||||
width: cons.maxWidth * c.progress,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [c.color, c.color.withValues(alpha: 0.6)]),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
)),
|
||||
]),
|
||||
// Claim button
|
||||
if (c.isCompleted && !c.isClaimed) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => cc.claimReward(c),
|
||||
icon: const Icon(Icons.card_giftcard_rounded, size: 18),
|
||||
label: Text('Claim Reward'.tr),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: c.color,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
)),
|
||||
],
|
||||
if (c.isClaimed) ...[
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.check_circle_rounded,
|
||||
color: FinanceDesignSystem.successGreen, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text('Claimed'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.successGreen)),
|
||||
])),
|
||||
],
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
262
siro_driver/lib/views/gamification/leaderboard_page.dart
Normal file
262
siro_driver/lib/views/gamification/leaderboard_page.dart
Normal file
@@ -0,0 +1,262 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../controller/gamification/leaderboard_controller.dart';
|
||||
|
||||
class LeaderboardPage extends StatelessWidget {
|
||||
LeaderboardPage({super.key});
|
||||
final LeaderboardController controller = Get.put(LeaderboardController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
body: GetBuilder<LeaderboardController>(builder: (lc) {
|
||||
if (lc.isLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark));
|
||||
}
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 200,
|
||||
pinned: true,
|
||||
backgroundColor: Get.isDarkMode
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: const Color(0xFF0A0E21),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: Colors.white, size: 20),
|
||||
onPressed: () => Get.back()),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text('Leaderboard'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18)),
|
||||
background: Stack(fit: StackFit.expand, children: [
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(colors: [
|
||||
Color(0xFF0A0E21),
|
||||
Color(0xFFB71C1C)
|
||||
]))),
|
||||
Positioned(
|
||||
right: -40,
|
||||
top: -20,
|
||||
child: Icon(Icons.leaderboard_rounded,
|
||||
size: 180, color: Colors.white.withValues(alpha: 0.04))),
|
||||
// Top 3 podium
|
||||
if (lc.currentLeaderboard.length >= 3)
|
||||
Positioned(
|
||||
bottom: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildPodium(lc)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
// Tab bar
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
child: Row(children: [
|
||||
_tab('Trips'.tr, Icons.local_taxi_rounded, 0, lc),
|
||||
_tab('Earnings'.tr, Icons.monetization_on_rounded, 1, lc),
|
||||
]),
|
||||
)),
|
||||
// List
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(ctx, i) {
|
||||
if (i >= lc.currentLeaderboard.length) return null;
|
||||
return _buildRow(
|
||||
lc.currentLeaderboard[i], lc.selectedTab == 1);
|
||||
},
|
||||
childCount: lc.currentLeaderboard.length,
|
||||
),
|
||||
)),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPodium(LeaderboardController lc) {
|
||||
final top3 = lc.currentLeaderboard.take(3).toList();
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (top3.length > 1)
|
||||
_podiumItem(top3[1], 2, 50, const Color(0xFFC0C0C0)),
|
||||
if (top3.isNotEmpty)
|
||||
_podiumItem(top3[0], 1, 70, const Color(0xFFFFD700)),
|
||||
if (top3.length > 2)
|
||||
_podiumItem(top3[2], 3, 35, const Color(0xFFCD7F32)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _podiumItem(LeaderboardEntry e, int rank, double height, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
CircleAvatar(
|
||||
radius: rank == 1 ? 22 : 18,
|
||||
backgroundColor: color.withValues(alpha: 0.3),
|
||||
child: Text(_getRankEmoji(rank),
|
||||
style: TextStyle(fontSize: rank == 1 ? 22 : 16))),
|
||||
const SizedBox(height: 4),
|
||||
Text(e.name.length > 8 ? '${e.name.substring(0, 8)}...' : e.name,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight:
|
||||
e.isCurrentUser ? FontWeight.bold : FontWeight.normal)),
|
||||
]));
|
||||
}
|
||||
|
||||
Widget _tab(String label, IconData icon, int idx, LeaderboardController lc) {
|
||||
final selected = lc.selectedTab == idx;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => lc.changeTab(idx),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
selected ? FinanceDesignSystem.cardColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05), blurRadius: 5)
|
||||
]
|
||||
: null),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(icon,
|
||||
size: 16,
|
||||
color: selected ? FinanceDesignSystem.primaryDark : Colors.grey),
|
||||
const SizedBox(width: 6),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: selected
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: Colors.grey)),
|
||||
]),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildRow(LeaderboardEntry e, bool isEarnings) {
|
||||
final rankColor = e.rank == 1
|
||||
? const Color(0xFFFFD700)
|
||||
: e.rank == 2
|
||||
? const Color(0xFFC0C0C0)
|
||||
: e.rank == 3
|
||||
? const Color(0xFFCD7F32)
|
||||
: Colors.grey.shade400;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: e.isCurrentUser
|
||||
? FinanceDesignSystem.accentBlue.withValues(alpha: 0.08)
|
||||
: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: e.isCurrentUser
|
||||
? Border.all(
|
||||
color: FinanceDesignSystem.accentBlue.withValues(alpha: 0.3),
|
||||
width: 1.5)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3))
|
||||
],
|
||||
),
|
||||
child: Row(children: [
|
||||
// Rank
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: rankColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Center(
|
||||
child: e.rank <= 3
|
||||
? Text(_getRankEmoji(e.rank),
|
||||
style: const TextStyle(fontSize: 16))
|
||||
: Text('${e.rank}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: rankColor)))),
|
||||
const SizedBox(width: 12),
|
||||
// Avatar
|
||||
CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
child: Text(e.name.isNotEmpty ? e.name[0] : '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.textSecondary))),
|
||||
const SizedBox(width: 12),
|
||||
// Name
|
||||
Expanded(
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Text(e.name,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight:
|
||||
e.isCurrentUser ? FontWeight.bold : FontWeight.w500,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
if (e.isCurrentUser) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
child: Text('You'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white)))
|
||||
],
|
||||
]),
|
||||
])),
|
||||
// Value
|
||||
Text(
|
||||
isEarnings
|
||||
? '${e.value.toStringAsFixed(0)} ${'SYP'.tr}'
|
||||
: '${e.value.toInt()} ${'Rides'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
String _getRankEmoji(int rank) => rank == 1
|
||||
? '🥇'
|
||||
: rank == 2
|
||||
? '🥈'
|
||||
: '🥉';
|
||||
}
|
||||
252
siro_driver/lib/views/gamification/referral_center_page.dart
Normal file
252
siro_driver/lib/views/gamification/referral_center_page.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../controller/gamification/referral_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
|
||||
class ReferralCenterPage extends StatelessWidget {
|
||||
ReferralCenterPage({super.key});
|
||||
final ReferralController controller = Get.put(ReferralController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
body: GetBuilder<ReferralController>(builder: (rc) {
|
||||
if (rc.isLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark));
|
||||
}
|
||||
return CustomScrollView(physics: const BouncingScrollPhysics(), slivers: [
|
||||
// ═══ Header ═══
|
||||
SliverAppBar(
|
||||
expandedHeight: 220, pinned: true,
|
||||
backgroundColor: const Color(0xFF0A0E21),
|
||||
leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 20), onPressed: () => Get.back()),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text('Referral Center'.tr, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
background: Stack(fit: StackFit.expand, children: [
|
||||
Container(decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFF0A0E21), Color(0xFF311B92)]))),
|
||||
Positioned(right: -50, bottom: -30, child: Icon(Icons.share_rounded, size: 200, color: Colors.white.withValues(alpha: 0.03))),
|
||||
// Stats in header
|
||||
Positioned(bottom: 60, left: 24, right: 24, child: Row(children: [
|
||||
_headerStat('${rc.totalReferrals}', 'Total Invites'.tr),
|
||||
_headerStat('${rc.activeReferrals}', 'Active'.tr),
|
||||
_headerStat('${rc.totalRewardsEarned.toStringAsFixed(0)}', 'Rewards'.tr),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
),
|
||||
SliverPadding(padding: const EdgeInsets.fromLTRB(16, 24, 16, 40), sliver: SliverList(delegate: SliverChildListDelegate([
|
||||
// ═══ Referral Code Card ═══
|
||||
_buildCodeCard(rc),
|
||||
const SizedBox(height: 20),
|
||||
// ═══ Share Buttons ═══
|
||||
_buildShareSection(rc),
|
||||
const SizedBox(height: 24),
|
||||
// ═══ How it Works ═══
|
||||
_buildHowItWorks(),
|
||||
const SizedBox(height: 24),
|
||||
// ═══ Driver Referrals ═══
|
||||
if (rc.driverReferrals.isNotEmpty) ...[
|
||||
Text('Driver Invitations'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
...rc.driverReferrals.map((r) => _referralItem(r, const Color(0xFF2196F3))),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
// ═══ Passenger Referrals ═══
|
||||
if (rc.passengerReferrals.isNotEmpty) ...[
|
||||
Text('Passenger Invitations'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
...rc.passengerReferrals.map((r) => _referralItem(r, const Color(0xFF4CAF50))),
|
||||
],
|
||||
if (rc.driverReferrals.isEmpty && rc.passengerReferrals.isEmpty)
|
||||
_buildEmptyState(),
|
||||
]))),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _headerStat(String value, String label) {
|
||||
return Expanded(child: Column(children: [
|
||||
Text(value, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: Colors.white)),
|
||||
Text(label, style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: 0.6))),
|
||||
]));
|
||||
}
|
||||
|
||||
Widget _buildCodeCard(ReferralController rc) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(colors: [Color(0xFF1A237E), Color(0xFF311B92)]),
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [BoxShadow(color: const Color(0xFF311B92).withValues(alpha: 0.3), blurRadius: 15, offset: const Offset(0, 8))],
|
||||
),
|
||||
child: Column(children: [
|
||||
Text('Your Referral Code'.tr, style: TextStyle(color: Colors.white.withValues(alpha: 0.7), fontSize: 14)),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withValues(alpha: 0.2))),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(rc.referralCode, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3)),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () { rc.copyCode(); mySnackbarSuccess('Code copied!'.tr); },
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8)),
|
||||
child: const Icon(Icons.copy_rounded, color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('Share this code to earn rewards'.tr, style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareSection(ReferralController rc) {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('Share via'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
Row(children: [
|
||||
_shareButton(Icons.share_rounded, 'Share'.tr, FinanceDesignSystem.accentBlue, () => Share.share(rc.shareMessage)),
|
||||
const SizedBox(width: 12),
|
||||
_shareButton(Icons.chat_rounded, 'WhatsApp'.tr, const Color(0xFF25D366), () => _shareWhatsApp(rc.shareMessage)),
|
||||
const SizedBox(width: 12),
|
||||
_shareButton(Icons.person_add_rounded, 'Invite Driver'.tr, const Color(0xFFFF9800), () => _showInviteDialog(rc, 'driver')),
|
||||
const SizedBox(width: 12),
|
||||
_shareButton(Icons.hail_rounded, 'Invite Rider'.tr, const Color(0xFF9C27B0), () => _showInviteDialog(rc, 'passenger')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _shareButton(IconData icon, String label, Color color, VoidCallback onTap) {
|
||||
return Expanded(child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(14)),
|
||||
child: Column(children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: 6),
|
||||
Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
]),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildHowItWorks() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4))]),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('How It Works'.tr, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 16),
|
||||
_step('1', 'Share your code'.tr, 'Send your referral code to friends'.tr, const Color(0xFF2196F3)),
|
||||
_step('2', 'Friend signs up'.tr, 'They register using your code'.tr, const Color(0xFFFF9800)),
|
||||
_step('3', 'Both earn rewards'.tr, 'You get 100 pts, they get 50 pts'.tr, const Color(0xFF4CAF50)),
|
||||
_step('4', 'Bonus at 10 trips'.tr, 'Extra 200 pts when they complete 10 trips'.tr, const Color(0xFF9C27B0)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _step(String num, String title, String desc, Color color) {
|
||||
return Padding(padding: const EdgeInsets.only(bottom: 12), child: Row(children: [
|
||||
Container(width: 32, height: 32, decoration: BoxDecoration(color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8)),
|
||||
child: Center(child: Text(num, style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: color))),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: FinanceDesignSystem.primaryDark)),
|
||||
Text(desc, style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
||||
])),
|
||||
]));
|
||||
}
|
||||
|
||||
Widget _referralItem(ReferralRecord r, Color color) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.02), blurRadius: 8, offset: const Offset(0, 3))]),
|
||||
child: Row(children: [
|
||||
Container(width: 40, height: 40, decoration: BoxDecoration(color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(r.type == 'driver' ? Icons.local_taxi_rounded : Icons.person_rounded, color: color, size: 20)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(r.name.isNotEmpty ? r.name : r.phone, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
Text(r.joinDate, style: TextStyle(fontSize: 10, color: Colors.grey.shade500)),
|
||||
])),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: r.status == 'active' ? FinanceDesignSystem.successGreen.withValues(alpha: 0.1)
|
||||
: r.status == 'registered' ? FinanceDesignSystem.accentBlue.withValues(alpha: 0.1) : Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(r.status.tr, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600,
|
||||
color: r.status == 'active' ? FinanceDesignSystem.successGreen
|
||||
: r.status == 'registered' ? FinanceDesignSystem.accentBlue : Colors.grey)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: Column(children: [
|
||||
Icon(Icons.people_outline_rounded, size: 60, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 12),
|
||||
Text('No invitation found yet!'.tr, style: TextStyle(fontSize: 14, color: Colors.grey.shade400)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Start sharing your code!'.tr, style: TextStyle(fontSize: 12, color: Colors.grey.shade300)),
|
||||
])));
|
||||
}
|
||||
|
||||
void _shareWhatsApp(String message) async {
|
||||
final url = 'https://wa.me/?text=${Uri.encodeComponent(message)}';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
|
||||
void _showInviteDialog(ReferralController rc, String type) {
|
||||
final phoneController = TextEditingController();
|
||||
Get.defaultDialog(
|
||||
title: type == 'driver' ? 'Invite Driver'.tr : 'Invite Rider'.tr,
|
||||
titleStyle: FinanceDesignSystem.headingStyle,
|
||||
content: Column(children: [
|
||||
Text('Enter phone number'.tr, style: FinanceDesignSystem.subHeadingStyle),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: phoneController, keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(hintText: '09XX XXX XXX', prefixIcon: const Icon(Icons.phone_rounded),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12))),
|
||||
),
|
||||
]),
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (phoneController.text.length >= 10) {
|
||||
Get.back();
|
||||
bool success = type == 'driver'
|
||||
? await rc.inviteDriver(phoneController.text)
|
||||
: await rc.invitePassenger(phoneController.text);
|
||||
if (success) mySnackbarSuccess('Invite sent successfully'.tr);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: FinanceDesignSystem.accentBlue, foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12)),
|
||||
child: Text('Send Invite'.tr),
|
||||
),
|
||||
cancel: TextButton(onPressed: () => Get.back(), child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||
);
|
||||
}
|
||||
}
|
||||
101
siro_driver/lib/views/home/Captin/About Us/about_us.dart
Executable file
101
siro_driver/lib/views/home/Captin/About Us/about_us.dart
Executable file
@@ -0,0 +1,101 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AboutPage extends StatelessWidget {
|
||||
const AboutPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'About Us'.tr,
|
||||
body: [
|
||||
// Company Logo (consider adding an image asset)
|
||||
ListView(
|
||||
children: [
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png', // Replace with your logo image asset path
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
),
|
||||
), // Company Name and Location
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Intaleq LLC\n${'Syria'.tr}',
|
||||
style: AppStyle.headTitle2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'Intaleq is a ride-sharing app designed with your safety and affordability in mind. We connect you with reliable drivers in your area, ensuring a convenient and stress-free travel experience.\n\nHere are some of the key features that set us apart:'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
), // Security Features List
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.lock, color: Colors.blue),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Most Secure Methods'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.phone, color: Colors.blue),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
'In-App VOIP Calls'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.videocam, color: Colors.blue),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Recorded Trips for Safety'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
), // Affordability Highlight
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'\nWe also prioritize affordability, offering competitive pricing to make your rides accessible.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// About Us Text
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
145
siro_driver/lib/views/home/Captin/About Us/frequantly_question.dart
Executable file
145
siro_driver/lib/views/home/Captin/About Us/frequantly_question.dart
Executable file
@@ -0,0 +1,145 @@
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../constant/style.dart';
|
||||
|
||||
class FrequentlyQuestionsPage extends StatelessWidget {
|
||||
const FrequentlyQuestionsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String selectedPayment = 'cash'; // Replace with your initial selection
|
||||
bool canCancelRide = false;
|
||||
return MyScafolld(
|
||||
title: 'Frequently Questions'.tr,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
// Question 1: How do I request a ride?
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'How do I request a ride?'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'Step-by-step instructions on how to request a ride through the Intaleq app.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Question 2: What types of vehicles are available?
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'What types of vehicles are available?'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'Intaleq offers a variety of vehicle options to suit your needs, including economy, comfort, and luxury. Choose the option that best fits your budget and passenger count.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Question 3: How can I pay for my ride?
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'How can I pay for my ride?'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'Intaleq offers multiple payment methods for your convenience. Choose between cash payment or credit/debit card payment during ride confirmation.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Question 4: Can I cancel my ride? (if applicable)
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'Can I cancel my ride?'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'Yes, you can cancel your ride under certain conditions (e.g., before driver is assigned). See the Intaleq cancellation policy for details.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Question 5 & 6: Driver-specific questions
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'Driver Registration & Requirements'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'${'How can I register as a driver?'.tr}\n${'What are the requirements to become a driver?'.tr}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
MyDialog().getDialog('title', 'midTitle', () {
|
||||
; //todo add in this dialog papers for driver
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Visit our website or contact Intaleq support for information on driver registration and requirements.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Question 7: How do I communicate with the other party?
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'How do I communicate with the other party (passenger/driver)?'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'Intaleq provides in-app chat functionality to allow you to communicate with your driver or passenger during your ride.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Question 8: What safety measures does Intaleq offer?
|
||||
ExpansionTile(
|
||||
title: Text(
|
||||
'What safety measures does Intaleq offer?'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
children: [
|
||||
Text(
|
||||
'Intaleq prioritizes your safety. We offer features like driver verification, in-app trip tracking, and emergency contact options.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
222
siro_driver/lib/views/home/Captin/About Us/settings_captain.dart
Executable file
222
siro_driver/lib/views/home/Captin/About Us/settings_captain.dart
Executable file
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/profile/setting_controller.dart';
|
||||
import 'package:siro_driver/views/lang/languages.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
// تأكد من صحة مسارات الاستيراد هذه
|
||||
import '../../../../controller/functions/vibrate.dart'; // Controller with isVibrate
|
||||
import '../../../auth/country_widget.dart';
|
||||
import 'about_us.dart';
|
||||
import 'frequantly_question.dart';
|
||||
import 'using_app_page.dart';
|
||||
|
||||
class SettingsCaptain extends StatelessWidget {
|
||||
const SettingsCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// تحميل الـ Controllers المطلوبة
|
||||
final settingsController = Get.put(SettingController());
|
||||
final homeController = Get.put(HomePageController());
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Settings'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
children: <Widget>[
|
||||
// --- القسم الأول: عام ---
|
||||
_buildSectionHeader('General'.tr, context),
|
||||
_buildSettingsCard(
|
||||
children: [
|
||||
_buildListTile(
|
||||
icon: Icons.language_outlined,
|
||||
title: 'Language'.tr,
|
||||
subtitle: 'Change the app language'.tr,
|
||||
onTap: () => Get.to(() => const Language()),
|
||||
),
|
||||
// _buildListTile(
|
||||
// icon: Icons.flag_outlined,
|
||||
// title: 'Change Country'.tr,
|
||||
// subtitle: 'Get features for your country'.tr,
|
||||
// onTap: () => Get.to(
|
||||
// () => MyScafolld(
|
||||
// title: 'Change Country'.tr,
|
||||
// body: [CountryPickerFromSetting()],
|
||||
// isleading: true,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- القسم الثاني: تفضيلات التطبيق ---
|
||||
_buildSectionHeader('App Preferences'.tr, context),
|
||||
_buildSettingsCard(
|
||||
children: [
|
||||
_buildSwitchTile(
|
||||
icon: Icons.dark_mode_outlined,
|
||||
title: 'App Dark Mode'.tr,
|
||||
subtitle: 'Switch between light and dark themes'.tr,
|
||||
controller: settingsController,
|
||||
valueGetter: (ctrl) => (ctrl).isDarkMode,
|
||||
onChanged: (ctrl) => (ctrl).toggleAppTheme(),
|
||||
),
|
||||
_buildSwitchTile(
|
||||
icon: Icons.map_outlined,
|
||||
color: AppColor.redColor,
|
||||
title: 'Google Map App'.tr,
|
||||
subtitle: 'Run Google Maps directly'.tr,
|
||||
controller: settingsController,
|
||||
valueGetter: (ctrl) => (ctrl).isGoogleMapsEnabled,
|
||||
onChanged: (ctrl) => (ctrl).onChangMapApp(),
|
||||
),
|
||||
_buildSwitchTile(
|
||||
icon: Icons.map_outlined,
|
||||
title: 'Map Dark Mode'.tr,
|
||||
subtitle: 'Switch between light and dark map styles'.tr,
|
||||
controller: settingsController,
|
||||
valueGetter: (ctrl) => (ctrl).isMapDarkMode,
|
||||
onChanged: (ctrl) => (ctrl).toggleMapTheme(),
|
||||
),
|
||||
|
||||
_buildSwitchTile(
|
||||
icon: Icons.vibration,
|
||||
title: 'Vibration'.tr,
|
||||
subtitle: 'Vibration feedback for buttons'.tr,
|
||||
controller: homeController,
|
||||
valueGetter: (ctrl) => (ctrl).isVibrate,
|
||||
onChanged: (ctrl) => (ctrl)
|
||||
.changeVibrateOption(true), // قد تحتاج لتعديل الدالة
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- القسم الثالث: المساعدة والدعم ---
|
||||
_buildSectionHeader('Help & Support'.tr, context),
|
||||
_buildSettingsCard(
|
||||
children: [
|
||||
_buildListTile(
|
||||
icon: Icons.quiz_outlined,
|
||||
title: 'Frequently Questions'.tr,
|
||||
onTap: () => Get.to(() => const FrequentlyQuestionsPage()),
|
||||
),
|
||||
_buildListTile(
|
||||
icon: Icons.support_agent,
|
||||
title: "How to use App".tr,
|
||||
onTap: () => Get.to(() => const UsingAppPage()),
|
||||
),
|
||||
_buildListTile(
|
||||
icon: Icons.info_outline,
|
||||
title: 'About Us'.tr,
|
||||
onTap: () => Get.to(() => const AboutPage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- القسم الرابع: تسجيل الخروج ---
|
||||
_buildSectionHeader('Account'.tr, context),
|
||||
_buildSettingsCard(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
title: Text(
|
||||
'Logout'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.red, fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () {
|
||||
MyDialog().getDialog(
|
||||
'Logout'.tr,
|
||||
'Are you sure you want to logout?'.tr,
|
||||
() {
|
||||
// أضف دالة تسجيل الخروج هنا
|
||||
Get.back(); // لإغلاق مربع الحوار
|
||||
},
|
||||
// isConfirmation: true,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ويدجت لبناء عنوان كل قسم
|
||||
Widget _buildSectionHeader(String title, BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, bottom: 12.0),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ويدجت لبناء بطاقة الإعدادات
|
||||
Widget _buildSettingsCard({required List<Widget> children}) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
clipBehavior: Clip.antiAlias, // مهم لجعل splash effect داخل حدود البطاقة
|
||||
child: Column(children: children),
|
||||
);
|
||||
}
|
||||
|
||||
// ويدجت لبناء عنصر قابل للضغط (مثل اللغة، عن التطبيق)
|
||||
Widget _buildListTile({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
String? subtitle,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: Colors.grey.shade700),
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: subtitle != null ? Text(subtitle) : null,
|
||||
trailing:
|
||||
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
// ويدجت لبناء عنصر يحتوي على مفتاح تفعيل/إلغاء (Switch)
|
||||
Widget _buildSwitchTile<T extends GetxController>({
|
||||
required IconData icon,
|
||||
Color? color,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required T controller,
|
||||
required bool Function(T) valueGetter,
|
||||
required Function(T) onChanged,
|
||||
}) {
|
||||
return GetBuilder<T>(
|
||||
init: controller,
|
||||
builder: (ctrl) {
|
||||
return SwitchListTile(
|
||||
secondary: Icon(icon, color: color ?? Colors.grey.shade700),
|
||||
title:
|
||||
Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(subtitle),
|
||||
value: valueGetter(ctrl),
|
||||
onChanged: (value) => onChanged(ctrl),
|
||||
activeColor: AppColor.primaryColor,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
112
siro_driver/lib/views/home/Captin/About Us/using_app_page.dart
Executable file
112
siro_driver/lib/views/home/Captin/About Us/using_app_page.dart
Executable file
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
// 1. إنشاء Class لتمثيل بيانات كل سؤال وجواب
|
||||
class FaqItem {
|
||||
final String question;
|
||||
final Widget answer; // استخدام Widget يسمح بوضع نصوص أو صور
|
||||
final IconData icon;
|
||||
|
||||
FaqItem({required this.question, required this.answer, required this.icon});
|
||||
}
|
||||
|
||||
class UsingAppPage extends StatelessWidget {
|
||||
const UsingAppPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 2. تجهيز قائمة البيانات بشكل منظم
|
||||
final List<FaqItem> faqItems = [
|
||||
FaqItem(
|
||||
question: "What are the order details we provide to you?".tr,
|
||||
icon: Icons.receipt_long_outlined,
|
||||
answer: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Image.network(
|
||||
'https://api.tripz-egypt.com/tripz/imageForUsingApp/order_page.jpg',
|
||||
fit: BoxFit.cover,
|
||||
// يمكنك إضافة مؤشر تحميل هنا
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
child:
|
||||
Icon(Icons.error_outline, color: Colors.red, size: 40));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
FaqItem(
|
||||
question: "What is the feature of our wallet?".tr,
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
answer: Text(
|
||||
'''Intaleq Wallet Features:
|
||||
|
||||
- Transfer money multiple times.
|
||||
- Transfer to anyone.
|
||||
- Make purchases.
|
||||
- Charge your account.
|
||||
- Charge a friend's Intaleq account.
|
||||
- Store your money with us and receive it in your bank as a monthly salary.'''
|
||||
.tr,
|
||||
style:
|
||||
TextStyle(fontSize: 15, height: 1.5, color: Colors.grey.shade700),
|
||||
),
|
||||
),
|
||||
FaqItem(
|
||||
question: "What is Types of Trips in Intaleq?".tr,
|
||||
icon: Icons.map_outlined,
|
||||
answer: Text(
|
||||
'''Types of Trips in Intaleq:
|
||||
|
||||
- Comfort: For cars newer than 2017 with air conditioning.
|
||||
- Lady: For girl drivers.
|
||||
- Speed: For fixed salary and endpoints.
|
||||
- Mashwari: For flexible trips where passengers choose the car and driver with prior arrangements.
|
||||
- Raih Gai: For same-day return trips longer than 50km.'''
|
||||
.tr,
|
||||
style:
|
||||
TextStyle(fontSize: 15, height: 1.5, color: Colors.grey.shade700),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// 3. بناء الواجهة الرسومية باستخدام البيانات
|
||||
return MyScafolld(
|
||||
title: "How to use App".tr, // تم تغيير العنوان ليكون أعم
|
||||
isleading: true,
|
||||
body: [
|
||||
ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
itemCount: faqItems.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final item = faqItems[index];
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ExpansionTile(
|
||||
leading: Icon(item.icon, color: Theme.of(context).primaryColor),
|
||||
title: Text(item.question,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 12),
|
||||
item.answer,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
193
siro_driver/lib/views/home/Captin/About Us/video_page.dart
Executable file
193
siro_driver/lib/views/home/Captin/About Us/video_page.dart
Executable file
@@ -0,0 +1,193 @@
|
||||
import 'package:siro_driver/constant/info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
|
||||
import '../../../../controller/home/captin/help/video_controller.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class VideoListPage extends StatelessWidget {
|
||||
final VideoController videoController = Get.put(VideoController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text('Videos Tutorials'.tr),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 10.0), // Outer padding around the list
|
||||
child: GetBuilder<VideoController>(
|
||||
builder: (videoController) {
|
||||
return ListView.builder(
|
||||
itemCount: videoController.videos.length,
|
||||
itemBuilder: (context, index) {
|
||||
final video = videoController.videos[index];
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8.0), // Spacing between each card
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(12.0), // Rounded corners
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CupertinoColors.systemGrey.withOpacity(0.3),
|
||||
offset: const Offset(
|
||||
0, 4), // Offset for shadow to appear below
|
||||
blurRadius: 10.0, // Blur for softer shadow effect
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: CupertinoColors.systemGrey4,
|
||||
width: 0.5, // Subtle border for a refined iOS-like look
|
||||
),
|
||||
),
|
||||
child: CupertinoListTile(
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Text(
|
||||
video['title'],
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
video['description'],
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
// Navigate to video player page (iOS-style)
|
||||
Get.to(() => VideoPlayerPage1(videoUrl: video['url']));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPlayerPage extends StatelessWidget {
|
||||
final String videoUrl;
|
||||
|
||||
VideoPlayerPage({required this.videoUrl});
|
||||
|
||||
final VideoController videoController = Get.put(VideoController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialize the video when the page is loaded
|
||||
videoController.initializeVideo(videoUrl);
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: const CupertinoNavigationBar(
|
||||
middle: Text(AppInformation.appName),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: GetBuilder<VideoController>(
|
||||
builder: (controller) {
|
||||
if (!controller.videoPlayerController.value.isInitialized) {
|
||||
return const CircularProgressIndicator(); // Show loading indicator while initializing
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Video player widget
|
||||
AspectRatio(
|
||||
aspectRatio:
|
||||
controller.videoPlayerController.value.aspectRatio,
|
||||
// child: VideoPlayer(controller.videoPlayerController),
|
||||
child: VideoPlayer(controller.videoPlayerController),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Play/pause button
|
||||
CupertinoButton(
|
||||
onPressed: () {
|
||||
if (controller.videoPlayerController.value.isPlaying) {
|
||||
controller.pause();
|
||||
} else {
|
||||
controller.play();
|
||||
}
|
||||
},
|
||||
child: Icon(
|
||||
controller.videoPlayerController.value.isPlaying
|
||||
? CupertinoIcons.pause
|
||||
: CupertinoIcons.play_arrow,
|
||||
color: CupertinoColors.activeBlue,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPlayerPage1 extends StatelessWidget {
|
||||
final String videoUrl;
|
||||
|
||||
const VideoPlayerPage1({required this.videoUrl});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Extract the video ID from the URL
|
||||
String videoId = YoutubePlayer.convertUrlToId(videoUrl)!;
|
||||
|
||||
// Create a YoutubePlayerController
|
||||
final YoutubePlayerController _controller = YoutubePlayerController(
|
||||
initialVideoId: videoId,
|
||||
flags: const YoutubePlayerFlags(
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
mute: false,
|
||||
captionLanguage: 'ar',
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Full-screen YouTube player
|
||||
Positioned.fill(
|
||||
child: YoutubePlayer(
|
||||
controller: _controller,
|
||||
showVideoProgressIndicator: true,
|
||||
),
|
||||
),
|
||||
// Overlay back button in the top left corner for exit
|
||||
Positioned(
|
||||
top: 40.0,
|
||||
left: 16.0,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
191
siro_driver/lib/views/home/Captin/assurance_health_page.dart
Executable file
191
siro_driver/lib/views/home/Captin/assurance_health_page.dart
Executable file
@@ -0,0 +1,191 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/home/captin/help/assurance_controller.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../widgets/my_scafold.dart';
|
||||
|
||||
class AssuranceHealthPage extends StatelessWidget {
|
||||
AssuranceHealthPage({super.key});
|
||||
final AssuranceHealthController assuranceHealthController =
|
||||
Get.put(AssuranceHealthController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return MyScafolld(
|
||||
title: "Health Insurance".tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<AssuranceHealthController>(
|
||||
builder: (controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"When you complete 500 trips, you will be eligible for exclusive health insurance offers."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color:
|
||||
theme.textTheme.bodyMedium?.color?.withOpacity(0.8),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
controller.getTripCountByCaptain(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text("Show My Trip Count".tr),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildTripCountAvatar(
|
||||
controller.tripCount['count'] == null
|
||||
? '0'
|
||||
: controller.tripCount['count'].toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
),
|
||||
child: Text(
|
||||
"We have partnered with health insurance providers to offer you special health coverage. Complete 500 trips and receive a 20% discount on health insurance premiums."
|
||||
.tr,
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: () => _showInsuranceDialog(context, controller),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(
|
||||
"Would you like to proceed with health insurance?".tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showInsuranceDialog(
|
||||
BuildContext context, AssuranceHealthController controller) {
|
||||
final TextEditingController providerController = TextEditingController();
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Text("Confirmation".tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Would you like to proceed with health insurance?".tr),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: providerController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Do you have a disease for a long time?".tr,
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text("No".tr, style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (providerController.text.isNotEmpty) {
|
||||
await controller.addDriverHealthAssurance(
|
||||
healthInsuranceProvider: providerController.text,
|
||||
);
|
||||
Get.back();
|
||||
} else {
|
||||
Get.snackbar("Error".tr,
|
||||
"Please provide details about any long-term diseases.".tr);
|
||||
}
|
||||
},
|
||||
child: Text("Yes".tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTripCountAvatar(String count) {
|
||||
return Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF42A5F5), Color(0xFF1976D2)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
count,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
111
siro_driver/lib/views/home/Captin/bottom_bar.dart
Executable file
111
siro_driver/lib/views/home/Captin/bottom_bar.dart
Executable file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
|
||||
class BottomBarController extends GetxController {
|
||||
var currentIndex = 0.obs;
|
||||
|
||||
void changePage(int index) {
|
||||
currentIndex.value = index;
|
||||
}
|
||||
}
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
final BottomBarController controller = Get.put(BottomBarController());
|
||||
|
||||
HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Bottom Bar Example'.tr),
|
||||
),
|
||||
body: Obx(() => IndexedStack(
|
||||
index: controller.currentIndex.value,
|
||||
children: const [
|
||||
HomeView(),
|
||||
ProfileView(),
|
||||
StatisticsView(),
|
||||
WalletView(),
|
||||
],
|
||||
)),
|
||||
bottomNavigationBar: Obx(() => BottomNavigationBar(
|
||||
backgroundColor: Theme.of(context).cardColor,
|
||||
currentIndex: controller.currentIndex.value,
|
||||
onTap: controller.changePage,
|
||||
selectedItemColor: AppColor.primaryColor,
|
||||
unselectedItemColor: Theme.of(context).hintColor,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.home),
|
||||
label: 'Home'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.person),
|
||||
label: 'Profile'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
label: 'Statistics'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.account_balance_wallet),
|
||||
label: 'Wallet'.tr,
|
||||
),
|
||||
],
|
||||
)),
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeView extends StatelessWidget {
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Map<String, dynamic> data;
|
||||
return const Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Home View'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileView extends StatelessWidget {
|
||||
const ProfileView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text('Profile View'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatisticsView extends StatelessWidget {
|
||||
const StatisticsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text('Statistics View'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WalletView extends StatelessWidget {
|
||||
const WalletView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: Text('Wallet View'),
|
||||
);
|
||||
}
|
||||
}
|
||||
302
siro_driver/lib/views/home/Captin/camer_widget.dart
Executable file
302
siro_driver/lib/views/home/Captin/camer_widget.dart
Executable file
@@ -0,0 +1,302 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:siro_driver/constant/colors.dart';
|
||||
// import 'package:siro_driver/constant/style.dart';
|
||||
// import 'package:siro_driver/controller/functions/camer_controller.dart';
|
||||
// import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
// import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
// class CameraWidgetCardId extends StatelessWidget {
|
||||
// final CameraClassController cameraClassController =
|
||||
// Get.put(CameraClassController());
|
||||
|
||||
// CameraWidgetCardId({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: 'Scan Id'.tr,
|
||||
// body: [
|
||||
// Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
// child: GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) =>
|
||||
// cameraClassController.isCameraInitialized
|
||||
// ? Stack(
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: FittedBox(
|
||||
// fit: BoxFit.fitWidth,
|
||||
// child: SizedBox(
|
||||
// width: Get.width * .9,
|
||||
// height: Get.width *
|
||||
// .9, // Set the desired aspect ratio here
|
||||
// child: CameraPreview(
|
||||
// cameraClassController.cameraController,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 72,
|
||||
// child: Container(
|
||||
// width: 230,
|
||||
// height: 25,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.yellowColor,
|
||||
// width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 60,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 230,
|
||||
// height: 25,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 156,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 20,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 175,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 191,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 207,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 225,
|
||||
// right: 5,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 15,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 115,
|
||||
// left: 25,
|
||||
// child: Container(
|
||||
// width: 120,
|
||||
// height: 110,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// : Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Camera not initilaized yet'.tr,
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID MklGoogle'.tr, onPressed: () {}),
|
||||
// // cameraClassController.takePictureAndMLGoogleScan()),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Tesseract'.tr, onPressed: () {}),
|
||||
// ],
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Api'.tr,
|
||||
// onPressed: () => cameraClassController.extractCardId()),
|
||||
// GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) => Expanded(
|
||||
// child:
|
||||
// Text(cameraClassController.scannedText.toString())))
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// isleading: true);
|
||||
// }
|
||||
// }
|
||||
|
||||
// class CameraWidgetPassPort extends StatelessWidget {
|
||||
// final CameraClassController cameraClassController =
|
||||
// Get.put(CameraClassController());
|
||||
|
||||
// CameraWidgetPassPort({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: 'Scan Id'.tr,
|
||||
// body: [
|
||||
// Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
// child: GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) =>
|
||||
// cameraClassController.isCameraInitialized
|
||||
// ? Stack(
|
||||
// children: [
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: FittedBox(
|
||||
// fit: BoxFit.fitWidth,
|
||||
// child: SizedBox(
|
||||
// width: Get.width * .9,
|
||||
// height: Get.width *
|
||||
// .9, // Set the desired aspect ratio here
|
||||
// child: CameraPreview(
|
||||
// cameraClassController.cameraController,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 35,
|
||||
// left: 35,
|
||||
// width: Get.width * .77,
|
||||
// height:
|
||||
// 17, //left":95.0,"top":134.0,"width":2909.0,"height":175.0
|
||||
// child: Container(
|
||||
// // width: 230,
|
||||
// // height: 25,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.yellowColor,
|
||||
// width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 60,
|
||||
// right: 25,
|
||||
// width: 90,
|
||||
// height: 25,
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// top: 110,
|
||||
// right: 90,
|
||||
// child: Container(
|
||||
// width: 140,
|
||||
// height: 20,
|
||||
// decoration: BoxDecoration(
|
||||
// // color: AppColor.blueColor,
|
||||
// border: Border.all(
|
||||
// color: AppColor.blueColor, width: 2),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// : Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// height: Get.width * 3 / 4,
|
||||
// width: Get.width,
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// 'Camera not initilaized yet',
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID MklGoogle'.tr, onPressed: () => {}),
|
||||
// // cameraClassController.takePictureAndMLGoogleScan()),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Tesseract'.tr, onPressed: () {}),
|
||||
// ],
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Scan ID Api'.tr,
|
||||
// onPressed: () => cameraClassController.extractCardId()),
|
||||
// GetBuilder<CameraClassController>(
|
||||
// builder: (cameraClassController) => Expanded(
|
||||
// child:
|
||||
// Text(cameraClassController.scannedText.toString())))
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// isleading: true);
|
||||
// }
|
||||
// }
|
||||
448
siro_driver/lib/views/home/Captin/driver_map_page.dart
Executable file
448
siro_driver/lib/views/home/Captin/driver_map_page.dart
Executable file
@@ -0,0 +1,448 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/functions/location_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../Rate/rate_passenger.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import 'mapDriverWidgets/driver_end_ride_bar.dart';
|
||||
import 'mapDriverWidgets/google_driver_map_page.dart';
|
||||
import 'mapDriverWidgets/google_map_app.dart';
|
||||
import 'mapDriverWidgets/passenger_info_window.dart';
|
||||
import 'mapDriverWidgets/sos_connect.dart';
|
||||
import 'mapDriverWidgets/sped_circle.dart';
|
||||
|
||||
class PassengerLocationMapPage extends StatelessWidget {
|
||||
PassengerLocationMapPage({super.key});
|
||||
final LocationController locationController = Get.put(LocationController());
|
||||
final MapDriverController mapDriverController =
|
||||
Get.put(MapDriverController());
|
||||
|
||||
// دالة ديالوج الخروج
|
||||
Future<bool> showExitDialog() async {
|
||||
bool? result = await Get.defaultDialog(
|
||||
title: "Warning".tr,
|
||||
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||
middleText:
|
||||
"Active ride in progress. Leaving might stop tracking. Exit?".tr,
|
||||
barrierDismissible: false,
|
||||
radius: 15,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Stay'.tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () => Get.back(result: false)),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Exit'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () => Get.back(result: true)),
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
|
||||
// 🔥 [Fix] argumentLoading ضرورية هنا للعودة للرحلة من صفحة الهوم
|
||||
// (عند العودة لا يُستدعى onInit() لأن الكنترولر موجود مسبقاً)
|
||||
// الحماية من التكرار موجودة داخل argumentLoading بواسطة _isRouteRequested flag
|
||||
mapDriverController.argumentLoading();
|
||||
mapDriverController.startTimerToShowPassengerInfoWindowFromDriver();
|
||||
mapDriverController
|
||||
.update(['PassengerInfo', 'DriverEndBar', 'SosConnect']);
|
||||
}
|
||||
});
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
final shouldExit = await showExitDialog();
|
||||
if (shouldExit) Get.back();
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
children: [
|
||||
// 1. الخريطة (الخلفية)
|
||||
Positioned.fill(
|
||||
child: GoogleDriverMap(locationController: locationController)),
|
||||
|
||||
// 2. واجهة المستخدم (فوق الخريطة)
|
||||
SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// أ) زر الإلغاء (أعلى اليسار)
|
||||
CancelWidget(mapDriverController: mapDriverController),
|
||||
|
||||
// ب) شريط إنهاء الرحلة (أعلى الوسط)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(child: driverEndRideBar())),
|
||||
|
||||
// ج) شريط التعليمات الملاحية (الأسفل)
|
||||
const InstructionsOfRoads(),
|
||||
|
||||
// د) نافذة معلومات الراكب (تعلو التعليمات ديناميكياً)
|
||||
const PassengerInfoWindow(),
|
||||
// SpeedCircle(),
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 20, // أو أي مسافة تناسبك
|
||||
child: GetBuilder<MapDriverController>(
|
||||
// id: 'SosConnect', // لتحديث الزر عند بدء الرحلة
|
||||
builder: (controller) {
|
||||
// حساب الهوامش ديناميكياً لرفع الأزرار فوق النوافذ السفلية
|
||||
double bottomPadding = 0;
|
||||
if (controller.currentInstruction.isNotEmpty)
|
||||
bottomPadding += 120;
|
||||
if (controller.isPassengerInfoWindow)
|
||||
bottomPadding += 220;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: EdgeInsets.only(bottom: bottomPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SosConnect(), // ويدجت نظيفة
|
||||
const SizedBox(height: 12),
|
||||
const GoogleMapApp(), // ويدجت نظيفة
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 3. النوافذ المنبثقة (Overlay)
|
||||
const PricesWindow(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. ويدجت شريط التعليمات (InstructionsOfRoads)
|
||||
// ---------------------------------------------------------------------------
|
||||
class InstructionsOfRoads extends StatelessWidget {
|
||||
const InstructionsOfRoads({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 20,
|
||||
left: 15,
|
||||
right: 15,
|
||||
child: GetBuilder<MapDriverController>(
|
||||
builder: (controller) {
|
||||
// إخفاء الشريط إذا لم يكن هناك تعليمات
|
||||
if (controller.currentInstruction.isEmpty) return const SizedBox();
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
builder: (context, value, child) {
|
||||
final theme = Theme.of(context);
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 50 * (1 - value)), // حركة انزلاق
|
||||
child: Opacity(
|
||||
opacity: value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor
|
||||
.withOpacity(0.95), // Adaptive background
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5)),
|
||||
],
|
||||
border: Border.all(
|
||||
color: theme.dividerColor.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// أيقونة الاتجاه
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(controller.currentManeuverIcon,
|
||||
color: Colors.white, size: 24),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// نص التعليمات
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${"NEXT STEP".tr} (${controller.distanceToNextStep})",
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
controller.currentInstruction,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600, height: 1.2),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// فاصل عمودي
|
||||
Container(
|
||||
width: 1,
|
||||
height: 30,
|
||||
color: Colors.white12,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10)),
|
||||
|
||||
// زر التحكم بالصوت
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => controller.toggleTts(),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
controller.isTtsEnabled
|
||||
? Icons.volume_up_rounded
|
||||
: Icons.volume_off_rounded,
|
||||
color: controller.isTtsEnabled
|
||||
? AppColor.greenColor
|
||||
: Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. ويدجت زر الإلغاء (CancelWidget) - كامل
|
||||
// ---------------------------------------------------------------------------
|
||||
class CancelWidget extends StatelessWidget {
|
||||
const CancelWidget({super.key, required this.mapDriverController});
|
||||
final MapDriverController mapDriverController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 10,
|
||||
left: 15,
|
||||
child: GetBuilder<MapDriverController>(builder: (controller) {
|
||||
// نخفي الزر إذا انتهت الرحلة
|
||||
if (controller.isRideFinished) return const SizedBox();
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
blurRadius: 8)
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
onTap: () => _showCancelDialog(context, controller),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Icon(Icons.close_rounded,
|
||||
color: AppColor.redColor, size: 26),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCancelDialog(BuildContext context, MapDriverController controller) {
|
||||
Get.defaultDialog(
|
||||
title: "Cancel Trip?".tr,
|
||||
titleStyle: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
radius: 16,
|
||||
content: Column(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded,
|
||||
size: 50, color: Colors.orangeAccent),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Please tell us why you want to cancel.".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Form(
|
||||
key: controller.formKeyCancel,
|
||||
child: MyTextForm(
|
||||
controller: controller.cancelTripCotroller,
|
||||
label: "Reason".tr,
|
||||
hint: "Write your reason...".tr,
|
||||
type: TextInputType.text,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: SizedBox(
|
||||
width: 100,
|
||||
child: MyElevatedButton(
|
||||
title: 'Confirm'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () async {
|
||||
// استدعاء دالة الإلغاء من الكنترولر
|
||||
await controller.cancelTripFromDriverAfterApplied();
|
||||
// Get.back(); // عادة موجودة داخل الدالة في الكنترولر
|
||||
},
|
||||
),
|
||||
),
|
||||
cancel: SizedBox(
|
||||
width: 100,
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Back'.tr, style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. ويدجت نافذة الأسعار (PricesWindow) - كامل
|
||||
// ---------------------------------------------------------------------------
|
||||
class PricesWindow extends StatelessWidget {
|
||||
const PricesWindow({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(builder: (controller) {
|
||||
// إخفاء إذا لم تكن مفعلة
|
||||
if (!controller.isPriceWindow) return const SizedBox();
|
||||
|
||||
return Container(
|
||||
color: Colors.black.withOpacity(0.6), // خلفية معتمة
|
||||
child: Center(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.8, end: 1.0),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
builder: (context, scale, child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
width: Get.width * 0.85,
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).shadowColor.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.check_circle_rounded,
|
||||
color: AppColor.primaryColor, size: 50),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Total Price'.tr,
|
||||
style: AppStyle.headTitle2
|
||||
.copyWith(fontSize: 18, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'${controller.totalCost} ${'\$'.tr}',
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: MyElevatedButton(
|
||||
title: 'Collect Payment'.tr,
|
||||
kolor: AppColor.primaryColor,
|
||||
onPressed: () {
|
||||
// الذهاب لصفحة التقييم
|
||||
Get.to(() => RatePassenger(), arguments: {
|
||||
'rideId': controller.rideId,
|
||||
'passengerId': controller.passengerId,
|
||||
'driverId': controller.driverId,
|
||||
'price': controller.paymentAmount,
|
||||
'walletChecked': controller.walletChecked
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
411
siro_driver/lib/views/home/Captin/history/history_captain.dart
Executable file
411
siro_driver/lib/views/home/Captin/history/history_captain.dart
Executable file
@@ -0,0 +1,411 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/auth/captin/history_captain.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
class HistoryCaptain extends StatelessWidget {
|
||||
const HistoryCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(HistoryCaptainController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
);
|
||||
}
|
||||
|
||||
final List trips = controller.historyData['message'] ?? [];
|
||||
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// 1. Custom App Bar
|
||||
SliverAppBar(
|
||||
expandedHeight: 180,
|
||||
pinned: true,
|
||||
stretch: true,
|
||||
backgroundColor: FinanceDesignSystem.primaryDark,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Ride History'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -50,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
Icons.history_rounded,
|
||||
size: 200,
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
trips.length.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Total Rides'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Trips List
|
||||
if (trips.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.history_toggle_off_rounded,
|
||||
size: 64,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'No Rides Yet'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Your completed trips will appear here'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final trip = trips[index];
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _TripHistoryCard(
|
||||
trip: trip,
|
||||
onTap: () {
|
||||
if (trip['status'] != 'Cancel' &&
|
||||
trip['status'] != 'CancelByPassenger') {
|
||||
controller
|
||||
.getHistoryDetails(trip['order_id']);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'This Trip Was Cancelled'.tr,
|
||||
'This Trip Was Cancelled'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: trips.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TripHistoryCard extends StatelessWidget {
|
||||
final Map trip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TripHistoryCard({required this.trip, required this.onTap});
|
||||
|
||||
String _formatDateToSyria(String? dateStr) {
|
||||
if (dateStr == null) return '';
|
||||
try {
|
||||
// Parse GMT date
|
||||
DateTime date = DateTime.parse(dateStr);
|
||||
// Add 3 hours for Syria (GMT+3)
|
||||
DateTime syriaDate = date.add(const Duration(hours: 3));
|
||||
// Format to readable string
|
||||
return DateFormat('yyyy-MM-dd HH:mm').format(syriaDate);
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.primaryDark
|
||||
.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.receipt_long_rounded,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${'OrderId'.tr} #${trip['order_id']}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDateToSyria(trip['created_at']),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildStatusChip(trip['status']),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Icon(Icons.circle,
|
||||
size: 12, color: FinanceDesignSystem.accentBlue),
|
||||
Container(
|
||||
width: 2,
|
||||
height: 20,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
Icon(Icons.location_on_rounded,
|
||||
size: 14, color: FinanceDesignSystem.dangerRed),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
trip['start_name'] ?? 'Pickup Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
trip['end_name'] ?? 'Destination Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trip['price'] != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${trip['price']} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 12, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusChip(String? status) {
|
||||
Color color;
|
||||
IconData icon;
|
||||
String label = status ?? 'Unknown';
|
||||
List<Color> gradientColors;
|
||||
|
||||
switch (status) {
|
||||
case 'Apply':
|
||||
color = FinanceDesignSystem.successGreen;
|
||||
icon = Icons.check_circle_rounded;
|
||||
label = 'Completed'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'Refused':
|
||||
color = FinanceDesignSystem.dangerRed;
|
||||
icon = Icons.cancel_rounded;
|
||||
label = 'Refused'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'Cancel':
|
||||
color = Colors.orange;
|
||||
icon = Icons.info_rounded;
|
||||
label = 'Cancelled'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'CancelByPassenger':
|
||||
color = const Color(0xFF6B4EFF);
|
||||
icon = Icons.person_off_rounded;
|
||||
label = 'Cancelled by Passenger'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
default:
|
||||
color = Colors.grey;
|
||||
icon = Icons.help_rounded;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: gradientColors,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
371
siro_driver/lib/views/home/Captin/history/history_details_page.dart
Executable file
371
siro_driver/lib/views/home/Captin/history/history_details_page.dart
Executable file
@@ -0,0 +1,371 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import 'package:siro_driver/controller/auth/captin/history_captain.dart';
|
||||
import 'package:siro_driver/controller/functions/launch.dart';
|
||||
|
||||
class HistoryDetailsPage extends StatefulWidget {
|
||||
const HistoryDetailsPage({super.key});
|
||||
|
||||
@override
|
||||
State<HistoryDetailsPage> createState() => _HistoryDetailsPageState();
|
||||
}
|
||||
|
||||
class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
|
||||
// Get the controller instance
|
||||
final HistoryCaptainController controller =
|
||||
Get.find<HistoryCaptainController>();
|
||||
|
||||
// Helper method to safely parse LatLng from a string 'lat,lng'
|
||||
LatLng? _parseLatLng(String? latLngString) {
|
||||
if (latLngString == null) return null;
|
||||
final parts = latLngString.split(',');
|
||||
if (parts.length != 2) return null;
|
||||
final lat = double.tryParse(parts[0]);
|
||||
final lng = double.tryParse(parts[1]);
|
||||
if (lat == null || lng == null) return null;
|
||||
return LatLng(lat, lng);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text('Trip Details'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
),
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final res = controller.historyDetailsData['data'];
|
||||
if (res == null) {
|
||||
return Center(child: Text('Could not load trip details.'.tr));
|
||||
}
|
||||
|
||||
final startLocation = _parseLatLng(res['start_location']);
|
||||
final endLocation = _parseLatLng(res['end_location']);
|
||||
|
||||
// Create markers for the map
|
||||
final Set<Marker> markers = {};
|
||||
if (startLocation != null) {
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('start'),
|
||||
position: startLocation,
|
||||
infoWindow: InfoWindow(title: 'Start'.tr)));
|
||||
}
|
||||
if (endLocation != null) {
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('end'),
|
||||
position: endLocation,
|
||||
infoWindow: InfoWindow(title: 'End'.tr)));
|
||||
}
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: const Duration(milliseconds: 375),
|
||||
childAnimationBuilder: (widget) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(child: widget),
|
||||
),
|
||||
children: [
|
||||
// --- Map Card ---
|
||||
_buildMapCard(context, startLocation, endLocation, markers),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Trip Info Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.receipt_long,
|
||||
title: 'Trip Info'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Order ID'.tr,
|
||||
value: res['id']?.toString() ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Date'.tr,
|
||||
value: res['date']?.toString() ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Earnings Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.account_balance_wallet,
|
||||
title: 'Earnings & Distance'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Your Earnings'.tr,
|
||||
value: '${res['price_for_driver']}'),
|
||||
_InfoTile(
|
||||
label: 'Distance'.tr,
|
||||
value: '${res['distance']} KM'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Timeline Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.timeline,
|
||||
title: 'Trip Timeline'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Time to Passenger'.tr,
|
||||
value: res['DriverIsGoingToPassenger'] ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Trip Started'.tr,
|
||||
value: res['rideTimeStart'] ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Trip Finished'.tr,
|
||||
value: res['rideTimeFinish'] ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Passenger & Status Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.person,
|
||||
title: 'Passenger & Status'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Passenger Name'.tr,
|
||||
value:
|
||||
'${res['passengerName']} ${res['last_name']}'),
|
||||
_InfoTile(
|
||||
label: 'Status'.tr,
|
||||
value: res['status'] ?? 'N/A',
|
||||
isStatus: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMapCard(BuildContext context, LatLng? startLocation,
|
||||
LatLng? endLocation, Set<Marker> markers) {
|
||||
// A fallback position if locations are not available
|
||||
final initialCameraPosition = (startLocation != null)
|
||||
? CameraPosition(target: startLocation, zoom: 14)
|
||||
: const CameraPosition(
|
||||
target: LatLng(31.96, 35.92), zoom: 12); // Fallback to Amman
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
clipBehavior:
|
||||
Clip.antiAlias, // Ensures the map respects the border radius
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 250,
|
||||
child: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: {
|
||||
if (startLocation != null && endLocation != null)
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: [startLocation, endLocation],
|
||||
color: Colors.deepPurple,
|
||||
width: 5,
|
||||
),
|
||||
},
|
||||
onMapCreated: (IntaleqMapController mapController) {
|
||||
// Animate camera to fit the route
|
||||
if (startLocation != null && endLocation != null) {
|
||||
LatLngBounds bounds = LatLngBounds(
|
||||
southwest: LatLng(
|
||||
startLocation.latitude < endLocation.latitude
|
||||
? startLocation.latitude
|
||||
: endLocation.latitude,
|
||||
startLocation.longitude < endLocation.longitude
|
||||
? startLocation.longitude
|
||||
: endLocation.longitude,
|
||||
),
|
||||
northeast: LatLng(
|
||||
startLocation.latitude > endLocation.latitude
|
||||
? startLocation.latitude
|
||||
: endLocation.latitude,
|
||||
startLocation.longitude > endLocation.longitude
|
||||
? startLocation.longitude
|
||||
: endLocation.longitude,
|
||||
),
|
||||
);
|
||||
mapController.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, left: 60, top: 60, right: 60, bottom: 60));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: 'open_maps',
|
||||
onPressed: () {
|
||||
if (startLocation != null && endLocation != null) {
|
||||
String mapUrl =
|
||||
'https://www.google.com/maps/dir/${startLocation.latitude},${startLocation.longitude}/${endLocation.latitude},${endLocation.longitude}/';
|
||||
showInBrowser(mapUrl);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.directions),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A reusable widget for the main detail cards
|
||||
class _DetailCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
const _DetailCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.05),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: Theme.of(context).primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A reusable widget for a label-value pair inside a card
|
||||
class _InfoTile extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool isStatus;
|
||||
|
||||
const _InfoTile({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.isStatus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Colors.grey[700])),
|
||||
if (isStatus)
|
||||
_buildStatusChip(value)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reusing the status chip from the previous page for consistency
|
||||
Widget _buildStatusChip(String status) {
|
||||
Color chipColor;
|
||||
Color textColor;
|
||||
IconData iconData;
|
||||
|
||||
switch (status.toLowerCase()) {
|
||||
case 'apply':
|
||||
case 'completed': // Assuming 'Apply' means completed
|
||||
chipColor = Colors.green.shade50;
|
||||
textColor = Colors.green.shade800;
|
||||
iconData = Icons.check_circle;
|
||||
status = 'Completed';
|
||||
break;
|
||||
case 'refused':
|
||||
chipColor = Colors.red.shade50;
|
||||
textColor = Colors.red.shade800;
|
||||
iconData = Icons.cancel;
|
||||
break;
|
||||
case 'cancel':
|
||||
chipColor = Colors.orange.shade50;
|
||||
textColor = Colors.orange.shade800;
|
||||
iconData = Icons.info;
|
||||
status = 'Cancelled';
|
||||
break;
|
||||
default:
|
||||
chipColor = Colors.grey.shade200;
|
||||
textColor = Colors.grey.shade800;
|
||||
iconData = Icons.hourglass_empty;
|
||||
}
|
||||
|
||||
return Chip(
|
||||
avatar: Icon(iconData, color: textColor, size: 16),
|
||||
label: Text(
|
||||
status.tr,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: chipColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
side: BorderSide.none,
|
||||
);
|
||||
}
|
||||
407
siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart
Executable file
407
siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart
Executable file
@@ -0,0 +1,407 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/upload_image.dart';
|
||||
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import 'package:siro_driver/device_compatibility_page.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
// استيراد الصفحات الأخرى... تأكد من صحة المسارات
|
||||
import 'package:siro_driver/views/Rate/rate_app_page.dart';
|
||||
import 'package:siro_driver/views/auth/captin/contact_us_page.dart';
|
||||
import 'package:siro_driver/views/auth/captin/invite_driver_screen.dart';
|
||||
import 'package:siro_driver/views/home/statistics/statistics_dashboard.dart';
|
||||
import 'package:siro_driver/views/gamification/challenges_page.dart';
|
||||
import 'package:siro_driver/views/gamification/leaderboard_page.dart';
|
||||
import 'package:siro_driver/views/gamification/referral_center_page.dart';
|
||||
import 'package:siro_driver/views/home/journal/schedule_page.dart';
|
||||
import 'package:siro_driver/views/notification/available_rides_page.dart';
|
||||
import 'package:siro_driver/views/auth/captin/logout_captain.dart';
|
||||
import 'package:siro_driver/views/home/Captin/history/history_captain.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/help_captain.dart';
|
||||
import 'package:siro_driver/views/home/Captin/About Us/settings_captain.dart';
|
||||
import 'package:siro_driver/views/home/my_wallet/walet_captain.dart';
|
||||
import 'package:siro_driver/views/home/profile/profile_captain.dart';
|
||||
import 'package:siro_driver/views/notification/notification_captain.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../About Us/video_page.dart';
|
||||
import '../assurance_health_page.dart';
|
||||
import '../maintain_center_page.dart';
|
||||
|
||||
// 1. إنشاء Class لتعريف بيانات كل عنصر في القائمة
|
||||
class DrawerItem {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
DrawerItem(
|
||||
{required this.title,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.onTap});
|
||||
}
|
||||
|
||||
// --- الويدجت الرئيسية للقائمة الجانبية ---
|
||||
class AppDrawer extends StatelessWidget {
|
||||
AppDrawer({super.key});
|
||||
|
||||
final ImageController imageController = Get.put(ImageController());
|
||||
|
||||
// 2. تعريف بيانات القائمة بشكل مركزي ومنظم
|
||||
final List<DrawerItem> drawerItems = [
|
||||
DrawerItem(
|
||||
title: 'Balance'.tr,
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: Colors.green,
|
||||
onTap: () => Get.to(() => WalletCaptainRefactored())),
|
||||
DrawerItem(
|
||||
title: 'Statistics'.tr,
|
||||
icon: Icons.bar_chart_rounded,
|
||||
color: Colors.deepPurple,
|
||||
onTap: () => Get.to(() => StatisticsDashboard())),
|
||||
DrawerItem(
|
||||
title: 'Challenges'.tr,
|
||||
icon: Icons.bolt_rounded,
|
||||
color: Colors.amber,
|
||||
onTap: () => Get.to(() => ChallengesPage())),
|
||||
DrawerItem(
|
||||
title: 'My Schedule'.tr,
|
||||
icon: Icons.calendar_today_rounded,
|
||||
color: Colors.teal,
|
||||
onTap: () => Get.to(() => SchedulePage())),
|
||||
DrawerItem(
|
||||
title: 'Leaderboard'.tr,
|
||||
icon: Icons.leaderboard_rounded,
|
||||
color: Colors.red,
|
||||
onTap: () => Get.to(() => LeaderboardPage())),
|
||||
DrawerItem(
|
||||
title: 'Profile'.tr,
|
||||
icon: Icons.person,
|
||||
color: Colors.blue,
|
||||
onTap: () => Get.to(() => ProfileCaptain())),
|
||||
DrawerItem(
|
||||
title: 'History of Trip'.tr,
|
||||
icon: Icons.history,
|
||||
color: Colors.orange,
|
||||
onTap: () => Get.to(() => const HistoryCaptain())),
|
||||
DrawerItem(
|
||||
title: 'Available for rides'.tr,
|
||||
icon: Icons.drive_eta,
|
||||
color: Colors.teal,
|
||||
onTap: () => Get.to(() => const AvailableRidesPage())),
|
||||
DrawerItem(
|
||||
title: 'Notifications'.tr,
|
||||
icon: Icons.notifications,
|
||||
color: Colors.purple,
|
||||
onTap: () => Get.to(() => const NotificationCaptain())),
|
||||
DrawerItem(
|
||||
title: 'Helping Center'.tr,
|
||||
icon: Icons.help_center,
|
||||
color: Colors.cyan,
|
||||
onTap: () => Get.to(() => HelpCaptain())),
|
||||
DrawerItem(
|
||||
title: 'Invite Driver'.tr,
|
||||
icon: Icons.person_add_rounded,
|
||||
color: Colors.indigo,
|
||||
onTap: () => Get.to(() => InviteScreen())),
|
||||
// DrawerItem(
|
||||
// title: 'Maintenance Center'.tr,
|
||||
// icon: Icons.build,
|
||||
// color: Colors.brown,
|
||||
// onTap: () => Get.to(() => MaintainCenterPage())),
|
||||
// DrawerItem(
|
||||
// title: 'Health Insurance'.tr,
|
||||
// icon: Icons.favorite,
|
||||
// color: Colors.pink,
|
||||
// onTap: () => Get.to(() => AssuranceHealthPage())),
|
||||
DrawerItem(
|
||||
title: 'Contact Us'.tr,
|
||||
icon: Icons.email,
|
||||
color: Colors.blueGrey,
|
||||
onTap: () => Get.to(() => ContactUsPage())),
|
||||
DrawerItem(
|
||||
title: 'Videos Tutorials'.tr,
|
||||
icon: Icons.video_library,
|
||||
color: Colors.redAccent,
|
||||
onTap: () => Get.to(() => VideoListPage())),
|
||||
DrawerItem(
|
||||
title: 'Rate Our App'.tr,
|
||||
icon: Icons.star,
|
||||
color: Colors.amber,
|
||||
onTap: () => Get.to(() => RatingScreen())),
|
||||
DrawerItem(
|
||||
title: 'Is device compatible'.tr,
|
||||
icon: Icons.memory,
|
||||
color: Colors.greenAccent,
|
||||
onTap: () => Get.to(() => DeviceCompatibilityPage())),
|
||||
DrawerItem(
|
||||
title: 'Privacy Policy'.tr,
|
||||
icon: Icons.memory,
|
||||
color: Colors.greenAccent,
|
||||
onTap: () =>
|
||||
launchUrl(Uri.parse('${AppLink.server}/privacy_policy.php'))),
|
||||
DrawerItem(
|
||||
title: 'Settings'.tr,
|
||||
icon: Icons.settings,
|
||||
color: Colors.grey.shade600,
|
||||
onTap: () => Get.to(() => const SettingsCaptain())),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Drawer(
|
||||
child: Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
children: [
|
||||
// --- الجزء العلوي من القائمة (بيانات المستخدم) ---
|
||||
UserHeader(), // استخدمنا الويدجت المحسنة بالأسفل
|
||||
|
||||
// --- قائمة العناصر المتحركة ---
|
||||
Expanded(
|
||||
child: AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
itemCount: drawerItems.length + 1, // +1 لزر تسجيل الخروج
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
// --- زر تسجيل الخروج في النهاية ---
|
||||
if (index == drawerItems.length) {
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _DrawerItemTile(
|
||||
item: DrawerItem(
|
||||
title: 'Sign Out'.tr,
|
||||
icon: Icons.logout,
|
||||
color: Colors.red,
|
||||
onTap: () =>
|
||||
Get.to(() => const LogoutCaptain())),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- بقية العناصر ---
|
||||
final item = drawerItems[index];
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _DrawerItemTile(item: item),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- ويدجت خاصة بكل عنصر في القائمة ---
|
||||
class _DrawerItemTile extends StatelessWidget {
|
||||
final DrawerItem item;
|
||||
const _DrawerItemTile({required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(item.icon, color: item.color, size: 24),
|
||||
),
|
||||
title: Text(
|
||||
item.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context); // لإغلاق القائمة عند الضغط بشكل آمن
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
item.onTap(); // الانتقال للصفحة بعد تأخير بسيط لإظهار الأنيميشن
|
||||
});
|
||||
},
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
splashColor: item.color.withValues(alpha: 0.2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- ويدجت محسنة للجزء العلوي من القائمة ---
|
||||
// ... (الاستيرادات السابقة تبقى كما هي)
|
||||
|
||||
// --- تم تعديل UserHeader لإضافة التحقق من الصورة ---
|
||||
class UserHeader extends StatelessWidget {
|
||||
UserHeader({super.key});
|
||||
final ImageController imageController = Get.find<ImageController>();
|
||||
final HomeCaptainController homeCaptainController =
|
||||
Get.find<HomeCaptainController>();
|
||||
|
||||
// دالة لإظهار التنبيه
|
||||
void _showUploadPhotoDialog(
|
||||
BuildContext context, ImageController controller) {
|
||||
// نستخدم addPostFrameCallback لضمان عدم ظهور الخطأ أثناء بناء الواجهة
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// نتأكد ألا يكون هناك dialog مفتوح بالفعل لتجنب التكرار
|
||||
if (Get.isDialogOpen == true) return;
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "Profile Photo Required".tr,
|
||||
titleStyle:
|
||||
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
middleText:
|
||||
"Please upload a clear photo of your face to be identified by passengers."
|
||||
.tr,
|
||||
barrierDismissible: false, // منع الإغلاق بالضغط خارج النافذة
|
||||
radius: 15,
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
confirm: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Get.back(); // إغلاق النافذة الحالية
|
||||
// فتح الكاميرا فوراً
|
||||
controller.choosImagePicture(
|
||||
AppLink.uploadImagePortrate, 'portrait');
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt, color: Colors.white),
|
||||
label: Text("Take Photo Now".tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor
|
||||
.primaryColor, // تأكد من وجود هذا اللون أو استبدله بـ Colors.blue
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text("Later".tr, style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UserAccountsDrawerHeader(
|
||||
accountName: Text(
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
shadows: [Shadow(blurRadius: 2, color: Colors.black26)]),
|
||||
),
|
||||
accountEmail:
|
||||
box.read(BoxName.emailDriver).toString().contains('intaleqapp')
|
||||
? Text('Your email not updated yet'.tr)
|
||||
: Text(box.read(BoxName.emailDriver)),
|
||||
currentAccountPicture: GetBuilder<ImageController>(
|
||||
builder: (controller) => Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 5)
|
||||
],
|
||||
),
|
||||
child: controller.isloading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: CircleAvatar(
|
||||
backgroundImage: NetworkImage((box
|
||||
.read(BoxName.driverPhotoUrl)
|
||||
?.toString()
|
||||
.isNotEmpty ==
|
||||
true)
|
||||
? box.read(BoxName.driverPhotoUrl)
|
||||
: '${AppLink.server}/portrate_captain_image/${box.read(BoxName.driverID)}.jpg'),
|
||||
|
||||
// [تعديل هام]: في حال فشل تحميل الصورة (غير موجودة)
|
||||
onBackgroundImageError: (exception, stackTrace) {
|
||||
// طباعة الخطأ في الكونسول للتوضيح
|
||||
debugPrint(
|
||||
"Profile image not found or error loading: $exception");
|
||||
// استدعاء نافذة التنبيه
|
||||
_showUploadPhotoDialog(context, controller);
|
||||
},
|
||||
|
||||
// أيقونة بديلة تظهر في الخلفية إذا لم تكن الصورة موجودة
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
child: const Icon(Icons.person,
|
||||
size: 40, color: Colors.white),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -5,
|
||||
right: -5,
|
||||
child: InkWell(
|
||||
onTap: () => controller.choosImagePicture(
|
||||
AppLink.uploadImagePortrate, 'portrait'),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.camera_alt,
|
||||
color: Theme.of(context).primaryColor, size: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
otherAccountsPictures: [
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
homeCaptainController.rating.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
const Icon(Icons.star, color: Colors.amber, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Theme.of(context).primaryColor, Colors.blue.shade700],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
189
siro_driver/lib/views/home/Captin/home_captain/driver_call_page.dart
Executable file
189
siro_driver/lib/views/home/Captin/home_captain/driver_call_page.dart
Executable file
@@ -0,0 +1,189 @@
|
||||
// import 'dart:async';
|
||||
// import 'package:SEFER/constant/box_name.dart';
|
||||
// import 'package:SEFER/main.dart';
|
||||
// import 'package:SEFER/views/widgets/my_scafold.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
// import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
|
||||
// import '../../../../constant/api_key.dart';
|
||||
// import '../../../../controller/functions/crud.dart';
|
||||
|
||||
// String appId = AK.agoraAppId;
|
||||
|
||||
// class DriverCallPage extends StatefulWidget {
|
||||
// const DriverCallPage({super.key});
|
||||
|
||||
// @override
|
||||
// State<DriverCallPage> createState() => _DriverCallPageState();
|
||||
// }
|
||||
|
||||
// class _DriverCallPageState extends State<DriverCallPage> {
|
||||
// String channelName = '';
|
||||
// String token = '';
|
||||
// // "00612994c6e707543e68d5638894d04f989IAAlydoFEC3ZeZkeUwl0dSswZTX8n+xyZR8PBWdwXFV6t6MLiA8AAAAAEACCHD/gn3TUZQEAAQAAAAAA";
|
||||
|
||||
// // int uid = int.parse(box.read(BoxName.phoneDriver)); // uid of the local user
|
||||
// int uid = 0;
|
||||
// int? _remoteUid; // uid of the remote user
|
||||
// bool _isJoined = false; // Indicates if the local user has joined the channel
|
||||
// late RtcEngine agoraEngine; // Agora engine instance
|
||||
|
||||
// final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
|
||||
// GlobalKey<ScaffoldMessengerState>(); // Global key to access the scaffold
|
||||
|
||||
// showMessage(String message) {
|
||||
// scaffoldMessengerKey.currentState?.showSnackBar(SnackBar(
|
||||
// content: Text(message),
|
||||
// ));
|
||||
// }
|
||||
|
||||
// initAgora() async {
|
||||
// await fetchToken();
|
||||
// await setupVoiceSDKEngine();
|
||||
// }
|
||||
|
||||
// fetchToken() async {
|
||||
// var res = await CRUD()
|
||||
// .getAgoraToken(channelName: channelName, uid: uid.toString());
|
||||
// setState(() {
|
||||
// token = res;
|
||||
// });
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _remoteUid = box.read(BoxName.phone) != null
|
||||
// ? int.parse(box.read(BoxName.phone))
|
||||
// : int.parse(box.read(BoxName.phoneDriver));
|
||||
// uid = box.read(BoxName.phoneDriver) != null
|
||||
// ? int.parse(box.read(BoxName.phoneDriver))
|
||||
// : int.parse(box.read(BoxName.phone));
|
||||
// // Set up an instance of Agora engine
|
||||
// initAgora();
|
||||
// }
|
||||
|
||||
// Future<void> setupVoiceSDKEngine() async {
|
||||
// // retrieve or request microphone permission
|
||||
// await [Permission.microphone].request();
|
||||
|
||||
// //create an instance of the Agora engine
|
||||
// agoraEngine = createAgoraRtcEngine();
|
||||
// await agoraEngine.initialize(RtcEngineContext(appId: AK.agoraAppId));
|
||||
// // Register the event handler
|
||||
// agoraEngine.registerEventHandler(
|
||||
// RtcEngineEventHandler(
|
||||
// onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
||||
// showMessage(
|
||||
// "Local user uid:${connection.localUid} joined the channel");
|
||||
// setState(() {
|
||||
// _isJoined = true;
|
||||
// });
|
||||
// },
|
||||
// onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
|
||||
// showMessage("Remote user uid:$remoteUid joined the channel");
|
||||
// setState(() {
|
||||
// _remoteUid = remoteUid;
|
||||
// });
|
||||
// },
|
||||
// onUserOffline: (RtcConnection connection, int remoteUid,
|
||||
// UserOfflineReasonType reason) {
|
||||
// showMessage("Remote user uid:$remoteUid left the channel");
|
||||
// setState(() {
|
||||
// _remoteUid = null;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// void join() async {
|
||||
// // Set channel options including the client role and channel profile
|
||||
// ChannelMediaOptions options = const ChannelMediaOptions(
|
||||
// clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||
// channelProfile: ChannelProfileType.channelProfileCommunication,
|
||||
// );
|
||||
|
||||
// await agoraEngine.joinChannel(
|
||||
// token: token,
|
||||
// channelId: channelName,
|
||||
// options: options,
|
||||
// uid: uid,
|
||||
// );
|
||||
// }
|
||||
// //https://console.agora.io/invite?sign=5e9e22d06f22caeeada9954c9e908572%253A5ba8aed978a35eab5a5113742502ded2a41478b2a81cb19c71a30776e125b58a
|
||||
|
||||
// void leave() {
|
||||
// setState(() {
|
||||
// _isJoined = false;
|
||||
// _remoteUid = null;
|
||||
// });
|
||||
// agoraEngine.leaveChannel();
|
||||
// }
|
||||
|
||||
// // Clean up the resources when you leave
|
||||
// @override
|
||||
// void dispose() async {
|
||||
// await agoraEngine.leaveChannel();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
// // Build UI
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MaterialApp(
|
||||
// scaffoldMessengerKey: scaffoldMessengerKey,
|
||||
// home: MyScafolld(
|
||||
// // appBar: AppBar(
|
||||
// // title: const Text('Get started with Voice Calling'),
|
||||
// // ),
|
||||
// title: 'Voice Calling'.tr,
|
||||
// isleading: true,
|
||||
// body: [
|
||||
// ListView(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
// children: [
|
||||
// // Status text
|
||||
// Container(height: 40, child: Center(child: _status())),
|
||||
// // Button Row
|
||||
// Row(
|
||||
// children: <Widget>[
|
||||
// Expanded(
|
||||
// child: ElevatedButton(
|
||||
// child: const Text("Join"),
|
||||
// onPressed: () => {join()},
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 10),
|
||||
// Expanded(
|
||||
// child: ElevatedButton(
|
||||
// child: const Text("Leave"),
|
||||
// onPressed: () => {leave()},
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ]),
|
||||
// );
|
||||
// }
|
||||
|
||||
// Widget _status() {
|
||||
// String statusText;
|
||||
|
||||
// if (!_isJoined)
|
||||
// statusText = 'Join a channel';
|
||||
// else if (_remoteUid == null)
|
||||
// statusText = 'Waiting for a remote user to join...';
|
||||
// else
|
||||
// statusText = 'Connected to remote user, uid:$_remoteUid';
|
||||
|
||||
// return Text(
|
||||
// statusText,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
331
siro_driver/lib/views/home/Captin/home_captain/help_captain.dart
Executable file
331
siro_driver/lib/views/home/Captin/home_captain/help_captain.dart
Executable file
@@ -0,0 +1,331 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/captin/help/help_controller.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/help_details_replay_page.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../widgets/my_scafold.dart';
|
||||
|
||||
class HelpCaptain extends StatelessWidget {
|
||||
HelpCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(HelpController());
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Helping Page'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
),
|
||||
child: Text(
|
||||
'If you need any help or have questions, this is the right place to do that. You are welcome!'
|
||||
.tr,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Submit Your Question'.tr,
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GetBuilder<HelpController>(
|
||||
builder: (helpController) => Form(
|
||||
key: helpController.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: helpController.helpQuestionController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your Question here'.tr,
|
||||
prefixIcon:
|
||||
const Icon(Icons.question_answer_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
maxLines: 3,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your question'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
helpController.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (helpController.formKey.currentState!
|
||||
.validate()) {
|
||||
helpController.addHelpQuestion();
|
||||
helpController.helpQuestionController
|
||||
.clear();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text('Submit Question'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Your Questions'.tr,
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GetBuilder<HelpController>(
|
||||
builder: (helpController) {
|
||||
final questions = helpController.helpQuestionDate['message'];
|
||||
if (questions == null || questions.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text('No questions asked yet.'.tr),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: questions.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
var list = questions[index];
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: theme.dividerColor),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
list['datecreated'],
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
helpController.getIndex(
|
||||
int.parse(EncryptionHelper.instance
|
||||
.decryptData(list['id'])),
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']));
|
||||
helpController.getHelpRepley(list['id'].toString());
|
||||
Get.to(() => const HelpDetailsReplayPage());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// class HelpCaptain extends StatelessWidget {
|
||||
// HelpCaptain({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// Get.put(HelpController());
|
||||
// return CupertinoPageScaffold(
|
||||
// navigationBar: CupertinoNavigationBar(
|
||||
// middle: Text('Helping Page'.tr),
|
||||
// leading: CupertinoButton(
|
||||
// padding: EdgeInsets.zero,
|
||||
// child: Icon(CupertinoIcons.back),
|
||||
// onPressed: () => Navigator.pop(context),
|
||||
// ),
|
||||
// ),
|
||||
// child: SafeArea(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(12.0),
|
||||
// border: Border.all(
|
||||
// color: CupertinoColors.systemGrey2,
|
||||
// ),
|
||||
// ),
|
||||
// child: Text(
|
||||
// 'If you need any help or have questions, this is the right place to do that. You are welcome!'
|
||||
// .tr,
|
||||
// style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Card(
|
||||
// elevation: 3,
|
||||
// color: CupertinoColors.systemGrey6,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12.0),
|
||||
// ),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: GetBuilder<HelpController>(
|
||||
// builder: (helpController) => Form(
|
||||
// key: helpController.formKey,
|
||||
// child: Column(
|
||||
// children: [
|
||||
// CupertinoTextField(
|
||||
// controller: helpController.helpQuestionController,
|
||||
// placeholder: 'Enter your Question here'.tr,
|
||||
// decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// border: Border.all(
|
||||
// color: CupertinoColors.systemGrey,
|
||||
// ),
|
||||
// ),
|
||||
// padding: const EdgeInsets.all(16),
|
||||
// clearButtonMode: OverlayVisibilityMode.editing,
|
||||
// ),
|
||||
// const SizedBox(height: 20),
|
||||
// helpController.isLoading
|
||||
// ? const CupertinoActivityIndicator()
|
||||
// : CupertinoButton.filled(
|
||||
// onPressed: () {
|
||||
// if (helpController.formKey.currentState!
|
||||
// .validate()) {
|
||||
// helpController.addHelpQuestion();
|
||||
|
||||
// // Clear the feedback form
|
||||
// helpController.formKey.currentState!
|
||||
// .reset();
|
||||
// }
|
||||
// },
|
||||
// child: Text('Submit Question'.tr),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 20),
|
||||
// Expanded(
|
||||
// child: GetBuilder<HelpController>(
|
||||
// builder: (helpController) => Padding(
|
||||
// padding: const EdgeInsets.all(10),
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// border: Border.all(
|
||||
// color: CupertinoColors.systemGrey2,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(12.0),
|
||||
// ),
|
||||
// child: ListView.builder(
|
||||
// itemCount: helpController.helpQuestionDate['message'] !=
|
||||
// null
|
||||
// ? helpController.helpQuestionDate['message'].length
|
||||
// : 0,
|
||||
// itemBuilder: (BuildContext context, int index) {
|
||||
// var list =
|
||||
// helpController.helpQuestionDate['message'][index];
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(3),
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// vertical: 12, horizontal: 16),
|
||||
// decoration: BoxDecoration(
|
||||
// border: Border.all(
|
||||
// color: CupertinoColors.activeGreen,
|
||||
// width: 2,
|
||||
// ),
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// child: GestureDetector(
|
||||
// onTap: () {
|
||||
// helpController.getIndex(
|
||||
// list['id'], list['helpQuestion']);
|
||||
// helpController
|
||||
// .getHelpRepley(list['id'].toString());
|
||||
// Get.to(() => const HelpDetailsReplayPage());
|
||||
// },
|
||||
// child: Row(
|
||||
// mainAxisAlignment:
|
||||
// MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// list['helpQuestion'],
|
||||
// style: CupertinoTheme.of(context)
|
||||
// .textTheme
|
||||
// .textStyle,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// ),
|
||||
// Text(
|
||||
// list['datecreated'],
|
||||
// style: CupertinoTheme.of(context)
|
||||
// .textTheme
|
||||
// .tabLabelTextStyle,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
110
siro_driver/lib/views/home/Captin/home_captain/help_details_replay_page.dart
Executable file
110
siro_driver/lib/views/home/Captin/home_captain/help_details_replay_page.dart
Executable file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../../controller/home/captin/help/help_controller.dart';
|
||||
import '../../../widgets/my_scafold.dart';
|
||||
|
||||
class HelpDetailsReplayPage extends StatelessWidget {
|
||||
const HelpDetailsReplayPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.find<HelpController>();
|
||||
return GetBuilder<HelpController>(
|
||||
builder: (helpController) => MyScafolld(
|
||||
title: 'Help Details'.tr,
|
||||
body: [
|
||||
helpController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Question Bubble (Aligned to Start/End based on locale, usually start for sender)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: Get.width * 0.75),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your Question'.tr,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
helpController.qustion,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Reply Bubble (Aligned to opposite side)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: Get.width * 0.75),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Support Reply'.tr,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Builder(builder: (context) {
|
||||
final replayData = helpController.helpQuestionRepleyDate['message'];
|
||||
final isNoReply = helpController.status == 'not yet' ||
|
||||
replayData == null ||
|
||||
EncryptionHelper.instance.decryptData(replayData['replay']).toString() == 'not yet';
|
||||
|
||||
return Text(
|
||||
isNoReply
|
||||
? 'No Response yet.'.tr
|
||||
: EncryptionHelper.instance.decryptData(replayData['replay']).toString(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
],
|
||||
isleading: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
672
siro_driver/lib/views/home/Captin/home_captain/home_captin.dart
Executable file
672
siro_driver/lib/views/home/Captin/home_captain/home_captin.dart
Executable file
@@ -0,0 +1,672 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bubble_head/bubble.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/drawer_captain.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/info.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/functions/overlay_permisssion.dart';
|
||||
import '../../../../controller/functions/package_info.dart';
|
||||
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../../controller/home/navigation/navigation_view.dart';
|
||||
import '../../../../controller/profile/setting_controller.dart';
|
||||
import '../../../../env/env.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../notification/available_rides_page.dart';
|
||||
import '../driver_map_page.dart';
|
||||
import 'widget/connect.dart';
|
||||
import 'widget/left_menu_map_captain.dart';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Design Tokens (Responsive to Theme)
|
||||
// ─────────────────────────────────────────────
|
||||
class _Token {
|
||||
static Color surface(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF1A1A2E)
|
||||
: Colors.white;
|
||||
|
||||
static Color surfaceCard(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF16213E)
|
||||
: const Color(0xFFF8F9FA);
|
||||
|
||||
static const Color accent = Color(0xFFF0A500);
|
||||
static Color accentSoft(BuildContext context) =>
|
||||
const Color(0xFFF0A500).withOpacity(0.12);
|
||||
static const Color danger = Color(0xFFE53935);
|
||||
static const Color success = Color(0xFF2ECC71);
|
||||
static const Color info = Color(0xFF3498DB);
|
||||
|
||||
static Color border(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF2A2A4A)
|
||||
: Colors.grey.withOpacity(0.2);
|
||||
|
||||
static Color text(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: const Color(0xFF2D3436);
|
||||
|
||||
static Color textDim(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white38
|
||||
: Colors.black45;
|
||||
|
||||
static const double radius = 16.0;
|
||||
static const double radiusSm = 10.0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Root Widget
|
||||
// ─────────────────────────────────────────────
|
||||
class HomeCaptain extends StatelessWidget {
|
||||
HomeCaptain({super.key});
|
||||
|
||||
final LocationController locationController =
|
||||
Get.put(LocationController(), permanent: true);
|
||||
final HomeCaptainController homeCaptainController =
|
||||
Get.put(HomeCaptainController(), permanent: true);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await closeOverlayIfFound();
|
||||
if (!context.mounted) return;
|
||||
await checkForUpdate(context);
|
||||
if (!context.mounted) return;
|
||||
await getPermissionOverlay();
|
||||
if (!context.mounted) return;
|
||||
await showDriverGiftClaim(context);
|
||||
if (!context.mounted) return;
|
||||
await checkForAppliedRide(context);
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: _Token.surface(context),
|
||||
appBar: const _HomeAppBar(),
|
||||
drawer: AppDrawer(),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
const _MapView(),
|
||||
leftMainMenuCaptainIcons(),
|
||||
const _BottomStatusBar(),
|
||||
const _FloatingControls(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// AppBar — no blur, solid gradient
|
||||
// ─────────────────────────────────────────────
|
||||
class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const _HomeAppBar();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ctrl = Get.find<HomeCaptainController>();
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: _Token.surface(context),
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
titleSpacing: 0,
|
||||
// ── Logo + App Name ──────────────────────
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
_LogoBadge(),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
AppInformation.appName.split(' ')[0].tr,
|
||||
style: const TextStyle(
|
||||
color: _Token.accent,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// ── Refuse Counter ───────────────────
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _PillBadge(
|
||||
icon: Icons.block_rounded,
|
||||
label: c.countRefuse.toString(),
|
||||
color: _Token.danger,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// ── Map Controls Row ─────────────────
|
||||
_AppBarControls(ctrl: ctrl),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogoBadge extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: _Token.accent, width: 1.5),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset('assets/images/logo.gif', fit: BoxFit.cover),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _PillBadge extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final Color color;
|
||||
const _PillBadge(
|
||||
{required this.icon, required this.label, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.35)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 13),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color, fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _AppBarControls extends StatelessWidget {
|
||||
final HomeCaptainController ctrl;
|
||||
const _AppBarControls({required this.ctrl});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
borderRadius: BorderRadius.circular(_Token.radiusSm),
|
||||
border: Border.all(color: _Token.border(context)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Navigation
|
||||
_IconBtn(
|
||||
icon: Icons.map_rounded,
|
||||
color: _Token.success,
|
||||
tooltip: 'Navigation'.tr,
|
||||
onTap: () => Get.to(() => const NavigationView()),
|
||||
),
|
||||
// Heatmap
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _IconBtn(
|
||||
icon: Icons.local_fire_department_rounded,
|
||||
color:
|
||||
c.isHeatmapVisible ? Colors.orange : Colors.grey.shade600,
|
||||
tooltip: 'Heatmap'.tr,
|
||||
onTap: c.toggleHeatmap,
|
||||
),
|
||||
),
|
||||
// Center on me
|
||||
_IconBtn(
|
||||
icon: Icons.my_location_rounded,
|
||||
color: _Token.accent,
|
||||
tooltip: 'My Location'.tr,
|
||||
onTap: () {
|
||||
ctrl.mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(
|
||||
Get.find<LocationController>().myLocation,
|
||||
17.5,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _IconBtn extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
const _IconBtn(
|
||||
{required this.icon,
|
||||
required this.color,
|
||||
required this.tooltip,
|
||||
required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Map View
|
||||
// ─────────────────────────────────────────────
|
||||
class _MapView extends StatelessWidget {
|
||||
const _MapView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<HomeCaptainController>(
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) return const MyCircularProgressIndicator();
|
||||
|
||||
return GetBuilder<SettingController>(
|
||||
builder: (s) => GetBuilder<LocationController>(
|
||||
builder: (loc) => IntaleqMap(
|
||||
apiKey: Env.mapSaasKey,
|
||||
onMapCreated: ctrl.onMapCreated,
|
||||
minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: (loc.myLocation.latitude == 0 ||
|
||||
loc.myLocation.latitude.isNaN)
|
||||
? ctrl.myLocation
|
||||
: loc.myLocation,
|
||||
zoom: 15,
|
||||
),
|
||||
mapType:
|
||||
s.isMapDarkMode ? IntaleqMapType.normal : IntaleqMapType.light,
|
||||
polygons: ctrl.heatmapPolygons,
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: MarkerId('MyLocation'.tr),
|
||||
position: loc.myLocation,
|
||||
rotation: loc.heading,
|
||||
flat: true,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
icon: ctrl.carIcon,
|
||||
)
|
||||
},
|
||||
myLocationButtonEnabled: false,
|
||||
myLocationEnabled: false,
|
||||
compassEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Bottom Status Bar — no blur, solid card
|
||||
// ─────────────────────────────────────────────
|
||||
class _BottomStatusBar extends StatelessWidget {
|
||||
const _BottomStatusBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
right: 12,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showDetailsSheet(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
borderRadius: BorderRadius.circular(_Token.radius),
|
||||
border: Border.all(color: _Token.border(context)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.35),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Online toggle
|
||||
const ConnectWidget(),
|
||||
const Spacer(),
|
||||
// Ride count
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _StatChip(
|
||||
icon: Icons.directions_car_rounded,
|
||||
value: c.countRideToday,
|
||||
label: 'Rides'.tr,
|
||||
color: _Token.info,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// Today earnings
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _StatChip(
|
||||
icon: Entypo.wallet,
|
||||
value: c.totalMoneyToday.toString(),
|
||||
label: 'Today'.tr,
|
||||
color: _Token.success,
|
||||
),
|
||||
),
|
||||
// Chevron hint
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.keyboard_arrow_up_rounded,
|
||||
color: Colors.grey, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDetailsSheet(BuildContext context) {
|
||||
final ctrl = Get.find<HomeCaptainController>();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _DetailsSheet(ctrl),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String value;
|
||||
final String label;
|
||||
final Color color;
|
||||
const _StatChip(
|
||||
{required this.icon,
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(label,
|
||||
style: const TextStyle(color: Colors.white38, fontSize: 11)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Details Bottom Sheet — replaces AlertDialog
|
||||
// ─────────────────────────────────────────────
|
||||
class _DetailsSheet extends StatelessWidget {
|
||||
final HomeCaptainController ctrl;
|
||||
const _DetailsSheet(this.ctrl);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Drag handle
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.border(context),
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Your Activity'.tr,
|
||||
style: TextStyle(
|
||||
color: _Token.text(context),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(color: _Token.border(context), height: 1),
|
||||
const SizedBox(height: 16),
|
||||
_SheetRow(
|
||||
icon: Entypo.wallet,
|
||||
color: _Token.success,
|
||||
label: 'Today'.tr,
|
||||
value: ctrl.totalMoneyToday.toString(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Entypo.wallet,
|
||||
color: _Token.accent,
|
||||
label: AppInformation.appName,
|
||||
value: ctrl.totalMoneyInSEFER.toString(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Divider(color: _Token.border(context), height: 1),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Icons.timer_outlined,
|
||||
color: _Token.success,
|
||||
label: 'Active Duration'.tr,
|
||||
value: ctrl.stringActiveDuration,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Icons.access_time_rounded,
|
||||
color: _Token.info,
|
||||
label: 'Total Connection'.tr,
|
||||
value: ctrl.totalDurationToday,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Divider(color: _Token.border(context), height: 1),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Icons.star_rounded,
|
||||
color: _Token.accent,
|
||||
label: 'Total Points'.tr,
|
||||
value: ctrl.totalPoints.toString(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: _Token.accentSoft(context),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(_Token.radiusSm)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text('Close'.tr,
|
||||
style: const TextStyle(
|
||||
color: _Token.accent, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SheetRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String label;
|
||||
final String value;
|
||||
const _SheetRow(
|
||||
{required this.icon,
|
||||
required this.color,
|
||||
required this.label,
|
||||
required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(label,
|
||||
style: TextStyle(color: _Token.textDim(context), fontSize: 14)),
|
||||
const Spacer(),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
color: color, fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Floating Action Buttons (right side)
|
||||
// ─────────────────────────────────────────────
|
||||
class _FloatingControls extends StatelessWidget {
|
||||
const _FloatingControls({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 88,
|
||||
right: 12,
|
||||
child: GetBuilder<HomeCaptainController>(
|
||||
builder: (ctrl) => Column(
|
||||
children: [
|
||||
// Bubble/overlay mode (Android only)
|
||||
if (Platform.isAndroid) ...[
|
||||
_Fab(
|
||||
onTap: () =>
|
||||
Bubble().startBubbleHead(sendAppToBackground: true),
|
||||
tooltip: 'Overlay'.tr,
|
||||
child: Image.asset('assets/images/logo1.png',
|
||||
width: 26, height: 26),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
// Available rides
|
||||
_Fab(
|
||||
onTap: () => Get.to(() => const AvailableRidesPage()),
|
||||
icon: Icons.list_alt_rounded,
|
||||
color: AppColor.primaryColor,
|
||||
tooltip: 'Available Rides'.tr,
|
||||
),
|
||||
// Continue active ride
|
||||
if (box.read(BoxName.rideStatus) == 'Applied' ||
|
||||
box.read(BoxName.rideStatus) == 'Begin') ...[
|
||||
const SizedBox(height: 10),
|
||||
_Fab(
|
||||
onTap: () {
|
||||
if (box.read(BoxName.rideStatus) == 'Applied') {
|
||||
Get.to(() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArguments));
|
||||
Get.put(MapDriverController())
|
||||
.changeRideToBeginToPassenger();
|
||||
} else {
|
||||
Get.to(() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArguments));
|
||||
Get.put(MapDriverController()).startRideFromStartApp();
|
||||
}
|
||||
},
|
||||
icon: Icons.navigation_rounded,
|
||||
color: _Token.info,
|
||||
tooltip: 'Continue Ride'.tr,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Fab extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
final IconData? icon;
|
||||
final Widget? child;
|
||||
final Color? color;
|
||||
final String? tooltip;
|
||||
const _Fab(
|
||||
{required this.onTap, this.icon, this.child, this.color, this.tooltip});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
message: tooltip ?? '',
|
||||
child: Material(
|
||||
color: color ?? _Token.surfaceCard(context),
|
||||
shape: const CircleBorder(),
|
||||
elevation: 6,
|
||||
shadowColor: (color ?? Colors.black).withOpacity(0.4),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
customBorder: const CircleBorder(),
|
||||
child: SizedBox(
|
||||
width: 52,
|
||||
height: 52,
|
||||
child: Center(
|
||||
child: child ??
|
||||
Icon(icon,
|
||||
color: color != null ? Colors.white : _Token.accent,
|
||||
size: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Helper
|
||||
// ─────────────────────────────────────────────
|
||||
Future<void> checkForAppliedRide(BuildContext context) async {
|
||||
checkForPendingOrderFromServer();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../../constant/api_key.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||
|
||||
class OsmMapView extends StatelessWidget {
|
||||
const OsmMapView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final LocationController locationController =
|
||||
Get.find<LocationController>();
|
||||
final HomeCaptainController homeCaptainController =
|
||||
Get.find<HomeCaptainController>();
|
||||
|
||||
return Obx(() {
|
||||
final LatLng currentLocation = LatLng(
|
||||
locationController.myLocation.latitude,
|
||||
locationController.myLocation.longitude);
|
||||
final double currentHeading = locationController.heading;
|
||||
|
||||
return IntaleqMap(
|
||||
apiKey: AK.mapSaasKey,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: currentLocation,
|
||||
zoom: 15,
|
||||
bearing: currentHeading,
|
||||
),
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: const MarkerId('myLocation'),
|
||||
position: currentLocation,
|
||||
rotation: currentHeading,
|
||||
icon: homeCaptainController.carIcon,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 2,
|
||||
)
|
||||
},
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
// You can assign this controller if needed
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
84
siro_driver/lib/views/home/Captin/home_captain/widget/call_page.dart
Executable file
84
siro_driver/lib/views/home/Captin/home_captain/widget/call_page.dart
Executable file
@@ -0,0 +1,84 @@
|
||||
// import 'package:SEFER/constant/colors.dart';
|
||||
// import 'package:SEFER/constant/style.dart';
|
||||
// import 'package:SEFER/controller/firebase/firbase_messge.dart';
|
||||
// import 'package:SEFER/controller/home/captin/map_driver_controller.dart';
|
||||
// import 'package:SEFER/views/widgets/my_scafold.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:SEFER/controller/home/captin/home_captain_controller.dart';
|
||||
|
||||
// import '../../../../../controller/functions/call_controller.dart';
|
||||
|
||||
// class CallPage extends StatelessWidget {
|
||||
// const CallPage({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: 'Call Page'.tr, isleading: true, body: [callPage()]);
|
||||
// }
|
||||
// }
|
||||
|
||||
// GetBuilder<HomeCaptainController> callPage() {
|
||||
// CallController callController = Get.put(CallController());
|
||||
// Get.put(MapDriverController());
|
||||
// // callController.join();
|
||||
// return GetBuilder<HomeCaptainController>(
|
||||
// builder: (controller) => Positioned(
|
||||
// top: Get.height * .2,
|
||||
// child: Container(
|
||||
// height: 100, width: Get.width,
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
// children: [
|
||||
// GestureDetector(
|
||||
// onTap: () async {
|
||||
// callController.join();
|
||||
// },
|
||||
// child: Container(
|
||||
// width: 50,
|
||||
// height: 50,
|
||||
// decoration: const BoxDecoration(
|
||||
// shape: BoxShape.circle, color: AppColor.greenColor),
|
||||
// child: const Icon(
|
||||
// Icons.phone,
|
||||
// size: 35,
|
||||
// color: AppColor.secondaryColor,
|
||||
// )),
|
||||
// ),
|
||||
// Column(
|
||||
// children: [
|
||||
// Text(callController.status),
|
||||
// Text(Get.find<MapDriverController>().passengerName.toString()),
|
||||
// ],
|
||||
// ),
|
||||
// GestureDetector(
|
||||
// onTap: () async {
|
||||
// FirebaseMessagesController().sendNotificationToPassengerToken(
|
||||
// 'Call End'.tr,
|
||||
// 'Call End',
|
||||
// Get.find<MapDriverController>().tokenPassenger,
|
||||
// [],
|
||||
// 'iphone_ringtone.wav');
|
||||
// callController.leave();
|
||||
// Get.back();
|
||||
// },
|
||||
// child: Container(
|
||||
// width: 50,
|
||||
// height: 50,
|
||||
// decoration: const BoxDecoration(
|
||||
// shape: BoxShape.circle, color: AppColor.redColor),
|
||||
// child: const Icon(
|
||||
// Icons.phone_disabled_sharp,
|
||||
// size: 35,
|
||||
// color: AppColor.secondaryColor,
|
||||
// )),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// // ignore: prefer_const_constructors
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
151
siro_driver/lib/views/home/Captin/home_captain/widget/connect.dart
Executable file
151
siro_driver/lib/views/home/Captin/home_captain/widget/connect.dart
Executable file
@@ -0,0 +1,151 @@
|
||||
import 'package:siro_driver/controller/functions/tts.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
|
||||
import '../../../../../constant/style.dart';
|
||||
import '../../../../widgets/elevated_btn.dart';
|
||||
import '../../../../../controller/home/captin/home_captain_controller.dart';
|
||||
|
||||
class ConnectWidget extends StatelessWidget {
|
||||
const ConnectWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// final OrderRequestController orderRequestController =
|
||||
// Get.put(OrderRequestController());
|
||||
CaptainWalletController captainWalletController =
|
||||
Get.put(CaptainWalletController());
|
||||
int refusedRidesToday = 0;
|
||||
captainWalletController.getCaptainWalletFromBuyPoints();
|
||||
return Center(
|
||||
child: GetBuilder<HomeCaptainController>(
|
||||
builder: (homeCaptainController) => double.parse(
|
||||
(captainWalletController.totalPoints)) <
|
||||
-200
|
||||
? CupertinoButton(
|
||||
onPressed: () {
|
||||
Get.defaultDialog(
|
||||
// backgroundColor: CupertinoColors.destructiveRed,
|
||||
barrierDismissible: false,
|
||||
title: double.parse(
|
||||
(captainWalletController.totalPoints)) <
|
||||
-200
|
||||
? 'You dont have Points'.tr
|
||||
: 'You Are Stopped For this Day !'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
double.parse((captainWalletController
|
||||
.totalPoints)) <
|
||||
-200
|
||||
? await Get.find<TextToSpeechController>()
|
||||
.speakText(
|
||||
'You must be recharge your Account'
|
||||
.tr)
|
||||
: await Get.find<TextToSpeechController>()
|
||||
.speakText(
|
||||
'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
|
||||
.tr);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
),
|
||||
Text(
|
||||
double.parse((captainWalletController
|
||||
.totalPoints)) <
|
||||
-200
|
||||
? 'You must be recharge your Account'.tr
|
||||
: 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: double.parse(
|
||||
(captainWalletController.totalPoints)) <
|
||||
-200
|
||||
? MyElevatedButton(
|
||||
title: 'Recharge my Account'.tr,
|
||||
onPressed: () {
|
||||
homeCaptainController.goToWalletFromConnect();
|
||||
})
|
||||
: MyElevatedButton(
|
||||
title: 'Ok , See you Tomorrow'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
},
|
||||
color: CupertinoColors.destructiveRed,
|
||||
child: Text(
|
||||
'You are Stopped'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: homeCaptainController.isActive
|
||||
? [const Color(0xFF00C853), const Color(0xFF00E676)]
|
||||
: [Colors.grey.shade600, Colors.grey.shade400],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (homeCaptainController.isActive
|
||||
? const Color(0xFF00C853)
|
||||
: Colors.grey)
|
||||
.withOpacity(0.4),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CupertinoButton(
|
||||
onPressed: homeCaptainController.onButtonSelected,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
homeCaptainController.isActive
|
||||
? Icons.power_settings_new_rounded
|
||||
: Icons.power_off_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
homeCaptainController.isActive
|
||||
? 'Online'.tr
|
||||
: 'Offline'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
323
siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart
Executable file
323
siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart
Executable file
@@ -0,0 +1,323 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/home/Captin/driver_map_page.dart';
|
||||
import 'package:siro_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
|
||||
import 'package:siro_driver/views/auth/syria/registration_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
import '../../../../../constant/links.dart';
|
||||
import '../../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../../controller/functions/crud.dart';
|
||||
import '../../../../../controller/home/captin/order_request_controller.dart';
|
||||
import '../../../../../print.dart';
|
||||
import '../../../../Rate/ride_calculate_driver.dart';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Design Tokens (Responsive)
|
||||
// ─────────────────────────────────────────────
|
||||
class _T {
|
||||
static Color surface(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF16213E)
|
||||
: Colors.white;
|
||||
|
||||
static const Color accent = Color(0xFFF0A500);
|
||||
static const Color blue = Color(0xFF3498DB);
|
||||
|
||||
static Color border(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF2A2A4A)
|
||||
: Colors.grey.withOpacity(0.2);
|
||||
|
||||
static const double radius = 14.0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Left Side Menu
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// Returns the vertical icon column anchored to the left-center of the map.
|
||||
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||
return GetBuilder<HomeCaptainController>(
|
||||
builder: (ctrl) => Positioned(
|
||||
// Place just above the bottom status bar
|
||||
bottom: 100,
|
||||
left: 10,
|
||||
child: Builder(builder: (context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _T.surface(context),
|
||||
borderRadius: BorderRadius.circular(_T.radius),
|
||||
border: Border.all(color: _T.border(context)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(2, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ── 1. Active Ride shortcut ──────────
|
||||
_MenuIcon(
|
||||
icon: Icons.directions_car_rounded,
|
||||
color:
|
||||
box.read(BoxName.rideArgumentsFromBackground) == 'failure'
|
||||
? Colors.red.shade400
|
||||
: Colors.green.shade400,
|
||||
tooltip: 'Active Ride'.tr,
|
||||
onTap: () async {
|
||||
await checkForPendingOrderFromServer();
|
||||
if (box.read(BoxName.rideArgumentsFromBackground) !=
|
||||
'failure') {
|
||||
Get.to(
|
||||
() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArgumentsFromBackground),
|
||||
);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Ride info'.tr,
|
||||
'you dont have accepted ride'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
box.write(BoxName.rideArgumentsFromBackground, 'failure');
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
box.write(BoxName.rideStatus, 'no_ride');
|
||||
Log.print(box.read(BoxName.statusDriverLocation));
|
||||
ctrl.update();
|
||||
},
|
||||
),
|
||||
|
||||
_Divider(context),
|
||||
|
||||
// ── 2. Earnings Chart ────────────────
|
||||
_MenuIcon(
|
||||
icon: FontAwesome5.chart_bar,
|
||||
color: _T.blue,
|
||||
tooltip: 'Earnings'.tr,
|
||||
onTap: () {
|
||||
final now = DateTime.now();
|
||||
final lastTimeRaw = box.read(BoxName.lastTimeStaticThrottle);
|
||||
DateTime? lastTime;
|
||||
|
||||
if (lastTimeRaw != null) {
|
||||
try {
|
||||
lastTime = DateTime.parse(lastTimeRaw.toString());
|
||||
} catch (_) {
|
||||
lastTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastTime == null ||
|
||||
now.difference(lastTime).inMinutes >= 2) {
|
||||
box.write(
|
||||
BoxName.lastTimeStaticThrottle, now.toIso8601String());
|
||||
Get.to(() => RideCalculateDriver());
|
||||
} else {
|
||||
final left = 2 - now.difference(lastTime).inMinutes;
|
||||
NotificationController().showNotification(
|
||||
'Intaleq Driver'.tr,
|
||||
'${'Please wait'.tr} $left ${"minutes before trying again.".tr}',
|
||||
'ding',
|
||||
'',
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () =>
|
||||
box.write(BoxName.statusDriverLocation, 'off'),
|
||||
),
|
||||
|
||||
// ── 3. VIP Orders (2023+ cars only) ──
|
||||
if (int.tryParse(box.read(BoxName.carYear).toString()) != null &&
|
||||
int.parse(box.read(BoxName.carYear).toString()) > 2023) ...[
|
||||
_Divider(context),
|
||||
_MenuIcon(
|
||||
icon: Octicons.watch,
|
||||
color: _T.accent,
|
||||
tooltip: 'VIP Orders'.tr,
|
||||
onTap: () => Get.to(() => const VipOrderPage()),
|
||||
),
|
||||
],
|
||||
|
||||
// _Divider(context),
|
||||
|
||||
// // ── 4. Driver Registration ──────────
|
||||
// _MenuIcon(
|
||||
// icon: Icons.person_add_alt_1_rounded,
|
||||
// color: Colors.purple.shade400,
|
||||
// tooltip: 'Registration'.tr,
|
||||
// onTap: () => Get.to(() => const RegistrationView()),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Reusable sub-widgets
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
class _MenuIcon extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
const _MenuIcon({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.tooltip,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
child: Icon(icon, color: color, size: 26),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Thin separator between icons
|
||||
class _Divider extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
const _Divider(this.context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
height: 1,
|
||||
width: 32,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
color: _T.border(this.context),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Server Helpers (unchanged logic)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
Future<void> checkForPendingOrderFromServer() async {
|
||||
bool isProcessing = false;
|
||||
if (isProcessing) return;
|
||||
|
||||
final driverId = box.read(BoxName.driverID)?.toString();
|
||||
if (driverId == null) return;
|
||||
|
||||
isProcessing = true;
|
||||
|
||||
try {
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.getArgumentAfterAppliedFromBackground,
|
||||
payload: {'driver_id': driverId},
|
||||
);
|
||||
Log.print('response: $response');
|
||||
|
||||
if (response['status'] == 'success') {
|
||||
final Map<String, dynamic> orderInfo = response['message'];
|
||||
final Map<String, dynamic> rideArgs =
|
||||
_transformServerDataToAppArguments(orderInfo);
|
||||
|
||||
final customerToken = response['message']['token_passenger'];
|
||||
final orderId = response['message']['ride_id'].toString();
|
||||
|
||||
box.write(BoxName.rideArgumentsFromBackground, rideArgs);
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
box.write(BoxName.rideStatus, 'Apply');
|
||||
Get.put(OrderRequestController()).changeApplied();
|
||||
|
||||
Get.to(
|
||||
() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArgumentsFromBackground),
|
||||
);
|
||||
} else {
|
||||
box.write(BoxName.rideArgumentsFromBackground, 'failure');
|
||||
}
|
||||
} catch (_) {
|
||||
// silent
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _transformServerDataToAppArguments(
|
||||
Map<String, dynamic> d) {
|
||||
String s(String key, [String def = 'unknown']) => d[key]?.toString() ?? def;
|
||||
|
||||
return {
|
||||
'passengerLocation': s('passenger_location'),
|
||||
'passengerDestination': s('passenger_destination'),
|
||||
'Duration': s('duration'),
|
||||
'totalCost': s('total_cost'),
|
||||
'Distance': s('distance'),
|
||||
'name': s('name'),
|
||||
'phone': s('phone'),
|
||||
'email': s('email'),
|
||||
'tokenPassenger': s('token_passenger'),
|
||||
'direction': s('direction_url'),
|
||||
'DurationToPassenger': s('duration_to_passenger'),
|
||||
'rideId': s('ride_id'),
|
||||
'passengerId': s('passenger_id'),
|
||||
'driverId': s('driver_id'),
|
||||
'durationOfRideValue': s('duration_of_ride'),
|
||||
'paymentAmount': s('payment_amount'),
|
||||
'paymentMethod': s('payment_method'),
|
||||
'passengerWalletBurc': s('passenger_wallet_burc'),
|
||||
'timeOfOrder': s('time_of_order'),
|
||||
'totalPassenger': s('total_passenger'),
|
||||
'carType': s('car_type'),
|
||||
'kazan': s('kazan'),
|
||||
'startNameLocation': s('start_name_location'),
|
||||
'endNameLocation': s('end_name_location'),
|
||||
'step0': s('step0'),
|
||||
'step1': s('step1'),
|
||||
'step2': s('step2'),
|
||||
'step3': s('step3'),
|
||||
'step4': s('step4'),
|
||||
'WalletChecked': (d['wallet_checked'] == 1).toString(),
|
||||
'isHaveSteps': (d['has_steps'] == 1) ? 'startEnd' : 'noSteps',
|
||||
};
|
||||
}
|
||||
|
||||
void _sendAcceptanceNotification(String? customerToken, dynamic rideId) {
|
||||
if (customerToken == null || customerToken.isEmpty) return;
|
||||
|
||||
List<String> body = [
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
box.read(BoxName.tokenDriver).toString(),
|
||||
rideId.toString(),
|
||||
];
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: customerToken,
|
||||
title: 'Accepted Ride'.tr,
|
||||
body: 'your ride is Accepted'.tr,
|
||||
isTopic: false,
|
||||
tone: 'start',
|
||||
driverList: body,
|
||||
category: 'Accepted Ride',
|
||||
);
|
||||
}
|
||||
67
siro_driver/lib/views/home/Captin/home_captain/widget/zones_controller.dart
Executable file
67
siro_driver/lib/views/home/Captin/home_captain/widget/zones_controller.dart
Executable file
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
class ZonesController extends GetxController {
|
||||
Map<String, List<LatLng>> generateZoneMap(
|
||||
LatLng southwest, LatLng southEast, LatLng northeast) {
|
||||
const double desiredZoneArea = 4; // in square kilometers
|
||||
|
||||
final double width = (southEast.longitude - southwest.longitude) * 100;
|
||||
final double height = (northeast.latitude - southEast.latitude) * 100;
|
||||
final double totalArea = width * height;
|
||||
|
||||
// final int numZones = (totalArea / desiredZoneArea).ceil();
|
||||
|
||||
final double zoneWidth = width / sqrt(desiredZoneArea);
|
||||
final double zoneHeight = height / sqrt(desiredZoneArea);
|
||||
final numRows =
|
||||
((northeast.latitude - southwest.latitude) / zoneHeight).ceil();
|
||||
final numCols =
|
||||
((southEast.longitude - southwest.longitude) / zoneWidth).ceil();
|
||||
List<String> zoneNames = [];
|
||||
List<LatLng> zoneCoordinates = [];
|
||||
|
||||
for (int row = 0; row < numRows; row++) {
|
||||
for (int col = 0; col < numCols; col++) {
|
||||
final double zoneSouthwestLat =
|
||||
southwest.latitude + (row * zoneHeight / 100);
|
||||
final double zoneSouthwestLng =
|
||||
southwest.longitude + (col * zoneWidth / 100);
|
||||
final double zoneNortheastLat = zoneSouthwestLat + zoneHeight / 100;
|
||||
final double zoneNortheastLng = zoneSouthwestLng + zoneWidth / 100;
|
||||
|
||||
LatLng zoneSouthwest = LatLng(zoneSouthwestLat, zoneSouthwestLng);
|
||||
LatLng zoneNortheast = LatLng(zoneNortheastLat, zoneNortheastLng);
|
||||
|
||||
String zoneName =
|
||||
'Zone${row + col}'; // Assign a unique name to each zone
|
||||
|
||||
zoneNames.add(zoneName);
|
||||
zoneCoordinates.add(zoneSouthwest);
|
||||
zoneCoordinates.add(zoneNortheast);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<LatLng>> zoneMap = {};
|
||||
for (int i = 0; i < zoneNames.length; i++) {
|
||||
zoneMap[zoneNames[i]] = [
|
||||
zoneCoordinates[i], // Southwest LatLng
|
||||
zoneCoordinates[i + 1], // Northeast LatLng
|
||||
];
|
||||
}
|
||||
|
||||
return zoneMap;
|
||||
}
|
||||
|
||||
void getJsonOfZones() {
|
||||
LatLng southwest = const LatLng(32.111107, 36.062222);
|
||||
LatLng southEast = const LatLng(32.108333, 36.101667);
|
||||
LatLng northeast = const LatLng(32.143889, 36.058889);
|
||||
Map<String, List<LatLng>> zoneMap =
|
||||
generateZoneMap(southwest, southEast, northeast);
|
||||
String jsonMap = json.encode(zoneMap);
|
||||
}
|
||||
}
|
||||
273
siro_driver/lib/views/home/Captin/maintain_center_page.dart
Executable file
273
siro_driver/lib/views/home/Captin/maintain_center_page.dart
Executable file
@@ -0,0 +1,273 @@
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/home/captin/help/maintain_center_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// class MaintainCenterPage extends StatelessWidget {
|
||||
// MaintainCenterPage({super.key});
|
||||
// MaintainCenterController maintainCenterController =
|
||||
// Get.put(MaintainCenterController());
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MyScafolld(
|
||||
// title: "Maintenance Center".tr,
|
||||
// body: [
|
||||
// GetBuilder<MaintainCenterController>(
|
||||
// builder: (maintainCenterController) {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Text(
|
||||
// "When you complete 600 trips, you will be eligible to receive offers for maintenance of your car."
|
||||
// .tr),
|
||||
// const SizedBox(
|
||||
// height: 10,
|
||||
// ),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// MyElevatedButton(
|
||||
// title: "Show My Trip Count".tr,
|
||||
// onPressed: () async {
|
||||
// maintainCenterController.getTripCountByCaptain();
|
||||
// }),
|
||||
// _buildPriceAvatar(
|
||||
// maintainCenterController.tripCount['count'] == null
|
||||
// ? '0'
|
||||
// : maintainCenterController.tripCount['count']
|
||||
// .toString())
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 10,
|
||||
// ),
|
||||
// Container(
|
||||
// decoration: AppStyle.boxDecoration,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(14),
|
||||
// child: Text(
|
||||
// "We have maintenance offers for your car. You can use them after completing 600 trips to get a 20% discount on car repairs. Enjoy using our Tripz app and be part of our Tripz family."
|
||||
// .tr,
|
||||
// style: AppStyle.title,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 10,
|
||||
// ),
|
||||
// MyElevatedButton(
|
||||
// title: 'Show maintenance center near my location'.tr,
|
||||
// onPressed: () {
|
||||
// if (maintainCenterController.tripCount['count'] > 600) {
|
||||
// } else {
|
||||
// Get.snackbar("You should complete 600 trips".tr, '',
|
||||
// backgroundColor: AppColor.yellowColor);
|
||||
// }
|
||||
// })
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// })
|
||||
// ],
|
||||
// isleading: true);
|
||||
// }
|
||||
|
||||
// Widget _buildPriceAvatar(String count) {
|
||||
// return Container(
|
||||
// width: 80,
|
||||
// height: 80,
|
||||
// decoration: BoxDecoration(
|
||||
// shape: BoxShape.circle,
|
||||
// gradient: const RadialGradient(
|
||||
// colors: [Color(0xFF4CAF50), Color(0xFF2E7D32)],
|
||||
// center: Alignment.center,
|
||||
// radius: 0.8,
|
||||
// ),
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black.withOpacity(0.2),
|
||||
// blurRadius: 8,
|
||||
// offset: const Offset(0, 4),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// count,
|
||||
// style: const TextStyle(
|
||||
// fontSize: 22,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
class MaintainCenterPage extends StatelessWidget {
|
||||
MaintainCenterPage({super.key});
|
||||
final MaintainCenterController maintainCenterController =
|
||||
Get.put(MaintainCenterController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: "Maintenance Center".tr,
|
||||
body: [
|
||||
GetBuilder<MaintainCenterController>(
|
||||
builder: (controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Introduction Text with better styling
|
||||
Text(
|
||||
"When you complete 600 trips, you will be eligible to receive offers for maintenance of your car."
|
||||
.tr,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Trip Count Section in a Card
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: "Show My Trip Count".tr,
|
||||
onPressed: () async {
|
||||
controller.getTripCountByCaptain();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildPriceAvatar(
|
||||
controller.tripCount['count'] == null
|
||||
? '0'
|
||||
: controller.tripCount['count'].toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Maintenance Offer Information in a Card
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Maintenance Offer".tr,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"We have maintenance offers for your car. You can use them after completing 600 trips to get a 20% discount on car repairs. Enjoy using our Tripz app and be part of our Tripz family."
|
||||
.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.9),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Show Maintenance Center Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: MyElevatedButton(
|
||||
title: 'Show maintenance center near my location'.tr,
|
||||
onPressed: () {
|
||||
if (controller.tripCount['count'] != null &&
|
||||
controller.tripCount['count'] >= 600) {
|
||||
// Implement navigation or action to show maintenance centers
|
||||
// For now, let's print a message
|
||||
print("Navigating to maintenance centers...");
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Ineligible for Offer".tr,
|
||||
"You should complete 500 trips to unlock this feature."
|
||||
.tr,
|
||||
backgroundColor: AppColor.yellowColor,
|
||||
colorText: Colors.black,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceAvatar(String count) {
|
||||
return Container(
|
||||
width: 70, // Slightly reduced size
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const LinearGradient(
|
||||
// Changed to LinearGradient
|
||||
colors: [Color(0xFF4CAF50), Color(0xFF2E7D32)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15), // Reduced opacity
|
||||
blurRadius: 6, // Slightly reduced blur
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
count,
|
||||
style: const TextStyle(
|
||||
fontSize: 20, // Slightly reduced size
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
218
siro_driver/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart
Executable file
218
siro_driver/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart
Executable file
@@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:slide_to_act/slide_to_act.dart';
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
|
||||
Widget driverEndRideBar() {
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (controller) {
|
||||
// 🔥 فحص هل السعر ثابت للعرض
|
||||
final String carType = controller.carType;
|
||||
final bool isFixed = (carType == 'Speed' ||
|
||||
carType == 'Awfar' ||
|
||||
carType == 'Fixed Price');
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeOutBack,
|
||||
transform: Matrix4.translationValues(
|
||||
0,
|
||||
controller.isRideStarted ? 0 : -300,
|
||||
0,
|
||||
), // Matrix4.translationValues مستخدمة للإزاحة [web:28]
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// لوحة العدادات
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 1) المسافة (تتغير دائماً)
|
||||
_buildLiveMetric(
|
||||
icon: Icons.alt_route_rounded,
|
||||
iconColor: Colors.blueAccent,
|
||||
value: controller.currentRideDistanceKm.toStringAsFixed(2),
|
||||
unit: 'KM'.tr,
|
||||
label: 'Traveled'.tr,
|
||||
),
|
||||
|
||||
_buildVerticalDivider(),
|
||||
|
||||
// 2) السعر (ثابت أو متغير)
|
||||
_buildLiveMetric(
|
||||
icon: isFixed
|
||||
? Icons.lock_outline_rounded
|
||||
: Icons.attach_money_rounded,
|
||||
iconColor: isFixed ? Colors.grey : AppColor.primaryColor,
|
||||
value: NumberFormat('#,##0').format(
|
||||
double.tryParse(controller.price.toString()) ?? 0,
|
||||
),
|
||||
unit: 'SYP'.tr,
|
||||
label: isFixed ? 'Fixed Price'.tr : 'Meter Fare'.tr,
|
||||
isHighlight: true,
|
||||
isFixedStyle: isFixed,
|
||||
),
|
||||
|
||||
_buildVerticalDivider(),
|
||||
|
||||
// 3) الوقت (تصغير الخط + إخفاء الساعات إذا 0)
|
||||
_buildLiveMetric(
|
||||
icon: Icons.timer_outlined,
|
||||
iconColor: Colors.orange,
|
||||
value: _formatDurationFromStart(controller),
|
||||
unit: '',
|
||||
label: 'Duration'.tr,
|
||||
valueFontSize: 14, // ✅ تصغير خط “المدة”
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// زر الإنهاء
|
||||
SlideAction(
|
||||
key: ValueKey(controller.isRideFinished),
|
||||
height: 60,
|
||||
borderRadius: 18,
|
||||
elevation: 0,
|
||||
text: 'Swipe to End Trip'.tr,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
outerColor: AppColor.redColor,
|
||||
innerColor: Colors.white,
|
||||
sliderButtonIcon: const Icon(
|
||||
Icons.stop_circle_outlined,
|
||||
color: AppColor.redColor,
|
||||
size: 32,
|
||||
),
|
||||
onSubmit: () async {
|
||||
await controller.finishRideFromDriver(isFromSlider: true);
|
||||
return null;
|
||||
},
|
||||
), // SlideAction مثال الاستخدام موجود في صفحة الحزمة [web:19]
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
); // GetBuilder يعيد البناء عند update() من الكنترولر [web:21]
|
||||
}
|
||||
|
||||
/// دالة تنسيق المدة:
|
||||
/// - أقل من ساعة: mm:ss
|
||||
/// - ساعة فأكثر: h:mm:ss (خانة واحدة للساعات بدون leading zero)
|
||||
String _formatDurationFromStart(MapDriverController controller) {
|
||||
if (controller.rideStartTime == null) return "00:00";
|
||||
|
||||
final d = DateTime.now().difference(controller.rideStartTime!);
|
||||
|
||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||
|
||||
final hours = d.inHours;
|
||||
final minutes = d.inMinutes.remainder(60);
|
||||
final seconds = d.inSeconds.remainder(60);
|
||||
|
||||
if (hours == 0) {
|
||||
// mm:ss
|
||||
final totalMinutes = d.inMinutes;
|
||||
return "${twoDigits(totalMinutes)}:${twoDigits(seconds)}";
|
||||
}
|
||||
|
||||
// h:mm:ss
|
||||
return "$hours:${twoDigits(minutes)}:${twoDigits(seconds)}";
|
||||
} // Duration وتفكيكه (inHours/inMinutes/inSeconds) من أساسيات Dart [web:11]
|
||||
|
||||
Widget _buildLiveMetric({
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required String value,
|
||||
required String unit,
|
||||
required String label,
|
||||
bool isHighlight = false,
|
||||
bool isFixedStyle = false,
|
||||
double? valueFontSize, // ✅ جديد: حجم خط القيمة فقط
|
||||
}) {
|
||||
final effectiveFontSize = valueFontSize ?? (isHighlight ? 20 : 18);
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: iconColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: effectiveFontSize, // ✅ هنا صار التحكم
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isFixedStyle
|
||||
? Colors.grey[800]
|
||||
: (isHighlight ? AppColor.primaryColor : Colors.black87),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (unit.isNotEmpty) ...[
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
unit,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalDivider() {
|
||||
return Container(height: 35, width: 1, color: Colors.grey.shade300);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../../controller/profile/setting_controller.dart';
|
||||
|
||||
class GoogleDriverMap extends StatelessWidget {
|
||||
const GoogleDriverMap({
|
||||
super.key,
|
||||
required this.locationController,
|
||||
});
|
||||
|
||||
final LocationController locationController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final MapDriverController controller = Get.put(MapDriverController());
|
||||
|
||||
// New: تحديد قيمة الـ padding لتحريك مركز الخريطة للأعلى
|
||||
final double mapPaddingBottom = MediaQuery.of(context).size.height * 0.3;
|
||||
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (controller) => Listener(
|
||||
onPointerDown: (_) => controller.onUserMapInteraction(),
|
||||
child: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
onMapCreated: (mapController) {
|
||||
controller.onMapCreated(mapController);
|
||||
},
|
||||
mapType: Get.isRegistered<SettingController>()
|
||||
? (Get.find<SettingController>().isMapDarkMode
|
||||
? IntaleqMapType.normal
|
||||
: IntaleqMapType.light)
|
||||
: IntaleqMapType.light,
|
||||
zoomControlsEnabled: false,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: controller.smoothedLocation ?? locationController.myLocation,
|
||||
zoom: 17,
|
||||
bearing: controller.smoothedHeading,
|
||||
tilt: 60,
|
||||
),
|
||||
// padding: EdgeInsets.only(bottom: 50, top: Get.height * 0.7),
|
||||
// minMaxZoomPreference: const MinMaxZoomPreference(8, 18),
|
||||
myLocationEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
polylines: controller.polyLines.toSet(),
|
||||
markers: {
|
||||
// 🔥 Car icon — always visible, moves with GPS location on map.
|
||||
// MarkerId matches exactly with updateMarker() in controller.
|
||||
Marker(
|
||||
markerId: const MarkerId('MyLocation'),
|
||||
position: controller.smoothedLocation ?? controller.myLocation,
|
||||
rotation: controller.smoothedHeading,
|
||||
flat: true,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
icon: controller.carIcon,
|
||||
zIndex: 100,
|
||||
),
|
||||
if (!controller.isRideStarted &&
|
||||
controller.latLngPassengerLocation.latitude != 0)
|
||||
Marker(
|
||||
markerId: const MarkerId('start'),
|
||||
position: controller.latLngPassengerLocation,
|
||||
icon: controller.startIcon,
|
||||
),
|
||||
if (controller.latLngPassengerDestination.latitude != 0 ||
|
||||
controller.latLngPassengerDestination.longitude != 0)
|
||||
Marker(
|
||||
markerId: const MarkerId('end'),
|
||||
position: controller.latLngPassengerDestination,
|
||||
icon: controller.endIcon,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
siro_driver/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart
Executable file
62
siro_driver/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart
Executable file
@@ -0,0 +1,62 @@
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class GoogleMapApp extends StatelessWidget {
|
||||
const GoogleMapApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (mapDriverController) {
|
||||
if (!mapDriverController.isRideStarted) return const SizedBox();
|
||||
|
||||
// REMOVED: Positioned wrapper
|
||||
return Material(
|
||||
elevation: 8,
|
||||
shadowColor: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
onTap: () async {
|
||||
var endLat =
|
||||
mapDriverController.latLngPassengerDestination.latitude;
|
||||
var endLng =
|
||||
mapDriverController.latLngPassengerDestination.longitude;
|
||||
String url = 'google.navigation:q=$endLat,$endLng';
|
||||
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} else {
|
||||
String webUrl =
|
||||
'https://www.google.com/maps/dir/?api=1&destination=$endLat,$endLng';
|
||||
if (await canLaunchUrl(Uri.parse(webUrl))) {
|
||||
await launchUrl(Uri.parse(webUrl));
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueAccent,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor.withOpacity(0.2), width: 1),
|
||||
),
|
||||
child: Icon(
|
||||
MaterialCommunityIcons.google_maps,
|
||||
size: 28,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
617
siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart
Executable file
617
siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart
Executable file
@@ -0,0 +1,617 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import '../../../../controller/voice_call_controller.dart';
|
||||
import '../../../../controller/functions/launch.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../widgets/error_snakbar.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import '../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../controller/functions/crud.dart';
|
||||
import '../../../../constant/links.dart';
|
||||
import '../../../widgets/my_textField.dart';
|
||||
|
||||
class PassengerInfoWindow extends StatelessWidget {
|
||||
const PassengerInfoWindow({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 1. حساب الهوامش الآمنة (SafeArea) من الأسفل
|
||||
final double safeBottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return GetBuilder<MapDriverController>(
|
||||
// id: 'PassengerInfo',
|
||||
builder: (controller) {
|
||||
// --- 1. تجهيز بيانات العرض ---
|
||||
String displayName = controller.passengerName ?? "Unknown";
|
||||
String avatarText = "";
|
||||
|
||||
// التحقق من اللغة (عربي/إنجليزي) للاسم المختصر
|
||||
bool isArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(displayName);
|
||||
|
||||
if (displayName.isNotEmpty) {
|
||||
if (isArabic) {
|
||||
avatarText = displayName.split(' ').first;
|
||||
if (avatarText.length > 4) {
|
||||
avatarText = avatarText.substring(0, 4);
|
||||
}
|
||||
} else {
|
||||
avatarText = displayName[0].toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. المنطق الذكي للموقع (Smart Positioning) ---
|
||||
// نرفع النافذة إذا ظهر شريط التعليمات في الأسفل لتجنب التغطية
|
||||
bool hasInstructions = controller.currentInstruction.isNotEmpty;
|
||||
double instructionsHeight = hasInstructions ? 110.0 : 0.0;
|
||||
|
||||
// الموقع النهائي: إذا كانت مفعلة تظهر، وإلا تختفي للأسفل
|
||||
double finalBottomPosition = controller.isPassengerInfoWindow
|
||||
? (safeBottomPadding + 10 + instructionsHeight)
|
||||
: -450.0;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
bottom: finalBottomPosition,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// --- مقبض السحب (Visual Handle) ---
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 8, bottom: 4),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- الصف العلوي: معلومات الراكب ---
|
||||
Row(
|
||||
children: [
|
||||
// الصورة الرمزية
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color:
|
||||
AppColor.primaryColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
avatarText,
|
||||
style: TextStyle(
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: isArabic ? 14 : 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// النصوص (الاسم والمسافة)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.location_on,
|
||||
size: 14, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
// 🔥 [Fix Overflow] Flexible لمنع الـ overflow + تحويل المسافة
|
||||
// السيرفر يُرجع المسافة بالأمتار (5864.022)
|
||||
Flexible(
|
||||
child: Text(
|
||||
_formatDistanceDisplay(
|
||||
controller.distance),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Icon(Icons.access_time_filled,
|
||||
size: 14, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.hours > 0
|
||||
? '${controller.hours}h ${controller.minutes}m'
|
||||
: '${controller.minutes} min',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// أزرار جانبية (سرعة + اتصال)
|
||||
Row(
|
||||
children: [
|
||||
_buildSpeedCircle(),
|
||||
const SizedBox(width: 10),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
controller.isSocialPressed = true;
|
||||
|
||||
// نفحص النتيجة: هل مسموح له يتصل؟
|
||||
bool canCall =
|
||||
await controller.driverCallPassenger();
|
||||
|
||||
if (canCall) {
|
||||
_showCallSelectionDialog(
|
||||
context, controller);
|
||||
} else {
|
||||
// هنا ممكن تظهر رسالة: تم منع الاتصال بسبب كثرة الإلغاءات
|
||||
mySnackeBarError(
|
||||
"You cannot call the passenger due to policy violations"
|
||||
.tr);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.green.withOpacity(0.2)),
|
||||
),
|
||||
child: const Icon(Icons.phone,
|
||||
color: Colors.green, size: 22),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
_showMessageOptions(context, controller),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
border:
|
||||
Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Icon(
|
||||
MaterialCommunityIcons
|
||||
.message_text_outline,
|
||||
color: AppColor.primaryColor,
|
||||
size: 22),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// خط فاصل
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Divider(height: 1, color: Colors.grey.shade100),
|
||||
),
|
||||
|
||||
// --- مؤشر الانتظار (يظهر عند الوصول) ---
|
||||
if (controller.remainingTimeInPassengerLocatioWait <
|
||||
300 &&
|
||||
controller.remainingTimeInPassengerLocatioWait != 0 &&
|
||||
!controller.isRideBegin) ...[
|
||||
_buildWaitingIndicator(controller),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// --- الأزرار الرئيسية (وصلت / ابدأ) ---
|
||||
if (!controller.isRideBegin)
|
||||
_buildActionButtons(controller),
|
||||
|
||||
// --- زر الإلغاء المدفوع (بعد انتهاء وقت الانتظار) ---
|
||||
if (controller.isdriverWaitTimeEnd &&
|
||||
!controller.isRideBegin)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFFF0F0),
|
||||
foregroundColor: Colors.red,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(
|
||||
color: Color(0xFFFFCDCD)),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
MyDialog().getDialog(
|
||||
'Confirm Cancellation'.tr,
|
||||
'Are you sure you want to cancel and collect the fee?'
|
||||
.tr, () async {
|
||||
// كود الإلغاء
|
||||
Get.back();
|
||||
controller
|
||||
.addWaitingTimeCostFromPassengerToDriverWallet();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.money_off, size: 20),
|
||||
label: Text('Cancel & Collect Fee'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- Widgets مساعدة ---
|
||||
|
||||
Widget _buildSpeedCircle() {
|
||||
return GetBuilder<LocationController>(builder: (locController) {
|
||||
int speedKmh = (locController.speed * 3.6).round();
|
||||
Color color = speedKmh > 100 ? Colors.red : const Color(0xFF0D47A1);
|
||||
|
||||
return Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 2),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('$speedKmh',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 13,
|
||||
height: 1)),
|
||||
Text('km/h',
|
||||
style: TextStyle(
|
||||
color: color.withOpacity(0.7), fontSize: 8, height: 1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildWaitingIndicator(MapDriverController controller) {
|
||||
bool isUrgent = controller.remainingTimeInPassengerLocatioWait < 60;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.timer_outlined,
|
||||
size: 16, color: isUrgent ? Colors.red : Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
controller.progressInPassengerLocationFromDriver.toDouble(),
|
||||
backgroundColor: Colors.grey[200],
|
||||
color: isUrgent ? Colors.red : Colors.green,
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
controller.stringRemainingTimeWaitingPassenger,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isUrgent ? Colors.red : Colors.green,
|
||||
fontFamily: 'monospace'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(MapDriverController controller) {
|
||||
if (controller.isArrivedSend) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF1C40F),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () async {
|
||||
await controller.markDriverAsArrived();
|
||||
},
|
||||
icon: const Icon(Icons.near_me_rounded),
|
||||
label: Text('I Have Arrived'.tr,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF27AE60),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
// 🔥 [Fix Start-Ride] استخدام Get.defaultDialog بدلاً من MyDialog
|
||||
// لأن MyDialog يستخدم Navigator.of(context, rootNavigator: true).pop()
|
||||
// الذي يتعارض مع Get.dialog() المستخدم في startRideFromDriver()
|
||||
// وقد يُسبب Get.back() اللاحق إغلاق صفحة الماب بدلاً من الـ loading dialog
|
||||
Get.defaultDialog(
|
||||
title: "Start Trip?".tr,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
middleText: "Ensure the passenger is in the car.".tr,
|
||||
barrierDismissible: true,
|
||||
radius: 14,
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF27AE60),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () async {
|
||||
// نُغلق الديالوج بـ Get.back() لضمان أن GetX يعرف أنه أُغلق
|
||||
Get.back();
|
||||
// ثم نُنفذ startRideFromDriver الذي يستخدم Get.dialog و Get.back بأمان
|
||||
await controller.startRideFromDriver();
|
||||
},
|
||||
child: Text('Start'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Cancel'.tr,
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.play_circle_fill_rounded),
|
||||
label: Text('Start Ride'.tr,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showCallSelectionDialog(
|
||||
BuildContext context, MapDriverController controller) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Call Options'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Choose how you want to call the passenger'.tr,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.green.withOpacity(0.1),
|
||||
child: const Icon(Icons.phone_android_rounded,
|
||||
color: Colors.green),
|
||||
),
|
||||
title: Text('Standard Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Uses cellular network'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
makePhoneCall(controller.passengerPhone.toString());
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.wifi_calling_3_rounded,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
title: Text('Free Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Voice call over internet'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
final voiceCtrl = Get.find<VoiceCallController>();
|
||||
final driverId = box.read(BoxName.driverID).toString();
|
||||
voiceCtrl.startCall(
|
||||
rideIdVal: controller.rideId,
|
||||
driverId: driverId,
|
||||
passengerId: controller.passengerId,
|
||||
remoteNameVal: controller.passengerName ?? "Passenger",
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMessageOptions(
|
||||
BuildContext context, MapDriverController controller) {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Quick Messages'.tr,
|
||||
style:
|
||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 15),
|
||||
_buildQuickMessageItem("Where are you, sir?".tr, controller),
|
||||
_buildQuickMessageItem("I've arrived.".tr, controller),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller.messageToPassenger,
|
||||
decoration:
|
||||
InputDecoration(hintText: 'Type a message...'.tr),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
_sendMessage(controller, controller.messageToPassenger.text,
|
||||
'cancel');
|
||||
controller.messageToPassenger.clear();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickMessageItem(String text, MapDriverController controller) {
|
||||
return ListTile(
|
||||
title: Text(text),
|
||||
onTap: () {
|
||||
_sendMessage(controller, text, 'ding');
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(
|
||||
MapDriverController controller, String body, String tone) async {
|
||||
try {
|
||||
await CRUD().post(
|
||||
link: AppLink.sendChatMessage,
|
||||
payload: {
|
||||
'ride_id': controller.rideId.toString(),
|
||||
'sender_id': box.read(BoxName.driverID).toString(),
|
||||
'receiver_id': controller.passengerId.toString(),
|
||||
'sender_type': 'driver',
|
||||
'message_content': body,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore or log error
|
||||
}
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'Driver Message'.tr,
|
||||
body: body,
|
||||
isTopic: false,
|
||||
tone: tone,
|
||||
driverList: [],
|
||||
category: 'message From Driver',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// تحويل المسافة من الأمتار إلى عرض مقروء
|
||||
/// السيرفر يُرجع المسافة بالأمتار (مثال: 5864.022)
|
||||
/// النتيجة: "5.9 km" أو "250 م"
|
||||
String _formatDistanceDisplay(String rawDistance) {
|
||||
final meters = double.tryParse(rawDistance) ?? 0.0;
|
||||
if (meters >= 1000) {
|
||||
return '${(meters / 1000).toStringAsFixed(1)} km';
|
||||
} else if (meters > 0) {
|
||||
return '${meters.toStringAsFixed(0)} م';
|
||||
}
|
||||
return rawDistance; // fallback للقيمة الأصلية
|
||||
}
|
||||
131
siro_driver/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart
Executable file
131
siro_driver/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart
Executable file
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart'; // Checked import
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../controller/functions/launch.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../../main.dart';
|
||||
|
||||
class SosConnect extends StatelessWidget {
|
||||
SosConnect({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
id: 'SosConnect', // Keep ID for updates
|
||||
builder: (controller) {
|
||||
bool showSos = controller.isRideStarted;
|
||||
|
||||
if (!showSos) return const SizedBox();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// === SOS Button ===
|
||||
_buildModernActionButton(
|
||||
icon: MaterialIcons.warning,
|
||||
color: Colors.white,
|
||||
bgColor: AppColor.redColor,
|
||||
tooltip: 'EMERGENCY SOS',
|
||||
isPulsing: true,
|
||||
onTap: () => _handleSosCall(controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModernActionButton({
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
required String tooltip,
|
||||
required VoidCallback onTap,
|
||||
bool isPulsing = false,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: isPulsing
|
||||
? [
|
||||
BoxShadow(
|
||||
color: bgColor.withOpacity(0.4),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Logic Functions ---
|
||||
void _handleSosCall(MapDriverController mapDriverController) {
|
||||
if (box.read(BoxName.sosPhoneDriver) == null) {
|
||||
Get.defaultDialog(
|
||||
title: 'Emergency Contact'.tr,
|
||||
content: Column(
|
||||
children: [
|
||||
Text('Please enter the emergency number.'.tr),
|
||||
Form(
|
||||
key: mapDriverController.formKey1,
|
||||
child: MyTextForm(
|
||||
controller: mapDriverController.sosEmergincyNumberCotroller,
|
||||
label: 'Phone Number'.tr,
|
||||
hint: '0923456789',
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Save & Call'.tr,
|
||||
onPressed: () {
|
||||
if (mapDriverController.formKey1.currentState!.validate()) {
|
||||
box.write(BoxName.sosPhoneDriver,
|
||||
mapDriverController.sosEmergincyNumberCotroller.text);
|
||||
Get.back();
|
||||
launchCommunication(
|
||||
'phone', box.read(BoxName.sosPhoneDriver), '');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
launchCommunication('phone', box.read(BoxName.sosPhoneDriver), '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
|
||||
// ويدجت للعرض فقط (بدون منطق فتح نوافذ)
|
||||
class SpeedCircle extends StatelessWidget {
|
||||
const SpeedCircle({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
id: 'SpeedCircle', // نحدد ID للتحديث الخفيف
|
||||
builder: (controller) {
|
||||
// إذا السرعة 0 أو أقل، نخفي الدائرة
|
||||
if (controller.speed <= 0) return const SizedBox();
|
||||
|
||||
bool isSpeeding = controller.speed > 100;
|
||||
|
||||
return Positioned(
|
||||
left: 20,
|
||||
top: 100, // مكانها المناسب
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
border: Border.all(
|
||||
color: _getSpeedColor(controller.speed),
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
controller.speed.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontFamily: AppStyle.title.fontFamily,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.0,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"km/h",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Color _getSpeedColor(double speed) {
|
||||
if (speed < 60) return AppColor.greenColor;
|
||||
if (speed < 100) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
25
siro_driver/lib/views/home/Captin/orderCaptin/call.dart
Executable file
25
siro_driver/lib/views/home/Captin/orderCaptin/call.dart
Executable file
@@ -0,0 +1,25 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:ride/constant/api_key.dart';
|
||||
// import 'package:ride/constant/box_name.dart';
|
||||
// import 'package:ride/main.dart';
|
||||
// import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart';
|
||||
|
||||
// class CallPage extends StatelessWidget {
|
||||
// const CallPage({Key? key, required this.callID}) : super(key: key);
|
||||
// final String callID;
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return ZegoUIKitPrebuiltCall(
|
||||
// appID: AK
|
||||
// .zegoCloudAppID, // Fill in the appID that you get from ZEGOCLOUD Admin Console.
|
||||
// appSign: AK
|
||||
// .zegoCloudAppSIGN, // Fill in the appSign that you get from ZEGOCLOUD Admin Console.
|
||||
// userID: box.read(BoxName.passengerID) ?? box.read(BoxName.driverID),
|
||||
// userName: box.read(BoxName.name) ?? box.read(BoxName.nameDriver),
|
||||
// callID: callID,
|
||||
// // You can also use groupVideo/groupVoice/oneOnOneVoice to make more types of calls.
|
||||
// config: ZegoUIKitPrebuiltCallConfig.oneOnOneVoiceCall(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class MarkerGenerator {
|
||||
// دالة لرسم ماركر يحتوي على نص (للوقت والمسافة)
|
||||
static Future<InlqBitmap> createCustomMarkerBitmap({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
required IconData iconData,
|
||||
}) async {
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
|
||||
// إعدادات القياسات
|
||||
const double width = 220.0;
|
||||
const double height = 110.0;
|
||||
const double circleRadius = 25.0;
|
||||
|
||||
// 1. رسم المربع (Info Box) مع تدرج لوني بسيط
|
||||
final Paint paint = Paint()..color = color;
|
||||
final RRect rRect = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(0, 0, width, 65),
|
||||
const Radius.circular(20), // زوايا أكثر استدارة لشكل عصري
|
||||
);
|
||||
|
||||
// ظل أقوى لمظهر بارز (Premium Feel)
|
||||
canvas.drawShadow(Path()..addRRect(rRect), Colors.black54, 10.0, true);
|
||||
canvas.drawRRect(rRect, paint);
|
||||
|
||||
// 2. رسم مثلث صغير أسفل المربع (Arrow Tail)
|
||||
final Path path = Path();
|
||||
path.moveTo(width / 2 - 10, 60);
|
||||
path.lineTo(width / 2, 75);
|
||||
path.lineTo(width / 2 + 10, 60);
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// 3. رسم الدائرة (مكان الأيقونة)
|
||||
canvas.drawCircle(const Offset(width / 2, 85), circleRadius, paint);
|
||||
|
||||
// 4. رسم الأيقونة داخل الدائرة
|
||||
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
iconPainter.text = TextSpan(
|
||||
text: String.fromCharCode(iconData.codePoint),
|
||||
style: TextStyle(
|
||||
fontSize: 30.0,
|
||||
fontFamily: iconData.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
iconPainter.layout();
|
||||
iconPainter.paint(
|
||||
canvas,
|
||||
Offset((width - iconPainter.width) / 2, 85 - (iconPainter.height / 2)),
|
||||
);
|
||||
|
||||
// 5. رسم النصوص (العنوان والوصف) داخل المربع
|
||||
// العنوان (مثلاً: المدة)
|
||||
TextPainter titlePainter = TextPainter(
|
||||
textDirection: TextDirection.rtl,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
titlePainter.text = TextSpan(
|
||||
text: title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
titlePainter.layout(minWidth: width);
|
||||
titlePainter.paint(canvas, const Offset(0, 8));
|
||||
|
||||
// الوصف (مثلاً: المسافة)
|
||||
TextPainter subTitlePainter = TextPainter(
|
||||
textDirection: TextDirection.rtl,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
subTitlePainter.text = TextSpan(
|
||||
text: subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
color: Colors.white70,
|
||||
),
|
||||
);
|
||||
subTitlePainter.layout(minWidth: width);
|
||||
subTitlePainter.paint(canvas, const Offset(0, 32));
|
||||
|
||||
// تحويل الرسم إلى صورة
|
||||
final ui.Image image = await pictureRecorder.endRecording().toImage(
|
||||
width.toInt(),
|
||||
(height + 20).toInt(), // مساحة إضافية
|
||||
);
|
||||
final ByteData? data =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return InlqBitmap.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
// دالة خاصة لرسم ماركر السائق (دائرة وخلفها سهم)
|
||||
static Future<InlqBitmap> createDriverMarker() async {
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
const double size = 100.0;
|
||||
|
||||
final Paint paint = Paint()..color = const Color(0xFF2E7D32); // أخضر غامق
|
||||
final Paint borderPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4.0;
|
||||
|
||||
// الدائرة
|
||||
canvas.drawCircle(const Offset(size / 2, size / 2), size / 2.5, paint);
|
||||
canvas.drawCircle(
|
||||
const Offset(size / 2, size / 2), size / 2.5, borderPaint);
|
||||
|
||||
// رسم السهم (Arrow Up)
|
||||
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
iconPainter.text = TextSpan(
|
||||
text: String.fromCharCode(Icons.navigation.codePoint), // سهم ملاحة
|
||||
style: TextStyle(
|
||||
fontSize: 40.0,
|
||||
fontFamily: Icons.navigation.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
iconPainter.layout();
|
||||
iconPainter.paint(
|
||||
canvas,
|
||||
Offset((size - iconPainter.width) / 2, (size - iconPainter.height) / 2),
|
||||
);
|
||||
|
||||
final ui.Image image = await pictureRecorder
|
||||
.endRecording()
|
||||
.toImage(size.toInt(), size.toInt());
|
||||
final ByteData? data =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return InlqBitmap.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
}
|
||||
814
siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart
Executable file
814
siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart
Executable file
@@ -0,0 +1,814 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/models/overlay_service.dart';
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/links.dart';
|
||||
import '../../../../controller/firebase/firbase_messge.dart';
|
||||
import '../../../../controller/firebase/local_notification.dart';
|
||||
import '../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../controller/functions/crud.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../../models/model/order_data.dart';
|
||||
import '../../../../print.dart';
|
||||
|
||||
// === Enhanced Colors for Better Readability ===
|
||||
class AppColors {
|
||||
static const primary = Color(0xFF1A252F);
|
||||
static const card = Color(0xFF2C3E50);
|
||||
static const white = Colors.white;
|
||||
static const gray = Color(0xFFBDC3C7);
|
||||
static const lightGray = Color(0xFFECF0F1);
|
||||
static const accent = Color(0xFF00BCD4);
|
||||
static const accept = Color(0xFF4CAF50);
|
||||
static const reject = Color(0xFFFF5722);
|
||||
static const highlight = Color(0xFFFFC107);
|
||||
static const priceHighlight = Color(0xFF00E676);
|
||||
static const urgentRed = Color(0xFFD32F2F);
|
||||
}
|
||||
|
||||
class OrderOverlay extends StatefulWidget {
|
||||
const OrderOverlay({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<OrderOverlay> createState() => _OrderOverlayState();
|
||||
}
|
||||
|
||||
class _OrderOverlayState extends State<OrderOverlay>
|
||||
with WidgetsBindingObserver {
|
||||
// === State Variables ===
|
||||
OrderData? orderData;
|
||||
Timer? timer;
|
||||
int remainingSeconds = 10;
|
||||
final AudioPlayer audioPlayer = AudioPlayer();
|
||||
bool buttonsEnabled = true;
|
||||
final String mapApiKey = AK.mapAPIKEY;
|
||||
final CRUD _crud = CRUD();
|
||||
|
||||
final NotificationController notificationController =
|
||||
Get.put(NotificationController());
|
||||
// === Getters ===
|
||||
bool get canShowMap {
|
||||
if (orderData == null || mapApiKey.isEmpty) return false;
|
||||
final start = orderData!.startCoordinates;
|
||||
final end = orderData!.endCoordinates;
|
||||
return start?['lat'] != null &&
|
||||
start?['lng'] != null &&
|
||||
end?['lat'] != null &&
|
||||
end?['lng'] != null;
|
||||
}
|
||||
|
||||
String get staticMapUrl {
|
||||
if (!canShowMap) return "";
|
||||
final start = orderData!.startCoordinates!;
|
||||
final end = orderData!.endCoordinates!;
|
||||
final startMarker = Uri.encodeComponent("${start['lat']},${start['lng']}");
|
||||
final endMarker = Uri.encodeComponent("${end['lat']},${end['lng']}");
|
||||
|
||||
return "https://maps.googleapis.com/maps/api/staticmap?"
|
||||
"size=600x150&maptype=roadmap"
|
||||
"&markers=color:green%7Clabel:S%7C$startMarker"
|
||||
"&markers=color:red%7Clabel:D%7C$endMarker"
|
||||
"&path=color:0x007bff%7Cweight:5%7C$startMarker%7C$endMarker"
|
||||
"&key=$mapApiKey";
|
||||
}
|
||||
|
||||
// === Lifecycle ===
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
FlutterOverlayWindow.overlayListener.listen((event) {
|
||||
if (mounted) _processEventData(event);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
_stopAudio();
|
||||
audioPlayer.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_checkOverlayStatus();
|
||||
}
|
||||
}
|
||||
|
||||
List myList = [];
|
||||
// === Setup & Listeners ===
|
||||
void _setupOverlayListener() {
|
||||
FlutterOverlayWindow.overlayListener.listen((event) {
|
||||
if (mounted) _processEventData(event);
|
||||
});
|
||||
}
|
||||
|
||||
void _processEventData(dynamic event) {
|
||||
_log("Received event: $event");
|
||||
if (event is List<dynamic>) {
|
||||
try {
|
||||
myList = event;
|
||||
final newOrder = OrderData.fromList(event);
|
||||
_log("Parsed OrderData: ${newOrder.toMap()}");
|
||||
setState(() {
|
||||
orderData = newOrder;
|
||||
});
|
||||
_resetAndStartTimer();
|
||||
} catch (e, s) {
|
||||
_log("Error parsing OrderData: $e\nStackTrace: $s");
|
||||
}
|
||||
} else {
|
||||
_log("Unexpected data format: $event");
|
||||
}
|
||||
}
|
||||
|
||||
void _checkOverlayStatus() async {
|
||||
bool isActive = await FlutterOverlayWindow.isActive();
|
||||
if (isActive && mounted && orderData != null) {
|
||||
if (remainingSeconds > 0 && (timer == null || !timer!.isActive)) {
|
||||
_resetAndStartTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Timer Management ===
|
||||
void _resetAndStartTimer() {
|
||||
timer?.cancel();
|
||||
audioPlayer.stop();
|
||||
setState(() {
|
||||
buttonsEnabled = true;
|
||||
remainingSeconds = _calculateTimerDuration();
|
||||
});
|
||||
_playAudio();
|
||||
_startTimer();
|
||||
}
|
||||
|
||||
int _calculateTimerDuration() {
|
||||
if (orderData?.durationToPassengerMinutes != null &&
|
||||
orderData!.durationToPassengerMinutes > 0) {
|
||||
int duration = orderData!.durationToPassengerMinutes * 60;
|
||||
return duration > 10 ? 10 : duration;
|
||||
}
|
||||
return 10;
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
if (orderData == null) return;
|
||||
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
_stopAudio();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
if (remainingSeconds > 0) {
|
||||
remainingSeconds--;
|
||||
} else {
|
||||
timer.cancel();
|
||||
_stopAudio();
|
||||
if (buttonsEnabled) _handleOrderTimeout();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// === Audio Management ===
|
||||
void _playAudio() async {
|
||||
try {
|
||||
await audioPlayer.setAsset('assets/order.mp3', preload: true);
|
||||
await audioPlayer.setLoopMode(LoopMode.one);
|
||||
await audioPlayer.play();
|
||||
} catch (e) {
|
||||
_log('Error playing audio: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _stopAudio() {
|
||||
audioPlayer.stop();
|
||||
}
|
||||
|
||||
String _getData(int index, {String defaultValue = ''}) {
|
||||
if (myList.length > index && myList[index] != null) {
|
||||
return myList[index].toString();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// === Order Actions ===
|
||||
Future<void> _acceptOrder() async {
|
||||
if (!buttonsEnabled || orderData == null) return;
|
||||
_disableButtonsAndProcess();
|
||||
_log("Order ACCEPTED: ${orderData!.orderId}");
|
||||
|
||||
try {
|
||||
final driverId = box.read(BoxName.driverID)?.toString();
|
||||
if (driverId == null) {
|
||||
_log("Error: Driver ID is null. Closing overlay.");
|
||||
await _closeOverlay();
|
||||
return;
|
||||
}
|
||||
var res = await CRUD()
|
||||
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
|
||||
'id': orderData!.orderId,
|
||||
'rideTimeStart': DateTime.now().toString(),
|
||||
'status': 'Apply',
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
'passengerToken': _getData(9),
|
||||
});
|
||||
|
||||
final payload = {
|
||||
// بيانات أساسية
|
||||
'driver_id': driverId,
|
||||
'status': 'Apply',
|
||||
'passengerLocation': '${_getData(0)},${_getData(1)}',
|
||||
'passengerDestination': '${_getData(3)},${_getData(4)}',
|
||||
'Duration': _getData(4),
|
||||
'totalCost': _getData(26),
|
||||
'Distance': _getData(5),
|
||||
'name': _getData(8),
|
||||
'phone': _getData(10),
|
||||
'email': _getData(28),
|
||||
'WalletChecked': _getData(13),
|
||||
'tokenPassenger': _getData(9),
|
||||
'direction': staticMapUrl.toString(),
|
||||
'DurationToPassenger': _getData(15),
|
||||
'rideId': orderData!.orderId,
|
||||
'passengerId': _getData(7),
|
||||
'durationOfRideValue': _getData(19),
|
||||
'paymentAmount': _getData(2),
|
||||
'paymentMethod': _getData(13) == 'true' ? 'visa' : 'cash',
|
||||
'isHaveSteps': _getData(20),
|
||||
'step0': myList[21].toString(),
|
||||
'step1': myList[22].toString(),
|
||||
'step2': myList[23].toString(),
|
||||
'step3': myList[24].toString(),
|
||||
'step4': myList[25].toString(),
|
||||
'passengerWalletBurc': myList[26].toString(),
|
||||
'carType': myList[31].toString(),
|
||||
'kazan': myList[32].toString(),
|
||||
'startNameLocation': myList[29].toString(),
|
||||
'endNameLocation': myList[30].toString(),
|
||||
// الحقول الإضافية التي يجب تضمينها
|
||||
'timeOfOrder': DateTime.now().toIso8601String(),
|
||||
'totalPassenger': _getData(2),
|
||||
};
|
||||
Log.print('payload: ${payload}');
|
||||
CRUD().post(
|
||||
link: AppLink.addOverLayStatus,
|
||||
payload: payload,
|
||||
);
|
||||
if (res != "failure") {
|
||||
// Using rideId (_getData(16)) for order_id consistently
|
||||
|
||||
_log("Server update successful. Writing to storage.");
|
||||
notificationController.showNotification(
|
||||
"Order Accepted".tr,
|
||||
"Open app and go to passenger".tr,
|
||||
'ding',
|
||||
'',
|
||||
);
|
||||
|
||||
// 3. الخطوة الأهم: فتح التطبيق وإغلاق النافذة
|
||||
try {
|
||||
// استدعاء الميثود التي تم تحديثها في الخطوة 1
|
||||
await OverlayMethodChannel.bringToForeground();
|
||||
} catch (e) {
|
||||
_log("Failed to bring app to foreground: $e");
|
||||
}
|
||||
await _closeOverlay();
|
||||
} else {
|
||||
_log("Failed to update order status on server: $res");
|
||||
notificationController.showNotification(
|
||||
"Order Accepted by another driver".tr,
|
||||
"Open app and go to passenger".tr,
|
||||
'ding',
|
||||
'',
|
||||
);
|
||||
await _closeOverlay();
|
||||
}
|
||||
} catch (e, s) {
|
||||
_log(
|
||||
"A critical error occurred during server update: $e\nStackTrace: $s");
|
||||
if (mounted) setState(() => buttonsEnabled = true);
|
||||
|
||||
_log("Error in accept order: $e");
|
||||
await _closeOverlay();
|
||||
// حتى في حال الخطأ، نحاول فتح التطبيق ليرى السائق ما حدث
|
||||
await OverlayMethodChannel.bringToForeground();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Your list parsing for 'customerToken' should be something like:
|
||||
// customerToken: list.length > a_certain_index ? list[a_certain_index].toString() : null,
|
||||
Future<void> _rejectOrder() async {
|
||||
if (!buttonsEnabled || orderData == null) return;
|
||||
_disableButtonsAndProcess();
|
||||
_log("Order REJECTED: ${orderData!.orderId}");
|
||||
box.write(BoxName.rideStatus, 'reject');
|
||||
Log.print('rideStatus from overlay 303 : ${box.read(BoxName.rideStatus)}');
|
||||
await _apiRefuseOrder(orderData!.orderId);
|
||||
await _closeOverlay();
|
||||
}
|
||||
|
||||
void _handleOrderTimeout() {
|
||||
if (orderData == null) return;
|
||||
_log("Order TIMED OUT: ${orderData!.orderId}");
|
||||
_rejectOrder();
|
||||
}
|
||||
|
||||
Future<void> _apiRefuseOrder(String orderID) async {
|
||||
if (orderID == "N/A") {
|
||||
_log("Cannot refuse order with N/A ID");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final driverId = box.read(BoxName.driverID)?.toString();
|
||||
if (driverId == null) {
|
||||
_log("Driver ID is null, cannot refuse order");
|
||||
return;
|
||||
}
|
||||
CRUD().post(link: AppLink.addDriverOrder, payload: {
|
||||
'driver_id': driverId,
|
||||
'order_id': orderID,
|
||||
'status': 'Refused'
|
||||
});
|
||||
|
||||
_log("Order $orderID refused successfully");
|
||||
} catch (e) {
|
||||
_log("Error in _apiRefuseOrder for $orderID: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// === Helper Methods ===
|
||||
void _disableButtonsAndProcess() {
|
||||
setState(() => buttonsEnabled = false);
|
||||
timer?.cancel();
|
||||
_stopAudio();
|
||||
}
|
||||
|
||||
Future<void> _closeOverlay() async {
|
||||
_stopAudio();
|
||||
timer?.cancel();
|
||||
if (await FlutterOverlayWindow.isActive()) {
|
||||
await FlutterOverlayWindow.closeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
// A simple logger to distinguish overlay logs
|
||||
print("OVERLAY_LOG: $message");
|
||||
}
|
||||
|
||||
// === UI Build Methods ===
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ... (Your entire UI build method remains unchanged) ...
|
||||
// The UI code is excellent and doesn't need modification.
|
||||
if (orderData == null) {
|
||||
return const Material(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(color: AppColors.accent)));
|
||||
}
|
||||
|
||||
return Material(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.card,
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
border: Border.all(
|
||||
color: AppColors.accent.withOpacity(0.3), width: 1.5),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
blurRadius: 15,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildQuickHeader(),
|
||||
const SizedBox(height: 12),
|
||||
_buildPrimaryInfo(),
|
||||
const SizedBox(height: 12),
|
||||
if (canShowMap) _buildCompactMap(),
|
||||
if (canShowMap) const SizedBox(height: 12),
|
||||
_buildSecondaryInfo(),
|
||||
const SizedBox(height: 16),
|
||||
_buildEnhancedActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// All your _build... widget methods (_buildQuickHeader, _buildPrimaryInfo, etc.)
|
||||
// are perfectly fine and do not need to be changed.
|
||||
// ... Paste all your existing _build... methods here ...
|
||||
Widget _buildQuickHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
remainingSeconds <= 3
|
||||
? AppColors.urgentRed
|
||||
: remainingSeconds <= 5
|
||||
? AppColors.highlight
|
||||
: AppColors.accent,
|
||||
remainingSeconds <= 3
|
||||
? AppColors.urgentRed.withOpacity(0.7)
|
||||
: remainingSeconds <= 5
|
||||
? AppColors.highlight.withOpacity(0.7)
|
||||
: AppColors.accent.withOpacity(0.7),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.drive_eta_rounded, color: AppColors.white, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"طلب جديد".tr,
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
"$remainingSeconds ث",
|
||||
style: TextStyle(
|
||||
color: remainingSeconds <= 3
|
||||
? AppColors.urgentRed
|
||||
: remainingSeconds <= 5
|
||||
? AppColors.highlight
|
||||
: AppColors.accent,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrimaryInfo() {
|
||||
final order = orderData!;
|
||||
|
||||
// FIX: Parse the price to a number safely before formatting
|
||||
// This handles cases where order.price is a String like "173"
|
||||
final num priceValue = num.tryParse(order.price.toString()) ?? 0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.priceHighlight.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Price and Distance - Most Important Info
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildHighlightInfo(
|
||||
// FIX: Use the parsed priceValue here
|
||||
"${NumberFormat('#,##0').format(priceValue)} ل.س",
|
||||
"السعر".tr,
|
||||
Icons.monetization_on_rounded,
|
||||
AppColors.priceHighlight,
|
||||
isLarge: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildHighlightInfo(
|
||||
// Ensure tripDistanceKm is treated safely too
|
||||
"${(num.tryParse(order.tripDistanceKm.toString()) ?? 0).toStringAsFixed(1)} كم",
|
||||
"المسافة".tr,
|
||||
Icons.straighten_rounded,
|
||||
AppColors.accent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Divider(color: AppColors.gray.withOpacity(0.2), thickness: 1),
|
||||
const SizedBox(height: 12),
|
||||
// Passenger Info and ETA
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildPassengerQuickInfo(),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildHighlightInfo(
|
||||
"${order.durationToPassengerMinutes} د",
|
||||
"للوصول".tr,
|
||||
Icons.access_time_filled_rounded,
|
||||
order.durationToPassengerMinutes <= 3
|
||||
? AppColors.priceHighlight
|
||||
: AppColors.gray,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHighlightInfo(
|
||||
String value, String label, IconData icon, Color color,
|
||||
{bool isLarge = false}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: color, size: isLarge ? 24 : 20),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: isLarge ? 20 : 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: AppColors.lightGray.withOpacity(0.7),
|
||||
fontSize: 11,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPassengerQuickInfo() {
|
||||
final order = orderData!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.highlight.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border:
|
||||
Border.all(color: AppColors.highlight.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person_rounded, color: AppColors.highlight, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
order.customerName,
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
order.rideType,
|
||||
style: TextStyle(
|
||||
color: AppColors.lightGray.withOpacity(0.7),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactMap() {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Image.network(
|
||||
staticMapUrl,
|
||||
height: 100, // Reduced from 110
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child:
|
||||
Icon(Icons.map_outlined, color: AppColors.gray, size: 32)),
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return SizedBox(
|
||||
height: 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
color: AppColors.accent,
|
||||
strokeWidth: 2.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSecondaryInfo() {
|
||||
final order = orderData!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildLocationRow(
|
||||
Icons.trip_origin_rounded,
|
||||
"من".tr,
|
||||
order.startLocationAddress,
|
||||
Colors.green.shade300,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildLocationRow(
|
||||
Icons.flag_rounded,
|
||||
"إلى".tr,
|
||||
order.endLocationAddress,
|
||||
Colors.red.shade300,
|
||||
),
|
||||
if (order.tripDurationMinutes > 0) ...[
|
||||
const SizedBox(height: 8),
|
||||
Divider(color: AppColors.gray.withOpacity(0.2), thickness: 0.5),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.timer_outlined, color: AppColors.accent, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"مدة الرحلة: ${order.tripDurationMinutes} دقيقة".tr,
|
||||
style: const TextStyle(
|
||||
color: AppColors.lightGray,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationRow(
|
||||
IconData icon, String label, String address, Color iconColor) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: iconColor, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"$label: ",
|
||||
style: TextStyle(
|
||||
color: AppColors.lightGray.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
address,
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEnhancedActionButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: buttonsEnabled ? _rejectOrder : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.reject,
|
||||
foregroundColor: AppColors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
disabledBackgroundColor: AppColors.reject.withOpacity(0.3),
|
||||
elevation: 3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.close_rounded, size: 20),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"رفض".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: buttonsEnabled ? _acceptOrder : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accept,
|
||||
foregroundColor: AppColors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
disabledBackgroundColor: AppColors.accept.withOpacity(0.3),
|
||||
elevation: 3,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.check_circle_rounded, size: 20),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"قبول".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
438
siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart
Executable file
438
siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart
Executable file
@@ -0,0 +1,438 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/home/captin/order_request_controller.dart';
|
||||
|
||||
class OrderRequestPage extends StatelessWidget {
|
||||
const OrderRequestPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حقن الكنترولر
|
||||
final OrderRequestController controller = Get.put(OrderRequestController());
|
||||
|
||||
return Scaffold(
|
||||
body: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: GetBuilder<OrderRequestController>(
|
||||
builder: (controller) {
|
||||
// 🔥 التعديل الأهم: التحقق من وجود أي بيانات (List أو Map)
|
||||
if (controller.myList == null && controller.myMapData == null) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator()); // شاشة تحميل بدلاً من فراغ
|
||||
}
|
||||
|
||||
// 🔥 استخدام دوال الكنترولر الآمنة لجلب البيانات بدلاً من الوصول المباشر
|
||||
// قمت بتحويل _safeGet إلى دالة عامة safeGet في الكنترولر (تأكد من جعلها public)
|
||||
// أو سأقوم بكتابة المنطق هنا مباشرة لضمان العمل:
|
||||
|
||||
String getValue(int index) {
|
||||
if (controller.myList != null &&
|
||||
index < controller.myList!.length) {
|
||||
return controller.myList![index].toString();
|
||||
}
|
||||
if (controller.myMapData != null &&
|
||||
controller.myMapData!.containsKey(index.toString())) {
|
||||
return controller.myMapData![index.toString()].toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
final String passengerName =
|
||||
getValue(8).isEmpty ? "عميل" : getValue(8);
|
||||
final String startAddr =
|
||||
getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29);
|
||||
final String endAddr =
|
||||
getValue(30).isEmpty ? "الوجهة" : getValue(30);
|
||||
final bool isVisa = (getValue(13) == 'true');
|
||||
|
||||
// منطق Speed = سعر ثابت
|
||||
final bool isSpeed =
|
||||
controller.tripType.toLowerCase().contains('speed');
|
||||
final String carTypeLabel =
|
||||
isSpeed ? "سعر ثابت" : controller.tripType;
|
||||
final Color carTypeColor =
|
||||
isSpeed ? Colors.red.shade700 : Colors.blue.shade700;
|
||||
final IconData carIcon =
|
||||
isSpeed ? Icons.local_offer : Icons.directions_car;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// 1. الخارطة
|
||||
Positioned.fill(
|
||||
bottom: 300,
|
||||
child: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(
|
||||
controller.latPassenger, controller.lngPassenger),
|
||||
zoom: 13.0,
|
||||
),
|
||||
markers: controller.markers,
|
||||
mapType: Get.isDarkMode
|
||||
? IntaleqMapType.normal
|
||||
: IntaleqMapType.light,
|
||||
polylines: controller.polylines,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
onMapCreated: (c) {
|
||||
controller.onMapCreated(c);
|
||||
controller.update();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 2. كبسولة الوصول للراكب
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 8)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.near_me,
|
||||
color: Colors.amber, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"الوصول للراكب: ${controller.timeToPassenger}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 3. البطاقة السفلية
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 360,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(25),
|
||||
topRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5)
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(2)))),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// الصف الأول: الراكب والسعر
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant,
|
||||
child: Icon(Icons.person,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: 28),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(passengerName,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star,
|
||||
color: Colors.amber, size: 14),
|
||||
Text(controller.passengerRating,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text("${controller.tripPrice} ل.س",
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primaryColor)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isVisa
|
||||
? Colors.purple.withOpacity(0.1)
|
||||
: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(isVisa ? "فيزا" : "كاش",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isVisa
|
||||
? Colors.purple
|
||||
: Colors.green)),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
isVisa
|
||||
? Icons.credit_card
|
||||
: Icons.money,
|
||||
size: 14,
|
||||
color: isVisa
|
||||
? Colors.purple
|
||||
: Colors.green),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// الصف الثاني: شريط المعلومات
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
context, carIcon, carTypeLabel, carTypeColor),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
Icons.route,
|
||||
controller.totalTripDistance,
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color ??
|
||||
Colors.black87),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
Icons.access_time_filled,
|
||||
controller.totalTripDuration,
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color ??
|
||||
Colors.black87),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// الصف الثالث: العناوين
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const Icon(Icons.my_location,
|
||||
size: 18, color: Colors.green),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 2,
|
||||
color: Colors.grey.shade300,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 2))),
|
||||
const Icon(Icons.location_on,
|
||||
size: 18, color: Colors.red),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("موقع الانطلاق",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey)),
|
||||
Text(startAddr,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("الوجهة",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey)),
|
||||
Text(endAddr,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// الصف الرابع: الأزرار
|
||||
Row(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
shape: BoxShape.circle,
|
||||
border:
|
||||
Border.all(color: Colors.red.shade100)),
|
||||
child: const Icon(Icons.close,
|
||||
color: Colors.red, size: 24),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => controller.acceptOrder(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text("قبول الرحلة",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 15),
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
value: controller.progress,
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
backgroundColor: Colors.white24),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text("${controller.remainingTime}",
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(
|
||||
BuildContext context, IconData icon, String text, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.bold, color: color)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
558
siro_driver/lib/views/home/Captin/orderCaptin/order_speed_request.dart
Executable file
558
siro_driver/lib/views/home/Captin/orderCaptin/order_speed_request.dart
Executable file
@@ -0,0 +1,558 @@
|
||||
// import 'dart:convert';
|
||||
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
// import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
// import 'package:siro_driver/constant/box_name.dart';
|
||||
// import 'package:siro_driver/controller/firebase/firbase_messge.dart';
|
||||
// import 'package:siro_driver/main.dart';
|
||||
// import 'package:siro_driver/print.dart';
|
||||
// import 'package:siro_driver/views/home/Captin/driver_map_page.dart';
|
||||
|
||||
// import '../../../../constant/colors.dart';
|
||||
// import '../../../../constant/links.dart';
|
||||
// import '../../../../constant/style.dart';
|
||||
// import '../../../../controller/firebase/notification_service.dart';
|
||||
// import '../../../../controller/functions/crud.dart';
|
||||
// import '../../../../controller/functions/launch.dart';
|
||||
// import '../../../../controller/home/captin/order_request_controller.dart';
|
||||
// import '../../../widgets/elevated_btn.dart';
|
||||
// import '../../../widgets/mydialoug.dart';
|
||||
|
||||
// class OrderSpeedRequest extends StatelessWidget {
|
||||
// OrderSpeedRequest({super.key});
|
||||
|
||||
// final OrderRequestController orderRequestController =
|
||||
// Get.put(OrderRequestController());
|
||||
|
||||
// // دالة مساعدة لاستخراج البيانات بأمان (Null Safety)
|
||||
// String _getData(int index, {String defaultValue = ''}) {
|
||||
// // if (orderRequestController.myList.length > index &&
|
||||
// // orderRequestController.myList[index] != null) {
|
||||
// // return orderRequestController.myList[index].toString();
|
||||
// // }
|
||||
// return defaultValue;
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return GetBuilder<OrderRequestController>(
|
||||
// builder: (controller) {
|
||||
// // --- استخراج البيانات بشكل نظيف ---
|
||||
// final String price =
|
||||
// double.tryParse(_getData(2))?.toStringAsFixed(2) ?? 'N/A';
|
||||
// final bool isComfortTrip = _getData(31) == 'Comfort';
|
||||
// final String carType = _getData(31).tr;
|
||||
|
||||
// final String pickupName = _getData(12);
|
||||
// final String pickupDetails = '(${_getData(11)})';
|
||||
// final String pickupFullAddress = _getData(29);
|
||||
// final String dropoffName = _getData(5);
|
||||
// final String dropoffDetails = '(${_getData(4)})';
|
||||
// final String dropoffFullAddress = _getData(30);
|
||||
|
||||
// final String passengerName = _getData(8);
|
||||
// final String passengerRating = _getData(33);
|
||||
// final bool isVisaPayment = _getData(13) == 'true';
|
||||
// final bool hasSteps = _getData(20) == 'haveSteps';
|
||||
|
||||
// final String mapUrl =
|
||||
// 'https://www.google.com/maps/dir/${_getData(0)}/${_getData(1)}/';
|
||||
// final String rideId = _getData(16);
|
||||
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: Text('Speed Order'.tr),
|
||||
// leading: IconButton(
|
||||
// icon: const Icon(Icons.arrow_back),
|
||||
// onPressed: () => Get.back(),
|
||||
// ),
|
||||
// backgroundColor: AppColor.primaryColor,
|
||||
// elevation: 2.0,
|
||||
// ),
|
||||
// backgroundColor: AppColor.secondaryColor ?? Colors.grey[100],
|
||||
// body: SafeArea(
|
||||
// child: Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// // 1. قسم الخريطة (ارتفاع ثابت)
|
||||
// SizedBox(
|
||||
// height: Get.height * 0.28,
|
||||
// child: ClipRRect(
|
||||
// borderRadius: BorderRadius.circular(15.0),
|
||||
// child: GoogleMap(
|
||||
// initialCameraPosition: CameraPosition(
|
||||
// zoom: 12,
|
||||
// target: Get.find<HomeCaptainController>().myLocation,
|
||||
// ),
|
||||
// cameraTargetBounds:
|
||||
// CameraTargetBounds(controller.bounds),
|
||||
// myLocationButtonEnabled: false,
|
||||
// trafficEnabled: false,
|
||||
// buildingsEnabled: false,
|
||||
// mapToolbarEnabled: false,
|
||||
// myLocationEnabled: true,
|
||||
// markers: {
|
||||
// Marker(
|
||||
// markerId: MarkerId('MyLocation'.tr),
|
||||
// position: LatLng(controller.latPassengerLocation,
|
||||
// controller.lngPassengerLocation),
|
||||
// icon: controller.startIcon),
|
||||
// Marker(
|
||||
// markerId: MarkerId('Destination'.tr),
|
||||
// position: LatLng(
|
||||
// controller.latPassengerDestination,
|
||||
// controller.lngPassengerDestination),
|
||||
// icon: controller.endIcon),
|
||||
// },
|
||||
// polylines: {
|
||||
// Polyline(
|
||||
// zIndex: 1,
|
||||
// consumeTapEvents: true,
|
||||
// geodesic: true,
|
||||
// endCap: Cap.buttCap,
|
||||
// startCap: Cap.buttCap,
|
||||
// visible: true,
|
||||
// polylineId: const PolylineId('routeOrder'),
|
||||
// points: controller.pointsDirection,
|
||||
// color: AppColor.primaryColor,
|
||||
// width: 3,
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// const SizedBox(height: 8),
|
||||
|
||||
// // 2. بطاقة السعر
|
||||
// _buildPriceCard(price, carType, isComfortTrip),
|
||||
|
||||
// const SizedBox(height: 8),
|
||||
|
||||
// // 3. التفاصيل القابلة للتمرير (تأخذ المساحة المتبقية)
|
||||
// Expanded(
|
||||
// child: SingleChildScrollView(
|
||||
// physics: const BouncingScrollPhysics(),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// _buildLocationCard(
|
||||
// icon: Icons.arrow_circle_up,
|
||||
// iconColor: AppColor.greenColor,
|
||||
// title: pickupName,
|
||||
// subtitle: pickupDetails,
|
||||
// fullAddress: pickupFullAddress,
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// _buildLocationCard(
|
||||
// icon: Icons.arrow_circle_down,
|
||||
// iconColor: AppColor.redColor,
|
||||
// title: dropoffName,
|
||||
// subtitle: dropoffDetails,
|
||||
// fullAddress: dropoffFullAddress,
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// _buildInfoCard(isVisaPayment, hasSteps, mapUrl),
|
||||
// const SizedBox(height: 8),
|
||||
// _buildPassengerCard(passengerName, passengerRating),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// // 4. الأزرار والمؤقت (مثبتة في الأسفل)
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(top: 8.0),
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// // زر القبول
|
||||
// Expanded(
|
||||
// child: MyElevatedButton(
|
||||
// kolor: AppColor.greenColor,
|
||||
// title: 'Accept Order'.tr,
|
||||
// onPressed: () => _handleAcceptOrder(controller),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 10),
|
||||
|
||||
// // المؤقت
|
||||
// GetBuilder<OrderRequestController>(
|
||||
// id: 'timerUpdate',
|
||||
// builder: (timerCtrl) {
|
||||
// final isNearEnd = timerCtrl.remainingTimeSpeed <= 5;
|
||||
// return SizedBox(
|
||||
// width: 60,
|
||||
// height: 60,
|
||||
// child: Stack(
|
||||
// alignment: Alignment.center,
|
||||
// children: [
|
||||
// CircularProgressIndicator(
|
||||
// value: timerCtrl.progressSpeed,
|
||||
// color: isNearEnd
|
||||
// ? Colors.redAccent
|
||||
// : AppColor.primaryColor,
|
||||
// strokeWidth: 5,
|
||||
// backgroundColor: Colors.grey.shade300,
|
||||
// ),
|
||||
// Text(
|
||||
// '${timerCtrl.remainingTimeSpeed}',
|
||||
// style: AppStyle.headTitle2.copyWith(
|
||||
// color: isNearEnd
|
||||
// ? Colors.redAccent
|
||||
// : AppColor.writeColor ?? Colors.black,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
|
||||
// const SizedBox(width: 10),
|
||||
// // زر الرفض
|
||||
// Expanded(
|
||||
// child: MyElevatedButton(
|
||||
// title: 'Refuse Order'.tr,
|
||||
// onPressed: () =>
|
||||
// _handleRefuseOrder(controller, rideId),
|
||||
// kolor: AppColor.redColor,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- WIDGET BUILDERS (لبناء الواجهة بشكل نظيف) ---
|
||||
|
||||
// // Widget _buildPriceCard(String price, String carType, bool isComfortTrip) {
|
||||
// // return Card(
|
||||
// // elevation: 3,
|
||||
// // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
// // child: Padding(
|
||||
// // padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||
// // child: Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// // children: [
|
||||
// // Text(
|
||||
// // price,
|
||||
// // style: AppStyle.headTitle.copyWith(
|
||||
// // color: AppColor.primaryColor,
|
||||
// // fontWeight: FontWeight.bold,
|
||||
// // fontSize: 28),
|
||||
// // ),
|
||||
// // Column(
|
||||
// // crossAxisAlignment: CrossAxisAlignment.end,
|
||||
// // children: [
|
||||
// // Text(
|
||||
// // carType,
|
||||
// // style: AppStyle.title.copyWith(
|
||||
// // color: AppColor.greenColor, fontWeight: FontWeight.bold),
|
||||
// // ),
|
||||
// // if (isComfortTrip)
|
||||
// // Row(
|
||||
// // children: [
|
||||
// // const Icon(Icons.ac_unit,
|
||||
// // color: AppColor.blueColor, size: 18),
|
||||
// // const SizedBox(width: 4),
|
||||
// // Text('Air condition Trip'.tr,
|
||||
// // style: AppStyle.subtitle.copyWith(fontSize: 13)),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ),
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// // Widget _buildInfoCard(bool isVisaPayment, bool hasSteps, String mapUrl) {
|
||||
// // return Card(
|
||||
// // elevation: 2,
|
||||
// // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
// // child: Padding(
|
||||
// // padding: const EdgeInsets.all(10.0),
|
||||
// // child: Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// // children: [
|
||||
// // Row(
|
||||
// // children: [
|
||||
// // Icon(
|
||||
// // isVisaPayment ? Icons.credit_card : Icons.payments_outlined,
|
||||
// // color: isVisaPayment
|
||||
// // ? AppColor.deepPurpleAccent
|
||||
// // : AppColor.greenColor,
|
||||
// // size: 24,
|
||||
// // ),
|
||||
// // const SizedBox(width: 8),
|
||||
// // Text(
|
||||
// // isVisaPayment ? 'Visa'.tr : 'Cash'.tr,
|
||||
// // style: AppStyle.title.copyWith(fontWeight: FontWeight.w600),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // if (hasSteps)
|
||||
// // Expanded(
|
||||
// // child: Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.center,
|
||||
// // children: [
|
||||
// // const Icon(Icons.format_list_numbered_rtl_outlined,
|
||||
// // color: AppColor.bronze, size: 24),
|
||||
// // const SizedBox(width: 4),
|
||||
// // Flexible(
|
||||
// // child: Text(
|
||||
// // 'Trip has Steps'.tr,
|
||||
// // style: AppStyle.title
|
||||
// // .copyWith(color: AppColor.bronze, fontSize: 13),
|
||||
// // overflow: TextOverflow.ellipsis,
|
||||
// // )),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ),
|
||||
// // TextButton.icon(
|
||||
// // style: TextButton.styleFrom(
|
||||
// // padding: EdgeInsets.zero,
|
||||
// // tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
// // alignment: Alignment.centerRight,
|
||||
// // ),
|
||||
// // onPressed: () => showInBrowser(mapUrl),
|
||||
// // icon: const Icon(Icons.directions_outlined,
|
||||
// // color: AppColor.blueColor, size: 20),
|
||||
// // label: Text("Directions".tr,
|
||||
// // style: AppStyle.subtitle
|
||||
// // .copyWith(color: AppColor.blueColor, fontSize: 13)),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ),
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// // Widget _buildPassengerCard(String name, String rating) {
|
||||
// // return Card(
|
||||
// // elevation: 2,
|
||||
// // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
// // child: Padding(
|
||||
// // padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
// // child: Row(
|
||||
// // children: [
|
||||
// // const Icon(Icons.person_outline,
|
||||
// // color: AppColor.greyColor, size: 22),
|
||||
// // const SizedBox(width: 10),
|
||||
// // Expanded(
|
||||
// // child: Text(
|
||||
// // name,
|
||||
// // style: AppStyle.title,
|
||||
// // overflow: TextOverflow.ellipsis,
|
||||
// // ),
|
||||
// // ),
|
||||
// // const SizedBox(width: 10),
|
||||
// // const Icon(Icons.star_rounded, color: Colors.amber, size: 20),
|
||||
// // Text(
|
||||
// // rating,
|
||||
// // style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ),
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// // Widget _buildLocationCard(
|
||||
// {required IconData icon,
|
||||
// required Color iconColor,
|
||||
// required String title,
|
||||
// required String subtitle,
|
||||
// required String fullAddress}) {
|
||||
// return Card(
|
||||
// elevation: 2,
|
||||
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
// margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Icon(icon, color: iconColor, size: 28),
|
||||
// const SizedBox(width: 12),
|
||||
// Expanded(
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(
|
||||
// "$title $subtitle".trim(),
|
||||
// style: AppStyle.title.copyWith(fontWeight: FontWeight.w600),
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// if (fullAddress.isNotEmpty) ...[
|
||||
// const SizedBox(height: 3),
|
||||
// Text(
|
||||
// fullAddress,
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(fontSize: 13, color: AppColor.greyColor),
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// ]
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// // // --- منطق التعامل مع الطلبات (Logic Handlers) ---
|
||||
|
||||
// // Future<void> _handleAcceptOrder(OrderRequestController controller) async {
|
||||
// // // 1. محاولة تحديث الحالة في السيرفر
|
||||
// // // 1. إظهار لودينج وإيقاف التفاعل
|
||||
// // Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
// // barrierDismissible: false);
|
||||
// // // هذا الرابط يجب أن يكون لملف PHP الآمن الذي يحتوي على rowCount
|
||||
// // var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: {
|
||||
// // 'id': (controller.myList[16]),
|
||||
// // 'rideTimeStart': DateTime.now().toString(),
|
||||
// // 'status': 'Apply',
|
||||
// // 'driver_id': box.read(BoxName.driverID),
|
||||
// // });
|
||||
// // Log.print('oreder response update: ${res}');
|
||||
// // // 2. إغلاق اللودينج بمجرد وصول الرد
|
||||
// // Get.back(); // إغلاق اللودينج
|
||||
// // // 2. معالجة الفشل (Robust Error Handling)
|
||||
// // // نفحص إذا كانت النتيجة فشل سواء وصلت كنص أو كـ JSON
|
||||
// // bool isFailed = false;
|
||||
// // if (res == 'failure') isFailed = true;
|
||||
|
||||
// // if (res is Map && res['status'] == 'failure') isFailed = true;
|
||||
|
||||
// // if (isFailed) {
|
||||
// // MyDialog().getDialog(
|
||||
// // "This ride is already applied by another driver.".tr, '', () {
|
||||
// // Get.back(); // يغلق نافذة التنبيه (Dialog)
|
||||
// // Get.back(); // يغلق صفحة الطلب بالكامل (Screen) ويرجع للخريطة
|
||||
// // });
|
||||
// // return; // توقف تام للكود هنا، لن يتم تنفيذ أي سطر بالأسفل
|
||||
// // }
|
||||
|
||||
// // // 3. معالجة النجاح (Success Handling)
|
||||
|
||||
// // // إيقاف المؤقت وتحديث الواجهة
|
||||
// // controller.endTimer();
|
||||
// // controller.changeApplied();
|
||||
|
||||
// // // تحديث حالة السائق في التطبيق
|
||||
// // Get.put(HomeCaptainController()).changeRideId();
|
||||
// // box.write(BoxName.statusDriverLocation, 'on');
|
||||
|
||||
// // // *هام*: تم حذف استدعاء الـ API الثاني المكرر هنا لأنه غير ضروري وقد يسبب مشاكل
|
||||
|
||||
// // // تسجيل الطلب في سجل السائقين (Driver History)
|
||||
// // CRUD().postFromDialogue(link: AppLink.addDriverOrder, payload: {
|
||||
// // 'driver_id': (controller.myList[6].toString()),
|
||||
// // 'order_id': (controller.myList[16].toString()),
|
||||
// // 'status': 'Apply'
|
||||
// // });
|
||||
|
||||
// // // إرسال إشعار للراكب
|
||||
// // List<String> bodyToPassenger = [
|
||||
// // controller.myList[6].toString(),
|
||||
// // controller.myList[8].toString(),
|
||||
// // controller.myList[9].toString(),
|
||||
// // ];
|
||||
|
||||
// // NotificationService.sendNotification(
|
||||
// // target: controller.myList[9].toString(),
|
||||
// // title: "Accepted Ride".tr,
|
||||
// // body: 'your ride is Accepted'.tr,
|
||||
// // isTopic: false,
|
||||
// // tone: 'start',
|
||||
// // driverList: bodyToPassenger,
|
||||
// // category: 'Accepted Ride',
|
||||
// // );
|
||||
|
||||
// // // حفظ البيانات في الصندوق (Box) للانتقال للصفحة التالية
|
||||
// // box.write(BoxName.rideArguments, {
|
||||
// // 'passengerLocation': controller.myList[0].toString(),
|
||||
// // 'passengerDestination': controller.myList[1].toString(),
|
||||
// // 'Duration': controller.myList[4].toString(),
|
||||
// // 'totalCost': controller.myList[26].toString(),
|
||||
// // 'Distance': controller.myList[5].toString(),
|
||||
// // 'name': controller.myList[8].toString(),
|
||||
// // 'phone': controller.myList[10].toString(),
|
||||
// // 'email': controller.myList[28].toString(),
|
||||
// // 'WalletChecked': controller.myList[13].toString(),
|
||||
// // 'tokenPassenger': controller.myList[9].toString(),
|
||||
// // 'direction':
|
||||
// // 'https://www.google.com/maps/dir/${controller.myList[0]}/${controller.myList[1]}/',
|
||||
// // 'DurationToPassenger': controller.myList[15].toString(),
|
||||
// // 'rideId': (controller.myList[16].toString()),
|
||||
// // 'passengerId': (controller.myList[7].toString()),
|
||||
// // 'driverId': (controller.myList[18].toString()),
|
||||
// // 'durationOfRideValue': controller.myList[19].toString(),
|
||||
// // 'paymentAmount': controller.myList[2].toString(),
|
||||
// // 'paymentMethod':
|
||||
// // controller.myList[13].toString() == 'true' ? 'visa' : 'cash',
|
||||
// // 'isHaveSteps': controller.myList[20].toString(),
|
||||
// // 'step0': controller.myList[21].toString(),
|
||||
// // 'step1': controller.myList[22].toString(),
|
||||
// // 'step2': controller.myList[23].toString(),
|
||||
// // 'step3': controller.myList[24].toString(),
|
||||
// // 'step4': controller.myList[25].toString(),
|
||||
// // 'passengerWalletBurc': controller.myList[26].toString(),
|
||||
// // 'timeOfOrder': DateTime.now().toString(),
|
||||
// // 'totalPassenger': controller.myList[2].toString(),
|
||||
// // 'carType': controller.myList[31].toString(),
|
||||
// // 'kazan': controller.myList[32].toString(),
|
||||
// // 'startNameLocation': controller.myList[29].toString(),
|
||||
// // 'endNameLocation': controller.myList[30].toString(),
|
||||
// // });
|
||||
|
||||
// // // الانتقال لصفحة تتبع الراكب
|
||||
// // Get.back(); // يغلق صفحة الطلب الحالية
|
||||
// // Get.to(() => PassengerLocationMapPage(),
|
||||
// // arguments: box.read(BoxName.rideArguments));
|
||||
// // Log.print(
|
||||
// // 'box.read(BoxName.rideArguments): ${box.read(BoxName.rideArguments)}');
|
||||
// // }
|
||||
|
||||
// // void _handleRefuseOrder(OrderRequestController controller, String rideId) {
|
||||
// controller.endTimer();
|
||||
// // controller.refuseOrder(rideId);
|
||||
|
||||
// // تسجيل الرفض في الإشعارات المحلية للسائق
|
||||
// controller.addRideToNotificationDriverString(
|
||||
// rideId,
|
||||
// _getData(29),
|
||||
// _getData(30),
|
||||
// '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}',
|
||||
// '${DateTime.now().hour}:${DateTime.now().minute}',
|
||||
// _getData(2),
|
||||
// _getData(7),
|
||||
// 'wait',
|
||||
// _getData(31),
|
||||
// _getData(33),
|
||||
// _getData(2),
|
||||
// _getData(5),
|
||||
// _getData(4));
|
||||
|
||||
// // الخروج من الصفحة بعد الرفض
|
||||
// Get.back();
|
||||
// }
|
||||
// // }
|
||||
228
siro_driver/lib/views/home/Captin/orderCaptin/test_order_page.dart
Executable file
228
siro_driver/lib/views/home/Captin/orderCaptin/test_order_page.dart
Executable file
@@ -0,0 +1,228 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||
|
||||
class OrderRequestPageTest extends StatefulWidget {
|
||||
const OrderRequestPageTest({super.key});
|
||||
|
||||
@override
|
||||
State<OrderRequestPageTest> createState() => _OrderRequestPageTestState();
|
||||
}
|
||||
|
||||
class _OrderRequestPageTestState extends State<OrderRequestPageTest> {
|
||||
late OrderRequestController _orderRequestController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize the controller and process arguments
|
||||
_initializeController();
|
||||
}
|
||||
|
||||
void _initializeController() {
|
||||
// Get the controller or create a new one if not exists
|
||||
_orderRequestController = Get.put(OrderRequestController());
|
||||
|
||||
// Process arguments passed to the page
|
||||
final arguments = Get.arguments;
|
||||
final myListString = arguments['myListString'];
|
||||
var myList =
|
||||
arguments['DriverList'] == null || arguments['DriverList'].isEmpty
|
||||
? jsonDecode(myListString)
|
||||
: arguments['DriverList'];
|
||||
|
||||
// Parse coordinates and prepare map data
|
||||
_orderRequestController.parseCoordinates(myList);
|
||||
|
||||
// Start timer and calculate fuel consumption
|
||||
_orderRequestController.startTimer(
|
||||
myList[6].toString(),
|
||||
myList[16].toString(),
|
||||
);
|
||||
_orderRequestController.calculateConsumptionFuel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: Container(
|
||||
color: const Color.fromARGB(255, 241, 238, 238),
|
||||
child: ListView(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: Get.height * .33,
|
||||
child: Obx(() => IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: CameraPosition(
|
||||
zoom: 12,
|
||||
target:
|
||||
Get.find<HomeCaptainController>().myLocation,
|
||||
),
|
||||
cameraTargetBounds: CameraTargetBounds(
|
||||
_orderRequestController.mapBounds.value),
|
||||
myLocationButtonEnabled: true,
|
||||
// trafficEnabled: false,
|
||||
// buildingsEnabled: false,
|
||||
// mapToolbarEnabled: true,
|
||||
myLocationEnabled: true,
|
||||
markers: _orderRequestController.markers.value,
|
||||
polylines: _orderRequestController.polylines.value,
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
_orderRequestController.mapController.value =
|
||||
controller;
|
||||
},
|
||||
onCameraMove: (CameraPosition position) {
|
||||
_orderRequestController
|
||||
.updateCameraPosition(position);
|
||||
},
|
||||
)),
|
||||
),
|
||||
// Rest of your UI components
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OrderRequestController extends GetxController {
|
||||
// Reactive variables for map-related data
|
||||
Rx<LatLngBounds> mapBounds = Rx<LatLngBounds>(LatLngBounds(
|
||||
southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)));
|
||||
|
||||
Rx<Set<Marker>> markers = Rx<Set<Marker>>({});
|
||||
Rx<Set<Polyline>> polylines = Rx<Set<Polyline>>({});
|
||||
|
||||
Rx<IntaleqMapController?> mapController = Rx<IntaleqMapController?>(null);
|
||||
|
||||
// Icons for start and end markers
|
||||
late InlqBitmap startIcon;
|
||||
late InlqBitmap endIcon;
|
||||
|
||||
// Coordinates for passenger location and destination
|
||||
Rx<LatLng?> passengerLocation = Rx<LatLng?>(null);
|
||||
Rx<LatLng?> passengerDestination = Rx<LatLng?>(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Initialize marker icons
|
||||
_initializeMarkerIcons();
|
||||
}
|
||||
|
||||
void _initializeMarkerIcons() async {
|
||||
// Load custom marker icons
|
||||
startIcon = InlqBitmap.fromAsset('assets/start_marker.png');
|
||||
endIcon = InlqBitmap.fromAsset('assets/end_marker.png');
|
||||
}
|
||||
|
||||
void parseCoordinates(List myList) {
|
||||
// Parse coordinates from the input list
|
||||
var cords = myList[0].split(',');
|
||||
var cordDestination = myList[1].split(',');
|
||||
|
||||
double latPassengerLocation = double.parse(cords[0]);
|
||||
double lngPassengerLocation = double.parse(cords[1]);
|
||||
double latPassengerDestination = double.parse(cordDestination[0]);
|
||||
double lngPassengerDestination = double.parse(cordDestination[1]);
|
||||
|
||||
// Update passenger location and destination
|
||||
passengerLocation.value =
|
||||
LatLng(latPassengerLocation, lngPassengerLocation);
|
||||
passengerDestination.value =
|
||||
LatLng(latPassengerDestination, lngPassengerDestination);
|
||||
|
||||
// Create markers
|
||||
_createMarkers();
|
||||
|
||||
// Create polyline
|
||||
_createPolyline();
|
||||
|
||||
// Calculate map bounds
|
||||
_calculateMapBounds();
|
||||
}
|
||||
|
||||
void _createMarkers() {
|
||||
if (passengerLocation.value == null || passengerDestination.value == null)
|
||||
return;
|
||||
|
||||
markers.value = {
|
||||
Marker(
|
||||
markerId: MarkerId('MyLocation'.tr),
|
||||
position: passengerLocation.value!,
|
||||
draggable: true,
|
||||
icon: startIcon,
|
||||
),
|
||||
Marker(
|
||||
markerId: MarkerId('Destination'.tr),
|
||||
position: passengerDestination.value!,
|
||||
draggable: true,
|
||||
icon: endIcon,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
void _createPolyline() {
|
||||
if (passengerLocation.value == null || passengerDestination.value == null)
|
||||
return;
|
||||
|
||||
polylines.value = {
|
||||
Polyline(
|
||||
zIndex: 1,
|
||||
// consumeTapEvents: true,
|
||||
geodesic: true,
|
||||
// endCap: Cap.buttCap,
|
||||
// startCap: Cap.buttCap,
|
||||
visible: true,
|
||||
polylineId: const PolylineId('routeOrder'),
|
||||
points: [passengerLocation.value!, passengerDestination.value!],
|
||||
color: AppColor.primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
void _calculateMapBounds() {
|
||||
if (passengerLocation.value == null || passengerDestination.value == null)
|
||||
return;
|
||||
|
||||
double minLatitude = math.min(passengerLocation.value!.latitude,
|
||||
passengerDestination.value!.latitude);
|
||||
double maxLatitude = math.max(passengerLocation.value!.latitude,
|
||||
passengerDestination.value!.latitude);
|
||||
double minLongitude = math.min(passengerLocation.value!.longitude,
|
||||
passengerDestination.value!.longitude);
|
||||
double maxLongitude = math.max(passengerLocation.value!.longitude,
|
||||
passengerDestination.value!.longitude);
|
||||
|
||||
mapBounds.value = LatLngBounds(
|
||||
southwest: LatLng(minLatitude, minLongitude),
|
||||
northeast: LatLng(maxLatitude, maxLongitude),
|
||||
);
|
||||
}
|
||||
|
||||
void updateCameraPosition(CameraPosition position) {
|
||||
// Implement any specific logic for camera position updates
|
||||
}
|
||||
|
||||
void startTimer(String param1, String param2) {
|
||||
// Implement timer start logic
|
||||
}
|
||||
|
||||
void calculateConsumptionFuel() {
|
||||
// Implement fuel consumption calculation
|
||||
}
|
||||
}
|
||||
237
siro_driver/lib/views/home/Captin/orderCaptin/vip_order_page.dart
Executable file
237
siro_driver/lib/views/home/Captin/orderCaptin/vip_order_page.dart
Executable file
@@ -0,0 +1,237 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../main.dart';
|
||||
import '../../../../print.dart';
|
||||
|
||||
class VipOrderPage extends StatelessWidget {
|
||||
const VipOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(VipOrderController());
|
||||
return MyScafolld(
|
||||
title: 'VIP Order'.tr,
|
||||
body: [
|
||||
GetBuilder<VipOrderController>(builder: (vipOrderController) {
|
||||
if (vipOrderController.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (vipOrderController.tripData.isEmpty) {
|
||||
return Center(
|
||||
child: Text('No orders available'.tr),
|
||||
);
|
||||
}
|
||||
final order = vipOrderController.tripData[0];
|
||||
Log.print('order: ${order}');
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Passenger Section
|
||||
_buildSectionTitle('Passenger Information'.tr),
|
||||
_buildInfoCard(
|
||||
children: [
|
||||
_buildDetailRow('Name'.tr,
|
||||
'${order['passengerName'] ?? 'Unknown'} ${order['passengerLastName'] ?? ''}'),
|
||||
_buildDetailRow(
|
||||
'Phone'.tr, order['passengerPhone'] ?? 'Unknown'),
|
||||
_buildDetailRow(
|
||||
'Gender'.tr, order['passengergender'] ?? 'Unknown'),
|
||||
_buildDetailRow('time Selected'.tr,
|
||||
order['timeSelected'] ?? 'Unknown'),
|
||||
_buildDetailRow(
|
||||
'Ride Status'.tr, order['status'] ?? 'Unknown'),
|
||||
// _buildDetailRow('Ride Status'.tr,
|
||||
// vipOrderController.myList[4] ?? 'Unknown'),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// print(vipOrderController.myList);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action Buttons
|
||||
_buildActionButtons(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard({required List<Widget> children}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(color: Colors.black54),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _onReject(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Reject'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _onApply(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Apply'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onReject(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Ride Rejected'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onApply(BuildContext context) {
|
||||
// TODO: Implement application logic
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Ride Applied'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class VipOrderController extends GetxController {
|
||||
bool isLoading = false;
|
||||
List tripData = [];
|
||||
|
||||
fetchOrder() async {
|
||||
isLoading = true; // Set loading state
|
||||
update(); // Notify listeners
|
||||
var res = await CRUD().get(link: AppLink.getMishwariDriver, payload: {
|
||||
'driverId': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
tripData = jsonDecode(res)['message'];
|
||||
} else {
|
||||
tripData = [];
|
||||
}
|
||||
isLoading = false; // Loading complete
|
||||
update(); // Notify listeners
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
fetchOrder();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
46
siro_driver/lib/views/home/Captin/passportimage.dart
Executable file
46
siro_driver/lib/views/home/Captin/passportimage.dart
Executable file
@@ -0,0 +1,46 @@
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import '../../../controller/functions/ocr_controller.dart';
|
||||
//
|
||||
// class PassportDataExtractorWidget extends StatelessWidget {
|
||||
// final PassportDataExtractor passportDataExtractor =
|
||||
// Get.put(PassportDataExtractor());
|
||||
// final PassportDataController controller = Get.put(PassportDataController());
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Passport Data Extractor'),
|
||||
// ),
|
||||
// body: Column(
|
||||
// children: [
|
||||
// ElevatedButton(
|
||||
// onPressed: controller.extractDataAndDrawBoundingBoxes,
|
||||
// child: const Text('Extract Data'),
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Center(
|
||||
// child: Stack(
|
||||
// children: [
|
||||
// GetBuilder<PassportDataController>(
|
||||
// builder: (controller) => CustomPaint(
|
||||
// painter: BoundingBoxPainter(
|
||||
// controller.extractedTextWithCoordinates),
|
||||
// child: GetBuilder<PassportDataExtractor>(
|
||||
// builder: (controller) =>
|
||||
// Image.file(File(passportDataExtractor.image!.path)),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
175
siro_driver/lib/views/home/Captin/text_scanner.dart
Executable file
175
siro_driver/lib/views/home/Captin/text_scanner.dart
Executable file
@@ -0,0 +1,175 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../controller/functions/ocr_controller.dart';
|
||||
|
||||
class TextRecognizerAPI extends StatelessWidget {
|
||||
const TextRecognizerAPI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(ScanDocumentsByApi());
|
||||
return GetBuilder<ScanDocumentsByApi>(
|
||||
builder: (controller) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Api'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.matchFaceApi();
|
||||
},
|
||||
icon: const Icon(Icons.face),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: controller.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.memory(
|
||||
controller.imagePortrait,
|
||||
width: 60,
|
||||
),
|
||||
Image.memory(
|
||||
controller.imageSignature,
|
||||
width: 60,
|
||||
),
|
||||
Image.memory(
|
||||
controller.imageDocumentFrontSide,
|
||||
width: 250,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(controller.responseMap['authenticity_meta']
|
||||
.toString()),
|
||||
Text(
|
||||
'countryName: ${controller.responseMap['data']['countryName'].toString()}'),
|
||||
Text(
|
||||
'Expiry Date: ${controller.responseMap['data']['ocr']['dateOfExpiry'].toString()}'),
|
||||
// Add more Text widgets to display other record values
|
||||
Text(
|
||||
'Address: ${controller.responseMap['data']['ocr']['address'].toString()}'),
|
||||
Text(
|
||||
'City: ${controller.responseMap['data']['ocr']['addressCity'].toString()}'),
|
||||
Text(
|
||||
'Jurisdiction Code: ${controller.responseMap['data']['ocr']['addressJurisdictionCode'].toString()}'),
|
||||
Text(
|
||||
'Postal Code: ${controller.responseMap['data']['ocr']['addressPostalCode'].toString()}'),
|
||||
Text(
|
||||
'Street: ${controller.responseMap['data']['ocr']['addressStreet'].toString()}'),
|
||||
Text(
|
||||
'Date of Birth: ${controller.responseMap['data']['ocr']['dateOfBirth'].toString()}'),
|
||||
Text(
|
||||
'Date of Issue: ${controller.responseMap['data']['ocr']['dateOfIssue'].toString()}'),
|
||||
Text(
|
||||
'Drivers License Class: ${controller.responseMap['data']['ocr']['dlClass'].toString()}'),
|
||||
Text(
|
||||
'Document Number: ${controller.responseMap['data']['ocr']['documentNumber'].toString()}'),
|
||||
Text(
|
||||
'Eye Color: ${controller.responseMap['data']['ocr']['eyesColor'].toString()}'),
|
||||
Text(
|
||||
'Given Names: ${controller.responseMap['data']['ocr']['givenNames'].toString()}'),
|
||||
Text(
|
||||
'Height: ${controller.responseMap['data']['ocr']['height'].toString()}'),
|
||||
Text(
|
||||
'Issuing State Code: ${controller.responseMap['data']['ocr']['issuingStateCode'].toString()}'),
|
||||
Text(
|
||||
'Name: ${controller.responseMap['data']['ocr']['name'].toString()}'),
|
||||
Text(
|
||||
'Sex: ${controller.responseMap['data']['ocr']['sex'].toString()}'),
|
||||
Text(
|
||||
'Surname: ${controller.responseMap['data']['ocr']['surname'].toString()}'),
|
||||
Text(
|
||||
'Valid State: ${controller.responseMap['data']['ocr']['validState'].toString()}'),
|
||||
],
|
||||
),
|
||||
))));
|
||||
}
|
||||
}
|
||||
// class TextExtractionView extends StatelessWidget {
|
||||
// TextExtractionView({super.key});
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// Get.put(TextExtractionController());
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Text Extraction'),
|
||||
// ),
|
||||
// body: GetBuilder<TextExtractionController>(builder: (controller) {
|
||||
// return Center(
|
||||
// child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// ElevatedButton(
|
||||
// onPressed: () {},
|
||||
// child: const Text('Pick Image and Extract Text'),
|
||||
// ),
|
||||
// const SizedBox(height: 20),
|
||||
// controller.isloading
|
||||
// ? const MyCircularProgressIndicator()
|
||||
// : Text(controller.extractedText),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class TextRecognizerWidget extends StatelessWidget {
|
||||
// const TextRecognizerWidget({super.key});
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// Get.put(TextMLGoogleRecognizerController());
|
||||
// return GetBuilder<TextMLGoogleRecognizerController>(
|
||||
// builder: (controller) => Scaffold(
|
||||
// appBar: AppBar(),
|
||||
// body: Center(
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text('${controller.decode['DRIVER_LICENSE'].toString()}'),
|
||||
// Text('DL: ${controller.decode['dl_number'].toString()}'),
|
||||
// Text(
|
||||
// 'Expiry Date: ${controller.decode['expiry_date'].toString()}'),
|
||||
// Text('Last Name: ${controller.decode['lastName'].toString()}'),
|
||||
// Text(
|
||||
// 'First Name: ${controller.decode['firstName'].toString()}'),
|
||||
// Text('Address: ${controller.decode['address'].toString()}'),
|
||||
// Text('Date of Birth: ${controller.decode['dob'].toString()}'),
|
||||
// Text('RSTR: ${controller.decode['rstr'].toString()}'),
|
||||
// Text('Class: ${controller.decode['class'].toString()}'),
|
||||
// Text('End: ${controller.decode['end'].toString()}'),
|
||||
// Text('DD: ${controller.decode['dd'].toString()}'),
|
||||
// Text('Sex: ${controller.decode['sex'].toString()}'),
|
||||
// Text('Hair: ${controller.decode['hair'].toString()}'),
|
||||
// Text('Eyes: ${controller.decode['eyes'].toString()}'),
|
||||
// // and so on for other fields
|
||||
// ],
|
||||
// ))));
|
||||
// }
|
||||
// }
|
||||
|
||||
// class PassportPage extends StatelessWidget {
|
||||
// PassportPage({super.key});
|
||||
// PassportRecognizerController passportRecognizerController =
|
||||
// Get.put(PassportRecognizerController());
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Driver License'),
|
||||
// ),
|
||||
// body: const Center(child: Text('data')));
|
||||
// }
|
||||
// }
|
||||
190
siro_driver/lib/views/home/journal/schedule_page.dart
Normal file
190
siro_driver/lib/views/home/journal/schedule_page.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../controller/home/journal/schedule_controller.dart';
|
||||
|
||||
class SchedulePage extends StatelessWidget {
|
||||
SchedulePage({super.key});
|
||||
final ScheduleController controller = Get.put(ScheduleController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('My Schedule'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back()),
|
||||
),
|
||||
body: GetBuilder<ScheduleController>(builder: (sc) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// Summary Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Weekly Plan'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Text('${sc.totalWeeklyHours.toStringAsFixed(1)}h',
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white)),
|
||||
Text('${sc.activeDays} ${'Days'.tr}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
fontSize: 13)),
|
||||
])),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
child: const Icon(Icons.calendar_today_rounded,
|
||||
color: Colors.white, size: 28),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Text('Work Days'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
...sc.schedule.map((slot) => _buildDayCard(context, slot, sc)),
|
||||
]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDayCard(
|
||||
BuildContext context, WorkSlot slot, ScheduleController sc) {
|
||||
final isAr = Get.locale?.languageCode == 'ar';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: slot.isActive ? Colors.white : Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: slot.isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3))
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(children: [
|
||||
// Toggle
|
||||
GestureDetector(
|
||||
onTap: () => sc.toggleDay(slot.dayOfWeek),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: slot.isActive
|
||||
? FinanceDesignSystem.accentBlue.withValues(alpha: 0.1)
|
||||
: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
isAr ? slot.dayNameAr.substring(0, 2) : slot.dayName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: slot.isActive
|
||||
? FinanceDesignSystem.accentBlue
|
||||
: Colors.grey.shade400),
|
||||
)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// Day name
|
||||
Expanded(
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(isAr ? slot.dayNameAr : slot.dayName.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: slot.isActive
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: Colors.grey.shade400)),
|
||||
if (slot.isActive)
|
||||
Text(slot.timeRange,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
|
||||
if (!slot.isActive)
|
||||
Text('Day Off'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade400,
|
||||
fontStyle: FontStyle.italic)),
|
||||
])),
|
||||
// Time pickers
|
||||
if (slot.isActive) ...[
|
||||
_timePicker(context, slot.startTime,
|
||||
(t) => sc.updateStartTime(slot.dayOfWeek, t)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text('-', style: TextStyle(color: Colors.grey.shade400))),
|
||||
_timePicker(context, slot.endTime,
|
||||
(t) => sc.updateEndTime(slot.dayOfWeek, t)),
|
||||
],
|
||||
// Toggle switch
|
||||
Switch(
|
||||
value: slot.isActive,
|
||||
onChanged: (_) => sc.toggleDay(slot.dayOfWeek),
|
||||
activeThumbColor: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timePicker(
|
||||
BuildContext context, TimeOfDay time, Function(TimeOfDay) onChanged) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final picked =
|
||||
await showTimePicker(context: context, initialTime: time);
|
||||
if (picked != null) onChanged(picked);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(
|
||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
140
siro_driver/lib/views/home/my_wallet/bank_account_egypt.dart
Executable file
140
siro_driver/lib/views/home/my_wallet/bank_account_egypt.dart
Executable file
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class BankController extends GetxController {
|
||||
String selectedBank = '';
|
||||
|
||||
Map<String, String> bankNames = {
|
||||
'Ahli United Bank'.tr: 'AUB',
|
||||
'Citi Bank N.A. Egypt'.tr: 'CITI',
|
||||
'MIDBANK'.tr: 'MIDB',
|
||||
'Banque Du Caire'.tr: 'BDC',
|
||||
'HSBC Bank Egypt S.A.E'.tr: 'HSBC',
|
||||
'Credit Agricole Egypt S.A.E'.tr: 'ECAE',
|
||||
'Egyptian Gulf Bank'.tr: 'EGB',
|
||||
'The United Bank'.tr: 'UB',
|
||||
'Qatar National Bank Alahli'.tr: 'QNB',
|
||||
'Arab Bank PLC'.tr: 'ARAB',
|
||||
'Emirates National Bank of Dubai'.tr: 'ENBD',
|
||||
'Al Ahli Bank of Kuwait – Egypt'.tr: 'ABK',
|
||||
'National Bank of Kuwait – Egypt'.tr: 'NBK',
|
||||
'Arab Banking Corporation - Egypt S.A.E'.tr: 'EABC',
|
||||
'First Abu Dhabi Bank'.tr: 'FAB',
|
||||
'Abu Dhabi Islamic Bank – Egypt'.tr: 'ADIB',
|
||||
'Commercial International Bank - Egypt S.A.E'.tr: 'CIB',
|
||||
'Housing And Development Bank'.tr: 'HDB',
|
||||
'Banque Misr'.tr: 'MISR',
|
||||
'Arab African International Bank'.tr: 'AAIB',
|
||||
'Egyptian Arab Land Bank'.tr: 'EALB',
|
||||
'Export Development Bank of Egypt'.tr: 'EDBE',
|
||||
'Faisal Islamic Bank of Egypt'.tr: 'FAIB',
|
||||
'Blom Bank'.tr: 'BLOM',
|
||||
'Abu Dhabi Commercial Bank – Egypt'.tr: 'ADCB',
|
||||
'Alex Bank Egypt'.tr: 'BOA',
|
||||
'Societe Arabe Internationale De Banque'.tr: 'SAIB',
|
||||
'National Bank of Egypt'.tr: 'NBE',
|
||||
'Al Baraka Bank Egypt B.S.C.'.tr: 'ABRK',
|
||||
'Egypt Post'.tr: 'POST',
|
||||
'Nasser Social Bank'.tr: 'NSB',
|
||||
'Industrial Development Bank'.tr: 'IDB',
|
||||
'Suez Canal Bank'.tr: 'SCB',
|
||||
'Mashreq Bank'.tr: 'MASHA',
|
||||
'Arab Investment Bank'.tr: 'AIB',
|
||||
'General Authority For Supply Commodities'.tr: 'GASCA',
|
||||
'Arab International Bank'.tr: 'AIB',
|
||||
'Agricultural Bank of Egypt'.tr: 'PDAC',
|
||||
'National Bank of Greece'.tr: 'NBG',
|
||||
'Central Bank Of Egypt'.tr: 'CBE',
|
||||
'ATTIJARIWAFA BANK Egypt'.tr: 'BBE',
|
||||
};
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
selectedBank = bankNames.values.first;
|
||||
}
|
||||
|
||||
void updateSelectedBank(String? bankShortName) {
|
||||
selectedBank = bankShortName ?? '';
|
||||
update();
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<String>> getDropdownItems() {
|
||||
return bankNames.keys.map<DropdownMenuItem<String>>((bankFullName) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: bankNames[bankFullName],
|
||||
child: Text(bankFullName),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void showBankPicker(BuildContext context) {
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoActionSheet(
|
||||
title: Text('Select a Bank'.tr),
|
||||
actions: bankNames.keys.map((String bankFullName) {
|
||||
return CupertinoActionSheetAction(
|
||||
child: Text(bankFullName),
|
||||
onPressed: () {
|
||||
updateSelectedBank(bankNames[bankFullName]);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: Text('Cancel'.tr),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BankDropdown extends StatelessWidget {
|
||||
final BankController bankController = Get.put(BankController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<BankController>(
|
||||
init: bankController,
|
||||
builder: (controller) {
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => controller.showBankPicker(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: CupertinoColors.systemGrey4),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
controller.selectedBank != null
|
||||
? controller.bankNames.keys.firstWhere(
|
||||
(key) =>
|
||||
controller.bankNames[key] ==
|
||||
controller.selectedBank,
|
||||
orElse: () => 'Select a Bank'.tr,
|
||||
)
|
||||
: 'Select a Bank'.tr,
|
||||
style: TextStyle(
|
||||
color: controller.selectedBank != null
|
||||
? CupertinoColors.black
|
||||
: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
const Icon(CupertinoIcons.chevron_down, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
313
siro_driver/lib/views/home/my_wallet/card_wallet_widget.dart
Executable file
313
siro_driver/lib/views/home/my_wallet/card_wallet_widget.dart
Executable file
@@ -0,0 +1,313 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/home/my_wallet/pay_out_screen.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/payment/captain_wallet_controller.dart';
|
||||
import '../../../controller/home/payment/paymob_payout.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
|
||||
// تذكير: ستحتاج إلى إضافة حزمة flutter_svg إلى ملف pubspec.yaml
|
||||
// dependencies:
|
||||
// flutter_svg: ^2.0.7
|
||||
|
||||
/// بطاقة المحفظة بتصميم سوري فاخر مستوحى من فن الأرابيسك والفسيفساء
|
||||
class CardSeferWalletDriver extends StatelessWidget {
|
||||
const CardSeferWalletDriver({super.key});
|
||||
|
||||
// SVG لنقشة أرابيسك هندسية لاستخدامها كخلفية
|
||||
final String arabesquePattern = '''
|
||||
<svg width="100%" height="100%" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="arabesque" patternUnits="userSpaceOnUse" width="50" height="50" patternTransform="scale(1.2)">
|
||||
<g fill="#E7C582" fill-opacity="0.1">
|
||||
<path d="M25 0 L35.35 9.65 L50 25 L35.35 40.35 L25 50 L14.65 40.35 L0 25 L14.65 9.65 Z"/>
|
||||
<path d="M50 0 L60.35 9.65 L75 25 L60.35 40.35 L50 50 L39.65 40.35 L25 25 L39.65 9.65 Z"/>
|
||||
<path d="M0 50 L9.65 39.65 L25 25 L9.65 10.35 L0 0 L-9.65 10.35 L-25 25 L-9.65 39.65 Z"/>
|
||||
</g>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#arabesque)"/>
|
||||
</svg>
|
||||
''';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: GetBuilder<CaptainWalletController>(
|
||||
builder: (captainWalletController) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showCashOutDialog(context, captainWalletController),
|
||||
child: Container(
|
||||
width: Get.width * 0.9,
|
||||
height: Get.height * 0.25,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF003C43).withOpacity(0.5),
|
||||
blurRadius: 25,
|
||||
spreadRadius: -5,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Stack(
|
||||
children: [
|
||||
// الخلفية الرئيسية
|
||||
Container(color: const Color(0xFF003C43)),
|
||||
// طبقة النقشة
|
||||
SvgPicture.string(arabesquePattern, fit: BoxFit.cover),
|
||||
// طبقة التأثير الزجاجي (Glassmorphism)
|
||||
BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
|
||||
child: Container(color: Colors.black.withOpacity(0.1)),
|
||||
),
|
||||
// محتوى البطاقة
|
||||
_buildCardContent(captainWalletController),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardContent(CaptainWalletController captainWalletController) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'رصيد انطلق',
|
||||
style: AppStyle.headTitle.copyWith(
|
||||
fontFamily: 'Amiri', // خط يوحي بالفخامة
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
// أيقونة شريحة البطاقة
|
||||
const Icon(Icons.sim_card_outlined,
|
||||
color: Color(0xFFE7C582), size: 30),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'الرصيد الحالي'.tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// استخدام AnimatedSwitcher لإضافة حركة عند تحديث الرصيد
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'${captainWalletController.totalAmountVisa} ${'ل.س'.tr}',
|
||||
key:
|
||||
ValueKey<String>(captainWalletController.totalAmountVisa),
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: const Color(0xFFE7C582), // Antique Gold
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
box.read(BoxName.nameDriver).toString().split(' ')[0],
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"سحب الرصيد".tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCashOutDialog(
|
||||
BuildContext context, CaptainWalletController captainWalletController) {
|
||||
double minAmount = 20000.0; // الحد الأدنى للسحب
|
||||
if (double.parse(captainWalletController.totalAmountVisa) >= minAmount) {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'هل تريد سحب أرباحك؟'.tr,
|
||||
titleStyle: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.account_balance_wallet,
|
||||
color: AppColor.primaryColor, size: 30),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
'${'رصيدك الإجمالي:'.tr} ${captainWalletController.totalAmountVisa} ${'ل.س'.tr}',
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'طريقة الدفع:'.tr,
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const MyDropDownSyria(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Form(
|
||||
key: captainWalletController.formKey,
|
||||
child: MyTextForm(
|
||||
controller: captainWalletController.phoneWallet,
|
||||
label: "أدخل رقم محفظتك".tr,
|
||||
hint: "مثال: 0912345678".tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'تأكيد'.tr,
|
||||
onPressed: () async {
|
||||
box.write(
|
||||
BoxName.phoneWallet, captainWalletController.phoneWallet);
|
||||
box.write(BoxName.walletType,
|
||||
Get.find<SyrianPayoutController>().dropdownValue.toString());
|
||||
if (captainWalletController.formKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
Get.to(() => PayoutScreen(
|
||||
amountToWithdraw:
|
||||
double.parse(captainWalletController.totalAmountVisa),
|
||||
payoutPhoneNumber:
|
||||
captainWalletController.phoneWallet.text.toString(),
|
||||
walletType: Get.find<SyrianPayoutController>()
|
||||
.dropdownValue
|
||||
.toString(),
|
||||
));
|
||||
// String amountAfterFee =
|
||||
// (double.parse(captainWalletController.totalAmountVisa) - 5)
|
||||
// .toStringAsFixed(0);
|
||||
// await Get.put(PaymobPayout()).payToWalletDriverAll(
|
||||
// amountAfterFee,
|
||||
// Get.find<SyrianPayoutController>().dropdownValue.toString(),
|
||||
// captainWalletController.phoneWallet.text.toString(),
|
||||
// );
|
||||
}
|
||||
},
|
||||
kolor: AppColor.greenColor,
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'إلغاء'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
kolor: AppColor.redColor,
|
||||
));
|
||||
} else {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text("تنبيه".tr),
|
||||
content: Text(
|
||||
'${'المبلغ في محفظتك أقل من الحد الأدنى للسحب وهو'.tr} $minAmount ${'ل.س'.tr}',
|
||||
),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: Text("موافق".tr),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// هذا الكود من الملف الأصلي وهو ضروري لعمل الحوار
|
||||
class MyDropDownSyria extends StatelessWidget {
|
||||
const MyDropDownSyria({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(SyrianPayoutController());
|
||||
return GetBuilder<SyrianPayoutController>(builder: (controller) {
|
||||
return DropdownButton<String>(
|
||||
value: controller.dropdownValue,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
elevation: 16,
|
||||
style: const TextStyle(color: Colors.deepPurple, fontSize: 16),
|
||||
underline: Container(
|
||||
height: 2,
|
||||
color: Colors.deepPurpleAccent,
|
||||
),
|
||||
onChanged: (String? newValue) {
|
||||
controller.changeValue(newValue);
|
||||
},
|
||||
items: <String>['Syriatel', 'Cash Mobile', 'Sham Cash']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value.tr),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// هذا المتحكم ضروري لعمل القائمة المنسدلة
|
||||
class SyrianPayoutController extends GetxController {
|
||||
String dropdownValue = 'Syriatel';
|
||||
|
||||
void changeValue(String? newValue) {
|
||||
if (newValue != null) {
|
||||
dropdownValue = newValue;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
170
siro_driver/lib/views/home/my_wallet/ecash.dart
Normal file
170
siro_driver/lib/views/home/my_wallet/ecash.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
// --- ملاحظات هامة ---
|
||||
// 1. تأكد من إضافة الرابط الجديد إلى ملف AppLink الخاص بك:
|
||||
// static const String payWithEcashDriver = "$server/payment/payWithEcashDriver.php";
|
||||
//
|
||||
// 2. تأكد من أنك تخزن 'driverId' في الـ box الخاص بك، مثلاً:
|
||||
// box.read(BoxName.driverID)
|
||||
|
||||
/// دالة جديدة لبدء عملية الدفع للسائق عبر ecash
|
||||
Future<void> payWithEcashDriver(BuildContext context, String amount) async {
|
||||
try {
|
||||
// يمكنك استخدام نفس طريقة التحقق بالبصمة إذا أردت
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
|
||||
);
|
||||
|
||||
if (didAuthenticate) {
|
||||
// استدعاء الـ Endpoint الجديد على السيرفر الخاص بك
|
||||
var res = await CRUD().postWallet(
|
||||
link: AppLink.payWithEcashDriver,
|
||||
payload: {
|
||||
// أرسل البيانات التي يحتاجها السيرفر
|
||||
"amount": amount,
|
||||
// "driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
|
||||
"driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
|
||||
},
|
||||
);
|
||||
|
||||
// التأكد من أن السيرفر أعاد رابط الدفع بنجاح
|
||||
if (res != null &&
|
||||
res['status'] == 'success' &&
|
||||
res['message'] != null) {
|
||||
final String paymentUrl = res['message'];
|
||||
// الانتقال إلى شاشة الدفع الجديدة الخاصة بـ ecash للسائق
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
EcashDriverPaymentScreen(paymentUrl: paymentUrl),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// عرض رسالة خطأ في حال فشل السيرفر في إنشاء الرابط
|
||||
Get.defaultDialog(
|
||||
title: 'Error'.tr,
|
||||
content: Text(
|
||||
'Failed to initiate payment. Please try again.'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Get.defaultDialog(
|
||||
title: 'Error'.tr,
|
||||
content: Text(
|
||||
'An error occurred during the payment process.'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// شاشة جديدة ومبسطة خاصة بدفع السائقين عبر ecash
|
||||
class EcashDriverPaymentScreen extends StatefulWidget {
|
||||
final String paymentUrl;
|
||||
|
||||
const EcashDriverPaymentScreen({required this.paymentUrl, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<EcashDriverPaymentScreen> createState() =>
|
||||
_EcashDriverPaymentScreenState();
|
||||
}
|
||||
|
||||
class _EcashDriverPaymentScreenState extends State<EcashDriverPaymentScreen> {
|
||||
late final WebViewController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onPageFinished: (url) {
|
||||
print('Ecash Driver WebView URL Finished: $url');
|
||||
|
||||
// هنا نتحقق فقط من أن المستخدم عاد إلى صفحة النجاح
|
||||
// لا حاجة لاستدعاء أي API هنا، فالـ Webhook يقوم بكل العمل
|
||||
if (url.contains("success.php")) {
|
||||
showProcessingDialog();
|
||||
}
|
||||
},
|
||||
))
|
||||
..loadRequest(Uri.parse(widget.paymentUrl));
|
||||
}
|
||||
|
||||
// دالة لعرض رسالة "العملية قيد المعالجة"
|
||||
void showProcessingDialog() {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Payment Successful".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
"Your payment is being processed and your wallet will be updated shortly."
|
||||
.tr,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// أغلق مربع الحوار، ثم أغلق شاشة الدفع
|
||||
Navigator.pop(context); // Close the dialog
|
||||
Navigator.pop(context); // Close the payment screen
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"OK".tr,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Complete Payment'.tr)),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
196
siro_driver/lib/views/home/my_wallet/pay_out_screen.dart
Normal file
196
siro_driver/lib/views/home/my_wallet/pay_out_screen.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
import '../../../controller/payment/smsPaymnet/pay_out_syria_controller.dart';
|
||||
|
||||
class PayoutScreen extends StatefulWidget {
|
||||
// استقبال كل البيانات المطلوبة جاهزة
|
||||
final double amountToWithdraw;
|
||||
final String payoutPhoneNumber;
|
||||
final String walletType;
|
||||
|
||||
const PayoutScreen({
|
||||
super.key,
|
||||
required this.amountToWithdraw,
|
||||
required this.payoutPhoneNumber,
|
||||
required this.walletType,
|
||||
});
|
||||
|
||||
@override
|
||||
_PayoutScreenState createState() => _PayoutScreenState();
|
||||
}
|
||||
|
||||
class _PayoutScreenState extends State<PayoutScreen> {
|
||||
final _payoutService = PayoutService();
|
||||
final _localAuth = LocalAuthentication();
|
||||
bool _isLoading = false;
|
||||
|
||||
Future<void> _handlePayoutRequest() async {
|
||||
try {
|
||||
// 1. طلب المصادقة البيومترية
|
||||
bool didAuthenticate = await _localAuth.authenticate(
|
||||
localizedReason: 'استخدم بصمة الإصبع لتأكيد عملية السحب',
|
||||
// options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
// ),
|
||||
);
|
||||
|
||||
if (didAuthenticate && mounted) {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// 2. إرسال الطلب إلى السيرفر بالبيانات الجاهزة
|
||||
final result = await _payoutService.requestPayout(
|
||||
driverId:
|
||||
box.read(BoxName.driverID).toString(), // استبدله بـ box.read
|
||||
amount: widget.amountToWithdraw,
|
||||
payoutPhoneNumber: widget.payoutPhoneNumber,
|
||||
walletType: widget.walletType,
|
||||
);
|
||||
|
||||
setState(() => _isLoading = false);
|
||||
|
||||
if (result != null && result.contains("successfully")) {
|
||||
// 3. عرض رسالة النجاح النهائية
|
||||
_showSuccessDialog();
|
||||
} else {
|
||||
_showErrorDialog(result ?? "حدث خطأ غير معروف.");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
_showErrorDialog("جهازك لا يدعم المصادقة البيومترية أو لم يتم إعدادها.");
|
||||
debugPrint("Biometric error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حساب المبلغ الإجمالي المخصوم
|
||||
final totalDeducted = widget.amountToWithdraw + PayoutService.payoutFee;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("تأكيد سحب الأموال")),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Icon(Icons.wallet, size: 64, color: Colors.blue),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"تأكيد تفاصيل عملية السحب",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSummaryCard(totalDeducted),
|
||||
const SizedBox(height: 32),
|
||||
_isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ElevatedButton.icon(
|
||||
onPressed: _handlePayoutRequest,
|
||||
icon: const Icon(Icons.fingerprint),
|
||||
label: const Text("تأكيد السحب بالبصمة"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard(double totalDeducted) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_summaryRow("المبلغ المسحوب:",
|
||||
"${widget.amountToWithdraw.toStringAsFixed(2)} ل.س"),
|
||||
const Divider(),
|
||||
_summaryRow("عمولة السحب:",
|
||||
"${PayoutService.payoutFee.toStringAsFixed(2)} ل.س"),
|
||||
const Divider(thickness: 1.5),
|
||||
_summaryRow(
|
||||
"الإجمالي المخصوم من رصيدك:",
|
||||
"${totalDeducted.toStringAsFixed(2)} ل.س",
|
||||
isTotal: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_summaryRow("سيتم التحويل إلى هاتف:", widget.payoutPhoneNumber),
|
||||
_summaryRow("عبر محفظة:", widget.walletType),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _summaryRow(String title, String value, {bool isTotal = false}) {
|
||||
final titleStyle = TextStyle(
|
||||
fontSize: 16,
|
||||
color: isTotal ? Theme.of(context).primaryColor : Colors.black87,
|
||||
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
|
||||
);
|
||||
final valueStyle = titleStyle.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: titleStyle),
|
||||
Text(value, style: valueStyle),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorDialog(String message) {
|
||||
if (!mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('حدث خطأ'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('موافق'),
|
||||
onPressed: () => Navigator.of(ctx).pop())
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSuccessDialog() {
|
||||
if (!mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('تم إرسال طلبك بنجاح'),
|
||||
content: Text(
|
||||
"سيتم تحويل المال إلى المحفظة التي أوردتها (${widget.walletType})، إلى الرقم ${widget.payoutPhoneNumber}، خلال مدة قصيرة. يرجى الانتظار، ستصلك رسالة تأكيد من محفظتك حال وصولها. شكراً لك."),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('موافق'),
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
siro_driver/lib/views/home/my_wallet/payment_history_driver_page.dart
Executable file
81
siro_driver/lib/views/home/my_wallet/payment_history_driver_page.dart
Executable file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class PaymentHistoryDriverPage extends StatelessWidget {
|
||||
const PaymentHistoryDriverPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(DriverWalletHistoryController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Payment History'.tr,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.archive.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.history_rounded, size: 80, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text('No transactions yet'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade400, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final tx = controller.archive[index];
|
||||
final double amount = double.tryParse(tx['amount']?.toString() ?? '0') ?? 0;
|
||||
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: TransactionPreviewItem(
|
||||
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['created_at'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date: tx['created_at']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
900
siro_driver/lib/views/home/my_wallet/points_captain.dart
Executable file
900
siro_driver/lib/views/home/my_wallet/points_captain.dart
Executable file
@@ -0,0 +1,900 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
import 'package:siro_driver/controller/payment/payment_controller.dart';
|
||||
import 'package:siro_driver/controller/payment/smsPaymnet/payment_services.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/payment/mtn_new/mtn_payment_new_screen.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import 'ecash.dart';
|
||||
|
||||
class PointsCaptain extends StatelessWidget {
|
||||
final PaymentController paymentController = Get.put(PaymentController());
|
||||
final CaptainWalletController captainWalletController =
|
||||
Get.put(CaptainWalletController());
|
||||
|
||||
PointsCaptain({
|
||||
super.key,
|
||||
required this.kolor,
|
||||
required this.countPoint,
|
||||
required this.pricePoint,
|
||||
});
|
||||
|
||||
final Color kolor;
|
||||
final String countPoint;
|
||||
final double pricePoint;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 12, bottom: 4),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
elevation: 4,
|
||||
shadowColor: kolor.withValues(alpha: 0.3),
|
||||
child: InkWell(
|
||||
onTap: () => _showPaymentOptions(context),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
width: 130,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
kolor.withValues(alpha: 0.05),
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
border:
|
||||
Border.all(color: kolor.withValues(alpha: 0.2), width: 1.5),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: kolor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.account_balance_wallet_rounded,
|
||||
color: kolor, size: 24),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'$countPoint ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${'Price:'.tr} ${pricePoint.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaymentOptions(BuildContext context) {
|
||||
Get.bottomSheet(
|
||||
isScrollControlled: true,
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(32)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Select Payment Method".tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_rounded, color: Colors.grey),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text("${'Amount to charge:'.tr} $countPoint ${'SYP'.tr}",
|
||||
style: FinanceDesignSystem.subHeadingStyle),
|
||||
const SizedBox(height: 24),
|
||||
_buildPaymentMethodTile(
|
||||
icon: Icons.credit_card_rounded,
|
||||
title: 'Debit Card'.tr,
|
||||
subtitle: 'E-Cash payment gateway'.tr,
|
||||
color: Colors.blue,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
payWithEcashDriver(context, pricePoint.toString());
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPaymentMethodTile(
|
||||
image: 'assets/images/syriatel.jpeg',
|
||||
title: 'Syriatel Cash'.tr,
|
||||
subtitle: 'Pay using Syriatel mobile wallet'.tr,
|
||||
color: Colors.red,
|
||||
onTap: () => _showPhoneInputDialog(context, 'Syriatel'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPaymentMethodTile(
|
||||
image: 'assets/images/shamCash.png',
|
||||
title: 'Sham Cash'.tr,
|
||||
subtitle: 'Pay using Sham Cash wallet'.tr,
|
||||
color: Colors.orange,
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
bool isAuthSupported =
|
||||
await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate =
|
||||
await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Confirm payment with biometrics'.tr,
|
||||
);
|
||||
if (!didAuthenticate) return;
|
||||
}
|
||||
Get.to(() => PaymentScreenSmsProvider(amount: pricePoint));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentMethodTile({
|
||||
IconData? icon,
|
||||
String? image,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.shade200, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: image != null
|
||||
? Image.asset(image, fit: BoxFit.contain)
|
||||
: Icon(icon, color: color, size: 30),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 14, color: Colors.grey.shade400),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPhoneInputDialog(BuildContext context, String provider) {
|
||||
Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'Wallet Phone Number'.tr,
|
||||
content: Form(
|
||||
key: paymentController.formKey,
|
||||
child: MyTextForm(
|
||||
controller: paymentController.walletphoneController,
|
||||
label: 'Phone Number'.tr,
|
||||
hint: provider == 'Syriatel' ? '963991234567' : '963941234567',
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Confirm'.tr,
|
||||
onPressed: () async {
|
||||
if (paymentController.formKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
box.write(BoxName.phoneWallet,
|
||||
paymentController.walletphoneController.text);
|
||||
if (provider == 'Syriatel') {
|
||||
await payWithSyriaTelWallet(
|
||||
context, pricePoint.toString(), 'SYP');
|
||||
} else {
|
||||
await payWithMTNWallet(context, pricePoint.toString(), 'SYP');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentScreen extends StatefulWidget {
|
||||
final String iframeUrl;
|
||||
final String countPrice;
|
||||
|
||||
const PaymentScreen(
|
||||
{required this.iframeUrl, super.key, required this.countPrice});
|
||||
|
||||
@override
|
||||
State<PaymentScreen> createState() => _PaymentScreenState();
|
||||
}
|
||||
|
||||
class _PaymentScreenState extends State<PaymentScreen> {
|
||||
late final WebViewController _controller;
|
||||
final controller = Get.find<CaptainWalletController>();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onPageFinished: (url) {
|
||||
if (url.contains("success")) {
|
||||
_fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع
|
||||
} else if (url.contains("failed")) {
|
||||
showCustomDialog(
|
||||
title: "Error".tr,
|
||||
message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
..loadRequest(Uri.parse(widget.iframeUrl));
|
||||
}
|
||||
|
||||
Future<void> _fetchPaymentStatus() async {
|
||||
final String userId = box.read(BoxName.phoneDriver);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
try {
|
||||
final response = await CRUD().postWallet(
|
||||
link: AppLink.paymetVerifyDriver,
|
||||
payload: {
|
||||
'user_id': userId,
|
||||
'driverID': box.read(BoxName.driverID),
|
||||
'paymentMethod': 'visa-in',
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure' && response != 'token_expired') {
|
||||
if (response['status'] == 'success') {
|
||||
final payment = response['message'];
|
||||
final amount = payment['amount'].toString();
|
||||
final bonus = payment['bonus'].toString();
|
||||
final paymentID = payment['paymentID'].toString();
|
||||
|
||||
await controller.getCaptainWalletFromBuyPoints();
|
||||
|
||||
showCustomDialog(
|
||||
title: "payment_success".tr,
|
||||
message:
|
||||
"${"transaction_id".tr}: $paymentID\n${"amount_paid".tr}: $amount EGP\n${"bonus_added".tr}: $bonus ${"points".tr}",
|
||||
isSuccess: true,
|
||||
);
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "transaction_failed".tr,
|
||||
message: response['message'].toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "connection_failed".tr,
|
||||
message: response.toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showCustomDialog(
|
||||
title: "server_error".tr,
|
||||
message: "server_error_message".tr,
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showCustomDialog({
|
||||
required String title,
|
||||
required String message,
|
||||
required bool isSuccess,
|
||||
}) {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: isSuccess ? Colors.green : Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"OK",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('إتمام الدفع')),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentScreenWallet extends StatefulWidget {
|
||||
final String iframeUrl;
|
||||
final String countPrice;
|
||||
|
||||
const PaymentScreenWallet(
|
||||
{required this.iframeUrl, super.key, required this.countPrice});
|
||||
|
||||
@override
|
||||
State<PaymentScreenWallet> createState() => _PaymentScreenWalletState();
|
||||
}
|
||||
|
||||
class _PaymentScreenWalletState extends State<PaymentScreenWallet> {
|
||||
late final WebViewController _controller;
|
||||
final controller = Get.find<CaptainWalletController>();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onPageFinished: (url) {
|
||||
if (url.contains("success")) {
|
||||
_fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع
|
||||
} else if (url.contains("failed")) {
|
||||
showCustomDialog(
|
||||
title: "Error".tr,
|
||||
message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
..loadRequest(Uri.parse(widget.iframeUrl));
|
||||
}
|
||||
|
||||
Future<void> _fetchPaymentStatus() async {
|
||||
final String userId = '+963${box.read(BoxName.phoneWallet)}';
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
try {
|
||||
final response = await CRUD().postWallet(
|
||||
link: AppLink.paymetVerifyDriver,
|
||||
payload: {
|
||||
'user_id': userId,
|
||||
'driverID': box.read(BoxName.driverID),
|
||||
'paymentMethod': 'visa-in',
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure' && response != 'token_expired') {
|
||||
if (response['status'] == 'success') {
|
||||
final payment = response['message'];
|
||||
final amount = payment['amount'].toString();
|
||||
final bonus = payment['bonus'].toString();
|
||||
final paymentID = payment['paymentID'].toString();
|
||||
|
||||
await controller.getCaptainWalletFromBuyPoints();
|
||||
|
||||
showCustomDialog(
|
||||
title: "payment_success".tr,
|
||||
message:
|
||||
"${"transaction_id".tr}: $paymentID\n${"amount_paid".tr}: $amount EGP\n${"bonus_added".tr}: $bonus ${"points".tr}",
|
||||
isSuccess: true,
|
||||
);
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "transaction_failed".tr,
|
||||
message: response['message'].toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "connection_failed".tr,
|
||||
message: response.toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showCustomDialog(
|
||||
title: "server_error".tr,
|
||||
message: "server_error_message".tr,
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showCustomDialog({
|
||||
required String title,
|
||||
required String message,
|
||||
required bool isSuccess,
|
||||
}) {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: isSuccess ? Colors.green : Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"OK",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('إتمام الدفع')),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payWithMTNWallet(
|
||||
BuildContext context, String amount, String currency) async {
|
||||
// استخدام مؤشر تحميل لتجربة مستخدم أفضل
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
try {
|
||||
String phone = box.read(BoxName.phoneWallet);
|
||||
String driverID = box.read(BoxName.driverID).toString();
|
||||
String formattedAmount = double.parse(amount).toStringAsFixed(0);
|
||||
|
||||
print("🚀 بدء عملية دفع MTN");
|
||||
print(
|
||||
"📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone");
|
||||
|
||||
// التحقق من البصمة (اختياري)
|
||||
bool isAuthSupported = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
if (!didAuthenticate) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
print("❌ المستخدم لم يؤكد بالبصمة/الوجه");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد)
|
||||
var responseData = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithMTNStart,
|
||||
payload: {
|
||||
"amount": formattedAmount,
|
||||
"passengerId": driverID,
|
||||
"phone": phone,
|
||||
"lang": box.read(BoxName.lang) ?? 'ar',
|
||||
},
|
||||
);
|
||||
|
||||
print("✅ استجابة الخادم (mtn_start_payment.php):");
|
||||
print(responseData);
|
||||
|
||||
// --- بداية التعديل المهم ---
|
||||
// التحقق القوي من الاستجابة لتجنب الأخطاء
|
||||
Map<String, dynamic> startRes;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
// إذا كانت الاستجابة بالفعل Map، استخدمها مباشرة
|
||||
startRes = responseData;
|
||||
} else if (responseData is String) {
|
||||
// إذا كانت نص، حاول تحليلها كـ JSON
|
||||
try {
|
||||
startRes = json.decode(responseData);
|
||||
} catch (e) {
|
||||
throw Exception(
|
||||
"فشل في تحليل استجابة الخادم. الاستجابة: $responseData");
|
||||
}
|
||||
} else {
|
||||
// نوع غير متوقع
|
||||
throw Exception("تم استلام نوع بيانات غير متوقع من الخادم.");
|
||||
}
|
||||
|
||||
if (startRes['status'] != 'success') {
|
||||
final errorMsg = startRes['message']['Error']?.toString().tr ??
|
||||
"فشل بدء عملية الدفع. حاول مرة أخرى.";
|
||||
throw Exception(errorMsg);
|
||||
}
|
||||
// --- نهاية التعديل المهم ---
|
||||
|
||||
// استخراج البيانات بأمان
|
||||
final messageData = startRes["message"];
|
||||
final invoiceNumber = messageData["invoiceNumber"].toString();
|
||||
final operationNumber = messageData["operationNumber"].toString();
|
||||
final guid = messageData["guid"].toString();
|
||||
|
||||
print(
|
||||
"📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
|
||||
|
||||
if (Get.isDialogOpen == true)
|
||||
Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP
|
||||
|
||||
// 2️⃣ عرض واجهة إدخال OTP
|
||||
String? otp = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
String input = "";
|
||||
return AlertDialog(
|
||||
title: const Text("أدخل كود التحقق"),
|
||||
content: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(hintText: "كود OTP"),
|
||||
onChanged: (val) => input = val,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("تأكيد"),
|
||||
onPressed: () => Navigator.of(context).pop(input),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("إلغاء"),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (otp == null || otp.isEmpty) {
|
||||
print("❌ لم يتم إدخال OTP");
|
||||
return;
|
||||
}
|
||||
print("🔐 تم إدخال OTP: $otp");
|
||||
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
// 3️⃣ استدعاء mtn_confirm.php
|
||||
var confirmRes = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithMTNConfirm,
|
||||
payload: {
|
||||
"invoiceNumber": invoiceNumber,
|
||||
"operationNumber": operationNumber,
|
||||
"guid": guid,
|
||||
"otp": otp,
|
||||
"phone": phone,
|
||||
},
|
||||
);
|
||||
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
print("✅ استجابة mtn_confirm.php:");
|
||||
// print(confirmRes);
|
||||
Log.print('confirmRes: ${confirmRes}');
|
||||
|
||||
if (confirmRes != null && confirmRes['status'] == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: "✅ نجاح",
|
||||
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
|
||||
);
|
||||
} else {
|
||||
String errorMsg =
|
||||
confirmRes?['message']['message']?.toString() ?? "فشل في تأكيد الدفع";
|
||||
Get.defaultDialog(
|
||||
title: "❌ فشل",
|
||||
content: Text(errorMsg.tr),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
print("🔥 خطأ أثناء الدفع عبر MTN:");
|
||||
print(e);
|
||||
print(s);
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'حدث خطأ',
|
||||
content: Text(e.toString().replaceFirst("Exception: ", "")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payWithSyriaTelWallet(
|
||||
BuildContext context, String amount, String currency) async {
|
||||
// Show a loading indicator for better user experience
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
try {
|
||||
String phone = box.read(BoxName.phoneWallet);
|
||||
String driverID = box.read(BoxName.driverID).toString();
|
||||
String formattedAmount = double.parse(amount).toStringAsFixed(0);
|
||||
|
||||
// --- CHANGE 1: Updated log messages for clarity ---
|
||||
print("🚀 Starting Syriatel payment process");
|
||||
print(
|
||||
"📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone");
|
||||
|
||||
// Optional: Biometric authentication
|
||||
bool isAuthSupported = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
if (!didAuthenticate) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
print("❌ User did not authenticate with biometrics");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- CHANGE 2: Updated API link and payload for starting payment ---
|
||||
// Make sure you have defined `payWithSyriatelStart` in your AppLink class
|
||||
var responseData = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithSyriatelStart, // Use the new Syriatel start link
|
||||
payload: {
|
||||
"amount": formattedAmount,
|
||||
"driverId": driverID, // Key changed from 'passengerId' to 'driverId'
|
||||
"phone": phone,
|
||||
"lang": box.read(BoxName.lang) ?? 'ar',
|
||||
},
|
||||
);
|
||||
|
||||
print("✅ Server response (start_payment.php):");
|
||||
Log.print('responseData: ${responseData}');
|
||||
|
||||
// Robustly parse the server's JSON response
|
||||
Map<String, dynamic> startRes;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
startRes = responseData;
|
||||
} else if (responseData is String) {
|
||||
try {
|
||||
startRes = json.decode(responseData);
|
||||
} catch (e) {
|
||||
throw Exception(
|
||||
"Failed to parse server response. Response: $responseData");
|
||||
}
|
||||
} else {
|
||||
throw Exception("Received an unexpected data type from the server.");
|
||||
}
|
||||
|
||||
if (startRes['status'] != 'success') {
|
||||
String errorMsg = startRes['message']?.toString() ??
|
||||
"Failed to start the payment process. Please try again.";
|
||||
throw Exception(errorMsg);
|
||||
}
|
||||
|
||||
// --- CHANGE 3: Extract `transactionID` from the response ---
|
||||
// The response structure is now simpler. We only need the transaction ID.
|
||||
final messageData = startRes["message"];
|
||||
final transactionID = messageData["transactionID"].toString();
|
||||
|
||||
print("📄 TransactionID: $transactionID");
|
||||
|
||||
if (Get.isDialogOpen == true) Get.back(); // Close loading indicator
|
||||
|
||||
// Show the OTP input dialog
|
||||
String? otp = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
String input = "";
|
||||
return AlertDialog(
|
||||
title: const Text("أدخل كود التحقق"),
|
||||
content: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(hintText: "كود OTP"),
|
||||
onChanged: (val) => input = val,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("تأكيد"),
|
||||
onPressed: () => Navigator.of(context).pop(input),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("إلغاء"),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (otp == null || otp.isEmpty) {
|
||||
print("❌ OTP was not entered.");
|
||||
return;
|
||||
}
|
||||
print("🔐 OTP entered: $otp");
|
||||
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
// --- CHANGE 4: Updated API link and payload for confirming payment ---
|
||||
// Make sure you have defined `payWithSyriatelConfirm` in your AppLink class
|
||||
var confirmRes = await CRUD().postWalletMtn(
|
||||
// Changed from postWalletMtn if they are different
|
||||
link: AppLink.payWithSyriatelConfirm, // Use the new Syriatel confirm link
|
||||
payload: {
|
||||
"transactionID": transactionID, // Use the transaction ID
|
||||
"otp": otp,
|
||||
// The other parameters (phone, guid, etc.) are no longer needed
|
||||
},
|
||||
);
|
||||
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
print("✅ Response from confirm_payment.php:");
|
||||
Log.print('confirmRes: ${confirmRes}');
|
||||
|
||||
if (confirmRes != null && confirmRes['status'] == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: "✅ نجاح",
|
||||
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
|
||||
);
|
||||
} else {
|
||||
// --- CHANGE 5: Simplified error message extraction ---
|
||||
// The new PHP script sends the error directly in the 'message' field.
|
||||
String errorMsg =
|
||||
confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع";
|
||||
Get.defaultDialog(
|
||||
title: "❌ فشل",
|
||||
content: Text(errorMsg.tr),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
// --- CHANGE 6: Updated general error log message ---
|
||||
print("🔥 Error during Syriatel Wallet payment:");
|
||||
print(e);
|
||||
print(s);
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'حدث خطأ',
|
||||
content: Text(e.toString().replaceFirst("Exception: ", "")),
|
||||
);
|
||||
}
|
||||
}
|
||||
145
siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart
Executable file
145
siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart
Executable file
@@ -0,0 +1,145 @@
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../controller/home/payment/captain_wallet_controller.dart';
|
||||
|
||||
class TransferBudgetPage extends StatelessWidget {
|
||||
const TransferBudgetPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(CaptainWalletController());
|
||||
return MyScafolld(
|
||||
title: "Transfer budget".tr,
|
||||
body: [
|
||||
GetBuilder<CaptainWalletController>(
|
||||
builder: (captainWalletController) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
height: Get.height * .7,
|
||||
width: double.infinity,
|
||||
child: Form(
|
||||
key: captainWalletController.formKeyTransfer,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
MyTextForm(
|
||||
controller: captainWalletController
|
||||
.newDriverPhoneController,
|
||||
label: 'phone number of driver'.tr,
|
||||
hint: 'phone number of driver',
|
||||
type: TextInputType.phone),
|
||||
MyTextForm(
|
||||
controller: captainWalletController
|
||||
.amountFromBudgetController,
|
||||
label: 'insert amount'.tr,
|
||||
hint:
|
||||
'${'You have in account'.tr} ${captainWalletController.totalAmountVisa}',
|
||||
type: TextInputType.number),
|
||||
captainWalletController.isNewTransfer
|
||||
? const MyCircularProgressIndicator()
|
||||
: captainWalletController
|
||||
.amountToNewDriverMap.isEmpty
|
||||
? MyElevatedButton(
|
||||
title: 'Next'.tr,
|
||||
onPressed: () async {
|
||||
await captainWalletController
|
||||
.detectNewDriverFromMyBudget();
|
||||
})
|
||||
: const SizedBox(),
|
||||
captainWalletController.amountToNewDriverMap.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Text(
|
||||
'Name :'.tr +
|
||||
captainWalletController
|
||||
.amountToNewDriverMap[0]['name']
|
||||
.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Text(
|
||||
"${"NationalID".tr} ${captainWalletController.amountToNewDriverMap[0]['national_number']}",
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Text(
|
||||
"${"amount".tr} ${captainWalletController.amountFromBudgetController.text} ${'LE'.tr}",
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
captainWalletController
|
||||
.amountToNewDriverMap.isNotEmpty
|
||||
? MyElevatedButton(
|
||||
title: 'Transfer'.tr,
|
||||
onPressed: () async {
|
||||
if (double.parse(
|
||||
captainWalletController
|
||||
.amountFromBudgetController
|
||||
.text) <
|
||||
double.parse(
|
||||
captainWalletController
|
||||
.totalAmountVisa) -
|
||||
5) {
|
||||
await captainWalletController
|
||||
.addTransferDriversWallet(
|
||||
'TransferFrom',
|
||||
'TransferTo',
|
||||
);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
"You dont have money in your Wallet"
|
||||
.tr,
|
||||
"You dont have money in your Wallet or you should less transfer 5 LE to activate"
|
||||
.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
})
|
||||
: const SizedBox()
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox()
|
||||
],
|
||||
)),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
443
siro_driver/lib/views/home/my_wallet/walet_captain.dart
Executable file
443
siro_driver/lib/views/home/my_wallet/walet_captain.dart
Executable file
@@ -0,0 +1,443 @@
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/home/my_wallet/payment_history_driver_page.dart';
|
||||
import 'package:siro_driver/controller/payment/driver_payment_controller.dart';
|
||||
|
||||
// Import new widgets
|
||||
import 'points_captain.dart';
|
||||
import 'transfer_budget_page.dart';
|
||||
import 'widgets/balance_card.dart';
|
||||
import 'widgets/quick_actions.dart';
|
||||
import 'widgets/financial_summary_card.dart';
|
||||
import 'widgets/promo_gamification_card.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class WalletCaptainRefactored extends StatelessWidget {
|
||||
WalletCaptainRefactored({super.key});
|
||||
|
||||
final CaptainWalletController controller = Get.put(CaptainWalletController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.refreshCaptainWallet();
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Driver Balance'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh_rounded,
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
onPressed: () => controller.refreshCaptainWallet(),
|
||||
tooltip: 'Refresh'.tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GetBuilder<CaptainWalletController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FinanceDesignSystem.horizontalPadding,
|
||||
vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1. Header / Balance
|
||||
BalanceCard(
|
||||
balance: controller.totalPoints.toString(),
|
||||
isNegative:
|
||||
double.tryParse(controller.totalPoints.toString()) !=
|
||||
null &&
|
||||
double.parse(controller.totalPoints.toString()) <
|
||||
-30000,
|
||||
lastUpdated: "Just now".tr,
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 2. Quick Actions
|
||||
QuickActionsGrid(
|
||||
onAddBalance: () =>
|
||||
_showAddBalanceOptions(context, controller),
|
||||
onWithdraw: () => addSyrianPaymentMethod(controller),
|
||||
onTransfer: () => Get.to(() => TransferBudgetPage()),
|
||||
onHistory: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getArchivePayment();
|
||||
Get.to(() => const PaymentHistoryDriverPage());
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. Earnings Summary
|
||||
FinancialSummaryCard(
|
||||
title: 'Earnings Summary'.tr,
|
||||
subtitle: 'ملخص الأرباح'.tr,
|
||||
items: [
|
||||
SummaryItem(
|
||||
icon: Icons.money_rounded,
|
||||
label: 'Cash Earnings'.tr,
|
||||
amount: controller.totalAmount,
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
),
|
||||
SummaryItem(
|
||||
icon: Icons.credit_card_rounded,
|
||||
label: 'Card Earnings'.tr,
|
||||
amount: controller.totalAmountVisa,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. Recharge Balance Packages
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Recharge Balance'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
Icon(Icons.info_outline_rounded,
|
||||
size: 18, color: Colors.grey.shade400),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 140, // Increased height for modern cards
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
PointsCaptain(
|
||||
kolor: Colors.blueGrey,
|
||||
pricePoint: 100,
|
||||
countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.brown,
|
||||
pricePoint: 200,
|
||||
countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.amber,
|
||||
pricePoint: 400,
|
||||
countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.orange,
|
||||
pricePoint: 1000,
|
||||
countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 5. Promotions
|
||||
Text('Promotions'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
PromoGamificationCard(
|
||||
title: 'Morning Promo'.tr,
|
||||
subtitle: "from 7:00am to 10:00am".tr,
|
||||
currentProgress: controller.walletDate['message']?[0]
|
||||
?['morning_count'] ??
|
||||
0,
|
||||
targetProgress: 5,
|
||||
reward: "+50 SYP",
|
||||
onTap: () =>
|
||||
controller.addDriverWalletFromPromo('Morning Promo', 50),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PromoGamificationCard(
|
||||
title: 'Afternoon Promo'.tr,
|
||||
subtitle: "from 3:00pm to 6:00 pm".tr,
|
||||
currentProgress: controller.walletDate['message']?[0]
|
||||
?['afternoon_count'] ??
|
||||
0,
|
||||
targetProgress: 5,
|
||||
reward: "+50 SYP",
|
||||
onTap: () => controller.addDriverWalletFromPromo(
|
||||
'Afternoon Promo', 50),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 6. Transactions Preview
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Recent Transactions'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getArchivePayment();
|
||||
Get.to(() => const PaymentHistoryDriverPage());
|
||||
},
|
||||
child: Text('View All'.tr,
|
||||
style: TextStyle(
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
GetBuilder<DriverWalletHistoryController>(
|
||||
init: DriverWalletHistoryController(),
|
||||
builder: (historyController) {
|
||||
if (historyController.archive.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Center(
|
||||
child: Text("No transactions yet".tr,
|
||||
style: TextStyle(color: Colors.grey.shade400))),
|
||||
);
|
||||
}
|
||||
// Show only last 3
|
||||
final lastThree =
|
||||
historyController.archive.take(3).toList();
|
||||
return Column(
|
||||
children: lastThree.map((tx) {
|
||||
final double amount =
|
||||
double.tryParse(tx['amount']?.toString() ?? '0') ??
|
||||
0;
|
||||
return TransactionPreviewItem(
|
||||
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['created_at'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date: tx['created_at']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
onTap: () {},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddBalanceOptions(
|
||||
BuildContext context, CaptainWalletController controller) {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Add Balance".tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 8),
|
||||
Text("Select how you want to charge your account".tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle),
|
||||
const SizedBox(height: 24),
|
||||
ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
FinanceDesignSystem.accentBlue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(Icons.account_balance_wallet_rounded,
|
||||
color: FinanceDesignSystem.accentBlue),
|
||||
),
|
||||
title: Text("Pay from my budget".tr),
|
||||
subtitle: Text(
|
||||
"${'You have in account'.tr} ${controller.totalAmountVisa}"),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
_showPayFromBudgetDialog(controller);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
Text("Recharge Balance Packages".tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle
|
||||
.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 140,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
PointsCaptain(
|
||||
kolor: Colors.blueGrey,
|
||||
pricePoint: 100,
|
||||
countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.brown, pricePoint: 200, countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.amber, pricePoint: 400, countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.orange,
|
||||
pricePoint: 1000,
|
||||
countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPayFromBudgetDialog(CaptainWalletController controller) {
|
||||
Get.defaultDialog(
|
||||
title: 'Pay from my budget'.tr,
|
||||
content: Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.amountFromBudgetController,
|
||||
label: '${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
hint: '${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
type: TextInputType.number,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Pay'.tr,
|
||||
onPressed: () async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
if (double.parse(controller.amountFromBudgetController.text) <
|
||||
double.parse(controller.totalAmountVisa)) {
|
||||
await controller.payFromBudget();
|
||||
} else {
|
||||
Get.back();
|
||||
mySnackeBarError('Your Budget less than needed'.tr);
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Authentication failed'.tr, ''.tr, () => Get.back());
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr,
|
||||
() => Get.back());
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(title: 'Cancel'.tr, onPressed: () => Get.back()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> addSyrianPaymentMethod(
|
||||
CaptainWalletController captainWalletController) {
|
||||
return Get.defaultDialog(
|
||||
title: "Insert Payment Details".tr,
|
||||
content: Form(
|
||||
key: captainWalletController.formKeyAccount,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Insert your mobile wallet details to receive your money weekly"
|
||||
.tr),
|
||||
MyTextForm(
|
||||
controller: captainWalletController.cardBank,
|
||||
label: "Insert mobile wallet number".tr,
|
||||
hint: '0912 345 678',
|
||||
type: TextInputType.phone),
|
||||
const SizedBox(height: 10),
|
||||
MyDropDownSyria()
|
||||
],
|
||||
)),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () async {
|
||||
if (captainWalletController.formKeyAccount.currentState!
|
||||
.validate()) {
|
||||
Get.back();
|
||||
var res =
|
||||
await CRUD().post(link: AppLink.updateAccountBank, payload: {
|
||||
"paymentProvider":
|
||||
Get.find<SyrianPayoutController>().dropdownValue.toString(),
|
||||
"accountNumber":
|
||||
captainWalletController.cardBank.text.toString(),
|
||||
"id": box.read(BoxName.driverID).toString()
|
||||
});
|
||||
if (res != 'failure') {
|
||||
mySnackbarSuccess('Payment details added successfully'.tr);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
class SyrianPayoutController extends GetxController {
|
||||
String dropdownValue = 'syriatel';
|
||||
void changeValue(String? newValue) {
|
||||
if (newValue != null) {
|
||||
dropdownValue = newValue;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyDropDownSyria extends StatelessWidget {
|
||||
const MyDropDownSyria({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(SyrianPayoutController());
|
||||
final theme = Theme.of(context);
|
||||
return GetBuilder<SyrianPayoutController>(builder: (controller) {
|
||||
return DropdownButton<String>(
|
||||
value: controller.dropdownValue,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
elevation: 16,
|
||||
isExpanded: true,
|
||||
dropdownColor: theme.cardColor,
|
||||
style: TextStyle(color: theme.textTheme.bodyLarge?.color),
|
||||
underline: Container(height: 2, color: theme.primaryColor),
|
||||
onChanged: (String? newValue) => controller.changeValue(newValue),
|
||||
items: <String>['syriatel', 'mtn']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(value: value, child: Text(value.tr));
|
||||
}).toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
224
siro_driver/lib/views/home/my_wallet/weekly_payment_page.dart
Executable file
224
siro_driver/lib/views/home/my_wallet/weekly_payment_page.dart
Executable file
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class WeeklyPaymentPage extends StatelessWidget {
|
||||
const WeeklyPaymentPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(DriverWalletHistoryController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Weekly Summary'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildWeeklyStatsHeader(controller),
|
||||
const SizedBox(height: 16),
|
||||
_buildWeekSelector(),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('Transactions this week'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
const Spacer(),
|
||||
Icon(Icons.list_rounded,
|
||||
color: Colors.grey.shade400, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: controller.weeklyList.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
itemCount: controller.weeklyList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final tx = controller.weeklyList[index];
|
||||
final double amount = double.tryParse(
|
||||
tx['amount']?.toString() ?? '0') ??
|
||||
0;
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 25,
|
||||
child: FadeInAnimation(
|
||||
child: TransactionPreviewItem(
|
||||
title:
|
||||
amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['dateUpdated'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date:
|
||||
tx['dateUpdated']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
method: tx['paymentMethod'],
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeeklyStatsHeader(DriverWalletHistoryController controller) {
|
||||
final totalAmount = controller.weeklyList.isEmpty
|
||||
? '0.00'
|
||||
: controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
|
||||
|
||||
final double earnings = double.tryParse(totalAmount) ?? 0;
|
||||
final int trips = controller.weeklyList.length;
|
||||
final double commission = earnings * 0.15; // Example 15%
|
||||
final double netProfit = earnings - commission;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: FinanceDesignSystem.primaryDark.withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Total Weekly Earnings'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8), fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Text('${earnings.toStringAsFixed(0)} SYP',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildStatItem('Total Trips'.tr, trips.toString(),
|
||||
Icons.directions_car_rounded),
|
||||
_buildStatItem('Commission'.tr, commission.toStringAsFixed(0),
|
||||
Icons.percent_rounded),
|
||||
_buildStatItem('Net Profit'.tr, netProfit.toStringAsFixed(0),
|
||||
Icons.account_balance_rounded),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value, IconData icon) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon, color: Colors.white, size: 18),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14)),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.6), fontSize: 10)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeekSelector() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02), blurRadius: 10)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left_rounded), onPressed: () {}),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('Dec 15 - Dec 21, 2024'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right_rounded),
|
||||
onPressed: () {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.bar_chart_rounded, size: 64, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text('No transactions this week'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
124
siro_driver/lib/views/home/my_wallet/widgets/balance_card.dart
Normal file
124
siro_driver/lib/views/home/my_wallet/widgets/balance_card.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class BalanceCard extends StatelessWidget {
|
||||
final String balance;
|
||||
final bool isNegative;
|
||||
final String lastUpdated;
|
||||
|
||||
const BalanceCard({
|
||||
super.key,
|
||||
required this.balance,
|
||||
required this.isNegative,
|
||||
this.lastUpdated = "Just now",
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
gradient: isNegative
|
||||
? FinanceDesignSystem.dangerGradient
|
||||
: FinanceDesignSystem.balanceGradient,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isNegative
|
||||
? FinanceDesignSystem.dangerRed
|
||||
: FinanceDesignSystem.primaryDark)
|
||||
.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Available Balance".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"الرصيد المتاح".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isNegative
|
||||
? Icons.warning_rounded
|
||||
: Icons.account_balance_wallet_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
balance,
|
||||
style: FinanceDesignSystem.balanceStyle,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"SYP".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(color: Colors.white.withOpacity(0.15)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history_rounded,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"${'Last updated:'.tr} $lastUpdated",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class FinancialSummaryCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final List<SummaryItem> items;
|
||||
|
||||
const FinancialSummaryCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: FinanceDesignSystem.headingStyle),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!, style: FinanceDesignSystem.subHeadingStyle),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
Divider(color: Colors.grey.withValues(alpha: 0.1), height: 24),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(item.icon, color: item.color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
if (item.subLabel != null)
|
||||
Text(
|
||||
item.subLabel!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${item.amount} ${'SYP'.tr}",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
if (item.trend != null)
|
||||
Text(
|
||||
item.trend!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: item.trendColor ??
|
||||
FinanceDesignSystem.successGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryItem {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String? subLabel;
|
||||
final String amount;
|
||||
final Color color;
|
||||
final String? trend;
|
||||
final Color? trendColor;
|
||||
|
||||
SummaryItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.subLabel,
|
||||
required this.amount,
|
||||
required this.color,
|
||||
this.trend,
|
||||
this.trendColor,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class PromoGamificationCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final int currentProgress;
|
||||
final int targetProgress;
|
||||
final String reward;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const PromoGamificationCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.currentProgress,
|
||||
required this.targetProgress,
|
||||
required this.reward,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double progress = (currentProgress / targetProgress).clamp(0.0, 1.0);
|
||||
final bool isCompleted = progress >= 1.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: (isCompleted
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue)
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
reward,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isCompleted
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${'Progress:'.tr} $currentProgress / $targetProgress",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
if (isCompleted)
|
||||
Icon(Icons.check_circle_rounded,
|
||||
color: FinanceDesignSystem.successGreen, size: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 10,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
height: 10,
|
||||
width: MediaQuery.of(context).size.width *
|
||||
progress, // Simplified for now
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isCompleted
|
||||
? [
|
||||
FinanceDesignSystem.successGreen,
|
||||
Colors.lightGreenAccent
|
||||
]
|
||||
: [FinanceDesignSystem.accentBlue, Colors.blueAccent],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isCompleted && onTap != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: FinanceDesignSystem.successGreen,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
),
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text("Claim Reward".tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
120
siro_driver/lib/views/home/my_wallet/widgets/quick_actions.dart
Normal file
120
siro_driver/lib/views/home/my_wallet/widgets/quick_actions.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class QuickActionsGrid extends StatelessWidget {
|
||||
final VoidCallback onAddBalance;
|
||||
final VoidCallback onWithdraw;
|
||||
final VoidCallback onTransfer;
|
||||
final VoidCallback onHistory;
|
||||
|
||||
const QuickActionsGrid({
|
||||
super.key,
|
||||
required this.onAddBalance,
|
||||
required this.onWithdraw,
|
||||
required this.onTransfer,
|
||||
required this.onHistory,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
_ActionItem(
|
||||
icon: Icons.add_circle_outline_rounded,
|
||||
label: "Add Balance".tr,
|
||||
subLabel: "شحن",
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
onTap: onAddBalance,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.file_download_outlined,
|
||||
label: "Withdraw".tr,
|
||||
subLabel: "سحب",
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
onTap: onWithdraw,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.swap_horiz_rounded,
|
||||
label: "Transfer".tr,
|
||||
subLabel: "تحويل",
|
||||
color: Colors.orange,
|
||||
onTap: onTransfer,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.history_rounded,
|
||||
label: "History".tr,
|
||||
subLabel: "السجل",
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
onTap: onHistory,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String subLabel;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.subLabel,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
subLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class TransactionPreviewItem extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String amount;
|
||||
final String date;
|
||||
final String type; // 'credit' or 'debit'
|
||||
final String? method;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const TransactionPreviewItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.amount,
|
||||
required this.date,
|
||||
required this.type,
|
||||
this.method,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isCredit = type.toLowerCase() == 'credit';
|
||||
final Color statusColor = isCredit
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.dangerRed;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.mainRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isCredit
|
||||
? Icons.arrow_downward_rounded
|
||||
: Icons.arrow_upward_rounded,
|
||||
color: statusColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
if (method != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade400,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
method!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${isCredit ? '+' : '-'}$amount ${'SYP'.tr}",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
siro_driver/lib/views/home/on_boarding_page.dart
Executable file
33
siro_driver/lib/views/home/on_boarding_page.dart
Executable file
@@ -0,0 +1,33 @@
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/auth/onboarding_controller.dart';
|
||||
import 'package:siro_driver/onbording_page.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class OnBoardingPage extends StatelessWidget {
|
||||
OnBoardingControllerImp onBoardingControllerImp =
|
||||
Get.put(OnBoardingControllerImp());
|
||||
|
||||
OnBoardingPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
body: SafeArea(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
SizedBox(
|
||||
height: Get.height * .7,
|
||||
child: const CustomSliderOnBoarding(),
|
||||
),
|
||||
const CustomDotControllerOnBoarding(),
|
||||
// const Spacer(flex: 2),
|
||||
const SizedBox(height: 20),
|
||||
MyElevatedButton(
|
||||
onPressed: () => onBoardingControllerImp.next(),
|
||||
title: 'Next'.tr,
|
||||
)
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
214
siro_driver/lib/views/home/profile/behavior_page.dart
Normal file
214
siro_driver/lib/views/home/profile/behavior_page.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../controller/home/captin/behavior_controller.dart';
|
||||
|
||||
class BehaviorPage extends StatelessWidget {
|
||||
const BehaviorPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(DriverBehaviorController());
|
||||
controller.fetchDriverBehavior();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Driver Behavior'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
centerTitle: true,
|
||||
backgroundColor: FinanceDesignSystem.cardColor,
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: FinanceDesignSystem.primaryDark),
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.accentBlue));
|
||||
}
|
||||
|
||||
double score = controller.overallScore.value;
|
||||
bool isExcellent = score >= 90;
|
||||
bool isGood = score >= 75 && score < 90;
|
||||
Color statusColor =
|
||||
isExcellent ? Colors.green : (isGood ? Colors.orange : Colors.red);
|
||||
String statusText = isExcellent
|
||||
? 'Excellent'.tr
|
||||
: (isGood ? 'Good'.tr : 'Needs Improvement'.tr);
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding:
|
||||
const EdgeInsets.all(FinanceDesignSystem.horizontalPadding),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Overall Score Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.shield_rounded,
|
||||
size: 48, color: statusColor),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Overall Behavior Score".tr,
|
||||
style: FinanceDesignSystem.headingStyle,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"${score.toStringAsFixed(1)} / 100",
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Text("Last 10 Trips".tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 16),
|
||||
]),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FinanceDesignSystem.horizontalPadding),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
var trip = controller.lastTrips[index];
|
||||
double tripScore =
|
||||
double.tryParse(trip['behavior_score'].toString()) ?? 0;
|
||||
Color tColor = tripScore >= 90
|
||||
? Colors.green
|
||||
: (tripScore >= 75 ? Colors.orange : Colors.red);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: tColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.drive_eta_rounded, color: tColor),
|
||||
),
|
||||
title: Text(
|
||||
"Trip ID: ${trip['trip_id']}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildBadge(Icons.speed,
|
||||
"${trip['max_speed']} km/h", Colors.blue),
|
||||
_buildBadge(
|
||||
Icons.warning_amber_rounded,
|
||||
"${trip['hard_brakes']} ${'Hard Brakes'.tr}",
|
||||
Colors.orange),
|
||||
_buildBadge(
|
||||
Icons.map_rounded,
|
||||
"${trip['total_distance']} km",
|
||||
Colors.purple),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${tripScore.toStringAsFixed(0)}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
color: tColor),
|
||||
),
|
||||
Text('Score'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: controller.lastTrips.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 40)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBadge(IconData icon, String label, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: color, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
171
siro_driver/lib/views/home/profile/captains_cars.dart
Executable file
171
siro_driver/lib/views/home/profile/captains_cars.dart
Executable file
@@ -0,0 +1,171 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../auth/captin/driver_car_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import 'cars_inserting_page.dart';
|
||||
|
||||
class CaptainsCars extends StatelessWidget {
|
||||
const CaptainsCars({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(DriverCarController());
|
||||
return MyScafolld(
|
||||
title: "Add new car".tr,
|
||||
body: [
|
||||
Column(
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: "Add new car".tr,
|
||||
onPressed: () async {
|
||||
Get.to(() => CarsInsertingPage());
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: GetBuilder<DriverCarController>(
|
||||
builder: (controller) {
|
||||
return controller.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: ListView.builder(
|
||||
itemCount: controller.cars.length,
|
||||
itemBuilder: (context, index) {
|
||||
final car = controller.cars[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? AppColor.primaryColor
|
||||
: Theme.of(context).cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: car['isDefault'].toString() == '1'
|
||||
? BorderSide.none
|
||||
: BorderSide(color: Theme.of(context).dividerColor)),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white12,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Fontisto.car,
|
||||
size: 32,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: Color(int.parse(car['color_hex']
|
||||
.replaceFirst('#', '0xff'))),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
car['make'],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
car['model'],
|
||||
style: TextStyle(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white70
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white54
|
||||
: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
car['car_plate'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
car['year'],
|
||||
style: TextStyle(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white70
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// trailing: IconButton(
|
||||
// icon: const Icon(
|
||||
// Icons.delete,
|
||||
// color: AppColor.redColor,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// // Add logic here to remove a car
|
||||
// MyDialog()
|
||||
// .getDialog('Are you sure to delete this car', '', () {
|
||||
// controller
|
||||
// .removeCar(car['id'].toString());
|
||||
// });
|
||||
|
||||
// },
|
||||
// ),
|
||||
onTap: () {
|
||||
MyDialog().getDialog(
|
||||
'Are you sure to make this car as default'
|
||||
.tr,
|
||||
'', () {
|
||||
Get.back();
|
||||
//make it default
|
||||
controller.updateCarRegistration(
|
||||
car['id'].toString(),
|
||||
box.read(BoxName.driverID).toString(),
|
||||
);
|
||||
});
|
||||
// Add logic to view or edit the car details
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
300
siro_driver/lib/views/home/profile/cars_inserting_page.dart
Executable file
300
siro_driver/lib/views/home/profile/cars_inserting_page.dart
Executable file
@@ -0,0 +1,300 @@
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../controller/functions/gemeni.dart';
|
||||
import '../../auth/captin/driver_car_controller.dart';
|
||||
|
||||
class CarsInsertingPage extends StatelessWidget {
|
||||
CarsInsertingPage({super.key});
|
||||
final driverCarController = Get.put(DriverCarController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(AI());
|
||||
return MyScafolld(
|
||||
title: "Add new car".tr,
|
||||
body: [
|
||||
Container(
|
||||
color: AppColor.accentColor.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(22),
|
||||
child: ListView(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
syriaCarLicenceFront(),
|
||||
syriaCarLicenceBack(),
|
||||
const SizedBox(height: 10),
|
||||
Text('Please make sure to read the license carefully.'.tr),
|
||||
Text(
|
||||
'If your car license has the new design, upload the front side with two images.'
|
||||
.tr),
|
||||
const SizedBox(height: 10),
|
||||
MyElevatedButton(
|
||||
title: 'Please upload this license.'.tr,
|
||||
onPressed: () {
|
||||
final aiFront =
|
||||
Get.find<AI>().responseIdCardDriverEgyptFront;
|
||||
final aiBack =
|
||||
Get.find<AI>().responseIdCardDriverEgyptBack;
|
||||
driverCarController.addCarsForDrivers(
|
||||
aiBack['chassis'].toString(),
|
||||
aiBack['car_plate'].toString(),
|
||||
aiBack['make'].toString(),
|
||||
aiBack['model'].toString(),
|
||||
aiBack['year'].toString(),
|
||||
aiFront['LicenseExpirationDate'].toString(),
|
||||
aiBack['color'].toString(),
|
||||
aiBack['color_hex'].toString(),
|
||||
aiFront['address'].toString(),
|
||||
aiFront['owner'].toString(),
|
||||
aiBack['inspection_date'].toString(),
|
||||
aiBack['engine'].toString(),
|
||||
aiBack['fuel'].toString(),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaCarLicenceFront() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
|
||||
// No need to access ai.responseIdCardDriverEgyptBack anymore
|
||||
final licenseExpiryDate = DateTime.parse(
|
||||
ai.responseIdCardDriverEgyptFront['LicenseExpirationDate']);
|
||||
|
||||
// Check if license has expired
|
||||
final today = DateTime.now();
|
||||
final isLicenseExpired = licenseExpiryDate.isBefore(today);
|
||||
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('Vehicle Details Front'.tr,
|
||||
style: AppStyle.headTitle2),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
ai.allMethodForAI((ai.prompts[3]['prompt'].toString()),
|
||||
AppLink.uploadEgypt, 'car_front');
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(color: AppColor.accentColor),
|
||||
const SizedBox(height: 8.0),
|
||||
// Removed Make, Model, etc. as they are not available
|
||||
|
||||
Text(
|
||||
'${'Plate Number'.tr}: ${ai.responseIdCardDriverEgyptFront['car_plate']}',
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'Owner Name'.tr}: ${ai.responseIdCardDriverEgyptFront['owner']}',
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'Address'.tr}: ${ai.responseIdCardDriverEgyptFront['address']}',
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'License Expiry Date'.tr}: ${licenseExpiryDate.toString().substring(0, 10)}',
|
||||
style: TextStyle(
|
||||
color: isLicenseExpired ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
// Removed Fuel as it's not available
|
||||
],
|
||||
),
|
||||
// Removed Inspection Date as it's not available
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
ai.allMethodForAINewCar((ai.prompts[3]['prompt'].toString()),
|
||||
AppLink.uploadEgypt1, 'car_front', 'carId'); //todo
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/3.png',
|
||||
height: Get.height * .25,
|
||||
width: double.maxFinite,
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
Text(
|
||||
'Capture an Image of Your car license front '.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaCarLicenceBack() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
|
||||
// Get the tax expiry date from the response
|
||||
final taxExpiryDate =
|
||||
ai.responseIdCardDriverEgyptBack['tax_expiry'].toString();
|
||||
// final displacement = ai.responseIdCardDriverEgyptBack['displacement'];
|
||||
// if (int.parse(displacement) < 1000) {}
|
||||
// Get the inspection date from the response
|
||||
final inspectionDate =
|
||||
ai.responseIdCardDriverEgyptBack['inspection_date'].toString();
|
||||
final year = int.parse(inspectionDate.toString().split('-')[0]);
|
||||
|
||||
// Set inspectionDateTime to December 31st of the given year
|
||||
final inspectionDateTime = DateTime(year, 12, 31);
|
||||
String carBackLicenseExpired =
|
||||
inspectionDateTime.toString().split(' ')[0];
|
||||
// Get the current date
|
||||
final today = DateTime.now();
|
||||
|
||||
// Try parsing the tax expiry date. If it fails, set it to null.
|
||||
final taxExpiryDateTime = DateTime.tryParse(taxExpiryDate ?? '');
|
||||
final isExpired =
|
||||
taxExpiryDateTime != null && taxExpiryDateTime.isBefore(today);
|
||||
// Check if the inspection date is before today
|
||||
bool isInspectionExpired = inspectionDateTime.isBefore(today);
|
||||
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Vehicle Details Back'.tr, style: AppStyle.headTitle2),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
ai.allMethodForAI((ai.prompts[4]['prompt'].toString()),
|
||||
AppLink.uploadEgypt, 'car_back');
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(color: AppColor.accentColor),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Make'.tr}: ${ai.responseIdCardDriverEgyptBack['make']}'),
|
||||
Text(
|
||||
'${'Model'.tr}: ${ai.responseIdCardDriverEgyptBack['model']}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Year'.tr}: ${ai.responseIdCardDriverEgyptBack['year']}'),
|
||||
Text(
|
||||
'${'Chassis'.tr}: ${ai.responseIdCardDriverEgyptBack['chassis']}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Color'.tr}: ${ai.responseIdCardDriverEgyptBack['color']}'),
|
||||
Text(
|
||||
'${'Displacement'.tr}: ${ai.responseIdCardDriverEgyptBack['displacement']} cc'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'Fuel'.tr}: ${ai.responseIdCardDriverEgyptBack['fuel']}'),
|
||||
const SizedBox(height: 8.0),
|
||||
if (taxExpiryDateTime != null)
|
||||
Text(
|
||||
'${'Tax Expiry Date'.tr}: $taxExpiryDate',
|
||||
style: TextStyle(
|
||||
color: isExpired ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Inspection Date'.tr}: $carBackLicenseExpired',
|
||||
style: TextStyle(
|
||||
color: isInspectionExpired ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
ai.allMethodForAI((ai.prompts[4]['prompt'].toString()),
|
||||
AppLink.uploadEgypt, 'car_back');
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/4.png',
|
||||
height: Get.height * .25,
|
||||
width: double.maxFinite,
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
Text(
|
||||
'Capture an Image of Your car license back'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
278
siro_driver/lib/views/home/profile/complaint_page.dart
Normal file
278
siro_driver/lib/views/home/profile/complaint_page.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/home/profile/complaint_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/functions/audio_recorder_controller.dart';
|
||||
|
||||
class ComplaintPage extends StatelessWidget {
|
||||
ComplaintPage({super.key});
|
||||
|
||||
final ComplaintController complaintController =
|
||||
Get.put(ComplaintController());
|
||||
final AudioRecorderController audioRecorderController =
|
||||
Get.put(AudioRecorderController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Submit a Complaint'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<ComplaintController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading && controller.ridesList.isEmpty) {
|
||||
return const MyCircularProgressIndicator();
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Form(
|
||||
key: controller.formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
// --- 1. Select Ride Section ---
|
||||
_buildSectionCard(
|
||||
title: '1. Select Ride'.tr,
|
||||
child: controller.ridesList.isEmpty
|
||||
? Text('No rides found to complain about.'.tr,
|
||||
style: AppStyle.subtitle)
|
||||
: DropdownButtonFormField<Map<String, dynamic>>(
|
||||
value: controller.selectedRide,
|
||||
dropdownColor: AppColor.surfaceColor,
|
||||
items: controller.ridesList.map((ride) {
|
||||
return DropdownMenuItem<Map<String, dynamic>>(
|
||||
value: ride,
|
||||
child: Text(
|
||||
'${'Ride'.tr} #${ride['id']} (${ride['date']})',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (ride) {
|
||||
if (ride != null) {
|
||||
controller.selectRide(ride);
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor:
|
||||
AppColor.secondaryColor.withOpacity(0.5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// --- 2. Describe Your Issue Section ---
|
||||
_buildSectionCard(
|
||||
title: '2. Describe Your Issue'.tr,
|
||||
child: TextFormField(
|
||||
controller: controller.complaintController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your complaint here...'.tr,
|
||||
filled: true,
|
||||
fillColor:
|
||||
AppColor.secondaryColor.withOpacity(0.5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
maxLines: 6,
|
||||
style: AppStyle.subtitle,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a description of the issue.'
|
||||
.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// --- 3. Attach Recorded Audio Section ---
|
||||
if (controller.selectedRide != null)
|
||||
_buildSectionCard(
|
||||
title: '3. Attach Recorded Audio (Optional)'.tr,
|
||||
child: FutureBuilder<List<String>>(
|
||||
future: audioRecorderController.getRecordedFiles(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.waiting) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final rideId =
|
||||
controller.selectedRide!['id'].toString();
|
||||
// Filter files to only show the audio file associated with the selected Ride ID
|
||||
final matchingFiles = snapshot.data
|
||||
?.where((path) =>
|
||||
path.endsWith('_${rideId}.m4a'))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (snapshot.hasError || matchingFiles.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0),
|
||||
child: Text(
|
||||
'No audio files found for this ride.'.tr,
|
||||
style: AppStyle.subtitle),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: matchingFiles.map((audioFilePath) {
|
||||
final audioFile = File(audioFilePath);
|
||||
final isUploaded =
|
||||
controller.audioLink.isNotEmpty &&
|
||||
controller.attachedFileName ==
|
||||
audioFilePath.split('/').last;
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isUploaded
|
||||
? Icons.check_circle
|
||||
: Icons.mic,
|
||||
color: isUploaded
|
||||
? AppColor.greenColor
|
||||
: AppColor.redColor),
|
||||
title: Text(audioFilePath.split('/').last,
|
||||
style: AppStyle.subtitle,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
subtitle: isUploaded
|
||||
? Text('Uploaded'.tr,
|
||||
style: const TextStyle(
|
||||
color: AppColor.greenColor))
|
||||
: null,
|
||||
onTap: isUploaded
|
||||
? null
|
||||
: () {
|
||||
MyDialogContent().getDialog(
|
||||
'Confirm Attachment'.tr,
|
||||
Text(
|
||||
'Attach this audio file?'
|
||||
.tr), () async {
|
||||
await controller
|
||||
.uploadAudioFile(audioFile);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// --- 4. Review Details & Response Section ---
|
||||
if (controller.selectedRide != null)
|
||||
_buildSectionCard(
|
||||
title: '4. Review Details & Response'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
Icons.calendar_today_outlined,
|
||||
'Date'.tr,
|
||||
controller.selectedRide!['date'] ?? ''),
|
||||
_buildDetailRow(
|
||||
Icons.monetization_on_outlined,
|
||||
'Price'.tr,
|
||||
'${controller.selectedRide!['price'] ?? ''}'),
|
||||
const Divider(height: 24),
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.support_agent_outlined,
|
||||
color: AppColor.primaryColor),
|
||||
title: Text("Intaleq's Response".tr,
|
||||
style: AppStyle.title),
|
||||
subtitle: Text(
|
||||
controller.driverReport?['body']
|
||||
?.toString() ??
|
||||
'Awaiting response...'.tr,
|
||||
style:
|
||||
AppStyle.subtitle.copyWith(height: 1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- 5. Submit Button ---
|
||||
const SizedBox(height: 24),
|
||||
MyElevatedButton(
|
||||
onPressed: () async {
|
||||
await controller.submitComplaintToServer();
|
||||
},
|
||||
title: 'Submit Complaint'.tr,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.isLoading)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
child: const MyCircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionCard({required String title, required Widget child}) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: AppStyle.headTitle.copyWith(fontSize: 18)),
|
||||
const SizedBox(height: 12),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(IconData icon, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColor.writeColor.withOpacity(0.6), size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text('${label.tr}:',
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.writeColor.withOpacity(0.7))),
|
||||
const Spacer(),
|
||||
Text(value,
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
60
siro_driver/lib/views/home/profile/feed_back_page.dart
Executable file
60
siro_driver/lib/views/home/profile/feed_back_page.dart
Executable file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/profile/feed_back_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
class FeedBackPage extends StatelessWidget {
|
||||
FeedBackPage({super.key});
|
||||
FeedBackController feedBackController = Get.put(FeedBackController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Feed Back'.tr,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(26),
|
||||
child: Form(
|
||||
key: feedBackController.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: feedBackController.feedbackController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'Enter your feedback here'.tr,
|
||||
labelText: 'Feedback',
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your feedback.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
feedBackController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: MyElevatedButton(
|
||||
onPressed: () {
|
||||
if (feedBackController.formKey.currentState!
|
||||
.validate()) {
|
||||
feedBackController.addFeedBack();
|
||||
|
||||
// Clear the feedback form
|
||||
feedBackController.formKey.currentState!.reset();
|
||||
}
|
||||
},
|
||||
title: 'Submit '.tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
isleading: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
303
siro_driver/lib/views/home/profile/passenger_profile_page.dart
Executable file
303
siro_driver/lib/views/home/profile/passenger_profile_page.dart
Executable file
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/profile/profile_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../controller/functions/log_out.dart';
|
||||
|
||||
class PassengerProfilePage extends StatelessWidget {
|
||||
PassengerProfilePage({super.key});
|
||||
LogOutController logOutController = Get.put(LogOutController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(ProfileController());
|
||||
|
||||
return MyScafolld(
|
||||
isleading: true,
|
||||
title: 'My Profile'.tr,
|
||||
body: [
|
||||
GetBuilder<ProfileController>(
|
||||
builder: (controller) => controller.isloading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: SizedBox(
|
||||
height: Get.height,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Edit Profile'.tr,
|
||||
style: AppStyle.headTitle2,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Name'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
leading: const Icon(
|
||||
Icons.person_pin_rounded,
|
||||
size: 35,
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
subtitle: Text(
|
||||
'${controller.prfoileData['first_name']} ${controller.prfoileData['last_name']}'),
|
||||
onTap: () {
|
||||
controller.updatField(
|
||||
'first_name', TextInputType.name);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Gender'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
leading: Image.asset(
|
||||
'assets/images/gender.png',
|
||||
width: 35,
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
subtitle: Text(
|
||||
controller.prfoileData['gender'].toString()),
|
||||
onTap: () {
|
||||
Get.defaultDialog(
|
||||
title: 'Update Gender'.tr,
|
||||
content: Column(
|
||||
children: [
|
||||
GenderPicker(),
|
||||
MyElevatedButton(
|
||||
title: 'Update'.tr,
|
||||
onPressed: () {
|
||||
controller.updateColumn({
|
||||
'id': controller.prfoileData['id']
|
||||
.toString(),
|
||||
'gender': controller.gender,
|
||||
});
|
||||
Get.back();
|
||||
},
|
||||
)
|
||||
],
|
||||
));
|
||||
// controller.updatField('gender');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Education'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
leading: Image.asset(
|
||||
'assets/images/education.png',
|
||||
width: 35,
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
subtitle: Text(controller.prfoileData['education']
|
||||
.toString()),
|
||||
onTap: () {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: true,
|
||||
title: 'Update Education'.tr,
|
||||
content: SizedBox(
|
||||
height: 200,
|
||||
child: Column(
|
||||
children: [
|
||||
EducationDegreePicker(),
|
||||
],
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Update Education'.tr,
|
||||
onPressed: () {
|
||||
controller.updateColumn({
|
||||
'id': controller.prfoileData['id']
|
||||
.toString(),
|
||||
'education':
|
||||
controller.selectedDegree,
|
||||
});
|
||||
Get.back();
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Employment Type'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
leading: Image.asset(
|
||||
'assets/images/employmentType.png',
|
||||
width: 35,
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
subtitle: Text(controller
|
||||
.prfoileData['employmentType']
|
||||
.toString()),
|
||||
onTap: () {
|
||||
controller.updatField(
|
||||
'employmentType', TextInputType.name);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Marital Status'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
leading: Image.asset(
|
||||
'assets/images/maritalStatus.png',
|
||||
width: 35,
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
subtitle: Text(controller
|
||||
.prfoileData['maritalStatus']
|
||||
.toString()),
|
||||
onTap: () {
|
||||
controller.updatField(
|
||||
'maritalStatus', TextInputType.name);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'SOS Phone'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
leading: const Icon(
|
||||
Icons.sos,
|
||||
color: AppColor.redColor,
|
||||
size: 35,
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
subtitle: Text(controller.prfoileData['sosPhone']
|
||||
.toString()),
|
||||
onTap: () async {
|
||||
await controller.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
box.write(BoxName.sosPhonePassenger,
|
||||
controller.prfoileData['sosPhone']);
|
||||
},
|
||||
),
|
||||
// const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Sign Out'.tr,
|
||||
onPressed: () {
|
||||
LogOutController().logOutPassenger();
|
||||
}),
|
||||
GetBuilder<LogOutController>(
|
||||
builder: (logOutController) {
|
||||
return MyElevatedButton(
|
||||
title: 'Delete My Account'.tr,
|
||||
onPressed: () {
|
||||
Get.defaultDialog(
|
||||
title:
|
||||
'Are you sure to delete your account?'
|
||||
.tr,
|
||||
content: Form(
|
||||
key: logOutController.formKey1,
|
||||
child: MyTextForm(
|
||||
controller: logOutController
|
||||
.emailTextController,
|
||||
label: 'Type your Email'.tr,
|
||||
hint: 'Type your Email'.tr,
|
||||
type:
|
||||
TextInputType.emailAddress,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Delete My Account'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () async {
|
||||
await logOutController
|
||||
.deletePassengerAccount();
|
||||
}),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'No I want'.tr,
|
||||
onPressed: () {
|
||||
logOutController
|
||||
.emailTextController
|
||||
.clear();
|
||||
logOutController.update();
|
||||
Get.back();
|
||||
}));
|
||||
});
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GenderPicker extends StatelessWidget {
|
||||
final ProfileController controller = Get.put(ProfileController());
|
||||
|
||||
final List<String> genderOptions = ['Male'.tr, 'Female'.tr, 'Other'.tr];
|
||||
|
||||
GenderPicker({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 100,
|
||||
child: CupertinoPicker(
|
||||
itemExtent: 32.0,
|
||||
onSelectedItemChanged: (int index) {
|
||||
controller.setGender(genderOptions[index]);
|
||||
},
|
||||
children: genderOptions.map((String value) {
|
||||
return Text(value);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EducationDegreePicker extends StatelessWidget {
|
||||
final ProfileController controller = Get.put(ProfileController());
|
||||
|
||||
final List<String> degreeOptions = [
|
||||
'High School Diploma'.tr,
|
||||
'Associate Degree'.tr,
|
||||
'Bachelor\'s Degree'.tr,
|
||||
'Master\'s Degree'.tr,
|
||||
'Doctoral Degree'.tr,
|
||||
];
|
||||
|
||||
EducationDegreePicker({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: CupertinoPicker(
|
||||
// backgroundColor: AppColor.accentColor,
|
||||
// looping: true,
|
||||
squeeze: 2,
|
||||
// diameterRatio: 5,
|
||||
itemExtent: 32,
|
||||
onSelectedItemChanged: (int index) {
|
||||
controller.setDegree(degreeOptions[index]);
|
||||
},
|
||||
children: degreeOptions.map((String value) {
|
||||
return Text(value);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
586
siro_driver/lib/views/home/profile/profile_captain.dart
Executable file
586
siro_driver/lib/views/home/profile/profile_captain.dart
Executable file
@@ -0,0 +1,586 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/profile/captain_profile_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import 'behavior_page.dart';
|
||||
import 'complaint_page.dart';
|
||||
|
||||
// الصفحة الرئيسية الجديدة
|
||||
class ProfileCaptain extends StatelessWidget {
|
||||
const ProfileCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get.put() يجب أن يكون في مكان يتم استدعاؤه مرة واحدة فقط،
|
||||
// لكن سنبقيه هنا حسب الكود الأصلي
|
||||
final controller = Get.put(CaptainProfileController());
|
||||
|
||||
return MyScafolld(
|
||||
title: 'My Profile'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<CaptainProfileController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
if (controller.captainProfileData.isEmpty) {
|
||||
return Center(child: Text('Failed to load profile data.'.tr));
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// 1. رأس الصفحة: صورة واسم وتقييم
|
||||
ProfileHeader(
|
||||
name:
|
||||
'${controller.captainProfileData['first_name'] ?? ''} ${controller.captainProfileData['last_name'] ?? ''}',
|
||||
rating:
|
||||
controller.captainProfileData['ratingDriver'] != null
|
||||
? double.tryParse(controller
|
||||
.captainProfileData['ratingDriver']
|
||||
.toString()) ??
|
||||
0.0
|
||||
: 0.0,
|
||||
ratingCount: int.tryParse(controller
|
||||
.captainProfileData['ratingCount']
|
||||
.toString()) ??
|
||||
0,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 2. قسم الإجراءات السريعة
|
||||
ActionsGrid(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 3. بطاقة المعلومات الشخصية
|
||||
PersonalInfoCard(
|
||||
data: controller.captainProfileData
|
||||
.cast<String, dynamic>()),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 4. بطاقة معلومات المركبة (قابلة للتوسيع)
|
||||
VehicleInfoCard(
|
||||
data: controller.captainProfileData
|
||||
.cast<String, dynamic>()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- الويدجتس الجديدة المنفصلة لتحسين التصميم ---
|
||||
|
||||
/// 1. ويدجت رأس الصفحة
|
||||
class ProfileHeader extends StatelessWidget {
|
||||
final String name;
|
||||
final double rating;
|
||||
final int ratingCount;
|
||||
|
||||
const ProfileHeader({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.rating,
|
||||
required this.ratingCount,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: theme.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.person, size: 60, color: theme.primaryColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
name,
|
||||
style: theme.textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.star, color: Colors.amber, size: 20),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${rating.toStringAsFixed(1)} (${'reviews'.tr} $ratingCount)',
|
||||
style:
|
||||
theme.textTheme.titleMedium?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 2. ويدجت شبكة الأزرار
|
||||
class ActionsGrid extends StatelessWidget {
|
||||
const ActionsGrid({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 2.5, // للتحكم في ارتفاع الأزرار
|
||||
children: [
|
||||
// _ActionTile(
|
||||
// title: 'My Cars'.tr,
|
||||
// icon: Icons.directions_car_filled,
|
||||
// onTap: () => Get.to(() => CaptainsCars()),
|
||||
// ),
|
||||
// _ActionTile(
|
||||
// title: 'Criminal Record'.tr,
|
||||
// icon: Icons.description,
|
||||
// onTap: () => Get.to(() => CriminalDocumemtPage()),
|
||||
// ),
|
||||
_ActionTile(
|
||||
title: 'ShamCash Account'.tr, // غيرت الاسم ليكون أوضح
|
||||
icon: Icons.account_balance_wallet_rounded, // أيقونة محفظة
|
||||
// trailing: Icon(Icons.arrow_forward_ios,
|
||||
// size: 16, color: Colors.grey), // سهم صغير للجمالية
|
||||
onTap: () {
|
||||
// استدعاء دالة فتح النافذة
|
||||
showShamCashInput();
|
||||
},
|
||||
),
|
||||
_ActionTile(
|
||||
title: 'Behavior Page'.tr,
|
||||
icon: Icons.checklist_rtl,
|
||||
onTap: () => Get.to(() => BehaviorPage()),
|
||||
),
|
||||
_ActionTile(
|
||||
title: 'Submit a Complaint'.tr,
|
||||
icon: Icons.note_add_rounded,
|
||||
onTap: () => Get.to(() => ComplaintPage()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showShamCashInput() {
|
||||
// 1. القراءة من الذاكرة المحلية (GetStorage) عند فتح النافذة
|
||||
// إذا لم يتم العثور على قيمة، يتم تعيينها إلى نص فارغ
|
||||
final String existingName = box.read('shamcash_name') ?? '';
|
||||
final String existingCode = box.read('shamcash_code') ?? '';
|
||||
|
||||
// تعريف أدوات التحكم للحقلين مع تحميل القيمة المحفوظة
|
||||
final TextEditingController nameController =
|
||||
TextEditingController(text: existingName);
|
||||
final TextEditingController codeController =
|
||||
TextEditingController(text: existingCode);
|
||||
|
||||
Get.bottomSheet(
|
||||
Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(25),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.shadowColor.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2))
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// --- 1. المقبض العلوي ---
|
||||
Center(
|
||||
child: Container(
|
||||
height: 5,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor,
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
),
|
||||
),
|
||||
|
||||
// --- 2. العنوان والأيقونة ---
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
height: 50,
|
||||
),
|
||||
// const Icon(Icons.account_balance_wallet_rounded,
|
||||
// size: 45, color: Colors.blueAccent),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"ربط حساب شام كاش 🔗",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blueGrey[900]),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Text(
|
||||
"أدخل بيانات حسابك لاستقبال الأرباح فوراً",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// --- 3. الحقل الأول: اسم الحساب (أعلى الباركود) ---
|
||||
const Text("1. اسم الحساب (أعلى الباركود)",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "مثال: intaleq",
|
||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: const Icon(Icons.person_outline_rounded,
|
||||
color: Colors.blueGrey),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 15, horizontal: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// --- 4. الحقل الثاني: الكود (أسفل الباركود) ---
|
||||
const Text("2. كود المحفظة (أسفل الباركود)",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: TextField(
|
||||
controller: codeController,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
letterSpacing: 0.5), // خط أصغر قليلاً للكود الطويل
|
||||
decoration: InputDecoration(
|
||||
hintText: "مثال: 80f23afe40...",
|
||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: const Icon(Icons.qr_code_2_rounded,
|
||||
color: Colors.blueGrey),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 15, horizontal: 10),
|
||||
|
||||
// زر لصق الكود
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.paste_rounded, color: Colors.blue),
|
||||
tooltip: "لصق الكود",
|
||||
onPressed: () async {
|
||||
ClipboardData? data =
|
||||
await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
codeController.text = data.text!;
|
||||
// تحريك المؤشر للنهاية بعد اللصق
|
||||
codeController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: codeController.text.length),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// --- 5. زر الحفظ ---
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
String name = nameController.text.trim();
|
||||
String code = codeController.text.trim();
|
||||
|
||||
// التحقق من صحة البيانات
|
||||
if (name.isNotEmpty && code.length > 5) {
|
||||
// 1. إرسال البيانات إلى السيرفر
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.updateShamCashDriver, payload: {
|
||||
"id": box.read(BoxName.driverID),
|
||||
"accountBank": name,
|
||||
"bankCode": code,
|
||||
});
|
||||
|
||||
if (res != 'failure') {
|
||||
// 2. 🔴 الحفظ في الذاكرة المحلية (GetStorage) بعد نجاح التحديث
|
||||
box.write('shamcash_name', name);
|
||||
box.write('shamcash_code', code);
|
||||
|
||||
Get.back(); // إغلاق النافذة
|
||||
Get.snackbar(
|
||||
"تم الحفظ بنجاح",
|
||||
"تم ربط حساب ($name) لاستلام الأرباح.",
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(20),
|
||||
icon: const Icon(Icons.check_circle_outline,
|
||||
color: Colors.white),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
// في حال فشل الإرسال إلى السيرفر
|
||||
Get.snackbar(
|
||||
"خطأ في السيرفر",
|
||||
"فشل تحديث البيانات، يرجى المحاولة لاحقاً.",
|
||||
backgroundColor: Colors.redAccent,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(20),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"بيانات ناقصة",
|
||||
"يرجى التأكد من إدخال الاسم والكود بشكل صحيح.",
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(20),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2ecc71), // الأخضر المالي
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 2,
|
||||
),
|
||||
child: const Text(
|
||||
"حفظ وتفعيل الحساب",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10), // مسافة سفلية إضافية للأمان
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// ويدجت داخلية لزر في الشبكة
|
||||
class _ActionTile extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionTile(
|
||||
{required this.title, required this.icon, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: theme.primaryColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.labelLarge,
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 3. بطاقة المعلومات الشخصية
|
||||
class PersonalInfoCard extends StatelessWidget {
|
||||
final Map<String, dynamic> data;
|
||||
PersonalInfoCard({super.key, required this.data});
|
||||
final controller = Get.find<CaptainProfileController>();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Personal Information'.tr, style: Get.textTheme.titleLarge),
|
||||
const Divider(height: 24),
|
||||
_InfoRow(
|
||||
icon: Icons.phone,
|
||||
label: 'Phone Number'.tr,
|
||||
value: data['phone'] ?? ''),
|
||||
if (data['email'] != null &&
|
||||
data['email'].toString().contains('intaleqapp')) ...[
|
||||
TextFormField(
|
||||
controller: controller.emailController,
|
||||
keyboardType:
|
||||
TextInputType.emailAddress, // ✅ لوحة مفاتيح خاصة بالإيميل
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Email'.tr,
|
||||
hintText: 'Enter your email'.tr,
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
autofillHints: [
|
||||
AutofillHints.email
|
||||
], // اختياري لتحسين تجربة المستخدم
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await controller.updateEmail();
|
||||
},
|
||||
child: Text('Update'.tr),
|
||||
),
|
||||
] else
|
||||
_InfoRow(
|
||||
icon: Icons.email,
|
||||
label: 'Email'.tr,
|
||||
value: data['email'] ?? '',
|
||||
),
|
||||
_InfoRow(
|
||||
icon: Icons.cake,
|
||||
label: 'Age'.tr,
|
||||
value: data['age']?.toString() ?? 'N/A'),
|
||||
_InfoRow(
|
||||
icon: Icons.wc,
|
||||
label: 'Gender'.tr,
|
||||
value: data['gender'] ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 4. بطاقة معلومات المركبة
|
||||
class VehicleInfoCard extends StatelessWidget {
|
||||
final Map<String, dynamic> data;
|
||||
const VehicleInfoCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ExpansionTile(
|
||||
title: Text('Vehicle Details'.tr, style: Get.textTheme.titleLarge),
|
||||
leading: Icon(Icons.directions_car, color: Get.theme.primaryColor),
|
||||
childrenPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
children: [
|
||||
_InfoRow(
|
||||
icon: Icons.branding_watermark,
|
||||
label: 'Make'.tr,
|
||||
value: data['make'] ?? ''),
|
||||
_InfoRow(
|
||||
icon: Icons.category,
|
||||
label: 'Model'.tr,
|
||||
value: data['model'] ?? ''),
|
||||
_InfoRow(
|
||||
icon: Icons.palette,
|
||||
label: 'Color'.tr,
|
||||
value: data['color'] ?? ''),
|
||||
_InfoRow(
|
||||
icon: Icons.pin,
|
||||
label: 'Plate Number'.tr,
|
||||
value: data['car_plate'] ?? ''),
|
||||
_InfoRow(
|
||||
icon: Icons.confirmation_number,
|
||||
label: 'VIN'.tr,
|
||||
value: data['vin'] ?? ''),
|
||||
_InfoRow(
|
||||
icon: Icons.event,
|
||||
label: 'Expiration Date'.tr,
|
||||
value: data['expiration_date'] ?? ''),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ويدجت لعرض سطر معلومة (أيقونة + عنوان + قيمة) لتجنب التكرار
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoRow(
|
||||
{required this.icon, required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: theme.hintColor.withOpacity(0.6), size: 20),
|
||||
const SizedBox(width: 16),
|
||||
Text(label, style: theme.textTheme.bodyLarge),
|
||||
const Spacer(),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.textTheme.bodyLarge?.color?.withOpacity(0.8),
|
||||
fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
146
siro_driver/lib/views/home/profile/promos_passenger_page.dart
Executable file
146
siro_driver/lib/views/home/profile/promos_passenger_page.dart
Executable file
@@ -0,0 +1,146 @@
|
||||
import 'package:animated_text_kit/animated_text_kit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/profile/promos_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
|
||||
class PromosPassengerPage extends StatelessWidget {
|
||||
const PromosPassengerPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(PromosController());
|
||||
return MyScafolld(
|
||||
title: 'Promos For today'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<PromosController>(
|
||||
builder: (orderHistoryController) => orderHistoryController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: ListView.builder(
|
||||
itemCount: orderHistoryController.promoList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final rides = orderHistoryController.promoList[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shadowColor:
|
||||
Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: AnimatedTextKit(
|
||||
animatedTexts: [
|
||||
ScaleAnimatedText(
|
||||
rides['promo_code'],
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
color: AppColor
|
||||
.primaryColor,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
)),
|
||||
WavyAnimatedText(
|
||||
rides['promo_code'],
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
color: AppColor
|
||||
.primaryColor,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
)),
|
||||
],
|
||||
repeatForever: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
rides['description'],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_buildDateBadge(context,
|
||||
rides['validity_start_date'], true),
|
||||
const SizedBox(height: 4),
|
||||
_buildDateBadge(context,
|
||||
rides['validity_end_date'], false),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Copy this Promo to use it in your Ride!'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateBadge(BuildContext context, String date, bool isStart) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isStart
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isStart ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
siro_driver/lib/views/home/profile/taarif_page.dart
Executable file
95
siro_driver/lib/views/home/profile/taarif_page.dart
Executable file
@@ -0,0 +1,95 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
|
||||
class TaarifPage extends StatelessWidget {
|
||||
const TaarifPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(isleading: true, title: 'Tariffs'.tr, body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: ListView(
|
||||
// mainAxisAlignment: MainAxisAlignment.start,
|
||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Minimum fare'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '1 ${'JOD'.tr}'
|
||||
: '20 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Maximum fare'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '200 ${'JOD'.tr}'
|
||||
: '15000 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Flag-down fee'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '0.47 ${'JOD'.tr}'
|
||||
: '15 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Rate'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '0.05 ${'JOD'.tr}/min and 0.21 ${'JOD'.tr}/km'
|
||||
: '1 ${'LE'.tr}/min and 4 ${'LE'.tr}/km'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text('Including Tax'.tr,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: Theme.of(context).hintColor)),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text('BookingFee'.tr, style: AppStyle.headTitle2),
|
||||
const SizedBox(height: 10),
|
||||
Text('10%', style: AppStyle.title),
|
||||
const SizedBox(height: 20),
|
||||
Text('Morning'.tr, style: AppStyle.headTitle2),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'from 07:30 till 10:30 (Thursday, Friday, Saturday, Monday)'.tr,
|
||||
style: AppStyle.title),
|
||||
const SizedBox(height: 20),
|
||||
Text('Evening'.tr, style: AppStyle.headTitle2),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'from 12:00 till 15:00 (Thursday, Friday, Saturday, Monday)'.tr,
|
||||
style: AppStyle.title),
|
||||
const SizedBox(height: 20),
|
||||
Text('Night'.tr, style: AppStyle.headTitle2),
|
||||
const SizedBox(height: 10),
|
||||
Text('from 23:59 till 05:30'.tr, style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTariffItem(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(label, style: AppStyle.title),
|
||||
),
|
||||
Text(value,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold, color: AppColor.primaryColor)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
519
siro_driver/lib/views/home/statistics/statistics_dashboard.dart
Normal file
519
siro_driver/lib/views/home/statistics/statistics_dashboard.dart
Normal file
@@ -0,0 +1,519 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/controller/gamification/gamification_controller.dart';
|
||||
import 'package:siro_driver/controller/home/statistics/statistics_controller.dart';
|
||||
|
||||
import 'widgets/stat_summary_card.dart';
|
||||
import 'widgets/daily_goal_widget.dart';
|
||||
import 'widgets/level_progress_widget.dart';
|
||||
import 'widgets/today_chart_widget.dart';
|
||||
import 'widgets/weekly_chart_widget.dart';
|
||||
import 'widgets/monthly_chart_widget.dart';
|
||||
|
||||
class StatisticsDashboard extends StatelessWidget {
|
||||
StatisticsDashboard({super.key});
|
||||
|
||||
final GamificationController gamController =
|
||||
Get.put(GamificationController());
|
||||
final StatisticsController statsController = Get.put(StatisticsController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
body: GetBuilder<GamificationController>(
|
||||
builder: (gc) {
|
||||
return GetBuilder<StatisticsController>(
|
||||
builder: (sc) {
|
||||
if (gc.isLoading || sc.isLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
);
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// ═══════ App Bar ═══════
|
||||
SliverAppBar(
|
||||
expandedHeight: 200,
|
||||
pinned: true,
|
||||
stretch: true,
|
||||
backgroundColor: Get.isDarkMode
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: const Color(0xFF0A0E21),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: Colors.white, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded,
|
||||
color: Colors.white),
|
||||
onPressed: () => gc.fetchGamificationData(),
|
||||
),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Statistics'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
),
|
||||
),
|
||||
// أيقونة زخرفية
|
||||
Positioned(
|
||||
right: -40,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
Icons.bar_chart_rounded,
|
||||
size: 200,
|
||||
color: Colors.white.withOpacity(0.04),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -30,
|
||||
bottom: 20,
|
||||
child: Icon(
|
||||
Icons.emoji_events_rounded,
|
||||
size: 120,
|
||||
color: Colors.white.withOpacity(0.03),
|
||||
),
|
||||
),
|
||||
// بطاقة المستوى في الهيدر
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
gc.currentLevel.emoji,
|
||||
style: const TextStyle(fontSize: 40),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
Get.locale?.languageCode == 'ar'
|
||||
? gc.currentLevel.nameAr
|
||||
: gc.currentLevel.nameEn,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${gc.unlockedCount}/${gc.totalAchievements} ${'Achievements'.tr}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ═══════ المحتوى ═══════
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// 1. بطاقات الإحصائيات الأربعة
|
||||
_buildSummaryCards(gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 2. الهدف اليومي
|
||||
DailyGoalWidget(controller: gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. تقدم المستوى
|
||||
LevelProgressWidget(controller: gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 4. إحصائيات اليوم
|
||||
TodayChartWidget(),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 5. الرسم البياني الأسبوعي
|
||||
const WeeklyChartWidget(),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 6. التقرير الشهري
|
||||
const MonthlyChartWidget(),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 7. تقييم سلوك القيادة
|
||||
_buildBehaviorSection(gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 8. الإنجازات
|
||||
_buildAchievementsSection(gc),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ بطاقات الإحصائيات ═══════
|
||||
Widget _buildSummaryCards(GamificationController gc) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
StatSummaryCard(
|
||||
icon: Icons.local_taxi_rounded,
|
||||
label: 'Total Trips'.tr,
|
||||
value: gc.totalTrips.toString(),
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
StatSummaryCard(
|
||||
icon: Icons.star_rounded,
|
||||
label: 'Rating'.tr,
|
||||
value: gc.averageRating.toStringAsFixed(1),
|
||||
color: const Color(0xFFFFD700),
|
||||
),
|
||||
StatSummaryCard(
|
||||
icon: Icons.whatshot_rounded,
|
||||
label: 'Day Streak'.tr,
|
||||
value: '${gc.consecutiveDays}',
|
||||
color: const Color(0xFFFF5722),
|
||||
),
|
||||
StatSummaryCard(
|
||||
icon: Icons.people_rounded,
|
||||
label: 'Referrals'.tr,
|
||||
value: gc.totalReferrals.toString(),
|
||||
color: const Color(0xFF9C27B0),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ قسم تقييم السلوك ═══════
|
||||
Widget _buildBehaviorSection(GamificationController gc) {
|
||||
bool isExcellent = gc.behaviorScore >= 90;
|
||||
bool isGood = gc.behaviorScore >= 75 && gc.behaviorScore < 90;
|
||||
|
||||
Color statusColor = isExcellent
|
||||
? Colors.green
|
||||
: isGood
|
||||
? Colors.orange
|
||||
: Colors.red;
|
||||
|
||||
String statusText = isExcellent
|
||||
? 'Excellent'.tr
|
||||
: isGood
|
||||
? 'Good'.tr
|
||||
: 'Needs Improvement'.tr;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.shield_rounded,
|
||||
color: FinanceDesignSystem.accentBlue),
|
||||
const SizedBox(width: 8),
|
||||
Text('Driving Behavior'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_behaviorStatItem(
|
||||
title: 'Score'.tr,
|
||||
value: '${gc.behaviorScore}%',
|
||||
icon: Icons.speed_rounded,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
_behaviorStatItem(
|
||||
title: 'Max Speed'.tr,
|
||||
value: '${gc.maxSpeed.toStringAsFixed(0)} km/h',
|
||||
icon: Icons.directions_car_rounded,
|
||||
color: Colors.orange,
|
||||
),
|
||||
_behaviorStatItem(
|
||||
title: 'Hard Brakes'.tr,
|
||||
value: '${gc.hardBrakes}',
|
||||
icon: Icons.warning_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: gc.behaviorScore / 100,
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(statusColor),
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _behaviorStatItem(
|
||||
{required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color}) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: FinanceDesignSystem.textSecondary)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ قسم الإنجازات ═══════
|
||||
Widget _buildAchievementsSection(GamificationController gc) {
|
||||
final unlockedAch = gc.achievements.where((a) => a.isUnlocked).toList();
|
||||
final lockedAch = gc.achievements.where((a) => !a.isUnlocked).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Achievements'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${gc.unlockedCount}/${gc.totalAchievements}',
|
||||
style: TextStyle(
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// الإنجازات المفتوحة
|
||||
if (unlockedAch.isNotEmpty) ...[
|
||||
...unlockedAch.map((a) => _buildAchievementCard(a, true)),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// الإنجازات المقفلة
|
||||
...lockedAch.map((a) => _buildAchievementCard(a, false)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAchievementCard(Achievement ach, bool unlocked) {
|
||||
final isAr = Get.locale?.languageCode == 'ar';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: unlocked ? Colors.white : Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
border: unlocked
|
||||
? Border.all(color: ach.color.withOpacity(0.3), width: 1.5)
|
||||
: null,
|
||||
boxShadow: unlocked
|
||||
? [
|
||||
BoxShadow(
|
||||
color: ach.color.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// أيقونة الإنجاز
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
unlocked ? ach.color.withOpacity(0.15) : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(
|
||||
ach.icon,
|
||||
color: unlocked ? ach.color : Colors.grey.shade400,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// المعلومات
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isAr ? ach.titleAr : ach.titleEn,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: unlocked
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
isAr ? ach.descriptionAr : ach.descriptionEn,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
if (!unlocked) ...[
|
||||
const SizedBox(height: 8),
|
||||
// شريط التقدم
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
FractionallySizedBox(
|
||||
widthFactor: ach.progress,
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: ach.color.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${ach.currentProgress}/${ach.target}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey.shade400,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// أيقونة الحالة
|
||||
if (unlocked)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: ach.color.withOpacity(0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_rounded,
|
||||
color: ach.color,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
color: Colors.grey.shade300,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/gamification/gamification_controller.dart';
|
||||
|
||||
class DailyGoalWidget extends StatelessWidget {
|
||||
final GamificationController controller;
|
||||
|
||||
const DailyGoalWidget({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen.withOpacity(0.1)
|
||||
: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
controller.isDailyGoalMet
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.track_changes_rounded,
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Daily Goal'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Daily Goal'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// زر تعديل الهدف
|
||||
InkWell(
|
||||
onTap: () => _showSetGoalDialog(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.dailyGoal > 0 ? 'Edit'.tr : 'Set Goal'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// مبلغ الأرباح والهدف
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
controller.dailyEarnings.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.primaryDark,
|
||||
fontFamily: 'digit',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'/ ${controller.dailyGoal.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: FinanceDesignSystem.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// شريط التقدم
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 12,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOutCubic,
|
||||
height: 12,
|
||||
width: constraints.maxWidth * controller.dailyGoalProgress,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: controller.isDailyGoalMet
|
||||
? [
|
||||
FinanceDesignSystem.successGreen,
|
||||
const Color(0xFF69F0AE)
|
||||
]
|
||||
: [
|
||||
FinanceDesignSystem.accentBlue,
|
||||
const Color(0xFF82B1FF)
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue)
|
||||
.withOpacity(0.3),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(controller.dailyGoalProgress * 100).toStringAsFixed(0)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
),
|
||||
if (controller.isDailyGoalMet)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.celebration_rounded,
|
||||
color: Color(0xFFFFD700), size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Goal Achieved!'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'${'Remaining:'.tr} ${(controller.dailyGoal - controller.dailyEarnings).clamp(0, double.infinity).toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSetGoalDialog(BuildContext context) {
|
||||
final textController = TextEditingController(
|
||||
text: controller.dailyGoal > 0
|
||||
? controller.dailyGoal.toStringAsFixed(0)
|
||||
: '',
|
||||
);
|
||||
|
||||
Get.defaultDialog(
|
||||
backgroundColor: FinanceDesignSystem.cardColor,
|
||||
title: 'Set Daily Goal'.tr,
|
||||
titleStyle: FinanceDesignSystem.headingStyle,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
'How much do you want to earn today?'.tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'digit',
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '5000',
|
||||
suffixText: 'SYP'.tr,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: FinanceDesignSystem.borderColor),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: FinanceDesignSystem.accentBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// الأهداف السريعة
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [1000, 3000, 5000, 10000].map((goal) {
|
||||
return ActionChip(
|
||||
label: Text('$goal'),
|
||||
labelStyle: const TextStyle(fontSize: 12),
|
||||
backgroundColor:
|
||||
FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||
side: BorderSide.none,
|
||||
onPressed: () {
|
||||
textController.text = goal.toString();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () {
|
||||
double? goal = double.tryParse(textController.text);
|
||||
if (goal != null && goal > 0) {
|
||||
controller.setDailyGoal(goal);
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: FinanceDesignSystem.accentBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
),
|
||||
child: Text('Save'.tr),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/gamification/gamification_controller.dart';
|
||||
|
||||
class LevelProgressWidget extends StatelessWidget {
|
||||
final GamificationController controller;
|
||||
const LevelProgressWidget({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final level = controller.currentLevel;
|
||||
final next = controller.nextLevel;
|
||||
final isAr = Get.locale?.languageCode == 'ar';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
level.color.withOpacity(0.08),
|
||||
level.gradientEnd.withOpacity(0.04)
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
border: Border.all(color: level.color.withOpacity(0.2), width: 1.5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Driver Level'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
LinearGradient(colors: [level.color, level.gradientEnd]),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(level.emoji, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: 6),
|
||||
Text(isAr ? level.nameAr : level.nameEn,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13)),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: List.generate(DriverLevels.all.length, (i) {
|
||||
final lvl = DriverLevels.all[i];
|
||||
final isCurrent = lvl.id == level.id;
|
||||
final isPast = DriverLevels.all.indexOf(level) > i;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrent
|
||||
? lvl.color
|
||||
: isPast
|
||||
? lvl.color.withOpacity(0.6)
|
||||
: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
));
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: DriverLevels.all
|
||||
.map((l) => Expanded(
|
||||
child: Text(l.emoji,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(fontSize: l.id == level.id ? 16 : 12))))
|
||||
.toList()),
|
||||
const SizedBox(height: 16),
|
||||
if (next != null) ...[
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text('${'Next Level:'.tr} ${isAr ? next.nameAr : next.nameEn}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: FinanceDesignSystem.textSecondary)),
|
||||
Text('${(controller.progressToNext * 100).toStringAsFixed(0)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: level.color)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Stack(children: [
|
||||
Container(
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(5))),
|
||||
LayoutBuilder(
|
||||
builder: (ctx, c) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOutCubic,
|
||||
height: 10,
|
||||
width: c.maxWidth * controller.progressToNext,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [level.color, level.gradientEnd]),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
)),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
Text('${controller.totalPoints} / ${next.minPoints} ${'Points'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textMuted,
|
||||
fontFamily: 'digit')),
|
||||
] else
|
||||
Center(
|
||||
child: Text('🏆 ${'Maximum Level Reached!'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: level.color))),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: level.perks
|
||||
.map((p) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: level.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(p.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: level.color.withOpacity(0.8))),
|
||||
))
|
||||
.toList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/home/statistics/statistics_controller.dart';
|
||||
|
||||
class MonthlyChartWidget extends StatelessWidget {
|
||||
const MonthlyChartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<StatisticsController>(
|
||||
builder: (sc) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('Monthly Report'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 16),
|
||||
// Summary Row
|
||||
Row(children: [
|
||||
_summaryTile(
|
||||
'Total Earnings'.tr,
|
||||
'${sc.monthlyTotalEarnings.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
FinanceDesignSystem.successGreen),
|
||||
const SizedBox(width: 12),
|
||||
_summaryTile('Total Trips'.tr, '${sc.monthlyTotalTrips}',
|
||||
FinanceDesignSystem.accentBlue),
|
||||
const SizedBox(width: 12),
|
||||
_summaryTile(
|
||||
'Best Day'.tr, '${sc.bestDay}', const Color(0xFFFFD700)),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
// Monthly Earnings Line Chart
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: sc.monthlyEarnings.isEmpty
|
||||
? Center(
|
||||
child: Text('No data yet'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade400)))
|
||||
: LineChart(LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (spots) => spots
|
||||
.map((s) => LineTooltipItem(
|
||||
'${s.y.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (v) => FlLine(
|
||||
color: FinanceDesignSystem.borderColor,
|
||||
strokeWidth: 1),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 5,
|
||||
getTitlesWidget: (v, m) => Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text('${v.toInt()}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: FinanceDesignSystem.textSecondary)),
|
||||
),
|
||||
)),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: sc.monthlyEarnings
|
||||
.map((e) =>
|
||||
FlSpot(e.day.toDouble(), e.pricePerDay))
|
||||
.toList(),
|
||||
isCurved: true,
|
||||
curveSmoothness: 0.3,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
barWidth: 3,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (s, p, d, i) => FlDotCirclePainter(
|
||||
radius: 3,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
strokeWidth: 1,
|
||||
strokeColor: Colors.white),
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
FinanceDesignSystem.accentBlue.withOpacity(0.2),
|
||||
FinanceDesignSystem.accentBlue.withOpacity(0.0)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _summaryTile(String label, String value, Color color) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Column(children: [
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w800, color: color)),
|
||||
const SizedBox(height: 2),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: FinanceDesignSystem.textSecondary),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class StatSummaryCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
|
||||
const StatSummaryCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
Icon(
|
||||
Icons.trending_up_rounded,
|
||||
color: Colors.grey.shade300,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
fontFamily: 'digit',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class TodayChartWidget extends StatelessWidget {
|
||||
TodayChartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<HomeCaptainController>(
|
||||
builder: (hc) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Today Overview'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Summary of your daily activity'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: FinanceDesignSystem.textSecondary)),
|
||||
const SizedBox(height: 20),
|
||||
_buildRow(
|
||||
Icons.monetization_on_rounded,
|
||||
'Earnings'.tr,
|
||||
'${hc.totalMoneyToday} ${'SYP'.tr}',
|
||||
FinanceDesignSystem.successGreen),
|
||||
const Divider(height: 24),
|
||||
_buildRow(Icons.local_taxi_rounded, 'Rides'.tr, hc.countRideToday,
|
||||
FinanceDesignSystem.accentBlue),
|
||||
const Divider(height: 24),
|
||||
Obx(() => _buildRow(Icons.timer_rounded, 'Online Duration'.tr,
|
||||
hc.totalDurationDisplay.value, const Color(0xFFFF9800))),
|
||||
const Divider(height: 24),
|
||||
_buildRow(Icons.cancel_outlined, 'Refused'.tr, hc.countRefuse,
|
||||
FinanceDesignSystem.dangerRed),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(IconData icon, String label, String value, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: FinanceDesignSystem.primaryDark))),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
fontFamily: 'digit')),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/home/statistics/statistics_controller.dart';
|
||||
|
||||
class WeeklyChartWidget extends StatelessWidget {
|
||||
const WeeklyChartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<StatisticsController>(
|
||||
builder: (sc) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text('Weekly Earnings'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.successGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(
|
||||
'${sc.weeklyEarnings.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.successGreen)),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
_miniStat(Icons.local_taxi_rounded, '${sc.weeklyTrips}',
|
||||
'Rides'.tr, FinanceDesignSystem.accentBlue),
|
||||
const SizedBox(width: 16),
|
||||
_miniStat(
|
||||
Icons.timer_rounded,
|
||||
'${sc.weeklyHours.toStringAsFixed(1)}h',
|
||||
'Hours'.tr,
|
||||
const Color(0xFFFF9800)),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: sc.weeklyStats.isEmpty
|
||||
? Center(
|
||||
child: Text('No data yet'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade400)))
|
||||
: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: _getMaxY(sc.weeklyStats),
|
||||
barTouchData: BarTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipItem: (group, gi, rod, ri) =>
|
||||
BarTooltipItem(
|
||||
'${rod.toY.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (v, m) {
|
||||
final idx = v.toInt();
|
||||
if (idx >= 0 &&
|
||||
idx < sc.weeklyStats.length) {
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
sc.weeklyStats[idx].dayName.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: FinanceDesignSystem
|
||||
.textSecondary)));
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
})),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
gridData: const FlGridData(show: false),
|
||||
barGroups: List.generate(sc.weeklyStats.length, (i) {
|
||||
final stat = sc.weeklyStats[i];
|
||||
final isToday = i == sc.weeklyStats.length - 1;
|
||||
return BarChartGroupData(x: i, barRods: [
|
||||
BarChartRodData(
|
||||
toY: stat.earnings,
|
||||
width: 20,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(6)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: isToday
|
||||
? [
|
||||
FinanceDesignSystem.accentBlue,
|
||||
const Color(0xFF82B1FF)
|
||||
]
|
||||
: [
|
||||
FinanceDesignSystem.primaryDark
|
||||
.withOpacity(0.6),
|
||||
FinanceDesignSystem.primaryDark
|
||||
.withOpacity(0.3)
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _miniStat(IconData icon, String value, String label, Color color) {
|
||||
return Row(children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(width: 4),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: FinanceDesignSystem.textSecondary)),
|
||||
]);
|
||||
}
|
||||
|
||||
double _getMaxY(List<DayStat> stats) {
|
||||
if (stats.isEmpty) return 100;
|
||||
final max = stats.map((s) => s.earnings).reduce((a, b) => a > b ? a : b);
|
||||
return max <= 0 ? 100 : max * 1.2;
|
||||
}
|
||||
}
|
||||
145
siro_driver/lib/views/lang/languages.dart
Executable file
145
siro_driver/lib/views/lang/languages.dart
Executable file
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
|
||||
import '../../constant/finance_design_system.dart';
|
||||
import '../../controller/local/local_controller.dart';
|
||||
|
||||
class Language extends StatelessWidget {
|
||||
const Language({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text('Choose Language'.tr),
|
||||
border: null,
|
||||
),
|
||||
child: Material(
|
||||
// Wrap SafeArea with Material widget
|
||||
child: SafeArea(
|
||||
child: GetBuilder<LocaleController>(
|
||||
builder: (controller) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
_buildLanguageButton(
|
||||
'العربية', 'ar', controller, context, 'AR'),
|
||||
_buildLanguageButton(
|
||||
'English', 'en', controller, context, 'EN'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Language Options'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.black, // Or your theme primary color
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Select your preferred language for the app interface.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: CupertinoColors.secondaryLabel,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLanguageButton(String title, String langCode,
|
||||
LocaleController controller, BuildContext context, String flagIcon) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
leading: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.primaryDark.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
flagIcon,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(CupertinoIcons.chevron_forward,
|
||||
color: CupertinoColors.inactiveGray),
|
||||
onTap: () async {
|
||||
controller.changeLang(langCode);
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text('You should restart app to change language'.tr),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text('Ok'.tr),
|
||||
onPressed: () {
|
||||
Get.offAll(() => HomeCaptain());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
404
siro_driver/lib/views/notification/available_rides_page.dart
Executable file
404
siro_driver/lib/views/notification/available_rides_page.dart
Executable file
@@ -0,0 +1,404 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart'; // لتحديد الأنواع إذا لزم
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../../controller/home/captin/home_captain_controller.dart';
|
||||
import '../../controller/notification/ride_available_controller.dart';
|
||||
import '../../main.dart'; // للوصول للـ box
|
||||
import '../home/Captin/driver_map_page.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import '../widgets/mycircular.dart';
|
||||
import '../widgets/mydialoug.dart';
|
||||
|
||||
class AvailableRidesPage extends StatelessWidget {
|
||||
const AvailableRidesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حقن الكنترولر (تأكد أنك تستخدم الكود الجديد الذي أعطيتك إياه للكنترولر)
|
||||
Get.lazyPut(() => RideAvailableController());
|
||||
Get.lazyPut(() => HomeCaptainController());
|
||||
|
||||
return GetBuilder<RideAvailableController>(
|
||||
builder: (controller) {
|
||||
return MyScafolld(
|
||||
title: 'Available for rides'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
controller.isLoading
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 50.0),
|
||||
child: MyCircularProgressIndicator(),
|
||||
))
|
||||
: Builder(
|
||||
builder: (context) {
|
||||
// 1. الفلترة حسب نوع السيارة (تم نقل المنطق للكنترولر، لكن هنا للعرض فقط)
|
||||
// الكنترولر الجديد يفلتر عند الإضافة، لكن لا ضرر من التأكيد هنا
|
||||
final ridesList = controller.availableRides;
|
||||
|
||||
if (ridesList.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
Icon(CupertinoIcons.car_detailed,
|
||||
size: 60,
|
||||
color:
|
||||
AppColor.primaryColor.withOpacity(0.5)),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"No rides available right now.".tr,
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.getRideAvailable(
|
||||
forceRefresh: true),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text("Refresh Market".tr),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. عرض القائمة
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.getRideAvailable(forceRefresh: true);
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 16),
|
||||
itemCount: ridesList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RideAvailableCard(
|
||||
rideInfo: ridesList[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// بطاقة الرحلة (The Card)
|
||||
// =============================================================================
|
||||
class RideAvailableCard extends StatelessWidget {
|
||||
final Map<String, dynamic> rideInfo;
|
||||
|
||||
const RideAvailableCard({Key? key, required this.rideInfo}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 4,
|
||||
shadowColor: Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRouteInfo(),
|
||||
const Divider(height: 24, thickness: 0.5),
|
||||
_buildRideDetails(),
|
||||
const SizedBox(height: 20),
|
||||
_buildAcceptButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// تصميم البطاقة (Header, Route, Details)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Price'.tr, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
Text(
|
||||
'${rideInfo['price']} ${'SYP'.tr}', // العملة
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 20, color: AppColor.primaryColor, height: 1.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.greenColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColor.greenColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Text(
|
||||
rideInfo['carType'] ?? 'Fixed Price'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.greenColor, fontSize: 13),
|
||||
),
|
||||
),
|
||||
if (rideInfo['has_steps']?.toString() == 'true') ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.alt_route, color: Colors.orange.shade800, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'متعددة التوقفات',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: Colors.orange.shade800,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRouteInfo() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const Icon(Icons.my_location,
|
||||
color: AppColor.primaryColor, size: 18),
|
||||
Container(
|
||||
height: 30,
|
||||
width: 1,
|
||||
color: Theme.of(Get.context!).dividerColor,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
),
|
||||
const Icon(Icons.location_on, color: Colors.red, size: 18),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(rideInfo['startName'] ?? 'Unknown Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.title.copyWith(fontSize: 14)),
|
||||
const SizedBox(height: 22),
|
||||
Text(rideInfo['endName'] ?? 'Destination'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.title.copyWith(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideDetails() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_infoItem(Icons.social_distance, '${rideInfo['distance']} KM'),
|
||||
_infoItem(Icons.access_time, '${rideInfo['duration']} Min'),
|
||||
_infoItem(Icons.star, '${rideInfo['passengerRate'] ?? 5.0}',
|
||||
iconColor: Colors.amber),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(IconData icon, String text, {Color? iconColor}) {
|
||||
return Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: iconColor ?? theme.hintColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(text, style: theme.textTheme.bodySmall?.copyWith(fontSize: 13)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// زر القبول والمنطق الكامل (Accept Logic) 🔥
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildAcceptButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: _acceptRideNewLogic,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Text(
|
||||
'Accept Ride'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥🔥🔥 الوظيفة الأهم: قبول الرحلة وتجهيز البيانات 🔥🔥🔥
|
||||
void _acceptRideNewLogic() async {
|
||||
// 1. إظهار Loading
|
||||
Get.dialog(
|
||||
const Center(child: MyCircularProgressIndicator()),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
try {
|
||||
String driverId = box.read(BoxName.driverID).toString();
|
||||
|
||||
// 2. إرسال الطلب للسيرفر (acceptRide.php الجديد)
|
||||
var response = await CRUD().post(
|
||||
link: "${AppLink.ride}/rides/acceptRide.php",
|
||||
payload: {
|
||||
'id': rideInfo['id'].toString(),
|
||||
'driver_id': driverId,
|
||||
'status': 'Apply', // الحالة المتفق عليها
|
||||
'passengerToken': rideInfo['passengerToken'].toString(),
|
||||
},
|
||||
);
|
||||
|
||||
// إخفاء الـ Loading
|
||||
Get.back();
|
||||
|
||||
// 3. تحليل الرد
|
||||
var jsonResponse = response;
|
||||
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
// ✅ نجاح: أنت الفائز بالرحلة
|
||||
|
||||
// تحديث حالة السائق محلياً
|
||||
Get.find<HomeCaptainController>().changeRideId();
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
|
||||
// 🔥 تجهيز الـ Arguments كاملة (Mapping) 📦
|
||||
// نأخذ البيانات من rideInfo (القادمة من getRideWaiting) ونمررها للخريطة
|
||||
Map<String, dynamic> fullRideArgs = {
|
||||
// معرفات الرحلة
|
||||
'rideId': rideInfo['id'].toString(),
|
||||
'passengerId': rideInfo['passengerId'].toString(),
|
||||
'driverId': driverId,
|
||||
|
||||
// المواقع (يجب أن تكون Strings بصيغة "lat,lng")
|
||||
'passengerLocation': rideInfo['start_location'].toString(),
|
||||
'passengerDestination': rideInfo['end_location'].toString(),
|
||||
|
||||
// الأسماء والعناوين
|
||||
'startNameLocation': rideInfo['startName'].toString(),
|
||||
'endNameLocation': rideInfo['endName'].toString(),
|
||||
|
||||
// تفاصيل الراكب
|
||||
'name': rideInfo['first_name'] ?? 'Passenger',
|
||||
'phone': rideInfo['phone'].toString(),
|
||||
'email': rideInfo['email'] ?? '',
|
||||
'tokenPassenger': rideInfo['passengerToken'].toString(),
|
||||
'passengerWalletBurc': rideInfo['bruc'].toString(), // رصيد الراكب
|
||||
|
||||
// التفاصيل المالية والرحلة
|
||||
'totalCost': rideInfo['price'].toString(), // السعر الكلي
|
||||
'paymentAmount': rideInfo['price'].toString(), // المبلغ المطلوب
|
||||
'Distance': rideInfo['distance'].toString(),
|
||||
'Duration': rideInfo['duration'].toString(),
|
||||
'durationOfRideValue':
|
||||
rideInfo['duration'].toString(), // تكرار للتأكد
|
||||
'carType': rideInfo['carType'].toString(),
|
||||
|
||||
// الدفع والمحفظة
|
||||
'paymentMethod': (rideInfo['payment_method'] == 'visa' ||
|
||||
rideInfo['payment_method'] == 'wallet')
|
||||
? 'visa'
|
||||
: 'cash',
|
||||
'WalletChecked':
|
||||
rideInfo['passenger_wallet'].toString() != '0' ? 'true' : 'false',
|
||||
'kazan': Get.find<HomeCaptainController>()
|
||||
.kazan
|
||||
.toString(), // نسبة الشركة (من الكنترولر)
|
||||
|
||||
// بيانات إضافية (لتجنب الـ Null Safety errors)
|
||||
'direction':
|
||||
'http://googleusercontent.com/maps.google.com/maps?saddr=${rideInfo['start_location']}&daddr=${rideInfo['end_location']}',
|
||||
'timeOfOrder': DateTime.now().toString(),
|
||||
'isHaveSteps': rideInfo['has_steps']?.toString() ?? 'false',
|
||||
'step0': rideInfo['step0'] ?? '',
|
||||
'step1': rideInfo['step1'] ?? '',
|
||||
'step2': rideInfo['step2'] ?? '',
|
||||
'step3': rideInfo['step3'] ?? '',
|
||||
'step4': rideInfo['step4'] ?? '',
|
||||
};
|
||||
|
||||
// حفظ البيانات في الصندوق احتياطياً (Crash Recovery)
|
||||
box.write(BoxName.rideArguments, fullRideArgs);
|
||||
|
||||
// الانتقال لصفحة الخريطة ومسح الصفحات السابقة لضمان عدم الرجوع للسوق
|
||||
Get.offAll(() => PassengerLocationMapPage(), arguments: fullRideArgs);
|
||||
} else {
|
||||
// ❌ فشل: الرحلة أخذها سائق آخر
|
||||
// نقوم بتحديث القائمة فوراً
|
||||
Get.find<RideAvailableController>()
|
||||
.getRideAvailable(forceRefresh: true);
|
||||
|
||||
MyDialog().getDialog(
|
||||
"Trip taken".tr,
|
||||
"This ride was just accepted by another driver.".tr,
|
||||
() => Get.back(), // زر الموافقة
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.back(); // إخفاء اللودينج في حال الخطأ
|
||||
print("Accept Ride Error: $e");
|
||||
MyDialog().getDialog(
|
||||
"Error".tr,
|
||||
"An unexpected error occurred. Please try again.".tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
siro_driver/lib/views/notification/notification_captain.dart
Executable file
87
siro_driver/lib/views/notification/notification_captain.dart
Executable file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/notification/notification_captain_controller.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class NotificationCaptain extends StatelessWidget {
|
||||
const NotificationCaptain({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(NotificationCaptainController());
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text('Notifications'.tr),
|
||||
leading: CupertinoNavigationBarBackButton(
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: GetBuilder<NotificationCaptainController>(
|
||||
builder: (notificationCaptainController) =>
|
||||
notificationCaptainController.isLoading
|
||||
? const Center(child: CupertinoActivityIndicator())
|
||||
: ListView.builder(
|
||||
itemCount: notificationCaptainController
|
||||
.notificationData['message'].length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (notificationCaptainController
|
||||
.notificationData['message'] ==
|
||||
"No notification data found") {
|
||||
_showCupertinoDialog(context, 'No Notifications',
|
||||
'There are no notifications at this time.');
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
var res = notificationCaptainController
|
||||
.notificationData['message'][index];
|
||||
return CupertinoListTile(
|
||||
leading: const Icon(CupertinoIcons.bell_fill),
|
||||
title: Text(
|
||||
res['title'],
|
||||
style:
|
||||
CupertinoTheme.of(context).textTheme.textStyle,
|
||||
),
|
||||
subtitle: Text(
|
||||
res['body'],
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.tabLabelTextStyle,
|
||||
),
|
||||
onTap: () {
|
||||
_showCupertinoDialog(
|
||||
context,
|
||||
res['title'],
|
||||
res['body'],
|
||||
onConfirm: () {
|
||||
notificationCaptainController
|
||||
.updateNotification(res['id'].toString());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCupertinoDialog(BuildContext context, String title, String content,
|
||||
{VoidCallback? onConfirm}) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
actions: <CupertinoDialogAction>[
|
||||
CupertinoDialogAction(
|
||||
child: const Text('OK'),
|
||||
onPressed: onConfirm ?? () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user