diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..56a25fe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "/Users/hamzaaleghwairyeen/development/App/intaleq_driver/linux" +} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 7891675..38365ea 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,10 +44,10 @@ android { applicationId = "com.intaleq_driver" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 23 - targetSdk = flutter.targetSdkVersion - versionCode = 8 - versionName = '1.0.8' + minSdk = 29 + targetSdk = 36 + versionCode = 13 + versionName = '1.0.13' multiDexEnabled =true } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6ae7deb..46436db 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -32,10 +32,14 @@ + + + + + + + + + intaleq.xyz + + + + XJXX7XthMj5VlSHfvo1q73sY7orJ9Wle0X4avj0/Vwo= + + C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHESsl= + + + + + \ No newline at end of file diff --git a/assets/alert.wav b/assets/alert.wav new file mode 100644 index 0000000..8722536 Binary files /dev/null and b/assets/alert.wav differ diff --git a/lib/constant/box_name.dart b/lib/constant/box_name.dart index 810fcab..c79b822 100755 --- a/lib/constant/box_name.dart +++ b/lib/constant/box_name.dart @@ -12,6 +12,7 @@ class BoxName { static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY"; static const String hmac = "hmac"; static const String fingerPrint = "fingerPrint"; + static const String updateInterval = "updateInterval"; static const String payMobApikey = "payMobApikey"; static const String refreshToken = "refreshToken"; static const String lang = "lang"; diff --git a/lib/constant/info.dart b/lib/constant/info.dart index 4568c1e..05fc897 100755 --- a/lib/constant/info.dart +++ b/lib/constant/info.dart @@ -11,4498 +11,281 @@ class AppInformation { 'for this data for complaint from driver or passenger i collect all data i want you analyze this complaint and show what is reason and what is solution .this data collected from many table to find solution if payment in visa not complete and if ride status is finished it will be paymnet in payment table if ride status is not finished there is no need to pay and payment table is null for this ride and if paymentFromPaymentTable not null and visa type not cash the payment sucssessed . if ratingpassenger is low or passengr rating drivers low grade then dont mine of this passenger ,look at driver too like passengerratingdriver with rating or ratingtopassenger .in json add status of complaint and message to passenger and message to driver and message to call center write in arabic in json output with key in english .for output please just json i want'; static const String addd = 'BlBlNl'; static const String privacyPolicy = ''' + + + + + + Intaleq - Privacy Policy & Terms of Use + + + +

Privacy Policy & Terms of Use

- - - - - - - - - - - -
-
-
-
-
-
-

Terms and Conditions

-

1 TERMS OF USE

-

By downloading, browsing, accessing or using the Mobile Application; “Intaleq”, Users agree to be bound by these Terms and Conditions of Use. We reserve the right to amend these terms and conditions at any time. If the User disagrees with any of these Terms and Conditions of Use, the User must immediately discontinue their access to the Mobile Application and their use of the services offered on the Mobile Application. Continued use of the Mobile Application will constitute acceptance of these Terms and Conditions of Use, as may be amended from time to time.

-

2.1. DEFINITIONS

-

In these Terms and Conditions of Use, the following capitalized terms shall have the following meanings, except where the context otherwise requires:

-

“Mobile Application” or “Intaleq” refers to the smartphone software through which the company mediates services between Users and Service Providers.

-

"Account" means an account created by a User or a Service Provider on the Mobile Application as part of Registration.

-

“Service Providers” refers to the individuals or companies registered within the Company to provide products or services approved and mediated by the Company/Mobile Application and that are requested/purchased by “Users”. “Service Provider” means any one of them.

-

"Users" means users of the Mobile Application, including you and "User" means any one of them. “Users” also includes Service Providers using the Mobile Application version dedicated to the use of Service Providers.

-

"Privacy Policy" means the privacy policy set out in Clause 14 of these Terms and Conditions of Use.

-

"Redeem" means to redeem a company’s products or services on these Terms and Conditions of Use and

-

"Redemption" means the act of redeeming such products or services.

-

"Register" means to create an Account on the Mobile Application and "Registration" means the act of creating such an Account.

-

"Services" means all the services provided by Service Providers via the Mobile Application and mediated by the Company to Users, and "Service" means any one of them.

-

2.2. WHAT IS THE MOBILE APPLICATION, “Intaleq”?

-

The “Intaleq” Mobile Application consists in a specialize service with the purpose to schedule/match the Service Provider with the User, however without creating any employee relationship with the Service Provider, meaning that the Mobile Application should be considered only as a service mediator with the purpose of scheduling/matching between Users and Service Providers for the latter to fulfill the service requested by Users. Through the system, the Company allows, totally free of charge, the Users to sign up and request the desired available services from Service Providers in a more efficient way than the conventional existing methods. The Service Providers, by their means, can register on the Mobile Application dedicated for Service Providers through the methods indicated by the Company, pending an evaluation from the Company and the necessary documentation when signing up in accordance with guidelines set by the Transportation General Authority (TGA) to provide services through the Mobile Application. The Service Provider will pay a profit margin fee determined by the Company -in knowledge of the Service Provider- per completed service. Users can pay their ride fare using Apple Pay as an additional payment method in Saudi Arabia on iPhones

-

-

3. GENERAL ISSUES ABOUT THE MOBILE APPLICATION AND THE SERVICES

-

3.1 Applicability of terms and conditions: The use of any Services and/or the Mobile Application and the making of any Redemptions are subject to these Terms and Conditions of Use.

-

3.2 Location: The Mobile Application, the Services and any Redemptions are intended solely for use by Users who access the Mobile Application where it operates and provides its services regionally. We make no representation that the Services (or any goods or services) are available or otherwise suitable for use outside of the regions indicated by the Company. Notwithstanding the above, if the User accesses the Mobile Application, use the Services or make any Redemptions from locations outside the regions indicated by the Service Provider, the User does so on their own initiative and are responsible for the consequences and for compliance with all applicable laws.

-

3.3 Scope: The Mobile Application, the Services and any Redemptions are for Users’ non-commercial, personal use only and must not be used for business purposes unless an official written permission is granted by the Company.

-

3.4 Prevention on use: We reserve the right to prevent the User using the Mobile Application and the Service (or any part of them) and to prevent the User from making any Redemptions.

-

3.5 Equipment and Networks: The provision of the Services and the Mobile Application does not include the provision of a mobile telephone or handheld device or other necessary equipment to access the Mobile Application or the Services or make any Redemptions. To use the Mobile Application or Services or to make Redemptions, the User will require Internet connectivity and appropriate telecommunication links. The User acknowledges that the terms of agreement with their respective mobile network provider will continue to apply when using the Mobile Application. As a result, the User may be charged by the Mobile Provider for access to network connection services for the duration of the connection while accessing the Mobile Application or any such third party charges as may arise. The User accepts responsibility for any such charges that arise.

-

3.6 Permission to use Mobile Application: If the User is not the bill payer for the mobile telephone or handheld device being used to access the Mobile Application, the User will be assumed to have received permission from the bill payer for using the Mobile Application.

-

3.7 License to Use Material: By submitting any text or images (including photographs) via the Application, the User represents that they are the owner of the Material, or have proper authorization from the owner of the Material to use, reproduce and distribute it. The User hereby grants us a worldwide, royalty-free, non-exclusive license to use the Material to promote any products or services.

-

4. REDEMPTIONS

-

4.1 Need for registration: a) Users must Register to make a Redemption from the Mobile Application. b) The Service Provider agrees that by registering they will be scrutinized to be accepted by the Company who may refuse or cancel the Service Provider account at any time, whether by complaints or by internal policy.

-

4.2 Application of these Terms and Conditions of Use: By making any Redemption, the User acknowledges that the Redemption is subject to these Terms and Conditions of Use.

-

4.3 Redemption: Any attempted Redemption not consistent with these Terms and Conditions of Use may be disallowed or rendered void at our or the relevant Service Provider’s discretion.

-

4.4 Responsibility for Redemptions of perishable products of services: Each Service Provider shall be responsible to ensure that any of its products or services for Redemption that are perishable has not expired.

-

4.5 Restrictions: (a) Reproduction, sale, resale or trading of any products or services or Redeemed products is prohibited. (b) If any product or service is Redeemed for less than its face value, there is no entitlement to a credit, cash or Sample equal to the difference between the face value and the amount Redeemed. (c) Redemption of products or services is subject to availability of the relevant Service Providers’ stocks.

-

4.6 Company Not Liable: For the avoidance of doubt, the Company shall not be liable for any losses or damages suffered by Users resulting from a failure by the relevant Service Provider to fulfil any Redemptions in accordance with Clause 4.4 or for a failure by us to deliver any products or services to Users due to the unavailability of such products or services pursuant to Clause 4.5(c). Users accept that the Mobile Application acts solely as a scheduling/matching service between its Users and Service Providers, and that it is not responsible for any issues that arise, including but not limited to accidents, delays, car discomfort, and unavailability of products or services.

-

4.7 Lost/stolen services: Neither we nor any Service Provider shall be responsible for lost or stolen Samples or products that have been Redeemed.

-

-

5. LOCATION ALERTS AND NOTIFICATIONS

-

5.1 The USer agrees to receive pre-programmed notifications (“Location Alerts”) on the Mobile Application from Service Providers if the User has turned on locational services on their mobile telephone or other handheld devices (as the case may be).

-

-

6. USERS’ OBLIGATIONS

-

6.1 Service Provider terms: Users agree to (and shall) abide by the terms and conditions of the relevant Service Provider for which their Redemption relates to, as may be amended from time to time.

-

6.2 Accurate information: The User warrants that all information provided on Registration and contained as part of their Account is true, complete and accurate and that the User will promptly inform us of any changes to such information by updating the information in their Account.

-

6.3 Content on the Mobile Application and Service: It is the User’s responsibility to ensure that any products, services or information available through the Mobile Application or the Services meet their specific requirements before making any Redemption.

-

6.4 Prohibitions in relation to usage of Services or Mobile Application: Without limitation, the User undertakes not to use or permit anyone else to use the Services or Mobile Application:-

-

6.4.1 to send or receive any material which is not civil or tasteful

-

6.4.2 to send or receive any material which is threatening, grossly offensive, of an indecent, obscene or menacing character, blasphemous or defamatory of any person, in contempt of court or in breach of confidence, copyright, rights of personality, publicity or privacy or any other third party rights;

-

6.4.3 to send or receive any material for which the User has not obtained all necessary licenses and/or approvals (from us or third parties); or which constitutes or encourages conduct that would be considered a criminal offence, give rise to civil liability, or otherwise be contrary to the law of or infringe the rights of any third party in any country in the world;

-

6.4.4 to send or receive any material which is technically harmful (including computer viruses, logic bombs, Trojan horses, worms, harmful components, corrupted data or other malicious software or harmful data);

-

6.4.5 to cause annoyance, inconvenience or needless anxiety;

-

6.4.6 to intercept or attempt to intercept any communications transmitted by way of a telecommunications system;

-

6.4.7 for a purpose other than which we have designed them or intended them to be used;

-

6.4.8 for any fraudulent purpose;

-

6.4.9 other than in conformance with accepted Internet practices and practices of any connected networks;

-

6.4.10 in any way which is calculated to incite hatred against any ethnic, religious or any other minority or is otherwise calculated to adversely affect any individual, group or entity; or

-

6.4.11 in such a way as to, or commit any act that would or does, impose an unreasonable or disproportionately large load on our infrastructure.

-

6.5 Prohibitions in relation to usage of Services, Mobile Application: Without limitation, the User further undertakes not to or permit anyone else to:-

-

6.5.1 resell any products or services;

-

6.5.2 furnish false data including false names, addresses and contact details and fraudulently use credit/debit card numbers;

-

6.5.3 attempt to circumvent our security or network including to access data not intended for the User, log into a server or account the User is not expressly authorized to access, or probe the security of other networks (such as running a port scan);

-

6.5.4 execute any form of network monitoring which will intercept data not intended for the User;

-

6.5.5 enter into fraudulent interactions or transactions with us or a Service Provider (including interacting or transacting purportedly on behalf of a third party where the User has no authority to bind that third party or the User is pretending to be a third party);

-

6.5.6 extract data from or hack into the Mobile Application;

-

6.5.7 use the Services or Mobile Application in breach of these Terms and Conditions of Use;

-

6.5.8 engage in any unlawful activity in connection with the use of the Mobile Application or the Services; or

-

6.5.9 engage in any conduct which, in our exclusive reasonable opinion, restricts or inhibits any other customer from properly using or enjoying the Mobile Application or Services.

-

-

7. RULES ABOUT USE OF THE SERVICE AND THE MOBILE APPLICATION

-

7.1 We will use reasonable endeavors to correct any errors or omissions as soon as practicable after being notified of them. However, we do not guarantee that the Services or the Mobile Application will be free of faults, and we do not accept liability for any such faults, errors or omissions. In the event of any such error, fault or omission, Users should report it by contacting us at JORDAN: 962798583052 .

-

7.2 We do not warrant that Users’ use of the Services or the Mobile Application will be uninterrupted and we do not warrant that any information (or messages) transmitted via the Services or the Mobile Application will be transmitted accurately, reliably, in a timely manner or at all. Notwithstanding that we will try to allow uninterrupted access to the Services and the Mobile Application, access to the Services and the Mobile Application may be suspended, restricted or terminated at any time.

-

7.3 We do not give any warranty that the Services and the Mobile Application are free from viruses or anything else which may have a harmful effect on any technology.

-

7.4 We reserve the right to change, modify, substitute, suspend or remove without notice any information or Services on the Mobile Application from time to time. Users’ access to the Mobile Application and/or the Services may also be occasionally restricted to allow for repairs, maintenance or the introduction of new facilities or services. We will attempt to restore such access as soon as we reasonably can. For the avoidance of doubt, we reserve the right to withdraw any information or Services from the Mobile Application at any time.

-

7.5 We reserve the right to block access to and/or to edit or remove any material which in our reasonable opinion may give rise to a breach of these Terms and Conditions of Use.

-

7.6 The acceptance and denial of the service request may occur at first by the Service Provider, who can accept or deny the service when receiving a notification for a request. The User may cancel the contract for any reason which may or may not apply cancellation fees for which the conditions and amount is determined and communicated by the Company. Both recognize that “Intaleq” is not liable for any delays, cancellations, failure to cancel the contract and miscommunication between the User and the Service Provider, nor for any delay or failure in the delivery of services from the Service Provider.

-

-

8. SUSPENSION AND TERMINATION

-

8.1 If the User uses (or others, with the User’s permission use) the Mobile Application, any Services in contravention of these Terms and Conditions of Use, we may suspend their use of the Services and/or Mobile Application.

-

8.2 If we suspend the Services or Mobile Application, we may refuse to restore the Services or Mobile Application for the User’s use until we receive an assurance from them, in a form we deem acceptable, that there will be no further breach of the provisions of these Terms and Conditions of Use.

-

8.3 The Company shall fully co-operate with any law enforcement authorities or court order requesting or directing the Company to disclose the identity or locate anyone in breach of these Terms and Conditions of Use.

-

8.4 Without limitation to anything else in this Clause 8, we shall be entitled immediately or at any time (in whole or in part) to: (a) suspend the Services and/or Mobile Application; (b) suspend Users’ use of the Services and/or Mobile Application; and/or (c) suspend the use of the Services and/or Mobile Application for persons we believe to be connected (in whatever manner) to the concerned User, if:

-

8.4.1 the User commits any breach of these Terms and Conditions of Use;

-

8.4.2 we suspect, on reasonable grounds, that the User has, might or will commit a breach of these Terms and Conditions of Use; or

-

8.4.3 we suspect, on reasonable grounds, that the User may have committed or will be committing any fraud against us or any person.

-

8.5 Our rights under this Clause 8 shall not prejudice any other right or remedy we may have in respect of any breach or any rights, obligations or liabilities accrued prior to termination.

-

-

9. DISCLAIMER AND EXCLUSION OF LIABILITY

-

9.1 The Mobile Application, the Services, the information on the Mobile Application and use of all related facilities are provided on an "as is, as available" basis without any warranties whether express or implied.

-

9.2 The credit balance shall remain valid for the specific period. The credit in the wallet will be expired after 6 months of inactivity.

-

9.3 To the fullest extent permitted by applicable law, we disclaim all representations and warranties relating to the Mobile Application and its contents, including in relation to any inaccuracies or omissions in the Mobile Application, warranties of merchantability, quality, fitness for a particular purpose, accuracy, availability, non-infringement or implied warranties from course of dealing or usage of trade.

-

9.4 We do not warrant that the Mobile Application will always be accessible, uninterrupted, timely, secure, error free or free from computer virus or other invasive or damaging code or that the Mobile Application will not be affected by any acts of nature or other force majeure events, including inability to obtain or shortage of necessary materials, equipment facilities, power or telecommunications, lack of telecommunications equipment or facilities and failure of information technology or telecommunications equipment or facilities.

-

9.5 While we may use reasonable efforts to include accurate and up-to-date information on the Mobile Application, we make no warranties or representations as to its accuracy, timeliness or completeness.

-

9.6 We shall not be liable for any acts or omissions of any third parties howsoever caused, and for any direct, indirect, incidental, special, consequential or punitive damages, howsoever caused, resulting from or in connection with the Mobile Application and the services offered in the mobile application, Users’ access to, use of or inability to use the mobile application or the services offered in the mobile application, reliance on or downloading from the mobile application and/or services, or any delays, inaccuracies in the information or in its transmission including but not limited to damages for loss of business or profits, use, data or other intangible, even if we have been advised of the possibility of such damages.

-

9.7 We shall not be liable in contract, tort (including negligence or breach of statutory duty) or otherwise howsoever and whatever the cause thereof, for any indirect, consequential, collateral, special or incidental loss or damage suffered or incurred by the User in connection with the Mobile Application and these Terms and Conditions of Use. For the purposes of these Terms and Conditions of Use, indirect or consequential loss or damage includes, without limitation, loss of revenue, profits, anticipated savings or business, loss of data or goodwill, loss of use or value of any equipment including software, claims of third parties, and all associated and incidental costs and expenses.

-

9.8 The above exclusions and limitations apply only to the extent permitted by law. None of the User’s statutory rights as a consumer that cannot be excluded or limited are affected.

-

9.9 Notwithstanding our efforts to ensure that our system is secure, the User acknowledges that all electronic data transfers are potentially susceptible to interception by others. We cannot, and do not, warrant that data transfers pursuant to the Mobile Application, or electronic mail transmitted to and from us, will not be monitored or read by others.

-

-

10. INDEMNITY

-

The User agrees to indemnify and keep us indemnified against any claim, action, suit or proceeding brought or threatened to be brought against us which is caused by or arising out of (a) the User’s use of the Services, (b) any other party’s use of the Services using the User’s user ID, verification PIN and/or any identifier number allocated by the Company, and/or (c) the User’s breach of any of these Terms and Conditions of Use, and to pay us damages, costs and interest in connection with such claim, action, suit or proceeding.

-

-

11. INTELLECTUAL PROPERTY RIGHTS

-

11.1 All editorial content, information, photographs, illustrations, artwork and other graphic materials, and names, logos and trade marks on the Mobile Application are protected by copyright laws and/or other laws and/or international treaties, and belong to us and/or our suppliers, as the case may be. These works, logos, graphics, sounds or images may not be copied, reproduced, retransmitted, distributed, disseminated, sold, published, broadcasted or circulated whether in whole or in part, unless expressly permitted by us and/or our suppliers, as the case may be.

-

11.2 Nothing contained on the Mobile Application should be construed as granting by implication, estoppel, or otherwise, any license or right to use any trademark displayed on the Mobile Application without our written permission. Misuse of any trademarks or any other content displayed on the Mobile Application is prohibited.

-

11.3 We will not hesitate to take legal action against any unauthorized usage of our trade marks, name or symbols to preserve and protect its rights in the matter. All rights not expressly granted herein are reserved. Other product and company names mentioned herein may also be the trademarks of their respective owners.

-

-

12. AMENDMENTS

-

12.1 We may periodically make changes to the contents of the Mobile Application, including to the descriptions and prices of goods and services advertised, at any time and without notice. We assume no liability or responsibility for any errors or omissions in the content of the Mobile Application.

-

12.2 We reserve the right to amend these Terms and Conditions of Use from time to time without notice. The revised Terms and Conditions of Use will be posted on the Mobile Application and shall take effect from the date of such posting. The User is advised to review these terms and conditions periodically as they are binding upon the User.

-

-

13. APPLICABLE LAW AND JURISDICTION

-

13.1 These Terms and Conditions of Use shall be governed by and construed in accordance with the applicable Federal laws of Kingdom of Saudi Arabia.

-

13.2 The Mobile Application can be accessed from all countries around the world where the local technology permits. As each of these places have differing laws, by accessing the Mobile Application both the User and we agree that the laws of the country where accessed, without regard to the conflicts of laws principles thereof, will apply to all matters relating to the use of the Mobile Application.

-

13.3 the User accepts and agrees that both the User and we shall submit to the exclusive jurisdiction of the courts of the country where accessed in respect of any dispute arising out of and/or in connection with these Terms and Conditions of Use.

-

-

14. PRIVACY POLICY

-

14.1 Access to the Mobile Application and use of the Services offered on the Mobile Application by the Company and/or its group of companies & partners is subject to this Privacy Policy. By accessing the Mobile Application and by continuing to use the Services offered, Users are deemed to have accepted this Privacy Policy, and in particular, they are deemed to have consented to our use and disclosure of their personal information in the manner prescribed in this Privacy Policy and for the purposes set out in Clauses 3.7 and/or 4.1.1 We reserve the right to amend this Privacy Policy from time to time. If the User disagrees with any part of this Privacy Policy, the User must immediately discontinue their access to the Mobile Application and their use of the Services.

-

14.2 As part of the normal operation of our Services, we collect, use and, in some cases, disclose information about the User to third parties. Accordingly, we have developed this Privacy Policy in order for Users to understand how we collect, use, communicate and disclose and make use of their personal information when they use the Services on the Mobile Application:-

-

(a) Before or at the time of collecting personal information, we will identify the purposes for which information is being collected.

-

(b) We will collect and use of personal information solely with the objective of fulfilling those purposes specified by us and for other compatible purposes, unless we obtain the consent of the individual concerned or as required by law.

-

(c) We will only retain personal information as long as necessary for the fulfillment of those purposes.

-

(d) We will collect personal information by lawful and fair means and, where appropriate, with the knowledge or consent of the individual concerned.

-

(e) Personal information should be relevant to the purposes for which it is to be used, and, to the extent necessary for those purposes, should be accurate, complete, and up-to-date.

-

(f) We will protect personal information by reasonable security safeguards against loss or theft, as well as unauthorized access, disclosure, copying, use or modification.

-

(g) The Company further reserves the right to use all legal means possible and to identify the Users, as well as to request, at any time, additional data and documents it considers appropriate in order to verify personal data informed by the user.

-

We are committed to conducting our business in accordance with these principles in order to ensure that the confidentiality of personal information is protected and maintained.

-

14.3 Social logins policy
Our Services offers you the ability to register and login using Facebook. Where you choose to do this, we will receive certain profile information about you from your social media provider. The profile Information may include your name and social token.
We will use the information we receive only to associate your social token with your Intaleq account or for the purposes that are described in this privacy policy.

14.4 Account and data deletion
Based on the applicable laws of your country, you may have the right to request the deletion of your personal data in some circumstances. If you want to delete your account or personal data in the application, please contact us via e-mail: support@mobile-app.store. We will respond to your request within 30 days.

-

15. In-Ride Policy

-

15.1 Smoking Policy:

-

No smoking or other use of tobacco products (including, but not limited to, cigarettes, pipes, cigars, snuff, or chewing tobacco) is permitted during rides. No cigarette butts or other traces of smell, litter, or tobacco use should be present in the vehicle. image

-

15.2 COVID-19 Policy: Face masks must be always worn during rides.

-
-
-
-
-
+
+

Effective Date: August 9, 2025

+

Last Updated: August 9, 2025

+

1. Introduction and Acceptance

+

By downloading, registering, or using the Intaleq application ("App"), you agree to be bound by this Privacy Policy and our Terms of Use. If you do not agree, you must stop using the App immediately. Your continued use constitutes acceptance of these terms and any future updates.

+

2. Definitions

+
    +
  • "Intaleq", "we", "us": Refers to the Intaleq for Ride Hailing company, Damascus – Syria (Owner & operator), which provides the technology platform.
  • +
  • "Driver": An independent service provider who uses the App to offer transportation services.
  • +
  • "Passenger", "you": An individual who uses the App to request transportation services.
  • +
  • "Services": The connection between Passengers and Drivers facilitated by our App.
  • +
+ +

3. Privacy Policy

- - -
-
- - - - -
-

- All rights reserved. Fast Global Technology Holding Limited. © 2022 -

-
-
- -
- - - - -
- - - +

A. Information You Provide:

+
    +
  • For Drivers: To ensure safety and compliance, we collect identity information, including your full name, phone number, personal photo, and official documents (e.g., driver's license, vehicle registration).
  • +
  • For Passengers: We only require a phone number for registration and communication. We are not authorized to request or view official identity documents for passengers.
  • +
+ +

B. Information Collected Automatically:

+
    +
  • Location Data: We collect precise location data when the App is in use to facilitate ride matching, navigation, and for safety purposes.
  • +
  • Device Data: We collect information about your device, such as model, operating system, and unique identifiers, to ensure App functionality and for security verification.
  • +
  • Usage Data: We log how you interact with our App, including trip history and features used, to improve our services.
  • +
+ +

3.2 Payment Information

+

We do not collect, process, or store any sensitive payment information like credit/debit card numbers. We facilitate payments by connecting you to licensed, local third-party providers:

+
    +
  • Mobile Carrier Billing: Payments via MTN and Syriatel are processed directly by them based on your registered phone number. A one-time password (OTP) sent by the carrier is required to confirm the transaction.
  • +
  • Bank Card Payments: We connect you with the Syrian company "eCash" to process card payments. They handle the transaction, and your bank will send an OTP to your phone to authorize it.
  • +
- - -
+

6. Policy Updates

+

We may update these terms. If we make significant changes, we will notify you within the App. You will be required to review and accept the new terms to continue using the Services, ensuring your consent is active and informed.

+

7. Account Deletion & Contact

+

You have the right to request the deletion of your account and personal data. To do so, or for any other questions, please contact us. We will respond to deletion requests within 30 days.

+

Email: support@intaleqapp.com

+ + + '''; static const String privacyPolicyArabic = ''' -

الشروط والأحكام

-

1. شروط الاستخدام

-

عبر تحميل أو تصفح أو استخدام تطبيق "انطلق"، يوافق المستخدمون على الالتزام بهذه الشروط والأحكام. نحتفظ بالحق في تعديل هذه الشروط في أي وقت. إذا كنت لا توافق على أي من هذه الشروط، يجب عليك التوقف فورًا عن استخدام التطبيق. استمرارك في الاستخدام يعتبر قبولًا للشروط وتعديلاتها.

+ + + + + + انطلق - سياسة الخصوصية وشروط الاستخدام + + + +

سياسة الخصوصية وشروط الاستخدام

-

2.1. التعريفات

-
    -
  • "التطبيق" أو "انطلق": يشير إلى برنامج الهاتف الذكي الذي تتوسط الشركة من خلاله في تقديم الخدمات بين المستخدمين ومقدمي الخدمة.
  • -
  • "مقدمو الخدمة": الأفراد أو الشركات المسجلون لتقديم خدمات معتمدة من الشركة.
  • -
  • "المستخدمون": مستخدمو التطبيق، بما فيهم أنت، ويشمل أيضًا مقدمي الخدمة.
  • -
  • "الخدمات": جميع الخدمات المقدمة من مقدمي الخدمة عبر التطبيق.
  • -
+
+

تاريخ النفاذ: 9 أغسطس 2025

+

آخر تحديث: 9 أغسطس 2025

+
-

2.2. ما هو تطبيق "انطلق"؟

-

تطبيق "انطلق" هو خدمة متخصصة تهدف إلى جدولة وتوفيق الخدمة بين مقدم الخدمة والمستخدم، دون إنشاء أي علاقة عمل مع مقدم الخدمة. يعمل التطبيق كوسيط فقط. يسمح النظام للمستخدمين بالتسجيل وطلب الخدمات المتاحة من مقدمي الخدمة بكفاءة. يدفع مقدم الخدمة هامش ربح تحدده الشركة عن كل خدمة مكتملة.

+

1. المقدمة والقبول

+

عبر تحميل أو تسجيل أو استخدام تطبيق "انطلق" ("التطبيق")، فإنك توافق على الالتزام بسياسة الخصوصية وشروط الاستخدام هذه. إذا كنت لا توافق، يجب عليك التوقف فورًا عن استخدام التطبيق. استمرارك في الاستخدام يُعد قبولاً لهذه الشروط وأي تحديثات مستقبلية لها.

-

3. أمور عامة حول التطبيق والخدمات

-

3.1 نطاق التطبيق: التطبيق والخدمات مخصصة للاستخدام الشخصي وغير التجاري فقط.

-

3.2 الأجهزة والشبكات: يتطلب استخدام التطبيق وجود اتصال بالإنترنت وروابط اتصالات مناسبة. أنت مسؤول عن أي رسوم قد تنشأ من مزود خدمة شبكة الهاتف المحمول الخاص بك.

+

2. التعريفات

+
    +
  • "انطلق"، "نحن": تشير إلى شركة انطلق لنقل الركاب، دمشق – سوريا (مالك ومشغل التطبيق)، التي توفر المنصة التقنية.
  • +
  • "السائق": مقدم خدمة مستقل يستخدم التطبيق لتقديم خدمات النقل.
  • +
  • "الراكب"، "أنت": الفرد الذي يستخدم التطبيق لطلب خدمات النقل.
  • +
  • "الخدمات": عملية الربط بين الركاب والسائقين التي يسهلها تطبيقنا.
  • +
-

6. التزامات المستخدمين

-

6.2 معلومات دقيقة: يتعهد المستخدم بأن جميع المعلومات المقدمة عند التسجيل وفي حسابه صحيحة وكاملة ودقيقة.

-

6.4 المحظورات: يتعهد المستخدم بعدم استخدام التطبيق في إرسال أو استقبال أي مواد غير لائقة، أو تهديدية، أو فاحشة، أو تشهيرية، أو ضارة تقنيًا (مثل الفيروسات)، أو لأي غرض احتيالي.

+

3. سياسة الخصوصية

+ +

3.1 المعلومات التي نجمعها

+

نحن نجمع المعلومات الضرورية لتقديم خدماتنا وتحسينها.

+ +

أ. المعلومات التي تقدمها بنفسك:

+
    +
  • بالنسبة للسائقين: لضمان السلامة والامتثال للقوانين، نجمع بيانات الهوية الشخصية، بما في ذلك الاسم الكامل، رقم الهاتف، صورة شخصية، والوثائق الرسمية (مثل رخصة القيادة وتسجيل المركبة).
  • +
  • بالنسبة للركاب: نطلب فقط رقم هاتف للتسجيل والتواصل. نحن غير مخولين بطلب أو الاطلاع على وثائق الهوية الرسمية للركاب.
  • +
-

9. إخلاء المسؤولية

-

يتم توفير التطبيق والخدمات "كما هي"، دون أي ضمانات صريحة أو ضمنية. نحن لا نتحمل المسؤولية عن أي أضرار مباشرة أو غير مباشرة تنتج عن استخدام التطبيق.

+

ب. المعلومات التي تُجمع تلقائيًا:

+
    +
  • بيانات الموقع: نجمع بيانات الموقع الجغرافي الدقيقة عند استخدام التطبيق لتسهيل تحديد أماكن الانطلاق والوصول، الملاحة، ولأغراض السلامة.
  • +
  • بيانات الجهاز: نجمع معلومات عن جهازك (طراز، نظام تشغيل، معرفات فريدة) لضمان عمل التطبيق وللتحقق الأمني.
  • +
  • بيانات الاستخدام: نسجل كيفية تفاعلك مع التطبيق، بما في ذلك سجل الرحلات والميزات المستخدمة، بهدف تحسين خدماتنا.
  • +
-

14. سياسة الخصوصية

-

14.1 باستخدامك للتطبيق، فأنت توافق على سياسة الخصوصية هذه وتوافق على استخدامنا وكشفنا لمعلوماتك الشخصية بالطريقة الموضحة.

-

14.2 نجمع معلوماتك الشخصية ونستخدمها فقط لتحقيق الأغراض المحددة من قبلنا، ما لم نحصل على موافقة الفرد المعني أو كما يقتضي القانون. سنحمي معلوماتك الشخصية بإجراءات أمنية معقولة.

-

14.4 حذف الحساب والبيانات: إذا كنت ترغب في حذف حسابك أو بياناتك الشخصية، يرجى الاتصال بنا عبر البريد الإلكتروني: support@intaleqapp.com. سنرد على طلبك في غضون 30 يومًا.

+

3.2 معلومات الدفع

+

نحن لا نجمع أو نعالج أو نخزن أي معلومات دفع حساسة مثل أرقام بطاقات الائتمان/الخصم. نحن نسهل عمليات الدفع عبر ربطك بمزودي خدمات محليين مرخصين:

+
    +
  • الدفع عبر رصيد الهاتف المحمول: تتم معالجة الدفعات عبر شركتي MTN و Syriatel مباشرة من خلالهما بناءً على رقم هاتفك المسجل لديهم. يتطلب تأكيد العملية إدخال رمز تحقق (OTP) يُرسل من قبل شركة الاتصالات.
  • +
  • الدفع عبر البطاقات البنكية: نربطك بشركة "eCash" السورية لمعالجة الدفعات بالبطاقات. هي التي تتولى المعاملة، وسيقوم البنك الذي تتعامل معه بإرسال رمز تحقق (OTP) إلى هاتفك لتفويض العملية.
  • +
+ +

3.3 كيف نستخدم معلوماتك

+
    +
  • لتشغيل وصيانة الخدمات (مثل الربط بين السائقين والركاب).
  • +
  • للتحقق من هوية السائقين وأهليتهم.
  • +
  • لتحسين أمان التطبيق ومنع الاحتيال.
  • +
  • لتقديم الدعم الفني للعملاء.
  • +
  • للامتثال للالتزامات القانونية.
  • +
-

15. سياسة أثناء الرحلة

-

15.1 سياسة التدخين: يمنع التدخين بجميع أشكاله أثناء الرحلات.

-

15.2 سياسة كوفيد-19: يجب ارتداء الكمامات دائمًا أثناء الرحلات.

+

3.4 مشاركة البيانات

+

نحن لا نبيع بياناتك الشخصية. نشاركها فقط في الحالات المحدودة التالية:

+
    +
  • بين الراكب والسائق: لتسهيل الرحلة، نشارك المعلومات الضرورية مثل الاسم، الصورة، والموقع المباشر.
  • +
  • مع مزودي الخدمات: مثل معالجي الدفع وخدمات الخرائط. هؤلاء المزودون ملزمون تعاقديًا بحماية بياناتك.
  • +
  • لأسباب قانونية: إذا طُلب ذلك بموجب القانون أو أمر قضائي ساري المفعول.
  • +
+ +

3.5 سياسة القاصرين

+

خدماتنا موجهة للأفراد الذين تزيد أعمارهم عن 18 عامًا. + بالنسبة للسائقين: نحن نتحقق بدقة من هوية وعمر جميع السائقين لضمان عدم وجود قاصرين يعملون على منصتنا. + بالنسبة للركاب: على الرغم من أننا لا نتحقق من هوية الركاب، فإن الخدمة غير موجهة للأطفال دون سن 18. إذا علم ولي الأمر أن طفله قد زودنا بمعلومات دون موافقته، فيجب عليه الاتصال بنا على الفور.

+ +

4. التزامات المستخدم وسلوكه

+
    +
  • يجب عليك تقديم معلومات دقيقة وحديثة عند التسجيل.
  • +
  • أنت مسؤول عن الحفاظ على أمان حسابك.
  • +
  • أنت توافق على عدم استخدام التطبيق لأي أنشطة غير قانونية، أو لمضايقة الآخرين، أو التسبب في ضرر لمركبة السائق.
  • +
+ +

5. إخلاء المسؤولية

+

يتم تقديم التطبيق "كما هو". "انطلق" هي منصة وسيطة وليست مسؤولة عن تصرفات السائقين أو الركاب، أو الحوادث، أو التأخير، أو أي نزاعات بين المستخدمين. مسؤوليتنا محدودة إلى أقصى حد يسمح به القانون.

+ +

6. تحديثات السياسة

+

قد نقوم بتحديث هذه الشروط. في حال إجراء تغييرات جوهرية، سنقوم بإعلامك داخل التطبيق. سيُطلب منك مراجعة الشروط الجديدة وقبولها لمواصلة استخدام الخدمات، لضمان أن موافقتك فعالة ومبنية على معرفة.

+ +

7. حذف الحساب والتواصل

+

لديك الحق في طلب حذف حسابك وبياناتك الشخصية. للقيام بذلك، أو لأي استفسارات أخرى، يرجى التواصل معنا. سنرد على طلبات الحذف في غضون 30 يومًا.

+

البريد الإلكتروني: support@intaleqapp.com

+ + + '''; } diff --git a/lib/constant/links.dart b/lib/constant/links.dart index f261885..7906b8b 100755 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -14,6 +14,7 @@ class AppLink { 'https://walletintaleq.intaleq.xyz/v1/main'; static final String endPoint = 'https://intaleq.xyz/intaleq'; + static final String syria = 'https://syria.intaleq.xyz/intaleq'; // 'https://api.tripz-egypt.com/tripz'; static final String server = endPoint; static String seferCairoServer = endPoint; @@ -97,6 +98,7 @@ class AppLink { //-----------------ridessss------------------ static String addRides = "$ride/rides/add.php"; static String getRides = "$ride/rides/get.php"; + static String getPlacesSyria = "$ride/places_syria/get.php"; static String getMishwari = "$ride/mishwari/get.php"; static String getMishwariDriver = "$ride/mishwari/getDriver.php"; static String getTripCountByCaptain = "$ride/rides/getTripCountByCaptain.php"; @@ -250,6 +252,7 @@ class AppLink { static String uploadImage = "$server/uploadImage.php"; static String uploadImage1 = "$server/uploadImage1.php"; static String uploadImagePortrate = "$server/uploadImagePortrate.php"; + static String uploadSyrianDocs = "$syria/auth/syria/uploadSyrianDocs.php"; static String uploadImageType = "$server/uploadImageType.php"; //=============egypt documents ============== static String uploadEgyptidFront = @@ -310,6 +313,8 @@ class AppLink { static String updatePassengerGift = "$ride/invitor/updatePassengerGift.php"; static String updateInvitationCodeFromRegister = "$ride/invitor/updateInvitationCodeFromRegister.php"; + static String register_driver_and_car = + "$auth/syria/driver/register_driver_and_car.php"; static String updateDriverInvitationDirectly = "$ride/invitor/updateDriverInvitationDirectly.php"; static String updatePassengersInvitation = diff --git a/lib/controller/auth/captin/login_captin_controller.dart b/lib/controller/auth/captin/login_captin_controller.dart index 33c90e3..8f008a3 100755 --- a/lib/controller/auth/captin/login_captin_controller.dart +++ b/lib/controller/auth/captin/login_captin_controller.dart @@ -5,9 +5,8 @@ import 'package:crypto/crypto.dart'; import 'dart:math'; import 'package:http/http.dart' as http; import 'package:permission_handler/permission_handler.dart'; -import 'package:secure_string_operations/secure_string_operations.dart'; -import 'package:sefer_driver/controller/functions/location_background_controller.dart'; import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart'; +import 'package:sefer_driver/views/auth/syria/registration_view.dart'; import 'package:sefer_driver/views/widgets/elevated_btn.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:flutter/material.dart'; @@ -21,13 +20,10 @@ import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart'; import 'package:location/location.dart'; import '../../../constant/api_key.dart'; -import '../../../constant/char_map.dart'; import '../../../constant/info.dart'; -import '../../../constant/table_names.dart'; -import '../../../print.dart'; -import '../../../views/auth/captin/cards/syrian_card_a_i.dart'; import '../../../views/auth/captin/otp_page.dart'; import '../../../views/auth/captin/otp_token_page.dart'; +import '../../../views/auth/syria/pending_driver_page.dart'; import '../../firebase/firbase_messge.dart'; import '../../functions/encrypt_decrypt.dart'; import '../../functions/package_info.dart'; @@ -79,11 +75,11 @@ class LoginDriverController extends GetxController { var res = await CRUD().get( link: AppLink.getTesterApp, payload: {'appPlatform': AppInformation.appName}); - Log.print('res: ${res}'); + // Log.print('res: ${res}'); if (res != 'failure') { var d = jsonDecode(res); isTest = d['message'][0]['isTest']; - Log.print('isTest: ${isTest}'); + // Log.print('isTest: ${isTest}'); box.write(BoxName.isTest, isTest); // Log.print('isTest: ${box.read(BoxName.isTest)}'); @@ -108,7 +104,8 @@ class LoginDriverController extends GetxController { ) }); if (res != 'failure') { - Get.offAll(() => SyrianCardAI()); + // Get.offAll(() => SyrianCardAI()); + Get.offAll(() => RegistrationView()); // isloading = false; // update(); @@ -322,46 +319,53 @@ class LoginDriverController extends GetxController { } else if (int.parse(d['year'].toString()) < 2002) { box.write(BoxName.carTypeOfDriver, 'Awfar Car'); } - updateAppTester(AppInformation.appName); + // updateAppTester(AppInformation.appName); + if (d['status'].toString() != 'yet') { + var token = await CRUD().get( + link: AppLink.getDriverToken, + payload: { + 'captain_id': (box.read(BoxName.driverID)).toString() + }); - var token = await CRUD().get( - link: AppLink.getDriverToken, - payload: {'captain_id': (box.read(BoxName.driverID)).toString()}); - - String fingerPrint = await DeviceHelper.getDeviceFingerprint(); - await storage.write( - key: BoxName.fingerPrint, value: fingerPrint.toString()); - // print(jsonDecode(token)['data'][0]['token'].toString()); - // print(box.read(BoxName.tokenDriver).toString()); - if (email == '962798583052@intaleqapp.com') { - } else { - if (token != 'failure') { - if ((jsonDecode(token)['data'][0]['token'].toString()) != - box.read(BoxName.tokenDriver).toString()) { - await Get.defaultDialog( - title: 'Device Change Detected'.tr, - middleText: 'Please verify your identity'.tr, - textConfirm: 'Verify'.tr, - confirmTextColor: Colors.white, - onConfirm: () { - // Get.back(); - // انتقل لصفحة OTP الجديدة - Get.to( - () => OtpVerificationPage( - phone: d['phone'].toString(), - deviceToken: fingerPrint.toString(), - token: token.toString(), - ptoken: - jsonDecode(token)['data'][0]['token'].toString(), - ), - ); - }, - ); + String fingerPrint = await DeviceHelper.getDeviceFingerprint(); + await storage.write( + key: BoxName.fingerPrint, value: fingerPrint.toString()); + // print(jsonDecode(token)['data'][0]['token'].toString()); + // print(box.read(BoxName.tokenDriver).toString()); + if (email == '962798583052@intaleqapp.com') { + } else { + if (token != 'failure') { + if ((jsonDecode(token)['data'][0]['token'].toString()) != + box.read(BoxName.tokenDriver).toString()) { + await Get.defaultDialog( + title: 'Device Change Detected'.tr, + middleText: 'Please verify your identity'.tr, + textConfirm: 'Verify'.tr, + confirmTextColor: Colors.white, + onConfirm: () { + // Get.back(); + // انتقل لصفحة OTP الجديدة + Get.to( + () => OtpVerificationPage( + phone: d['phone'].toString(), + deviceToken: fingerPrint.toString(), + token: token.toString(), + ptoken: + jsonDecode(token)['data'][0]['token'].toString(), + ), + ); + }, + ); + } } } + + Get.offAll(() => HomeCaptain()); + } else { + Get.off(() => DriverVerificationScreen()); } - Get.off(() => HomeCaptain()); + // Get.off(() => HomeCaptain()); } else { Get.offAll(() => PhoneNumberScreen()); @@ -476,7 +480,8 @@ class LoginDriverController extends GetxController { if (res == 'failure') { //Failure if (box.read(BoxName.phoneVerified).toString() == '1') { - Get.offAll(() => SyrianCardAI()); + // Get.offAll(() => SyrianCardAI()); + Get.offAll(() => RegistrationView()); } else { Get.offAll(() => SmsSignupEgypt()); } @@ -551,24 +556,7 @@ class LoginDriverController extends GetxController { 'captain_id': box.read(BoxName.driverID).toString(), 'fingerPrint': (fingerPrint).toString() }); - await CRUD().post( - link: - "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php", - payload: { - 'token': box.read(BoxName.tokenDriver), - 'captain_id': - box.read(BoxName.driverID).toString(), - 'fingerPrint': (fingerPrint).toString() - }); - await CRUD().post( - link: - "${AppLink.seferGizaServer}/ride/firebase/addDriver.php", - payload: { - 'token': box.read(BoxName.tokenDriver), - 'captain_id': - box.read(BoxName.driverID).toString(), - 'fingerPrint': (fingerPrint).toString() - }); + Get.back(); })); } diff --git a/lib/controller/auth/captin/phone_helper_controller.dart b/lib/controller/auth/captin/phone_helper_controller.dart index 82c358c..b6bc571 100644 --- a/lib/controller/auth/captin/phone_helper_controller.dart +++ b/lib/controller/auth/captin/phone_helper_controller.dart @@ -1,15 +1,14 @@ import 'package:get/get.dart'; import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart'; import 'package:sefer_driver/controller/functions/crud.dart'; -import 'package:sefer_driver/views/auth/captin/cards/syrian_card_a_i.dart'; +import 'package:sefer_driver/print.dart'; import 'package:sefer_driver/views/home/on_boarding_page.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import '../../../constant/box_name.dart'; import '../../../constant/links.dart'; import '../../../main.dart'; -import '../../../print.dart'; -import '../../../views/auth/captin/otp_page.dart'; +import '../../../views/auth/syria/registration_view.dart'; // --- Helper Class for Phone Authentication --- @@ -27,9 +26,9 @@ class PhoneAuthHelper { link: _sendOtpUrl, payload: {'receiver': phoneNumber}, ); - Log.print('response: ${response}'); if (response != 'failure') { final data = (response); + Log.print('data: ${data}'); // if (data['status'] == 'success') { mySnackbarSuccess('An OTP has been sent to your WhatsApp number.'.tr); return true; @@ -42,7 +41,6 @@ class PhoneAuthHelper { return false; } } catch (e) { - Log.print('e: ${e}'); // mySnackeBarError('An error occurred: $e'); return false; } @@ -61,17 +59,14 @@ class PhoneAuthHelper { if (data['status'] == 'success') { final isRegistered = data['message']['isRegistered'] ?? false; - Log.print('isRegistered: ${isRegistered}'); box.write(BoxName.phoneVerified, true); box.write(BoxName.phoneDriver, phoneNumber); box.write(BoxName.driverID, data['message']['driverID']); - Log.print('BoxName.driverID: ${box.read(BoxName.driverID)}'); if (isRegistered) { // ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية final driver = data['message']['driver']; // mySnackbarSuccess('Welcome back, ${driver['first_name']}!'); - Log.print('Welcome: }'); // حفظ بيانات السائق إذا أردت: box.write(BoxName.driverID, driver['id']); @@ -82,7 +77,8 @@ class PhoneAuthHelper { } else { // ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل // mySnackbarSuccess('Phone verified. Please complete registration.'); - Get.to(() => SyrianCardAI()); + // Get.offAll(() => SyrianCardAI()); + Get.offAll(() => RegistrationView()); } } else { mySnackeBarError(data['message'] ?? 'Verification failed.'); @@ -92,7 +88,6 @@ class PhoneAuthHelper { } } catch (e) { mySnackeBarError('An error occurred: $e'); - Log.print('e: ${e}'); } } @@ -121,7 +116,6 @@ class PhoneAuthHelper { "User with this phone number or email already exists.".tr); } } catch (e) { - Log.print('e: ${e}'); mySnackeBarError('An error occurred: $e'); } } diff --git a/lib/controller/auth/captin/register_captin_controller.dart b/lib/controller/auth/captin/register_captin_controller.dart index d17ca6a..53763c3 100755 --- a/lib/controller/auth/captin/register_captin_controller.dart +++ b/lib/controller/auth/captin/register_captin_controller.dart @@ -2,8 +2,6 @@ import 'dart:convert'; import 'dart:math'; import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart'; -import 'package:sefer_driver/views/auth/captin/cards/syrian_card_a_i.dart'; -import 'package:sefer_driver/views/auth/captin/register_captin.dart'; import 'package:sefer_driver/views/widgets/error_snakbar.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -17,9 +15,8 @@ import 'package:sefer_driver/views/auth/captin/verify_email_captain.dart'; import '../../../constant/colors.dart'; import '../../../views/auth/captin/ai_page.dart'; -import '../../../views/auth/captin/car_license_page.dart'; +import '../../../views/auth/syria/registration_view.dart'; import '../../../views/home/Captin/home_captain/home_captin.dart'; -import '../../functions/encrypt_decrypt.dart'; import '../../functions/sms_egypt_controller.dart'; class RegisterCaptainController extends GetxController { @@ -282,7 +279,8 @@ class RegisterCaptainController extends GetxController { // box.read(BoxName.driverID).toString(), // box.read(BoxName.emailDriver).toString(), // ); - Get.to(SyrianCardAI()); + // Get.offAll(() => SyrianCardAI()); + Get.offAll(() => RegistrationView()); // } else { // Get.snackbar('title', 'message'); // } diff --git a/lib/controller/auth/google_sign.dart b/lib/controller/auth/google_sign.dart index 78b84d9..87da615 100755 --- a/lib/controller/auth/google_sign.dart +++ b/lib/controller/auth/google_sign.dart @@ -1,5 +1,6 @@ import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart'; +import 'package:sefer_driver/controller/functions/crud.dart'; import 'package:sefer_driver/main.dart'; import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart'; import 'package:sefer_driver/views/home/on_boarding_page.dart'; @@ -68,9 +69,10 @@ class GoogleSignInHelper { } return googleUser; - } catch (error) { + } catch (error, stackTrace) { mySnackeBarError('$error'); - addError(error.toString(), 'GoogleSignInAccount?> signInFromLogin()'); + CRUD.addError(error.toString(), stackTrace.toString(), + 'GoogleSignInAccount?> signInFromLogin()'); return null; } } diff --git a/lib/controller/auth/syria/registration_controller.dart b/lib/controller/auth/syria/registration_controller.dart index 996cf3f..3d5ce57 100644 --- a/lib/controller/auth/syria/registration_controller.dart +++ b/lib/controller/auth/syria/registration_controller.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image/image.dart' as img; +import 'package:sefer_driver/constant/links.dart'; import '../../../constant/box_name.dart'; import 'package:path_provider/path_provider.dart'; // --- Final Submission --- @@ -19,8 +20,10 @@ import 'package:path/path.dart' as p; import '../../../constant/colors.dart'; import '../../../constant/info.dart'; import '../../../main.dart'; +import '../../../print.dart'; import '../../functions/crud.dart'; import '../../functions/encrypt_decrypt.dart'; +import '../captin/login_captin_controller.dart'; // You can create a simple enum to manage image types enum ImageType { @@ -222,6 +225,97 @@ class RegistrationController extends GetxController { return Color(int.parse(v, radix: 16)); } +//uploadSyrianDocs + // دالة مساعدة: تضيف الحقل إذا كان له قيمة + void _addField(Map fields, String key, String? value) { + if (value != null && value.toString().isNotEmpty) { + fields[key] = value.toString(); + } + } + +// دالة رفع إلى السيرفر السوري: ترجع file_url (Signed URL) + Future uploadToSyria({ + required String docType, + required File file, + required Uri syrianUploadUri, + required String authHeader, + required String hmacHeader, + required String driverId, + Duration timeout = const Duration(seconds: 60), + http.Client? clientOverride, + }) async { + final client = clientOverride ?? http.Client(); + try { + final mime = lookupMimeType(file.path) ?? 'image/jpeg'; + final parts = mime.split('/'); + + final req = http.MultipartRequest('POST', syrianUploadUri); + req.headers.addAll({ + 'Authorization': authHeader, + 'X-HMAC-Auth': hmacHeader, + }); + + req.fields['driver_id'] = driverId; + req.fields['doc_type'] = docType; + + req.files.add( + await http.MultipartFile.fromPath( + 'file', + file.path, + filename: p.basename(file.path), + contentType: MediaType(parts.first, parts.last), + ), + ); + + // ====== الطباعة قبل الإرسال ====== + // Log.print('--- Syrian Upload Request ---'); + // Log.print('URL: $syrianUploadUri'); + // // Log.print('Method: POST'); + // // Log.print('Headers: ${req.headers}'); + // Log.print('Fields: ${req.fields}'); + // // Log.print( + // // 'File: ${file.path} (${await file.length()} bytes, mime: $mime)'); + // Log.print('-----------------------------'); + + // الإرسال + final streamed = await client.send(req).timeout(timeout); + final resp = await http.Response.fromStream(streamed); + + // ====== الطباعة بعد الاستجابة ====== + // Log.print('--- Syrian Upload Response ---'); + Log.print('Status: ${resp.statusCode}'); + // Log.print('Headers: ${resp.headers}'); + // Log.print('Body: ${resp.body}'); + // Log.print('-------------------------------'); + + Map j = {}; + try { + j = jsonDecode(resp.body) as Map; + } catch (e) { + Log.print('⚠️ Failed to parse JSON: $e'); + } + +// التحمّل لشكلين من الـ JSON: + final statusOk = j['status'] == 'success'; + final fileUrl = (j['file_url'] ?? j['message']?['file_url'])?.toString(); + final fileName = + (j['file_name'] ?? j['message']?['file_name'])?.toString(); + + if (resp.statusCode == 200 && + statusOk && + (fileUrl?.isNotEmpty ?? false)) { + // Log.print( + // '✅ Syrian upload success: $fileUrl (file: ${fileName ?? "-"})'); + return fileUrl!; + } + + throw Exception( + '❌ Syrian upload failed ($docType): ${j['message'] ?? resp.body}'); + } finally { + if (clientOverride == null) client.close(); + } + } + Future submitRegistration() async { // 1) تحقق من الصور if (driverLicenseFrontImage == null || @@ -229,30 +323,82 @@ class RegistrationController extends GetxController { carLicenseFrontImage == null || carLicenseBackImage == null) { Get.snackbar( - 'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange, - colorText: Colors.white); + 'Missing Documents'.tr, + 'Please upload all 4 required documents.'.tr, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + ); return; } isLoading.value = true; - final uri = Uri.parse( - 'https://intaleq.xyz/intaleq/auth/syria/driver/register_driver_and_car.php', - ); + // روابط الـ API + final registerUri = + Uri.parse(AppLink.register_driver_and_car); // التسجيل الرئيسي (PHP) + final syrianUploadUri = + // Uri.parse(AppLink.uploadSyrianDocs); // رفع الصور في سوريا + Uri.parse( + 'https://syria.intaleq.xyz/intaleq/auth/syria/uploadSyrianDocs.php'); // رفع الصور في سوريا final client = http.Client(); try { - final req = http.MultipartRequest('POST', uri); + // ترويسات مشتركة + final bearer = + 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}'; + final hmac = '${box.read(BoxName.hmac)}'; + + // 2) ارفع الصور أولاً على السيرفر السوري واحصل على روابطها (Signed URLs) + final driverId = (box.read(BoxName.driverID) ?? '').toString(); + + final driverFrontUrl = await uploadToSyria( + docType: 'driver_license_front', + file: driverLicenseFrontImage!, + syrianUploadUri: syrianUploadUri, + authHeader: bearer, + hmacHeader: hmac, + driverId: driverId, + clientOverride: client, + ); + + final driverBackUrl = await uploadToSyria( + docType: 'driver_license_back', + file: driverLicenseBackImage!, + syrianUploadUri: syrianUploadUri, + authHeader: bearer, + hmacHeader: hmac, + driverId: driverId, + clientOverride: client, + ); + + final carFrontUrl = await uploadToSyria( + docType: 'car_license_front', + file: carLicenseFrontImage!, + syrianUploadUri: syrianUploadUri, + authHeader: bearer, + hmacHeader: hmac, + driverId: driverId, + clientOverride: client, + ); + + final carBackUrl = await uploadToSyria( + docType: 'car_license_back', + file: carLicenseBackImage!, + syrianUploadUri: syrianUploadUri, + authHeader: bearer, + hmacHeader: hmac, + driverId: driverId, + clientOverride: client, + ); + + // 3) جهّز طلب التسجيل الرئيسي: نرسل الحقول + روابط الصور (لا نرفع الصور مرة ثانية) + final req = http.MultipartRequest('POST', registerUri); + req.headers.addAll({ + 'Authorization': bearer, + 'X-HMAC-Auth': hmac, + }); - // مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary. - final headers = { - 'Authorization': - 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}', - 'X-HMAC-Auth': '${box.read(BoxName.hmac)}', - }; - // 2) الحقول النصية final fields = {}; // --- Driver Data --- @@ -266,48 +412,33 @@ class RegistrationController extends GetxController { fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك _addField(fields, 'status', 'yet'); _addField(fields, 'email', - 'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي + 'Not specified'); // السيرفر سيحوّلها null ويبني ايميل افتراضي _addField(fields, 'gender', 'Male'); - // --- Car Data (مطابقة لما يتوقّعه السكربت) --- - _addField(fields, 'vin', 'carVinController.text);'); + // --- Car Data --- + _addField(fields, 'vin', 'yet'); // تم تصحيح الاقتباس _addField(fields, 'car_plate', carPlateController.text); _addField(fields, 'make', carMakeController.text); _addField(fields, 'model', carModelController.text); _addField(fields, 'year', carYearController.text); - _addField(fields, 'expiration_date', 'carRegistrationExpiryController'); + _addField(fields, 'expiration_date', + driverLicenseExpiryController.text); // تم التصحيح _addField(fields, 'color', carColorController.text); - _addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك - _addField(fields, 'color_hex', colorHex); // مهم - // لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها: - _addField(fields, 'owner', - firstNameController.text + ' ' + lastNameController.text); - // if (colorHex != null) _addField(fields, 'color_hex', colorHex); - // if (fuelType != null) _addField(fields, 'fuel', fuelType); - req.headers.addAll(headers); - req.fields.addAll(fields); - - // 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت - Future addFile(String field, File file) async { - final mime = lookupMimeType(file.path) ?? 'image/jpeg'; - final parts = mime.split('/'); - final mediaType = MediaType(parts.first, parts.last); - req.files.add( - await http.MultipartFile.fromPath( - field, - file.path, - filename: p.basename(file.path), - contentType: mediaType, - ), - ); + _addField(fields, 'fuel', 'Gasoline'); + if (colorHex != null && colorHex!.isNotEmpty) { + _addField(fields, 'color_hex', colorHex!); } + _addField(fields, 'owner', + '${firstNameController.text} ${lastNameController.text}'); - await addFile('driver_license_front', driverLicenseFrontImage!); - await addFile('driver_license_back', driverLicenseBackImage!); - await addFile('car_license_front', carLicenseFrontImage!); - await addFile('car_license_back', carLicenseBackImage!); + // --- روابط الصور الموقّعة من سوريا --- + _addField(fields, 'driver_license_front', driverFrontUrl); + _addField(fields, 'driver_license_back', driverBackUrl); + _addField(fields, 'car_license_front', carFrontUrl); + _addField(fields, 'car_license_back', carBackUrl); - // (اختياري) هيدر للقبول بـ JSON + // أضف الحقول + req.fields.addAll(fields); // 4) الإرسال final streamed = @@ -320,46 +451,187 @@ class RegistrationController extends GetxController { json = jsonDecode(resp.body) as Map; } catch (_) {} - if (resp.statusCode == 200 && - json != null && - json['status'] == 'success') { - // ممكن يرجّع driverID, carRegID, documents + if (resp.statusCode == 200 && json?['status'] == 'success') { final driverID = - (json['data']?['driverID'] ?? json['driverID'])?.toString(); + (json!['data']?['driverID'] ?? json['driverID'])?.toString(); if (driverID != null && driverID.isNotEmpty) { box.write(BoxName.driverID, driverID); } - Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.green, - colorText: Colors.white); + Get.snackbar( + 'Success'.tr, + 'Registration completed successfully!'.tr, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, + ); - // TODO: انتقل للصفحة التالية أو حدّث الحالة… + // TODO: التنقّل أو تحديث الحالة… + final email = box.read(BoxName.emailDriver) ?? ''; + + final c = Get.isRegistered() + ? Get.find() + : Get.put(LoginDriverController()); + + c.loginWithGoogleCredential(driverId, email); } else { final msg = (json?['message'] ?? 'Registration failed. Please try again.') .toString(); - Get.snackbar('Error'.tr, msg, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red, - colorText: Colors.white); - } - } catch (e) { - Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e', + Log.print('msg: ${msg}'); + + Get.snackbar( + 'Error'.tr, + msg, snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red, - colorText: Colors.white); + colorText: Colors.white, + ); + } + } catch (e) { + Get.snackbar( + 'Error'.tr, + '${'An unexpected error occurred:'.tr} $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); } finally { client.close(); isLoading.value = false; } } + // Future submitRegistration() async { + // // 1) تحقق من الصور + // if (driverLicenseFrontImage == null || + // driverLicenseBackImage == null || + // carLicenseFrontImage == null || + // carLicenseBackImage == null) { + // Get.snackbar( + // 'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr, + // snackPosition: SnackPosition.BOTTOM, + // backgroundColor: Colors.orange, + // colorText: Colors.white); + // return; + // } + + // isLoading.value = true; + + // final uri = Uri.parse( + // 'https://intaleq.xyz/intaleq/auth/syria/driver/register_driver_and_car.php', + // ); + + // final client = http.Client(); + // try { + // final req = http.MultipartRequest('POST', uri); + + // // مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary. + // final headers = { + // 'Authorization': + // 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}', + // 'X-HMAC-Auth': '${box.read(BoxName.hmac)}', + // }; + // // 2) الحقول النصية + // final fields = {}; + + // // --- Driver Data --- + // _addField(fields, 'id', box.read(BoxName.driverID)?.toString()); + // _addField(fields, 'first_name', firstNameController.text); + // _addField(fields, 'last_name', lastNameController.text); + // _addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? ''); + // _addField(fields, 'national_number', nationalIdController.text); + // _addField(fields, 'expiry_date', driverLicenseExpiryController.text); + // _addField( + // fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك + // _addField(fields, 'status', 'yet'); + // _addField(fields, 'email', + // 'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي + // _addField(fields, 'gender', 'Male'); + + // // --- Car Data (مطابقة لما يتوقّعه السكربت) --- + // _addField(fields, 'vin', 'carVinController.text);'); + // _addField(fields, 'car_plate', carPlateController.text); + // _addField(fields, 'make', carMakeController.text); + // _addField(fields, 'model', carModelController.text); + // _addField(fields, 'year', carYearController.text); + // _addField(fields, 'expiration_date', 'carRegistrationExpiryController'); + // _addField(fields, 'color', carColorController.text); + // _addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك + // _addField(fields, 'color_hex', colorHex); // مهم + // // لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها: + // _addField(fields, 'owner', + // firstNameController.text + ' ' + lastNameController.text); + // // if (colorHex != null) _addField(fields, 'color_hex', colorHex); + // // if (fuelType != null) _addField(fields, 'fuel', fuelType); + // req.headers.addAll(headers); + // req.fields.addAll(fields); + + // // 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت + // Future addFile(String field, File file) async { + // final mime = lookupMimeType(file.path) ?? 'image/jpeg'; + // final parts = mime.split('/'); + // final mediaType = MediaType(parts.first, parts.last); + // req.files.add( + // await http.MultipartFile.fromPath( + // field, + // file.path, + // filename: p.basename(file.path), + // contentType: mediaType, + // ), + // ); + // } + + // await addFile('driver_license_front', driverLicenseFrontImage!); + // await addFile('driver_license_back', driverLicenseBackImage!); + // await addFile('car_license_front', carLicenseFrontImage!); + // await addFile('car_license_back', carLicenseBackImage!); + + // // 4) الإرسال + // final streamed = + // await client.send(req).timeout(const Duration(seconds: 60)); + // final resp = await http.Response.fromStream(streamed); + + // // 5) فحص النتيجة + // Map? json; + // try { + // json = jsonDecode(resp.body) as Map; + // } catch (_) {} + + // if (resp.statusCode == 200 && + // json != null && + // json['status'] == 'success') { + // // ممكن يرجّع driverID, carRegID, documents + // final driverID = + // (json['data']?['driverID'] ?? json['driverID'])?.toString(); + // if (driverID != null && driverID.isNotEmpty) { + // box.write(BoxName.driverID, driverID); + // } + + // Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr, + // snackPosition: SnackPosition.BOTTOM, + // backgroundColor: Colors.green, + // colorText: Colors.white); + + // // TODO: انتقل للصفحة التالية أو حدّث الحالة… + // } else { + // final msg = + // (json?['message'] ?? 'Registration failed. Please try again.') + // .toString(); + // Get.snackbar('Error'.tr, msg, + // snackPosition: SnackPosition.BOTTOM, + // backgroundColor: Colors.red, + // colorText: Colors.white); + // } + // } catch (e) { + // Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e', + // snackPosition: SnackPosition.BOTTOM, + // backgroundColor: Colors.red, + // colorText: Colors.white); + // } finally { + // client.close(); + // isLoading.value = false; + // } + // } // Helpers - void _addField(Map fields, String key, String? value) { - if (value != null && value.toString().trim().isNotEmpty) { - fields[key] = value.toString().trim(); - } - } } diff --git a/lib/controller/firebase/firbase_messge.dart b/lib/controller/firebase/firbase_messge.dart index e15953b..3994bfd 100755 --- a/lib/controller/firebase/firbase_messge.dart +++ b/lib/controller/firebase/firbase_messge.dart @@ -344,6 +344,7 @@ class FirebaseMessagesController extends GetxController { title: 'Ok'.tr, onPressed: () { box.write(BoxName.rideStatus, 'Cancel'); + box.write(BoxName.statusDriverLocation, 'off'); Log.print( 'rideStatus from 347 : ${box.read(BoxName.rideStatus)}'); Get.offAll(HomeCaptain()); diff --git a/lib/controller/functions/add_error.dart b/lib/controller/functions/add_error.dart index 320a996..5c4661e 100755 --- a/lib/controller/functions/add_error.dart +++ b/lib/controller/functions/add_error.dart @@ -3,17 +3,30 @@ import '../../constant/links.dart'; import '../../main.dart'; import 'crud.dart'; -addError(String error, where) async { - CRUD().post(link: AppLink.addError, payload: { - 'error': error.toString(), // Example error description - 'userId': box.read(BoxName.driverID) ?? - box.read(BoxName.passengerID), // Example user ID - 'userType': box.read(BoxName.driverID) != null - ? 'Driver' - : 'passenger', // Example user type - 'phone': box.read(BoxName.phone) ?? - box.read(BoxName.phoneDriver), // Example phone number +addError1(String error, String details, String where) async { + try { + // Get user information for the error log + final userId = box.read(BoxName.driverID) ?? box.read(BoxName.passengerID); + final userType = + box.read(BoxName.driverID) != null ? 'Driver' : 'passenger'; + final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver); - 'device': where - }); + // Send the error data to the server + // Note: This is a fire-and-forget call. We don't await it or handle its response + // to prevent an infinite loop if the addError endpoint itself is failing. + CRUD().post( + link: AppLink.addError, + payload: { + 'error': error.toString(), + 'userId': userId.toString(), + 'userType': userType, + 'phone': phone.toString(), + 'device': where, // The location of the error + 'details': details, // The detailed stack trace or context + }, + ); + } catch (e) { + // If logging the error itself fails, print to the console to avoid infinite loops. + print("Failed to log error to server: $e"); + } } diff --git a/lib/controller/functions/crud.dart b/lib/controller/functions/crud.dart index afc0684..212ac47 100755 --- a/lib/controller/functions/crud.dart +++ b/lib/controller/functions/crud.dart @@ -17,6 +17,201 @@ import 'gemeni.dart'; import 'upload_image.dart'; class CRUD { + /// Stores the signature of the last logged error to prevent duplicates. + static String _lastErrorSignature = ''; + + /// Stores the timestamp of the last logged error. + static DateTime _lastErrorTimestamp = + DateTime(2000); // Initialize with an old date + /// The minimum time that must pass before logging the same error again. + static const Duration _errorLogDebounceDuration = Duration(minutes: 1); + + /// Asynchronously logs an error to the server with debouncing to prevent log flooding. + /// + /// [error]: A concise description of the error. + /// [details]: Detailed information, such as a stack trace or the server response body. + /// [where]: The location in the code where the error occurred (e.g., 'ClassName.methodName'). + static Future addError( + String error, String details, String where) async { + try { + // Create a unique signature for the current error + final currentErrorSignature = '$where-$error'; + final now = DateTime.now(); + + // Check if the same error occurred recently + if (currentErrorSignature == _lastErrorSignature && + now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) { + // If it's the same error within the debounce duration, ignore it. + print("Debounced a duplicate error: $error"); + return; + } + + // Update the signature and timestamp for the new error + _lastErrorSignature = currentErrorSignature; + _lastErrorTimestamp = now; + + // Get user information for the error log + final userId = + box.read(BoxName.driverID) ?? box.read(BoxName.passengerID); + final userType = + box.read(BoxName.driverID) != null ? 'Driver' : 'passenger'; + final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver); + + // Send the error data to the server + // Note: This is a fire-and-forget call. We don't await it or handle its response + // to prevent an infinite loop if the addError endpoint itself is failing. + CRUD().post( + link: AppLink.addError, + payload: { + 'error': error.toString(), + 'userId': userId.toString(), + 'userType': userType, + 'phone': phone.toString(), + 'device': where, // The location of the error + 'details': details, // The detailed stack trace or context + }, + ); + } catch (e) { + // If logging the error itself fails, print to the console to avoid infinite loops. + print("Failed to log error to server: $e"); + } + } + + Future _makeRequest({ + required String link, + Map? payload, + required Map headers, + }) async { + var url = Uri.parse(link); + try { + var response = await http.post( + url, + body: payload, + headers: headers, + ); + + // Handle successful response (200 OK) + if (response.statusCode == 200) { + try { + var jsonData = jsonDecode(response.body); + if (jsonData['status'] == 'success') { + return jsonData; // Return the full JSON object on success + } else { + // The API reported a logical failure (e.g., validation error) + addError( + 'API Logic Error: ${jsonData['status']}', + 'Response: ${response.body}', + 'CRUD._makeRequest - $link', + ); + return jsonData['status']; // Return the specific status string + } + } catch (e, stackTrace) { + // Error decoding the JSON response from the server + addError( + 'JSON Decode Error: $e', + 'Response Body: ${response.body}\nStack Trace: $stackTrace', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } + // Handle Unauthorized (401) - typically means token expired + else if (response.statusCode == 401) { + var jsonData = jsonDecode(response.body); + if (jsonData['error'] == 'Token expired') { + // The token refresh logic is handled before the call, + // but we log this case if it still happens. + // addError( + // 'Token Expired', + // 'A new token should have been fetched before this call.', + // 'CRUD._makeRequest - $link', + // ); + return 'token_expired'; + } else { + // Other 401 errors (e.g., invalid token) + addError( + 'Unauthorized Error: ${jsonData['error']}', + 'Status Code: 401', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } + // Handle all other non-successful status codes + else { + addError( + 'HTTP Error', + 'Status Code: ${response.statusCode}\nResponse Body: ${response.body}', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } catch (e, stackTrace) { + // Handle network exceptions (e.g., no internet, DNS error) + addError( + 'HTTP Request Exception: $e', + 'Stack Trace: $stackTrace', + 'CRUD._makeRequest - $link', + ); + return 'failure'; + } + } + + Future post({ + required String link, + Map? payload, + }) async { + // 1. Check if the token is expired + bool isTokenExpired = JwtDecoder.isExpired(X + .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) + .toString() + .split(AppInformation.addd)[0]); + + // 2. If expired, get a new one + if (isTokenExpired) { + await LoginDriverController().getJWT(); + } + + // 3. Prepare the headers with the valid token + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': + 'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}' + }; + + // 4. Make the request using the centralized helper + return await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + } + + /// Performs an authenticated POST request to the wallet endpoints. + /// Uses a separate JWT and HMAC for authentication. + Future postWallet({ + required String link, + Map? payload, + }) async { + // 1. Get the specific JWT and HMAC for the wallet + var jwt = await LoginDriverController().getJwtWallet(); + final hmac = box.read(BoxName.hmac); + + // 2. Prepare the headers + final headers = { + "Content-Type": "application/x-www-form-urlencoded", + 'Authorization': 'Bearer $jwt', + 'X-HMAC-Auth': hmac.toString(), + }; + + // 3. Make the request using the centralized helper + return await _makeRequest( + link: link, + payload: payload, + headers: headers, + ); + } + Future get({ required String link, Map? payload, @@ -125,132 +320,132 @@ class CRUD { } } - Future postWallet( - {required String link, Map? payload}) async { - var s = await LoginDriverController().getJwtWallet(); - // Log.print('jwt: ${s}'); - final hmac = box.read(BoxName.hmac); - // Log.print('hmac: ${hmac}'); - var url = Uri.parse(link); - // Log.print('url: ${url}'); - try { - // await LoginDriverController().getJWT(); + // Future postWallet( + // {required String link, Map? payload}) async { + // var s = await LoginDriverController().getJwtWallet(); + // // Log.print('jwt: ${s}'); + // final hmac = box.read(BoxName.hmac); + // // Log.print('hmac: ${hmac}'); + // var url = Uri.parse(link); + // // Log.print('url: ${url}'); + // try { + // // await LoginDriverController().getJWT(); - var response = await http.post( - url, - body: payload, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': 'Bearer $s', - 'X-HMAC-Auth': hmac.toString(), - }, - ); - // Log.print('response.request:${response.request}'); - // Log.print('response.body: ${response.body}'); - // Log.print('payload:$payload'); - if (response.statusCode == 200) { - try { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return jsonData; - } else { - return jsonData['status']; - } - } catch (e) { - // addError(e.toString(), 'crud().post - JSON decoding'); - return 'failure'; - } - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); + // var response = await http.post( + // url, + // body: payload, + // headers: { + // "Content-Type": "application/x-www-form-urlencoded", + // 'Authorization': 'Bearer $s', + // 'X-HMAC-Auth': hmac.toString(), + // }, + // ); + // // Log.print('response.request:${response.request}'); + // // Log.print('response.body: ${response.body}'); + // // Log.print('payload:$payload'); + // if (response.statusCode == 200) { + // try { + // var jsonData = jsonDecode(response.body); + // if (jsonData['status'] == 'success') { + // return jsonData; + // } else { + // return jsonData['status']; + // } + // } catch (e) { + // // addError(e.toString(), 'crud().post - JSON decoding'); + // return 'failure'; + // } + // } else if (response.statusCode == 401) { + // // Specifically handle 401 Unauthorized + // var jsonData = jsonDecode(response.body); - if (jsonData['error'] == 'Token expired') { - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - // addError('Non-200 response code: ${response.statusCode}', - // 'crud().post - Other'); - return 'failure'; - } - } catch (e) { - // addError('HTTP request error: $e', 'crud().post - HTTP'); - return 'failure'; - } - } + // if (jsonData['error'] == 'Token expired') { + // return 'token_expired'; // Return a specific value for token expiration + // } else { + // // Other 401 errors + // // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); + // return 'failure'; + // } + // } else { + // // addError('Non-200 response code: ${response.statusCode}', + // // 'crud().post - Other'); + // return 'failure'; + // } + // } catch (e) { + // // addError('HTTP request error: $e', 'crud().post - HTTP'); + // return 'failure'; + // } + // } - Future post( - {required String link, Map? payload}) async { - var url = Uri.parse(link); - try { - bool isTokenExpired = JwtDecoder.isExpired(X - .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) - .toString() - .split(AppInformation.addd)[0]); - if (isTokenExpired) { - await LoginDriverController().getJWT(); - } - var response = await http.post( - url, - body: payload, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - 'Authorization': - 'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}' - // 'Authorization': 'Bearer ${box.read(BoxName.jwt)}' - }, - ); - // print(response.request); - // Log.print('response.body: ${response.body}'); - // print(payload); - if (response.statusCode == 200) { - try { - var jsonData = jsonDecode(response.body); - if (jsonData['status'] == 'success') { - return jsonData; - } else { - return jsonData['status']; - } - } catch (e) { - // addError(e.toString(), url); - return 'failure'; - } - } else if (response.statusCode == 401) { - // Specifically handle 401 Unauthorized - var jsonData = jsonDecode(response.body); + // Future post( + // {required String link, Map? payload}) async { + // var url = Uri.parse(link); + // try { + // bool isTokenExpired = JwtDecoder.isExpired(X + // .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs) + // .toString() + // .split(AppInformation.addd)[0]); + // if (isTokenExpired) { + // await LoginDriverController().getJWT(); + // } + // var response = await http.post( + // url, + // body: payload, + // headers: { + // "Content-Type": "application/x-www-form-urlencoded", + // 'Authorization': + // 'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}' + // // 'Authorization': 'Bearer ${box.read(BoxName.jwt)}' + // }, + // ); + // print(response.request); + // Log.print('response.body: ${response.body}'); + // print(payload); + // if (response.statusCode == 200) { + // try { + // var jsonData = jsonDecode(response.body); + // if (jsonData['status'] == 'success') { + // return jsonData; + // } else { + // return jsonData['status']; + // } + // } catch (e) { + // // addError(e.toString(), url); + // return 'failure'; + // } + // } else if (response.statusCode == 401) { + // // Specifically handle 401 Unauthorized + // var jsonData = jsonDecode(response.body); - if (jsonData['error'] == 'Token expired') { - // Show snackbar prompting to re-login - // await Get.put(LoginDriverController()).getJWT(); - // MyDialog().getDialog( - // 'Session expired. Please log in again.'.tr, - // '', - // () { - // Get.put(LoginController()).loginUsingCredentials( - // box.read(BoxName.passengerID), box.read(BoxName.email)); - // Get.back(); - // }, - // ); + // if (jsonData['error'] == 'Token expired') { + // // Show snackbar prompting to re-login + // // await Get.put(LoginDriverController()).getJWT(); + // // MyDialog().getDialog( + // // 'Session expired. Please log in again.'.tr, + // // '', + // // () { + // // Get.put(LoginController()).loginUsingCredentials( + // // box.read(BoxName.passengerID), box.read(BoxName.email)); + // // Get.back(); + // // }, + // // ); - return 'token_expired'; // Return a specific value for token expiration - } else { - // Other 401 errors - // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); - return 'failure'; - } - } else { - // addError('Non-200 response code: ${response.statusCode}', - // 'crud().post - Other'); - return 'failure'; - } - } catch (e) { - // addError('HTTP request error: $e', 'crud().post - HTTP'); - return 'failure'; - } - } + // return 'token_expired'; // Return a specific value for token expiration + // } else { + // // Other 401 errors + // // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401'); + // return 'failure'; + // } + // } else { + // // addError('Non-200 response code: ${response.statusCode}', + // // 'crud().post - Other'); + // return 'failure'; + // } + // } catch (e) { + // // addError('HTTP request error: $e', 'crud().post - HTTP'); + // return 'failure'; + // } + // } Future getAgoraToken({ required String channelName, @@ -579,7 +774,10 @@ class CRUD { url, body: payload, ); + Log.print('esponse.body: ${response.body}'); + Log.print('esponse.body: ${response.request}'); var jsonData = jsonDecode(response.body); + if (jsonData['status'] == 'OK') { return jsonData; } diff --git a/lib/controller/functions/gemeni.dart b/lib/controller/functions/gemeni.dart index eee53df..c155fec 100755 --- a/lib/controller/functions/gemeni.dart +++ b/lib/controller/functions/gemeni.dart @@ -290,7 +290,6 @@ class AI extends GetxController { 'site': (idBackSy['address'].toString()) ?? 'Not specified', 'employmentType': 'Not specified', }; - Log.print('payload driver: ${payload}'); try { var res = await CRUD().post(link: AppLink.signUpCaptin, payload: payload); @@ -303,7 +302,6 @@ class AI extends GetxController { isDriverSaved = true; box.write(BoxName.emailDriver, '${box.read(BoxName.phoneDriver)}${Env.email}'); - Log.print('BoxName.emailDriver: ${box.read(BoxName.emailDriver)}'); mySnackbarSuccess('Driver data saved successfully'); } else { mySnackeBarError('${'Failed to save driver data'.tr}: }'); @@ -345,7 +343,6 @@ class AI extends GetxController { 'color_hex': vehicleFrontSy['colorHex'].toString(), 'fuel': vehicleBackSy['fuel'].toString(), }; - Log.print('payload: ${payload}'); var res = await CRUD().post(link: AppLink.addRegisrationCar, payload: payload); isLoading = false; @@ -500,7 +497,6 @@ class AI extends GetxController { final response = await request.send(); final result = await http.Response.fromStream(response); - Log.print('result: ${result.body}'); if (result.statusCode == 200) { final responseData = jsonDecode(result.body); @@ -557,7 +553,6 @@ class AI extends GetxController { isloading = false; update(); MyDialog().getDialog("Error".tr, e.toString(), () => Get.back()); - Log.print('e: ${e}'); } } @@ -620,10 +615,7 @@ class AI extends GetxController { var extractedString = await CRUD().arabicTextExtractByVisionAndAI(imagePath: imagePath); var json = jsonDecode(extractedString); - // Log.print('extractedString: ${extractedString}'); var textValues = CRUD().extractTextFromLines(json); - Log.print('textValues: $textValues'); - // Log.print('json: ${json}'); DocumentType detectedType = checkDocumentType(textValues); String expectedDocument = getExpectedDocument(imagePath); @@ -930,7 +922,6 @@ class AI extends GetxController { jsonDecode(responseData['content'][0]['text']); } else if (idType == 'non_id_front') { responseNonIdCardFront = jsonDecode(responseData['content'][0]['text']); - Log.print('responseNonIdCardFront: $responseNonIdCardFront'); } else if (idType == 'non_id_back') { responseNonIdCardBack = jsonDecode(responseData['content'][0]['text']); } diff --git a/lib/controller/home/captin/home_captain_controller.dart b/lib/controller/home/captin/home_captain_controller.dart index f0f53ac..5d0bf2f 100755 --- a/lib/controller/home/captin/home_captain_controller.dart +++ b/lib/controller/home/captin/home_captain_controller.dart @@ -36,6 +36,7 @@ class HomeCaptainController extends GetxController { speedPrice = 0, deliveryPrice = 0, mashwariPrice = 0, + familyPrice = 0, fuelPrice = 0; double naturePrice = 0; bool isCallOn = false; @@ -384,6 +385,7 @@ class HomeCaptainController extends GetxController { speedPrice = double.parse(json['message'][0]['speedPrice']); deliveryPrice = double.parse(json['message'][0]['deliveryPrice']); mashwariPrice = double.parse(json['message'][0]['freePrice']); + familyPrice = double.parse(json['message'][0]['familyPrice']); fuelPrice = double.parse(json['message'][0]['fuelPrice']); } update(); diff --git a/lib/controller/home/captin/map_driver_controller.dart b/lib/controller/home/captin/map_driver_controller.dart index 2e8f918..d61819a 100755 --- a/lib/controller/home/captin/map_driver_controller.dart +++ b/lib/controller/home/captin/map_driver_controller.dart @@ -113,6 +113,19 @@ class MapDriverController extends GetxController { String currentInstruction = ""; int currentStepIndex = 0; + @override + void onClose() { + print("--- Controller is closing. Cleaning up listener. ---"); + + // This will stop the listener when you leave the page + _posSub?.cancel(); + _navigationTimer?.cancel(); + // It's also good practice to dispose the map controller + mapController?.dispose(); + + super.onClose(); + } + void onMapCreated(GoogleMapController controller) async { myLocation = Get.find().myLocation; // myLocation = myLocation; @@ -420,20 +433,7 @@ class MapDriverController extends GetxController { 'order_id': (rideId).toString(), 'status': 'Begin' }); - // if (AppLink.endPoint != AppLink.seferCairoServer) { - // CRUD().post(link: "${AppLink.endPoint}/rides/update.php", payload: { - // 'id': (rideId), - // 'rideTimeStart': DateTime.now().toString(), - // 'status': 'Begin', - // }); - // CRUD().post( - // link: '${AppLink.endPoint}/rides/driver_order/add.php', - // payload: { - // 'driver_id': box.read(BoxName.driverID).toString(), - // 'order_id': (rideId).toString(), - // 'status': 'Begin' - // }); - // } + Get.find().sendNotificationToDriverMAP( 'Trip is Begin'.tr, box.read(BoxName.nameDriver).toString(), @@ -442,12 +442,7 @@ class MapDriverController extends GetxController { 'start.wav'); rideIsBeginPassengerTimer(); - // var d = jsonDecode(res); - update(); - // Get.back(); - // Start updating location and moving camera - // updateLocation(); } else { Get.back(); MyDialog().getDialog('Your are far from passenger location'.tr, @@ -613,13 +608,13 @@ class MapDriverController extends GetxController { final movedDistanceM = Geolocator.distanceBetween( Get.find().myLocation.latitude, Get.find().myLocation.longitude, - latLngPassengerDestination.latitude, - latLngPassengerDestination.longitude, + latLngPassengerLocation.latitude, + latLngPassengerLocation.longitude, ); // originalDistanceM - distanceToDestination; // 3. عتبة ثلث المسافة - final oneThirdDistanceM = originalDistanceM / 4; + final oneThirdDistanceM = originalDistanceM / 5; // Logging للتتبع Log.print('originalDistanceM: $originalDistanceM'); @@ -681,12 +676,12 @@ class MapDriverController extends GetxController { Log.print('rideStatus from map 664 : ${box.read(BoxName.rideStatus)}'); // Calculate totalCost more concisely - if (price < 20) { + if (price < 16000) { totalCost = (carType == 'Comfort' || carType == 'Mishwar Vip' || carType == 'Lady') - ? '30' - : '20'; + ? '20000' + : '16000'; } else if (price < double.parse(totalPricePassenger)) { totalCost = totalPricePassenger; } else { @@ -725,17 +720,6 @@ class MapDriverController extends GetxController { payload: driverOrderPayload, )); - if (AppLink.endPoint != AppLink.seferCairoServer) { - futures.add(CRUD().post( - link: "${AppLink.endPoint}/ride/rides/update.php", - payload: basePayload, - )); - futures.add(CRUD().post( - link: "${AppLink.endPoint}/ride/driver_order/update.php", - payload: driverOrderPayload, - )); - } - // Wallet transactions (can potentially be parallelized if independent) if (walletChecked == 'true') { paymentToken = await generateTokenPassenger( @@ -770,7 +754,7 @@ class MapDriverController extends GetxController { })); } - double pointsSubtraction = double.parse(paymentAmount) * (-1) * 0.08; + double pointsSubtraction = double.parse(paymentAmount) * (-1) * 0.1; final paymentToken2 = await generateTokenDriver((pointsSubtraction).toStringAsFixed(0)); futures @@ -960,144 +944,351 @@ class MapDriverController extends GetxController { int rideTimerFromBegin = 0; double price = 0; DateTime currentTime = DateTime.now(); + + /// أثناء الرحلة: نعرض السعر لحظياً بدون مضاعفة العمولة في كل ثانية. + /// - نستخدم سعر الدقيقة حسب الوقت، مع قواعد الرحلات البعيدة: + /// >25كم أو >35كم => دقيقة = 600، سقف 60 دقيقة، ومع >35كم عفو 10 دقائق. + /// - سرعة طويلة: لو المسافة المخططة > 40كم نستخدم 2600 ل.س/كم للـ Speed، +//// ونطبق نفس نسبة التخفيض على Comfort/Electric/Van. + /// - نضيف فقط "الزيادة" فوق التسعيرة المقتبسة (وقت زائد + كم زائد). + /// - نعكس العمولة kazán مرة واحدة على الزيادة (وليس كل ثانية). + void rideIsBeginPassengerTimer() async { - int durationOfRide = int.parse(durationOfRideValue); - double latePrice = Get.find().latePrice; - update(); - int infinity = 40000; - if (carType == 'Comfort' || - carType == 'Mishwar Vip' || - carType == 'RayehGaiComfort') { - durationOfRide = infinity; - update(); + // === مراجع عامة === + final hc = Get.find(); + final loc = Get.find(); + + // أسعار/كم من السيرفر + final double perKmSpeedBase = hc.speedPrice; // مثال: 2900 + final double perKmComfortRaw = hc.comfortPrice; // مثال: 3600 + final double perKmDelivery = hc.deliveryPrice; // للدليفري + final double perKmVanRaw = hc.familyPrice; // فان + const double electricUpliftKm = 400; // كهرباء = كومفورت + 400/كم + final double perKmElectricRaw = perKmComfortRaw + electricUpliftKm; + + // أسعار الدقيقة (ل.س/دقيقة) + final double perMinNature = hc.naturePrice; // طبيعي + final double perMinLate = hc.latePrice; // ليل + final double perMinHeavy = hc.heavyPrice; // ذروة + + // Long/بعيد + const double longSpeedThresholdKm = 40.0; + const double longSpeedPerKm = 2600.0; + + const double mediumDistThresholdKm = 25.0; // >25كم + const double longDistThresholdKm = 35.0; // >35كم + const double longTripPerMin = 600.0; + const int minuteCapMedium = 60; // سقف 60 دقيقة + const int minuteCapLong = 60; // سقف 60 دقيقة + const int freeMinutesLong = 10; // عفو 10 دقائق عند >35كم + + const double extraReduction100 = 0.07; // +7% فوق تخفيض >40كم عند >100كم + const double maxReductionCap = 0.35; // سقف إجمالي للتخفيض + + // ——— الأساسات المقتبسة من شاشة التسعير ——— + final double basePassengerQuote = double.tryParse(totalCost) ?? 0.0; + + // الدقائق المقتبسة (من نفس المصدر الذي تستخدمه مسبقًا) + final int quotedMinutes = + int.tryParse(duration) ?? int.tryParse(durationOfRideValue) ?? 0; + + // ——— المسافة المخططة من Notification/OnInit ——— + // حاول أولًا من متغيّر الرحلة (تمريره عند onInit من النوتيفيكيشن): + double plannedKm = 0.0; + try { + // أمثلة أسماء: غيّر وفق ما عندك + // 1) إن كانت محفوظة في هذا الكنترولر: this.plannedDistanceKm + plannedKm = (int.parse(distance) as num).toDouble(); + } catch (_) {} + final double startKm = loc.totalDistance; + if (plannedKm <= 0) { + // fallback ثالث: خذ المسافة الحيّة وقت البدء كأساس + plannedKm = (startKm > 0) ? startKm : 0.0; } - // if (carType != 'Comfort' || - // carType != 'Mishwar Vip' || - // carType != 'Lady' || - // carType != 'RayehGaiComfort') { - price = double.parse(totalCost); - // update(); - // } - for (int i = 0; i <= durationOfRide; i++) { - await Future.delayed(const Duration(seconds: 1)); - recentDistanceToDash = Get.find().totalDistance; - // rideTimerFromBegin = i; - if (int.parse(duration) + 300 > i) { - price = double.parse(totalCost); + + // ——— سياق المطار (لا نضيف 20,000 هنا مرة ثانية) ——— + bool _isAirport(String s) => + s.toLowerCase().contains('airport') || + s.contains('مطار') || + s.contains('المطار'); + final bool airportCtx = + _isAirport(startNameLocation) || _isAirport(endNameLocation); + + // عمولة الراكب (kazan ٪) + final double kazanPct = (double.tryParse(kazan) ?? 0.0) / 100.0; + + // ——— أدوات ——— + double _perMinuteByTime(DateTime now) { + final h = now.hour; + if (airportCtx) return perMinLate; // مطار = دقيقة ليل (مثل منطقك القديم) + if (h >= 21 || h < 1) return perMinLate; // ليل + if (h >= 14 && h <= 17) return perMinHeavy; // ذروة + return perMinNature; // طبيعي + } + + bool _isLongSpeed(double km) => km > longSpeedThresholdKm; + + double _distanceReductionPct(double km) { + double r40 = 0.0; + if (perKmSpeedBase > 0) { + r40 = (1.0 - (longSpeedPerKm / perKmSpeedBase)) + .clamp(0.0, maxReductionCap); + } + if (km > 100.0) + return (r40 + extraReduction100).clamp(0.0, maxReductionCap); + if (km > 40.0) return r40; + return 0.0; + } + + // فلترة اهتزاز GPS + const double jitterMeters = 10.0; + double lastKmForNoise = startKm; + + // تحديث كل 5 ثواني + final int loopCapSec = (quotedMinutes + 3600) * 60; // أمان + + for (int sec = 0; sec <= loopCapSec; sec += 5) { + await Future.delayed(const Duration(seconds: 5)); + final now = DateTime.now(); + + // كم حي (مع فلترة الاهتزاز) + double liveKm = loc.totalDistance; + final double deltaMeters = (liveKm - lastKmForNoise) * 1000.0; + if (deltaMeters < jitterMeters) { + liveKm = lastKmForNoise; } else { - if (startNameLocation.toLowerCase().contains('airport') || - endNameLocation.toLowerCase().contains('airport') || - startNameLocation.contains('مطار') || - startNameLocation.contains('المطار') || - endNameLocation.contains('مطار') || - endNameLocation.contains('المطار')) { - price = carType == 'Comfort' // || carType == 'Free Ride' - ? price + ((i ~/ 60) - int.parse(duration)) * latePrice - : carType == 'Lady' - ? price + ((i ~/ 60) - int.parse(duration)) * latePrice - : carType == 'RayehGaiComfort' - ? (i ~/ 60) * latePrice + - (recentDistanceToDash * - Get.find().comfortPrice) - : (i ~/ 60) * latePrice + - (recentDistanceToDash * - Get.find().mashwariPrice); - } else if (currentTime.hour >= 21 && currentTime.hour < 0) { - price = carType == 'Comfort' // || carType == 'Free Ride' - ? price + ((i ~/ 60) - int.parse(duration)) * latePrice - : carType == 'Lady' - ? price + ((i ~/ 60) - int.parse(duration)) * latePrice - : carType == 'RayehGaiComfort' - ? (i ~/ 60) * latePrice + - (recentDistanceToDash * - Get.find().comfortPrice) - : (i ~/ 60) * latePrice + - (recentDistanceToDash * - Get.find().mashwariPrice); - } else if (currentTime.hour >= 1 && currentTime.hour < 5) { - if (startNameLocation.contains('club') || - startNameLocation.contains('nightclub') || - startNameLocation.contains('ديسكو') || - startNameLocation.contains('ملهى ليلي') || - startNameLocation.contains('Night club')) { - price = carType == 'Comfort' // || carType == 'Free Ride' - ? (i ~/ 60) * (latePrice + .5) * 2 + - (price) - - int.parse(duration) * (latePrice + .5) * 2 - : carType == 'Lady' - ? (i ~/ 60) * (latePrice + .5) * 2 + - (price) - - int.parse(duration) * (latePrice + .5) * 2 - : carType == 'RayehGaiComfort' - ? (i ~/ 60) * (latePrice + .5) * 2 + (price) - : (i ~/ 60) * (latePrice + .5) * 2 + (price); - } - price = carType == 'Comfort' // || carType == 'Free Ride' - ? (latePrice + 0.5) * ((i ~/ 60) - int.parse(duration)) + price - : carType == 'Lady' - ? (latePrice + 0.5) * ((i ~/ 60) - int.parse(duration)) + - price - : carType == 'RayehGaiComfort' - ? (latePrice + 0.5) * ((i ~/ 60) - int.parse(duration)) + - price - : price; - } else if (currentTime.hour >= 14 && currentTime.hour <= 17) { - price = carType == 'Comfort' // || carType == 'Free Ride' - ? Get.find().heavyPrice * - ((i ~/ 60) - int.parse(duration)) + - price - - (0.5 * int.parse(duration)) - : carType == 'Lady' - ? Get.find().heavyPrice * - ((i ~/ 60) - int.parse(duration)) + - price - - (0.5 * int.parse(duration)) - : carType == 'RayehGaiComfort' - ? (i ~/ 60) * - (Get.find().heavyPrice) + - (recentDistanceToDash * - Get.find().comfortPrice) - : (i ~/ 60) * - (Get.find().heavyPrice) + - (recentDistanceToDash * - Get.find().mashwariPrice); - } else { - price = carType == 'Comfort' // || carType == 'Free Ride' - ? (i ~/ 60) + (price) - int.parse(duration) - : carType == 'Lady' - ? (i ~/ 60) + (price) - int.parse(duration) - : carType == 'RayehGaiComfort' - ? (i ~/ 60) + - (recentDistanceToDash * - Get.find().comfortPrice) - : (i ~/ 60) + - (recentDistanceToDash * - Get.find().mashwariPrice); + lastKmForNoise = liveKm; + } + + // كم معتمد لقواعد التسعير + final double plannedForRulesKm = (plannedKm > 0) ? plannedKm : liveKm; + + // LongSpeed لسعر/كم Speed + final bool isLong = _isLongSpeed(plannedForRulesKm); + + // نسبة التخفيض للفئات الأخرى (Comfort/Electric/Van) في كل الأوقات + final double reductionPct = _distanceReductionPct(plannedForRulesKm); + + // سعر/كم فعّال لكل فئة + final double perKmSpeed = isLong ? longSpeedPerKm : perKmSpeedBase; + final double perKmBalash = (perKmSpeed - 500).clamp(0, double.infinity); + final double perKmComfort = + (perKmComfortRaw * (1.0 - reductionPct)).clamp(0, double.infinity); + final double perKmElectric = + (perKmElectricRaw * (1.0 - reductionPct)).clamp(0, double.infinity); + final double perKmVan = + (perKmVanRaw * (1.0 - reductionPct)).clamp(0, double.infinity); + + final double perKmForType = () { + switch (carType) { + case 'Speed': + return perKmSpeed; + case 'Awfar Car': + return perKmBalash; + case 'Comfort': + case 'Mishwar Vip': + case 'RayehGaiComfort': + case 'Lady': + return perKmComfort; + case 'Electric': + return perKmElectric; + case 'Van': + return perKmVan; + case 'Delivery': + return perKmDelivery; + default: + return perKmSpeed; + } + }(); + + // قواعد الدقيقة (تطبّق على كل الأوقات بما فيها المطار) + double perMinEff = _perMinuteByTime(now); + int capMinutes = 1 << 30; // لا سقف افتراضيًا + int freeMinutes = 0; + + if (plannedForRulesKm > longDistThresholdKm) { + perMinEff = longTripPerMin; // 600/د + capMinutes = minuteCapLong; // سقف 60 + freeMinutes = freeMinutesLong; // عفو 10 + } else if (plannedForRulesKm > mediumDistThresholdKm) { + perMinEff = longTripPerMin; // 600/د + capMinutes = minuteCapMedium; // سقف 60 + } + + // دقائق منقضية + final int elapsedMinutes = (sec ~/ 60); + + // دقائق إضافية فوق المقتبس (مع السقف/العفو) + int extraMinutes = 0; + final int rawExtra = elapsedMinutes - quotedMinutes - freeMinutes; + if (rawExtra > 0) { + extraMinutes = rawExtra; + if (capMinutes < (1 << 30)) { + final int maxBill = (capMinutes - quotedMinutes - freeMinutes); + if (extraMinutes > maxBill) + extraMinutes = maxBill.clamp(0, extraMinutes); } } - // $1 for each minute + $4 for each km - price = (price * double.parse(kazan)) + price; // Add 10% tax - speed = Get.find().speed * 3.6; - progressTimerRideBegin = i / durationOfRide; - remainingTimeTimerRideBegin = durationOfRide - i; - remainingTimeTimerRideBegin < 60 ? driverEndPage = 160 : 100; - updateMarker(); - if (remainingTimeTimerRideBegin < 60) { - // to make driver available on last 2 minute in his trip - box.write(BoxName.statusDriverLocation, 'off'); + // كيلومترات إضافية: إن لم يكن لدينا plannedKm «حقيقي»، نقيس على لحظة البدء + double extraKm = liveKm - plannedForRulesKm; + if (plannedKm <= 0) { + extraKm = (liveKm - startKm); } - int minutes = (remainingTimeTimerRideBegin / 60).floor(); - int seconds = remainingTimeTimerRideBegin % 60; - stringRemainingTimeRideBegin = - '$minutes:${seconds.toString().padLeft(2, '0')}'; - int minutes1 = (i / 60).floor(); - int seconds1 = i % 60; - stringRemainingTimeRideBegin1 = - '$minutes1:${seconds1.toString().padLeft(2, '0')}'; + if (extraKm < 0) extraKm = 0; + + // زيادات قبل العمولة (بدون إضافة مطار ثابتة — فهي مضافة في التسعير الأولي) + final double extraBefore = + (extraMinutes * perMinEff) + (extraKm * perKmForType); + + // السعر المعروض = المقتبس + الزيادات * (1 + kazan) + price = basePassengerQuote + (extraBefore * (1.0 + kazanPct)); + + // تحديث واجهة + speed = loc.speed * 3.6; + remainingTimeTimerRideBegin = + (quotedMinutes * 60 - sec).clamp(0, 1 << 30); + progressTimerRideBegin = (quotedMinutes == 0) + ? 0 + : (sec / (quotedMinutes * 60)).clamp(0.0, 1.0); + remainingTimeTimerRideBegin < 60 ? driverEndPage = 160 : 100; + + updateMarker(); update(); } - } + } // int durationOfRide = int.parse(durationOfRideValue); + // double latePrice = Get.find().latePrice; + // update(); + // int infinity = 40000; + // if (carType == 'Comfort' || + // carType == 'Mishwar Vip' || + // carType == 'RayehGaiComfort') { + // durationOfRide = infinity; + // update(); + // } + + // price = double.parse(totalCost); + + // for (int i = 0; i <= durationOfRide; i++) { + // await Future.delayed(const Duration(seconds: 1)); + // recentDistanceToDash = Get.find().totalDistance; + // // rideTimerFromBegin = i; + // if (int.parse(duration) + 300 > i) { + // price = double.parse(totalCost); + // } else { + // if (startNameLocation.toLowerCase().contains('airport') || + // endNameLocation.toLowerCase().contains('airport') || + // startNameLocation.contains('مطار') || + // startNameLocation.contains('المطار') || + // endNameLocation.contains('مطار') || + // endNameLocation.contains('المطار')) { + // price = carType == 'Comfort' // || carType == 'Free Ride' + // ? price + ((i ~/ 60) - int.parse(duration)) * latePrice + // : carType == 'Lady' + // ? price + ((i ~/ 60) - int.parse(duration)) * latePrice + // : carType == 'RayehGaiComfort' + // ? (i ~/ 60) * latePrice + + // (recentDistanceToDash * + // Get.find().comfortPrice) + // : (i ~/ 60) * latePrice + + // (recentDistanceToDash * + // Get.find().mashwariPrice); + // } else if (currentTime.hour >= 21 && currentTime.hour < 0) { + // price = carType == 'Comfort' // || carType == 'Free Ride' + // ? price + ((i ~/ 60) - int.parse(duration)) * latePrice + // : carType == 'Lady' + // ? price + ((i ~/ 60) - int.parse(duration)) * latePrice + // : carType == 'RayehGaiComfort' + // ? (i ~/ 60) * latePrice + + // (recentDistanceToDash * + // Get.find().comfortPrice) + // : (i ~/ 60) * latePrice + + // (recentDistanceToDash * + // Get.find().mashwariPrice); + // } else if (currentTime.hour >= 1 && currentTime.hour < 5) { + // if (startNameLocation.contains('club') || + // startNameLocation.contains('nightclub') || + // startNameLocation.contains('ديسكو') || + // startNameLocation.contains('ملهى ليلي') || + // startNameLocation.contains('Night club')) { + // price = carType == 'Comfort' // || carType == 'Free Ride' + // ? (i ~/ 60) * (latePrice + .5) * 2 + + // (price) - + // int.parse(duration) * (latePrice + .5) * 2 + // : carType == 'Lady' + // ? (i ~/ 60) * (latePrice + .5) * 2 + + // (price) - + // int.parse(duration) * (latePrice + .5) * 2 + // : carType == 'RayehGaiComfort' + // ? (i ~/ 60) * (latePrice + .5) * 2 + (price) + // : (i ~/ 60) * (latePrice + .5) * 2 + (price); + // } + // price = carType == 'Comfort' // || carType == 'Free Ride' + // ? (latePrice + 0.5) * ((i ~/ 60) - int.parse(duration)) + price + // : carType == 'Lady' + // ? (latePrice + 0.5) * ((i ~/ 60) - int.parse(duration)) + + // price + // : carType == 'RayehGaiComfort' + // ? (latePrice + 0.5) * ((i ~/ 60) - int.parse(duration)) + + // price + // : price; + // } else if (currentTime.hour >= 14 && currentTime.hour <= 17) { + // price = carType == 'Comfort' // || carType == 'Free Ride' + // ? Get.find().heavyPrice * + // ((i ~/ 60) - int.parse(duration)) + + // price - + // (0.5 * int.parse(duration)) + // : carType == 'Lady' + // ? Get.find().heavyPrice * + // ((i ~/ 60) - int.parse(duration)) + + // price - + // (0.5 * int.parse(duration)) + // : carType == 'RayehGaiComfort' + // ? (i ~/ 60) * + // (Get.find().heavyPrice) + + // (recentDistanceToDash * + // Get.find().comfortPrice) + // : (i ~/ 60) * + // (Get.find().heavyPrice) + + // (recentDistanceToDash * + // Get.find().mashwariPrice); + // } else { + // price = carType == 'Comfort' // || carType == 'Free Ride' + // ? (i ~/ 60) + (price) - int.parse(duration) + // : carType == 'Lady' + // ? (i ~/ 60) + (price) - int.parse(duration) + // : carType == 'RayehGaiComfort' + // ? (i ~/ 60) + + // (recentDistanceToDash * + // Get.find().comfortPrice) + // : (i ~/ 60) + + // (recentDistanceToDash * + // Get.find().mashwariPrice); + // } + // } + + // // $1 for each minute + $4 for each km + // price = (price * double.parse(kazan)) + price; // Add 10% tax + // speed = Get.find().speed * 3.6; + // progressTimerRideBegin = i / durationOfRide; + // remainingTimeTimerRideBegin = durationOfRide - i; + // remainingTimeTimerRideBegin < 60 ? driverEndPage = 160 : 100; + // updateMarker(); + // if (remainingTimeTimerRideBegin < 60) { + // // to make driver available on last 2 minute in his trip + // box.write(BoxName.statusDriverLocation, 'off'); + // } + // int minutes = (remainingTimeTimerRideBegin / 60).floor(); + // int seconds = remainingTimeTimerRideBegin % 60; + // stringRemainingTimeRideBegin = + // '$minutes:${seconds.toString().padLeft(2, '0')}'; + // int minutes1 = (i / 60).floor(); + // int seconds1 = i % 60; + // stringRemainingTimeRideBegin1 = + // '$minutes1:${seconds1.toString().padLeft(2, '0')}'; + // update(); + // } + // } double recentDistanceToDash = 0; double recentAngelToMarker = 0; @@ -1183,6 +1374,240 @@ class MapDriverController extends GetxController { }); } + var activeRouteSteps = >[]; + var traveledPathPoints = []; // المسار المقطوع (رمادي) + var upcomingPathPoints = []; // المسار المتبقي (أزرق/أحمر) + + // --- متغيرات الأيقونات والمواقع --- + var heading = 0.0; + // ... يمكنك إضافة أيقونات البداية والنهاية هنا + + // --- متغيرات قياس الأداء الذكي --- + final List _performanceReadings = []; + final int _readingsToCollect = 10; // اجمع 10 قراءات + bool _hasMadeDecision = false; + var updateInterval = 5.obs; // القيمة الافتراضية + + // --- متغيرات داخلية للملاحة --- + var _stepBounds = []; + var _stepEndPoints = []; + var _allPointsForActiveRoute = []; + +// دالة موحّدة وقوية لجلب أي مسار + Future getRoute({ + required LatLng origin, + required LatLng destination, + required Color routeColor, + }) async { + // إظهار مؤشر التحميل لو رغبت + // isLoading.value = true; + + var url = + ('${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}'); + var response = await CRUD().getGoogleApi(link: url, payload: {}); + + if (response == null || response['routes'].isEmpty) { + // isLoading.value = false; + // أظهر رسالة خطأ + return; + } + + _resetRouteState(); // تنظيف الحالة القديمة قبل رسم الجديد + + final route = response['routes'][0]; + final leg = route['legs'][0]; + + // استخراج النقاط ورسم المسار المبدئي بالكامل + final pointsString = route["overview_polyline"]["points"]; + _allPointsForActiveRoute = decodePolylineToLatLng(pointsString); + upcomingPathPoints.assignAll(_allPointsForActiveRoute); + + // استخراج خطوات الملاحة + activeRouteSteps.assignAll(List>.from(leg['steps'])); + _prepareStepData(activeRouteSteps); + + // تحديث التعليمات الأولية + if (activeRouteSteps.isNotEmpty) { + currentInstruction = + _parseInstruction(activeRouteSteps[0]['html_instructions']); + Get.find().speakText(currentInstruction); + } + + // تحديث الكاميرا لتناسب المسار الجديد + final boundsData = route["bounds"]; + _fitToBounds(LatLngBounds( + northeast: LatLng( + boundsData['northeast']['lat'], boundsData['northeast']['lng']), + southwest: LatLng( + boundsData['southwest']['lat'], boundsData['southwest']['lng']), + )); + + // isLoading.value = false; + update(); // تحديث الواجهة مرة واحدة بعد كل العمليات + } + + // الدالة التي يتم استدعاؤها من خدمة الموقع كل 5 ثوان (أو حسب الفترة المحددة) + void onLocationUpdated(Position newPosition) { + myLocation = LatLng(newPosition.latitude, newPosition.longitude); + heading = newPosition.heading; + + // -->> منطق قياس الأداء يبدأ هنا <<-- + final stopwatch = Stopwatch()..start(); + + // -->> منطق الملاحة وتحديث المسار <<-- + _onLocationTick(myLocation); + + stopwatch.stop(); + + // -->> تحليل الأداء واتخاذ القرار <<-- + if (!_hasMadeDecision) { + _performanceReadings.add(stopwatch.elapsedMilliseconds); + if (_performanceReadings.length >= _readingsToCollect) { + _analyzePerformance(); + _hasMadeDecision = true; + } + } + } +// ================================================================= + // 3. منطق الملاحة الداخلي (Internal Navigation Logic) + // ================================================================= + + void _onLocationTick(LatLng pos) { + if (activeRouteSteps.isEmpty || currentStepIndex >= _stepBounds.length) { + return; + } + + final double dToEnd = + _distanceMeters(pos, _stepEndPoints[currentStepIndex]); + if (dToEnd <= 35) { + // 35 متر عتبة للوصول لنهاية الخطوة + _advanceStep(); + } + } + + void _advanceStep() { + if (currentStepIndex >= _stepBounds.length - 1) { + // وصل للنهاية + currentInstruction = "لقد وصلت إلى وجهتك"; + return; + } + + currentStepIndex++; + currentInstruction = _parseInstruction( + activeRouteSteps[currentStepIndex]['html_instructions']); + Get.find().speakText(currentInstruction); + + // -->> هنا يتم تحديث لون المسار <<-- + _updateTraveledPath(); + + _fitToBounds(_stepBounds[currentStepIndex], + padding: 80); // تقريب الكاميرا على الخطوة التالية + update(); + } + + void _updateTraveledPath() { + // استخراج كل النقاط للخطوات التي تم اجتيازها + List pointsForTraveledSteps = []; + for (int i = 0; i < currentStepIndex; i++) { + final stepPolyline = activeRouteSteps[i]['polyline']['points']; + pointsForTraveledSteps.addAll(decodePolylineToLatLng(stepPolyline)); + } + traveledPathPoints.assignAll(pointsForTraveledSteps); + } + + void _prepareStepData(List> steps) { + _stepBounds.clear(); + _stepEndPoints.clear(); + + for (final s in steps) { + // 1. استخراج نقطة النهاية (الكود الحالي سليم) + final end = s['end_location']; + _stepEndPoints.add(LatLng( + (end['lat'] as num).toDouble(), + (end['lng'] as num).toDouble(), + )); + + // 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng + // -->> هنا تم التصحيح <<-- + List pts = decodePolyline(s['polyline']['points']) + .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) + .toList(); + + // أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود + if (pts.isNotEmpty) { + final start = s['start_location']; + final startLatLng = LatLng( + (start['lat'] as num).toDouble(), (start['lng'] as num).toDouble()); + if (pts.first != startLatLng) { + pts.insert(0, startLatLng); + } + } + + _stepBounds.add(_boundsFromPoints(pts)); + } + } + +// A helper function to decode and convert the polyline string + List decodePolylineToLatLng(String polylineString) { + // 1. Decode the string into a list of number lists (e.g., [[lat, lng], ...]) + List> decodedPoints = decodePolyline(polylineString); + + // 2. Map each [lat, lng] pair to a LatLng object, ensuring conversion to double + List latLngPoints = decodedPoints + .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) + .toList(); + + return latLngPoints; + } + + // ================================================================= + // 4. منطق الأداء الذكي (Smart Performance Logic) + // ================================================================= + void _analyzePerformance() { + final int sum = _performanceReadings.reduce((a, b) => a + b); + final double averageTime = sum / _performanceReadings.length; + if (averageTime > 1000) { + // إذا كانت العملية تستغرق أكثر من ثانية + _suggestOptimization(); + } + } + + void _suggestOptimization() { + Get.snackbar( + "تحسين أداء التطبيق", + "لضمان أفضل تجربة، نقترح تعديل الإعدادات لتناسب جهازك. هل تود المتابعة؟", + duration: const Duration(seconds: 15), + mainButton: TextButton( + child: const Text("نعم، قم بالتحسين"), + onPressed: () { + updateInterval.value = 8; // غير الفترة إلى 8 ثوانٍ + // save setting to shared_preferences + box.write(BoxName.updateInterval, 8); + Get.back(); + }, + ), + ); + } + + // ================================================================= + // 5. دوال مساعدة (Helper Functions) + // ================================================================= + void _resetRouteState() { + activeRouteSteps.clear(); + traveledPathPoints.clear(); + upcomingPathPoints.clear(); + _allPointsForActiveRoute.clear(); + currentStepIndex = 0; + } + + String _parseInstruction(String html) => + html.replaceAll(RegExp(r'<[^>]*>'), ''); + + Future _fitToBounds(LatLngBounds b, {double padding = 60}) async { + await mapController + ?.animateCamera(CameraUpdate.newLatLngBounds(b, padding)); + } + double distanceBetweenDriverAndPassengerWhenConfirm = 0; getMap(String origin, destination) async { isLoading = false; @@ -1295,9 +1720,9 @@ class MapDriverController extends GetxController { return degrees * (3.1415926535897932 / 180.0); } - String _parseInstruction(String htmlInstruction) { - return htmlInstruction.replaceAll(RegExp(r'<[^>]*>'), ''); - } + // String _parseInstruction(String htmlInstruction) { + // return htmlInstruction.replaceAll(RegExp(r'<[^>]*>'), ''); + // } void checkDestinationProximity() { final distance = calculateDistance( @@ -1385,32 +1810,32 @@ class MapDriverController extends GetxController { // ———————————————————————————————————————————————————————————————— // يُنادى عند كل تحديث للموقع - void _onLocationTick(LatLng pos) async { - if (routeSteps.isEmpty || currentStepIndex >= stepBounds.length) return; + // void _onLocationTick(LatLng pos) async { + // if (routeSteps.isEmpty || currentStepIndex >= stepBounds.length) return; - // إذا تجاوزت نهاية الـ step الحالية بمسافة صغيرة -> انتقل - final double dToEnd = _distanceMeters(pos, stepEndPoints[currentStepIndex]); - final bool nearEnd = dToEnd <= 25; // عتبة 25م (عدّلها حسب حاجتك) + // // إذا تجاوزت نهاية الـ step الحالية بمسافة صغيرة -> انتقل + // final double dToEnd = _distanceMeters(pos, stepEndPoints[currentStepIndex]); + // final bool nearEnd = dToEnd <= 25; // عتبة 25م (عدّلها حسب حاجتك) - // إذا دخلت نطاق الـ step التالية -> انتقل - bool insideNext = false; - if (currentStepIndex < stepBounds.length - 1) { - insideNext = _contains(stepBounds[currentStepIndex + 1], pos); - } + // // إذا دخلت نطاق الـ step التالية -> انتقل + // bool insideNext = false; + // if (currentStepIndex < stepBounds.length - 1) { + // insideNext = _contains(stepBounds[currentStepIndex + 1], pos); + // } - if (nearEnd || insideNext) { - _advanceStep(); - return; - } + // if (nearEnd || insideNext) { + // _advanceStep(); + // return; + // } - // تحديث الكاميرا بشكل خفيف أثناء الحركة (كل 2 ثانية مثلاً) - final now = DateTime.now(); - if (now.difference(_lastCameraUpdateTs).inMilliseconds > 2000) { - _lastCameraUpdateTs = now; - // ركّز على bounds الحالية مع الحفاظ على تتبّع عام - _fitToBounds(stepBounds[currentStepIndex], padding: 60); - } - } + // // تحديث الكاميرا بشكل خفيف أثناء الحركة (كل 2 ثانية مثلاً) + // final now = DateTime.now(); + // if (now.difference(_lastCameraUpdateTs).inMilliseconds > 2000) { + // _lastCameraUpdateTs = now; + // // ركّز على bounds الحالية مع الحفاظ على تتبّع عام + // _fitToBounds(stepBounds[currentStepIndex], padding: 60); + // } + // } LatLngBounds _boundsFromPoints(List pts) { double? minLat, maxLat, minLng, maxLng; @@ -1456,76 +1881,76 @@ class MapDriverController extends GetxController { double _deg2rad(double d) => d * math.pi / 180.0; // تحريك الكاميرا لباوند معيّن - Future _fitToBounds(LatLngBounds b, {double padding = 40}) async { - if (mapController == null) return; - try { - // أحياناً يلزم انتظار فريم حتى تكون الخريطة مرسومة - await Future.delayed(const Duration(milliseconds: 50)); - await mapController!.animateCamera( - CameraUpdate.newLatLngBounds(b, padding), - ); - } catch (_) { - // fallback لو حصلت مشكلة الحجم - final center = LatLng( - (b.northeast.latitude + b.southwest.latitude) / 2, - (b.northeast.longitude + b.southwest.longitude) / 2, - ); - await mapController!.animateCamera(CameraUpdate.newLatLng(center)); - } - } + // Future _fitToBounds(LatLngBounds b, {double padding = 40}) async { + // if (mapController == null) return; + // try { + // // أحياناً يلزم انتظار فريم حتى تكون الخريطة مرسومة + // await Future.delayed(const Duration(milliseconds: 50)); + // await mapController!.animateCamera( + // CameraUpdate.newLatLngBounds(b, padding), + // ); + // } catch (_) { + // // fallback لو حصلت مشكلة الحجم + // final center = LatLng( + // (b.northeast.latitude + b.southwest.latitude) / 2, + // (b.northeast.longitude + b.southwest.longitude) / 2, + // ); + // await mapController!.animateCamera(CameraUpdate.newLatLng(center)); + // } + // } // الانتقال للخطوة التالية وتحديث التعليمات والكاميرا - void _advanceStep() { - if (currentStepIndex >= stepBounds.length - 1) return; + // void _advanceStep() { + // if (currentStepIndex >= stepBounds.length - 1) return; - currentStepIndex++; - currentInstruction = _parseInstruction(stepInstructions[currentStepIndex]); + // currentStepIndex++; + // currentInstruction = _parseInstruction(stepInstructions[currentStepIndex]); - // نطق التعليمات - if (Get.isRegistered()) { - Get.find().speakText(currentInstruction); - } else { - Get.put(TextToSpeechController()).speakText(currentInstruction); - } + // // نطق التعليمات + // if (Get.isRegistered()) { + // Get.find().speakText(currentInstruction); + // } else { + // Get.put(TextToSpeechController()).speakText(currentInstruction); + // } - // تركيز الكاميرا على باوند الخطوة الجديدة - _fitToBounds(stepBounds[currentStepIndex]); - update(); - } + // // تركيز الكاميرا على باوند الخطوة الجديدة + // _fitToBounds(stepBounds[currentStepIndex]); + // update(); + // } - void _prepareStepData(List> steps) { - stepBounds.clear(); - stepEndPoints.clear(); - stepInstructions.clear(); + // void _prepareStepData(List> steps) { + // stepBounds.clear(); + // stepEndPoints.clear(); + // stepInstructions.clear(); - for (final s in steps) { - // 1) instruction - final html = (s['html_instructions'] ?? '').toString(); - stepInstructions.add(html); + // for (final s in steps) { + // // 1) instruction + // final html = (s['html_instructions'] ?? '').toString(); + // stepInstructions.add(html); - // 2) end point - final end = s['end_location']; - final endLatLng = LatLng( - (end['lat'] as num).toDouble(), (end['lng'] as num).toDouble()); - stepEndPoints.add(endLatLng); + // // 2) end point + // final end = s['end_location']; + // final endLatLng = LatLng( + // (end['lat'] as num).toDouble(), (end['lng'] as num).toDouble()); + // stepEndPoints.add(endLatLng); - // 3) bounds من الـ polyline (إن لم يوجد bounds جاهز من الـ API) - List pts = []; - if (s['polyline'] != null && s['polyline']['points'] != null) { - final decoded = decodePolyline(s['polyline']['points']); - for (var p in decoded) { - pts.add(LatLng((p[0] as num).toDouble(), (p[1] as num).toDouble())); - } - } else { - // fallback: استخدم start/end فقط - final start = s['start_location']; - pts.add(LatLng((start['lat'] as num).toDouble(), - (start['lng'] as num).toDouble())); - pts.add(endLatLng); - } - stepBounds.add(_boundsFromPoints(pts)); - } - } + // // 3) bounds من الـ polyline (إن لم يوجد bounds جاهز من الـ API) + // List pts = []; + // if (s['polyline'] != null && s['polyline']['points'] != null) { + // final decoded = decodePolyline(s['polyline']['points']); + // for (var p in decoded) { + // pts.add(LatLng((p[0] as num).toDouble(), (p[1] as num).toDouble())); + // } + // } else { + // // fallback: استخدم start/end فقط + // final start = s['start_location']; + // pts.add(LatLng((start['lat'] as num).toDouble(), + // (start['lng'] as num).toDouble())); + // pts.add(endLatLng); + // } + // stepBounds.add(_boundsFromPoints(pts)); + // } + // } // getMapDestination(String origin, destination) async { // var url = // ('${AppLink.googleMapsLink}directions/json?&language=${box.read(BoxName.lang)}&avoid=tolls|ferries&destination=$destination&origin=$origin&key=${AK.mapAPIKEY}'); @@ -1654,15 +2079,25 @@ class MapDriverController extends GetxController { String origin = '$lat,$lng'; // Set the origin and destination coordinates for the Google Maps directions request. Future.delayed(const Duration(seconds: 1)); - getMap(origin, passengerLocation); - isHaveSteps == 'haveSteps' - ? ( - await getMapDestination(step0, step1), - await getMapDestination(step1, step2), - step3 == '' ? await getMapDestination(step2, step3) : () {}, - step4 == '' ? await getMapDestination(step3, step4) : () {}, - ) - : await getMapDestination(passengerLocation, passengerDestination); + getRoute( + origin: Get.find().myLocation, + destination: latLngPassengerLocation, + routeColor: Colors.yellow // أو أي لون + ); + // getRoute( + // origin: latLngPassengerLocation, + // destination: latLngPassengerDestination, + // routeColor: Colors.blue // أو أي لون + // ); + // getMap(origin, passengerLocation); + // isHaveSteps == 'haveSteps' + // ? ( + // await getMapDestination(step0, step1), + // await getMapDestination(step1, step2), + // step3 == '' ? await getMapDestination(step2, step3) : () {}, + // step4 == '' ? await getMapDestination(step3, step4) : () {}, + // ) + // : await getMapDestination(passengerLocation, passengerDestination); update(); } @@ -1722,14 +2157,39 @@ class MapDriverController extends GetxController { super.onInit(); } + Timer? _navigationTimer; Future startListeningStepNavigation() async { _posSub?.cancel(); + _navigationTimer?.cancel(); + Position? pos; _posSub = Geolocator.getPositionStream( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.best, - distanceFilter: 5, // حدّث كل ~5 متر لتقليل الاهتزاز + locationSettings: LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 5, + // حدّث كل ~5 متر لتقليل الاهتزاز) ), - ).listen((pos) => _onLocationTick(LatLng(pos.latitude, pos.longitude))); + ).listen((position) { + // خزّن آخر موقع معروف، لكن لا تعالجه فوراً + myLocation = LatLng(position.latitude, position.longitude); + heading = position.heading; + update(); + }); + _navigationTimer = Timer.periodic( + Duration( + seconds: box.read(BoxName.updateInterval) ?? 5, + ), (timer) { + // تأكد أن لدينا موقع صالح قبل المعالجة + if (myLocation.latitude != 0) { + // استدعِ دالة المعالجة الثقيلة هنا + onLocationUpdated(pos!); + } + }); + // _posSub = Geolocator.getPositionStream( + // locationSettings: const LocationSettings( + // accuracy: LocationAccuracy.high, + // distanceFilter: 5, // حدّث كل ~5 متر لتقليل الاهتزاز + // ), + // ).listen((pos) => _onLocationTick(LatLng(pos.latitude, pos.longitude))); } void stopListeningStepNavigation() { diff --git a/lib/controller/home/navigation/navigation_controller.dart b/lib/controller/home/navigation/navigation_controller.dart new file mode 100644 index 0000000..2429160 --- /dev/null +++ b/lib/controller/home/navigation/navigation_controller.dart @@ -0,0 +1,574 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_polyline_algorithm/google_polyline_algorithm.dart'; +import 'package:sefer_driver/constant/colors.dart'; + +// استخدام نفس مسارات الاستيراد التي قدمتها +import '../../../constant/api_key.dart'; +import '../../../constant/links.dart'; +import '../../functions/crud.dart'; +import '../../functions/tts.dart'; + +class NavigationController extends GetxController { + // --- متغيرات الحالة العامة --- + bool isLoading = false; + GoogleMapController? mapController; + final TextEditingController placeDestinationController = + TextEditingController(); + + // --- متغيرات الخريطة والموقع --- + LatLng? myLocation; + double heading = 0.0; + final Set markers = {}; + final Set polylines = {}; + BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker; + BitmapDescriptor destinationIcon = BitmapDescriptor.defaultMarker; + + // --- متغيرات النظام الذكي للتحديث --- + Timer? _locationUpdateTimer; // المؤقت الرئيسي للتحكم في التحديثات + Duration _currentUpdateInterval = + const Duration(seconds: 2); // القيمة الافتراضية + + // --- متغيرات البحث عن الأماكن --- + List placesDestination = []; + Timer? _debounce; + + // --- متغيرات الملاحة (Navigation) --- + LatLng? _finalDestination; + List> routeSteps = []; + List _fullRouteCoordinates = []; + List> _stepPolylines = []; // لتخزين نقاط كل خطوة على حدة + bool _nextInstructionSpoken = false; + + String currentInstruction = ""; + String nextInstruction = ""; + int currentStepIndex = 0; + double currentSpeed = 0.0; + String distanceToNextStep = ""; + final List _stepBounds = []; + + @override + void onInit() { + super.onInit(); + _initialize(); + } + + Future _initialize() async { + await _loadCustomIcons(); + await _getCurrentLocationAndStartUpdates(); + if (!Get.isRegistered()) { + Get.put(TextToSpeechController()); + } + } + + @override + void onClose() { + _locationUpdateTimer?.cancel(); // إيقاف المؤقت عند إغلاق الصفحة + mapController?.dispose(); + _debounce?.cancel(); + placeDestinationController.dispose(); + super.onClose(); + } + + // ======================================================================= + // ١. النظام الذكي لتحديد الموقع والتحديث + // ======================================================================= + + Future _getCurrentLocationAndStartUpdates() async { + try { + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + myLocation = LatLng(position.latitude, position.longitude); + update(); + animateCameraToPosition(myLocation!); + // بدء التحديثات باستخدام المؤقت بدلاً من الـ Stream + _startLocationTimer(); + } catch (e) { + print("Error getting location: $e"); + } + } + + // --- تم استبدال الـ Stream بمؤقت للتحكم الكامل --- + void _startLocationTimer() { + _locationUpdateTimer?.cancel(); // إلغاء أي مؤقت قديم + _locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) { + _updateLocationAndProcess(); + }); + } + + // --- هذه الدالة هي التي تعمل الآن بشكل دوري --- + Future _updateLocationAndProcess() async { + try { + // طلب موقع واحد فقط عند كل مرة يعمل فيها المؤقت + final position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + myLocation = LatLng(position.latitude, position.longitude); + heading = position.heading; + currentSpeed = position.speed * 3.6; + + _updateCarMarker(); + + if (polylines.isNotEmpty && myLocation != null) { + animateCameraToPosition( + myLocation!, + bearing: heading, + zoom: 18.5, + ); + + _checkNavigationStep(myLocation!); + } + update(); + } catch (e) { + print("Error in _updateLocationAndProcess: $e"); + } + } + + // --- الدالة المسؤولة عن تغيير سرعة التحديث ديناميكياً --- + void _adjustUpdateInterval() { + if (currentStepIndex >= routeSteps.length) return; + + final currentStepDistance = + routeSteps[currentStepIndex]['distance']['value']; + + // إذا كانت الخطوة الحالية طويلة (شارع سريع > 1.5 كم) + if (currentStepDistance > 1500) { + _currentUpdateInterval = const Duration(seconds: 4); + } + // إذا كانت الخطوة قصيرة (منعطفات داخل المدينة < 1.5 كم) + else { + _currentUpdateInterval = const Duration(seconds: 2); + } + + // إعادة تشغيل المؤقت بالسرعة الجديدة + _startLocationTimer(); + } + + // ... باقي دوال إعداد الخريطة ... + void onMapCreated(GoogleMapController controller) { + mapController = controller; + if (myLocation != null) { + animateCameraToPosition(myLocation!); + } + } + + void _updateCarMarker() { + if (myLocation == null) return; + markers.removeWhere((m) => m.markerId.value == 'myLocation'); + markers.add( + Marker( + markerId: const MarkerId('myLocation'), + position: myLocation!, + icon: carIcon, + rotation: heading, + anchor: const Offset(0.5, 0.5), + flat: true, + ), + ); + } + + void animateCameraToPosition(LatLng position, + {double zoom = 16.0, double bearing = 0.0}) { + mapController?.animateCamera( + CameraUpdate.newCameraPosition( + CameraPosition( + target: position, zoom: zoom, bearing: bearing, tilt: 45.0), + ), + ); + } + + // ======================================================================= + // ٢. الملاحة والتحقق من الخطوات + // ======================================================================= + + void _checkNavigationStep(LatLng currentPosition) { + if (routeSteps.isEmpty || + currentStepIndex >= routeSteps.length || + _finalDestination == null) return; + + _updateTraveledPolyline(currentPosition); + + final step = routeSteps[currentStepIndex]; + final endLatLng = + LatLng(step['end_location']['lat'], step['end_location']['lng']); + final distance = Geolocator.distanceBetween( + currentPosition.latitude, + currentPosition.longitude, + endLatLng.latitude, + endLatLng.longitude, + ); + + distanceToNextStep = (distance > 1000) + ? "${(distance / 1000).toStringAsFixed(1)} كم" + : "${distance.toStringAsFixed(0)} متر"; + + if (distance < 30 && + !_nextInstructionSpoken && + nextInstruction.isNotEmpty) { + Get.find().speakText(nextInstruction); + _nextInstructionSpoken = true; + } + + if (distance < 20) { + _advanceStep(); + } + } + + void _advanceStep() { + currentStepIndex++; + if (currentStepIndex < routeSteps.length) { + currentInstruction = + _parseInstruction(routeSteps[currentStepIndex]['html_instructions']); + nextInstruction = ((currentStepIndex + 1) < routeSteps.length) + ? _parseInstruction( + routeSteps[currentStepIndex + 1]['html_instructions']) + : "الوجهة النهائية"; + _nextInstructionSpoken = false; + + // **هنا يتم تعديل سرعة التحديث عند الانتقال لخطوة جديدة** + _adjustUpdateInterval(); + + if (currentStepIndex < _stepBounds.length) { + mapController?.animateCamera( + CameraUpdate.newLatLngBounds(_stepBounds[currentStepIndex], 70.0)); + } + update(); + } else { + currentInstruction = "لقد وصلت إلى وجهتك."; + nextInstruction = ""; + distanceToNextStep = ""; + _locationUpdateTimer?.cancel(); // إيقاف التحديثات عند الوصول + Get.find().speakText(currentInstruction); + update(); + } + } + + // ======================================================================= + // ٣. تحسين خوارزمية البحث ورسم المسار المقطوع + // ======================================================================= + + void _updateTraveledPolyline(LatLng currentPosition) { + // **التحسين:** البحث فقط في الخطوة الحالية والخطوة التالية + int searchEndIndex = (currentStepIndex + 1 < _stepPolylines.length) + ? currentStepIndex + 1 + : currentStepIndex; + + int overallClosestIndex = -1; + double minDistance = double.infinity; + + // البحث في نقاط الخطوة الحالية والتالية فقط + for (int i = currentStepIndex; i <= searchEndIndex; i++) { + for (int j = 0; j < _stepPolylines[i].length; j++) { + final distance = Geolocator.distanceBetween( + currentPosition.latitude, + currentPosition.longitude, + _stepPolylines[i][j].latitude, + _stepPolylines[i][j].longitude); + if (distance < minDistance) { + minDistance = distance; + // نحتاج إلى حساب الفهرس العام في القائمة الكاملة + overallClosestIndex = _getOverallIndex(i, j); + } + } + } + + if (overallClosestIndex == -1) return; + + List traveledPoints = + _fullRouteCoordinates.sublist(0, overallClosestIndex + 1); + traveledPoints.add(currentPosition); + + List remainingPoints = + _fullRouteCoordinates.sublist(overallClosestIndex); + remainingPoints.insert(0, currentPosition); + + polylines.removeWhere((p) => p.polylineId.value == 'traveled_route'); + polylines.add(Polyline( + polylineId: const PolylineId('traveled_route'), + points: traveledPoints, + color: Colors.grey.shade600, + width: 7, + )); + + polylines.removeWhere((p) => p.polylineId.value == 'remaining_route'); + polylines.add(Polyline( + polylineId: const PolylineId('remaining_route'), + points: remainingPoints, + color: const Color(0xFF4A80F0), + width: 7, + )); + } + + // دالة مساعدة لحساب الفهرس العام + int _getOverallIndex(int stepIndex, int pointInStepIndex) { + int overallIndex = 0; + for (int i = 0; i < stepIndex; i++) { + overallIndex += _stepPolylines[i].length; + } + return overallIndex + pointInStepIndex; + } + + // ======================================================================= + // ٤. دوال مساعدة وتجهيز البيانات + // ======================================================================= + + void _prepareStepData() { + _stepBounds.clear(); + _stepPolylines.clear(); + if (routeSteps.isEmpty) return; + for (final step in routeSteps) { + final pointsString = step['polyline']['points']; + final List> points = + decodePolyline(pointsString).cast>(); + final polylineCoordinates = points + .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) + .toList(); + _stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة + _stepBounds.add(_boundsFromLatLngList(polylineCoordinates)); + } + } + + // ... باقي دوال الكنترولر بدون تغيير ... + // (selectDestination, onMapLongPressed, startNavigationTo, getRoute, etc.) + Future selectDestination(dynamic place) async { + placeDestinationController.clear(); + placesDestination = []; + + final double lat = double.parse(place['latitude'].toString()); + final double lng = double.parse(place['longitude'].toString()); + final LatLng destination = LatLng(lat, lng); + + await startNavigationTo(destination, + infoWindowTitle: place['name'] ?? 'وجهة محددة'); + } + + Future onMapLongPressed(LatLng tappedPoint) async { + Get.dialog( + AlertDialog( + title: const Text('بدء الملاحة؟'), + content: const Text('هل تريد الذهاب إلى هذا الموقع المحدد؟'), + actionsAlignment: MainAxisAlignment.spaceBetween, + actions: [ + TextButton( + child: const Text('إلغاء', style: TextStyle(color: Colors.grey)), + onPressed: () => Get.back(), + ), + TextButton( + child: const Text('اذهب الآن'), + onPressed: () { + Get.back(); + startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد'); + }, + ), + ], + ), + ); + } + + Future startNavigationTo(LatLng destination, + {String infoWindowTitle = ''}) async { + isLoading = true; + update(); + + try { + _finalDestination = destination; + clearRoute(isNewRoute: true); + + markers.add( + Marker( + markerId: const MarkerId('destination'), + position: destination, + icon: destinationIcon, + infoWindow: InfoWindow(title: infoWindowTitle), + ), + ); + + await getRoute(myLocation!, destination); + } catch (e) { + Get.snackbar('خطأ', 'حدث خطأ أثناء تحديد الوجهة.'); + print("Error starting navigation: $e"); + } finally { + isLoading = false; + update(); + } + } + + Future getRoute(LatLng origin, LatLng destination) async { + final url = + '${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}'; + var response = await CRUD().getGoogleApi(link: url, payload: {}); + + if (response == null || response['routes'].isEmpty) { + Get.snackbar('خطأ', 'لم يتم العثور على مسار.'); + return; + } + + polylines.clear(); + final pointsString = response['routes'][0]['overview_polyline']['points']; + final List> points = + decodePolyline(pointsString).cast>(); + _fullRouteCoordinates = points + .map((point) => LatLng(point[0].toDouble(), point[1].toDouble())) + .toList(); + + polylines.add( + Polyline( + polylineId: const PolylineId('remaining_route'), + points: _fullRouteCoordinates, + color: const Color(0xFF4A80F0), + width: 7, + startCap: Cap.roundCap, + endCap: Cap.roundCap, + ), + ); + polylines.add( + const Polyline( + polylineId: PolylineId('traveled_route'), + points: [], + color: Colors.grey, + width: 7, + ), + ); + + routeSteps = List>.from( + response['routes'][0]['legs'][0]['steps']); + _prepareStepData(); + + currentStepIndex = 0; + _nextInstructionSpoken = false; + if (routeSteps.isNotEmpty) { + currentInstruction = + _parseInstruction(routeSteps[0]['html_instructions']); + nextInstruction = (routeSteps.length > 1) + ? _parseInstruction(routeSteps[1]['html_instructions']) + : "الوجهة النهائية"; + Get.find().speakText(currentInstruction); + } + _adjustUpdateInterval(); // تحديد سرعة التحديث لأول مرة + + final boundsData = response['routes'][0]['bounds']; + mapController?.animateCamera(CameraUpdate.newLatLngBounds( + LatLngBounds( + northeast: LatLng( + boundsData['northeast']['lat'], boundsData['northeast']['lng']), + southwest: LatLng( + boundsData['southwest']['lat'], boundsData['southwest']['lng']), + ), + 100.0, + )); + } + + Future recalculateRoute() async { + if (myLocation == null || _finalDestination == null || isLoading) return; + + isLoading = true; + update(); + + Get.snackbar( + 'إعادة التوجيه', + 'جاري حساب مسار جديد من موقعك الحالي...', + backgroundColor: AppColor.goldenBronze, + ); + await getRoute(myLocation!, _finalDestination!); + + isLoading = false; + update(); + } + + void clearRoute({bool isNewRoute = false}) { + polylines.clear(); + if (!isNewRoute) { + markers.removeWhere((m) => m.markerId.value == 'destination'); + _finalDestination = null; + } + routeSteps.clear(); + currentInstruction = ""; + nextInstruction = ""; + distanceToNextStep = ""; + currentSpeed = 0.0; + _stepBounds.clear(); + _fullRouteCoordinates.clear(); + _stepPolylines.clear(); + _nextInstructionSpoken = false; + _locationUpdateTimer?.cancel(); // إيقاف التحديثات عند إلغاء المسار + update(); + } + + LatLngBounds _boundsFromLatLngList(List list) { + assert(list.isNotEmpty); + double? x0, x1, y0, y1; + for (LatLng latLng in list) { + if (x0 == null) { + x0 = x1 = latLng.latitude; + y0 = y1 = latLng.longitude; + } else { + if (latLng.latitude > x1!) x1 = latLng.latitude; + if (latLng.latitude < x0) x0 = latLng.latitude; + if (latLng.longitude > y1!) y1 = latLng.longitude; + if (latLng.longitude < y0!) y0 = latLng.longitude; + } + } + return LatLngBounds( + northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!)); + } + + Future _loadCustomIcons() async { + carIcon = await BitmapDescriptor.fromAssetImage( + const ImageConfiguration(size: Size(40, 40)), 'assets/images/car.png'); + destinationIcon = await BitmapDescriptor.fromAssetImage( + const ImageConfiguration(size: Size(25, 25)), 'assets/images/b.png'); + } + + String _parseInstruction(String html) => + html.replaceAll(RegExp(r'<[^>]*>'), ' '); + + Future getPlaces() async { + if (placeDestinationController.text.trim().isEmpty) { + placesDestination = []; + update(); + return; + } + if (myLocation == null) { + Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...'); + return; + } + final query = placeDestinationController.text.trim(); + final lat = myLocation!.latitude; + final lng = myLocation!.longitude; + const double range = 2.2; + final lat_min = lat - range, + lat_max = lat + range, + lng_min = lng - range, + lng_max = lng + range; + + try { + final response = await CRUD().post( + link: AppLink.getPlacesSyria, + payload: { + 'query': query, + 'lat_min': lat_min.toString(), + 'lat_max': lat_max.toString(), + 'lng_min': lng_min.toString(), + 'lng_max': lng_max.toString(), + }, + ); + if (response != 'failure') { + placesDestination = response['message'] ?? []; + } else { + placesDestination = []; + } + } catch (e) { + print('Exception in getPlaces: $e'); + } finally { + update(); + } + } + + void onSearchChanged(String query) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 700), () => getPlaces()); + } +} diff --git a/lib/controller/home/navigation/navigation_view.dart b/lib/controller/home/navigation/navigation_view.dart new file mode 100644 index 0000000..c982c44 --- /dev/null +++ b/lib/controller/home/navigation/navigation_view.dart @@ -0,0 +1,296 @@ +// lib/views/navigation_view.dart + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'dart:ui'; + +import 'navigation_controller.dart'; // For BackdropFilter + +// استخدام نفس مسار الاستيراد الذي قدمته + +class NavigationView extends StatelessWidget { + const NavigationView({super.key}); + + @override + Widget build(BuildContext context) { + final NavigationController controller = Get.put(NavigationController()); + + return Scaffold( + body: GetBuilder( + builder: (_) => Stack( + children: [ + // --- الخريطة --- + GoogleMap( + onMapCreated: controller.onMapCreated, + // --- السطر المضاف والمهم هنا --- + onLongPress: controller.onMapLongPressed, + initialCameraPosition: CameraPosition( + target: controller.myLocation ?? + const LatLng(33.5138, 36.2765), // Default to Damascus + zoom: 16.0, + ), + markers: controller.markers, + polylines: controller.polylines, + myLocationEnabled: false, + myLocationButtonEnabled: false, + compassEnabled: false, + zoomControlsEnabled: false, + // تعديل الـ padding لإعطاء مساحة للعناصر العلوية والسفلية + padding: EdgeInsets.only( + bottom: controller.currentInstruction.isNotEmpty ? 130 : 0, + top: 140), + ), + + // --- واجهة البحث ونتائجه --- + _buildSearchUI(controller), + + // --- إرشادات الملاحة المطورة --- + if (controller.currentInstruction.isNotEmpty) + _buildNavigationInstruction(controller), + + // --- أزرار التحكم على الخريطة --- + _buildMapControls(controller), + + // --- مؤشر التحميل --- + if (controller.isLoading) + Container( + color: Colors.black.withOpacity(0.5), + child: const Center( + child: CircularProgressIndicator(color: Colors.white)), + ), + ], + ), + ), + ); + } + + // --- ويدجت خاصة بواجهة البحث --- + Widget _buildSearchUI(NavigationController controller) { + return Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + child: TextField( + controller: controller.placeDestinationController, + onChanged: (val) { + controller.onSearchChanged(val); + }, + decoration: InputDecoration( + hintText: 'إلى أين تريد الذهاب؟', + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: controller + .placeDestinationController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, color: Colors.grey), + onPressed: () { + controller.placeDestinationController.clear(); + controller.placesDestination = []; + controller.update(); + }, + ) + : (controller.polylines.isNotEmpty + ? IconButton( + icon: + const Icon(Icons.close, color: Colors.red), + tooltip: 'إلغاء المسار', + onPressed: () => controller.clearRoute(), + ) + : null), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 15), + ), + ), + ), + const SizedBox(height: 8), + if (controller.placesDestination.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(15.0), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + constraints: const BoxConstraints(maxHeight: 220), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.85), + borderRadius: BorderRadius.circular(15.0), + ), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: controller.placesDestination.length, + itemBuilder: (context, index) { + final place = controller.placesDestination[index]; + return ListTile( + title: Text(place['name'] ?? 'اسم غير معروف', + style: const TextStyle( + fontWeight: FontWeight.bold)), + subtitle: Text(place['address'] ?? '', + maxLines: 1, overflow: TextOverflow.ellipsis), + leading: const Icon(Icons.location_on_outlined, + color: Colors.blue), + onTap: () => controller.selectDestination(place), + ); + }, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + // --- ويدجت خاصة بأزرار التحكم --- + Widget _buildMapControls(NavigationController controller) { + return Positioned( + bottom: controller.currentInstruction.isNotEmpty ? 150 : 20, + right: 12, + child: Column( + children: [ + if (controller.polylines.isNotEmpty) ...[ + FloatingActionButton( + heroTag: 'rerouteBtn', + mini: true, + backgroundColor: Colors.white, + tooltip: 'إعادة حساب المسار', + onPressed: () => controller.recalculateRoute(), + child: const Icon(Icons.sync_alt, color: Colors.blue), + ), + const SizedBox(height: 10), + ], + FloatingActionButton( + heroTag: 'gpsBtn', + mini: true, + backgroundColor: Colors.white, + onPressed: () { + if (controller.myLocation != null) { + controller.animateCameraToPosition( + controller.myLocation!, + bearing: controller.heading, + zoom: 18.5, + ); + } + }, + child: const Icon(Icons.gps_fixed, color: Colors.black54), + ), + ], + ), + ); + } + + // --- ويدجت خاصة بإرشادات الطريق المطورة --- + Widget _buildNavigationInstruction(NavigationController controller) { + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade900, Colors.blue.shade600], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, -5), + ), + ], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // --- الصف العلوي: السرعة والمسافة --- + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + controller.distanceToNextStep, + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold), + ), + Row( + children: [ + Text( + controller.currentSpeed.toStringAsFixed(0), + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold), + ), + const SizedBox(width: 4), + const Text( + "كم/س", + style: TextStyle(color: Colors.white70, fontSize: 14), + ), + ], + ), + ], + ), + const Divider(color: Colors.white38, height: 20, thickness: 0.8), + // --- الصف السفلي: الإرشاد القادم --- + Row( + children: [ + const Icon(Icons.navigation_rounded, + color: Colors.white, size: 32), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("الخطوة التالية", + style: + TextStyle(color: Colors.white70, fontSize: 12)), + Text( + controller.nextInstruction.isNotEmpty + ? controller.nextInstruction + : controller.currentInstruction, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index a9fb46f..002be2a 100755 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -9,6 +9,7 @@ class MyTranslation extends Translations { "Cancel Trip": "إلغاء الرحلة", "Passenger Cancel Trip": "الراكب ألغى الرحلة", "VIP Order": "طلب VIP", + 'Hi ,I Arrive your site': "مرحبًا، لقد وصلت إلى موقعك", "The driver accepted your trip": "السائق قبل رحلتك", "message From passenger": "رسالة من الراكب", "Cancel": "إلغاء", @@ -34,7 +35,6 @@ class MyTranslation extends Translations { "نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.", "Driver Applied the Ride for You": "السائق قدم الطلب لك", "Applied": "تم التقديم", - "Hi ,I Arrive your site": "مرحبًا، لقد وصلت إلى موقعك", "Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة", "Ok I will go now.": "حسنًا، سأذهب الآن.", "Accepted Ride": "تم قبول الرحلة", @@ -146,7 +146,11 @@ Raih Gai: For same-day return trips longer than 50km. رحّي غاي: للرحلات ذات العودة في نفس اليوم التي تزيد عن 50 كم. """, "I will go now": "هروح دلوقتي", - "Yes": "أيوة", + "Yes": "نعم", + 'Privacy Policy': "سياسة الخصوصية", + 'Ride info': "معلومات الرحلة", + 'you dont have accepted ride': "ليس لديك رحلة مقبولة", + 'Total Points': "إجمالي النقاط", "No,I want": "لا، أنا عاوز", "Your fee is": "المبلغ بتاعك هو", "Do you want to pay Tips for this Driver": @@ -424,7 +428,25 @@ Raih Gai: For same-day return trips longer than 50km. "color.beige": "بيج", "color.brown": "بني", "color.maroon": "خمري", + 'Ride History': "تاريخ الرحلات", "color.burgundy": "برغندي", + 'Name must be at least 2 characters': + "الاسم يجب أن يكون على الأقل 2 حرف", + 'This Trip Was Cancelled': "تم إلغاء هذه الرحلة", + 'Trip Details': "تفاصيل الرحلة", + 'Could not load trip details.': "تعذر تحميل تفاصيل الرحلة.", + 'Trip Info': "معلومات الرحلة", + 'Order ID': "رقم الطلب", + 'Date': "التاريخ", + 'Earnings & Distance': "الأرباح والمسافة", + 'Trip Timeline': "جدول الرحلة", + 'Time to Passenger': "الوقت للراكب", + 'Trip Started': "بدأت الرحلة", + 'Trip Finished': "انتهت الرحلة", + 'Passenger & Status': "الراكب والحالة", + 'Status': "الحالة", + 'Passenger Name': "اسم الراكب", + 'National ID must be 11 digits': "الرقم الوطني يجب أن يكون 11 رقمًا", "color.yellow": "أصفر", "color.orange": "برتقالي", "color.gold": "ذهبي", @@ -756,8 +778,8 @@ Raih Gai: For same-day return trips longer than 50km. "لم نجد أي سائقين بعد. ضع في اعتبارك زيادة رسوم رحلتك لجعل عرضك أكثر جاذبية للسائقين.", "Allow Location Access": "السماح بالوصول إلى الموقع", "Show My Trip Count": "عرض عدد رحلاتي", - "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.": - "انطلق هو التطبيق الأكثر أمانًا لمشاركة الركوب الذي يقدم العديد من الميزات لكل من السائقين والركاب. نحن نقدم أقل عمولة بنسبة 8% فقط، مما يضمن حصولك على أفضل قيمة لرحلاتك. يتضمن تطبيقنا التأمين لأفضل السائقين، الصيانة المنتظمة للسيارات مع أفضل المهندسين، والخدمات على الطريق لضمان تجربة محترمة وعالية الجودة لجميع المستخدمين.", + "Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, 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.": + "انطلق هو التطبيق الأكثر أمانًا لمشاركة الركوب الذي يقدم العديد من الميزات لكل من السائقين والركاب. نحن نقدم أقل عمولة بنسبة 15% فقط، مما يضمن حصولك على أفضل قيمة لرحلاتك. يتضمن تطبيقنا التأمين لأفضل السائقين، الصيانة المنتظمة للسيارات مع أفضل المهندسين، والخدمات على الطريق لضمان تجربة محترمة وعالية الجودة لجميع المستخدمين.", "You can contact us during working hours from 12:00 - 19:00.": "يمكنك الاتصال بنا خلال ساعات العمل من 12:00 - 7:00.", "Show maintenance center near my location": @@ -1550,17 +1572,40 @@ Raih Gai: For same-day return trips longer than 50km. "otp verification failed": "رمز التحقق غير صحيح.", "registration failed": "فشلت عملية التسجيل.", "welcome user": "أهلاً بك، @firstName!", - "Driver Wallet": "محفظة السائق", + 'Balance': 'الرصيد', + "Today's Promo": "عرض اليوم", + 'Credit': 'رصيد', 'Debit': 'خصم', + 'Transactions this week': 'المعاملات هذا الأسبوع', + 'Weekly Summary': 'ملخص أسبوعي', + 'Total Weekly Earnings': 'إجمالي الأرباح الأسبوعية', + 'No transactions this week': 'لا توجد معاملات هذا الأسبوع', + "Driver Balance": "رصيد السائق", "The 30000 points equal 30000 S.P for you \nSo go and gain your money": "الـ 30000 نقطة تساوي 30000 ل.س لك \nلذا اذهب واكسب أموالك", "OK": "موافق", + "Your Application is Under Review": "طلبك قيد المراجعة", + "We have received your application to join us as a driver. Our team is currently reviewing it. Thank you for your patience.": + "لقد استلمنا طلبك للانضمام إلينا كسائق. يقوم فريقنا حاليًا بمراجعته. شكرًا لك على صبرك.", + "You Will Be Notified": "سيتم إشعارك قريباً", + "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.": + "سنرسل لك إشعاراً فور الموافقة على حسابك. يمكنك إغلاق هذه الصفحة بأمان، وسنعلمك عند اكتمال المراجعة.", + "Refresh Status": "تحديث الحالة", + "Checking for updates...": "جاري التحقق من التحديثات...", "Total Points is": "إجمالي النقاط هو", "Charge your Account": "اشحن حسابك", + '''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.''': + "أنواع الرحلات في انطلق:\n\n- مريح: للسيارات الأحدث من 2017 مع تكييف الهواء.\n- سيدة: للسائقات الإناث.\n- سرعة: لرحلات ذات راتب ثابت ونقاط نهاية محددة.\n- مشاوير: لرحلات مرنة حيث يختار الركاب السيارة والسائق مع ترتيبات مسبقة.\n- رايح جاي: لرحلات العودة في نفس اليوم لأكثر من 50 كم.", 'L.S': 'ل.س', "Total Amount:": "المبلغ الإجمالي:", - "Intaleq Wallet": "محفظة انطلق", + "Intaleq Wallet": "رصيد انطلق", "Current Balance": "الرصيد الحالي", - "S.P.": "ل.س.", + "SYP": "ل.س.", "Your total balance:": "رصيدك الإجمالي:", "Payment Method:": "طريقة الدفع:", "e.g., 0912345678": "مثال: 0912345678", @@ -1643,23 +1688,12 @@ Raih Gai: For same-day return trips longer than 50km. "الشريك السائق في انتظارك في الموقع المُحدَّد .", "Pay with Your": "ادفع باستخدام", "Pay with Credit Card": "ادفع ببطاقة الائتمان", - "Payment History": "سجل الدفعات", "Show Promos to Charge": "إظهار العروض الترويجية للشحن", "Point": "نقطة", - "Driver Wallet": "محفظة الشريك السائق", - "Total Points is": "‏رصيد التشغيل", "Total Budget from trips is": "الميزانية الإجمالية من الرحلات هي", - "Total Amount:": "المبلغ الإجمالي:", "Total Budget from trips by": "الميزانية الإجمالية من الرحلات حسب", "Credit card is": "بطاقة الائتمان", - "This amount for all trip I get from Passengers": - "هذا المبلغ لجميع الرحلات التي أحصل عليها من الركاب", - "Pay from my budget": "ادفع من ميزانيتي", - // "This amount for all trip I get from Passengers and Collected For me in": - // "هذا المبلغ لجميع الرحلات التي أحصل عليها من الركاب والتي تم جمعها من أجلي في", - // "You can buy points from your budget": - // "يمكنك شراء النقاط من ميزانيتك", - // "insert amount": "أدخل المبلغ", + "You can buy Points to let you online": "يمكنك شراء النقاط لتمكينك من الدخول عبر الإنترنت", "by this list below": "من خلال هذه القائمة أدناه", @@ -2572,7 +2606,7 @@ Raih Gai: For same-day return trips longer than 50km. "Payment History": "Historique des paiements", "Show Promos to Charge": "Afficher les promotions d'expédition", "Point": "Points", - "Driver Wallet": "Portefeuille chauffeur", + "Driver Balance": "Portefeuille chauffeur", "Total Points is": "Le score total est de", "Total Budget from trips is": "Le budget total des voyages est de", "Total Amount:": "Montant total", @@ -3439,7 +3473,7 @@ Raih Gai: For same-day return trips longer than 50km. "Payment History": "Historique des paiements", "Show Promos to Charge": "Afficher les promotions d'expédition", "Point": "Points", - "Driver Wallet": "Portefeuille chauffeur", + "Driver Balance": "Portefeuille chauffeur", "Total Points is": "Le score total est de", "Total Budget from trips is": "Le budget total des voyages est de", "Total Amount:": "Montant total", @@ -4300,7 +4334,7 @@ Raih Gai: For same-day return trips longer than 50km. "Payment History": "Historique des paiements", "Show Promos to Charge": "Afficher les promotions d'expédition", "Point": "Points", - "Driver Wallet": "Portefeuille chauffeur", + "Driver Balance": "Portefeuille chauffeur", "Total Points is": "Le score total est de", "Total Budget from trips is": "Le budget total des voyages est de", "Total Amount:": "Montant total", @@ -5165,7 +5199,7 @@ Raih Gai: For same-day return trips longer than 50km. "Payment History": "Historique des paiements", "Show Promos to Charge": "Afficher les promotions d'expédition", "Point": "Points", - "Driver Wallet": "Portefeuille chauffeur", + "Driver Balance": "Portefeuille chauffeur", "Total Points is": "Le score total est de", "Total Budget from trips is": "Le budget total des voyages est de", "Total Amount:": "Montant total", @@ -6099,7 +6133,7 @@ Raih Gai: For same-day return trips longer than 50km. "Payment History": "Historique des paiements", "Show Promos to Charge": "Afficher les promotions d'expédition", "Point": "Points", - "Driver Wallet": "Portefeuille chauffeur", + "Driver Balance": "Portefeuille chauffeur", "Total Points is": "Le score total est de", "Total Budget from trips is": "Le budget total des voyages est de", "Total Amount:": "Montant total", @@ -6948,7 +6982,7 @@ Raih Gai: For same-day return trips longer than 50km. "Payment History": "भुगतान इतिहास", "Show Promos to Charge": "शिपिंग प्रमोशन दिखाएँ", "Point": "pt", - "Driver Wallet": "ड्राइवर पार्टनर का वॉलेट", + "Driver Balance": "ड्राइवर पार्टनर का वॉलेट", "Total Points is": "कुल स्कोर है", "Total Budget from trips is": "ट्रिप का कुल बजट है", "Total Amount:": "कुल राश", @@ -7796,7 +7830,7 @@ Raih Gai: For same-day return trips longer than 50km. "Show Promos to Charge": "نمایش تبلیغات حمل و نقل", "Point": "نقطه , خال , لکه , نقطه دار کردن , نوک , سر , نکته , ماده , اصل , موضوع , جهت , درجه , امتياز بازي , نمره درس , پوان , هدف , مسير , مرحله , قله , پايان , تيزکردن , گوشه دارکردن , نوکدار کردن , نوک گذاشتن (به) , خاطر نشان کردن , نشان دادن , متوجه ساختن , نقطه گذاري کردن , لک , لکه يا خال ميوه , ذره , لکه دار کردن , خالدار کردن", - "Driver Wallet": "کیف پول راننده", + "Driver Balance": "کیف پول راننده", "Total Points is": "مجموع امتیاز است", "Total Budget from trips is": "بودجه کل انطلقها می باشد", "Total Amount:": "مبلغ کل:", @@ -8560,7 +8594,7 @@ Raih Gai: For same-day return trips longer than 50km. "Payment History": "付款紀錄", "Show Promos to Charge": "顯示運送優惠", "Point": "一個點", - "Driver Wallet": "職業駕駛錢包", + "Driver Balance": "職業駕駛錢包", "Total Points is": "總分為", "Total Budget from trips is": "行程總預算為", "Total Amount:": "總金額:", diff --git a/lib/device_compatibility_page.dart b/lib/device_compatibility_page.dart index 6f3a02f..052e11d 100644 --- a/lib/device_compatibility_page.dart +++ b/lib/device_compatibility_page.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/foundation.dart' + show kIsWeb, defaultTargetPlatform, TargetPlatform; import 'controller/functions/device_analyzer.dart'; -// --- CompatibilityDetailCard Widget (Updated to use 'max_score') --- +// --- CompatibilityDetailCard Widget (كما هو) --- class CompatibilityDetailCard extends StatelessWidget { final Map detail; const CompatibilityDetailCard({super.key, required this.detail}); @@ -16,17 +18,14 @@ class CompatibilityDetailCard extends StatelessWidget { IconData _getIconForLabel(String label) { if (label.contains('رام')) return Icons.memory; - if (label.contains('معالج') || label.contains('CPU')) { + if (label.contains('معالج') || label.contains('CPU')) return Icons.developer_board; - } - if (label.contains('تخزين') || label.contains('كتابة')) { + if (label.contains('تخزين') || label.contains('كتابة')) return Icons.sd_storage_outlined; - } if (label.contains('أندرويد')) return Icons.android; if (label.contains('خدمات')) return Icons.g_mobiledata; - if (label.contains('حساسات') || label.contains('Gyroscope')) { + if (label.contains('حساسات') || label.contains('Gyroscope')) return Icons.sensors; - } return Icons.smartphone; } @@ -35,7 +34,6 @@ class CompatibilityDetailCard extends StatelessWidget { final bool status = detail['status'] ?? false; final String label = detail['label'] ?? ""; final int achievedScore = detail['achieved_score'] ?? 0; - // Corrected to use 'max_score' from the analyzer final int maxScore = detail['max_score'] ?? 1; final Color color = _getStatusColor(status, achievedScore, maxScore); final double progress = @@ -49,10 +47,9 @@ class CompatibilityDetailCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.08), - blurRadius: 15, - offset: const Offset(0, 5), - ) + color: Colors.grey.withOpacity(0.08), + blurRadius: 15, + offset: const Offset(0, 5)) ], ), child: Column( @@ -64,20 +61,15 @@ class CompatibilityDetailCard extends StatelessWidget { color: Colors.grey.shade600, size: 20), const SizedBox(width: 8), Expanded( - child: Text( - label, + child: Text(label, + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade800, + fontWeight: FontWeight.w600)), + ), + Text("$achievedScore/$maxScore نقطة", style: TextStyle( - fontSize: 15, - color: Colors.grey.shade800, - fontWeight: FontWeight.w600), - ), - ), - // Corrected to display points out of max_score - Text( - "$achievedScore/$maxScore نقطة", - style: TextStyle( - color: color, fontWeight: FontWeight.bold, fontSize: 14), - ), + color: color, fontWeight: FontWeight.bold, fontSize: 14)), ], ), const SizedBox(height: 12), @@ -94,7 +86,7 @@ class CompatibilityDetailCard extends StatelessWidget { } } -// --- Main Page Widget --- +// --- Main Page Widget (Android-only) --- class DeviceCompatibilityPage extends StatefulWidget { const DeviceCompatibilityPage({super.key}); @override @@ -107,23 +99,28 @@ class _DeviceCompatibilityPageState extends State { List> details = []; bool isLoading = true; + bool get _isAndroid => + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + @override void initState() { super.initState(); - _initializePage(); + if (_isAndroid) { + _initializePage(); + } else { + // منصّة غير أندرويد: لا تعمل أي تحليلات + setState(() => isLoading = false); + } } Future _initializePage() async { - // await BatteryNotifier.checkBatteryAndNotify(); final result = await DeviceAnalyzer().analyzeDevice(); - - if (mounted) { - setState(() { - score = result['score']; - details = List>.from(result['details']); - isLoading = false; - }); - } + if (!mounted) return; + setState(() { + score = result['score']; + details = List>.from(result['details']); + isLoading = false; + }); } Color _getColorForScore(int value) { @@ -141,6 +138,41 @@ class _DeviceCompatibilityPageState extends State { @override Widget build(BuildContext context) { + // حظر الصفحة على غير أندرويد + if (!_isAndroid) { + return Scaffold( + backgroundColor: const Color(0xFFF7F8FC), + appBar: AppBar( + title: const Text("توافق الجهاز", + style: TextStyle( + color: Colors.black87, fontWeight: FontWeight.bold)), + centerTitle: true, + backgroundColor: Colors.transparent, + elevation: 0, + iconTheme: const IconThemeData(color: Colors.black87), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.phone_iphone, size: 56, color: Colors.grey), + const SizedBox(height: 12), + const Text("هذه الصفحة متاحة لأجهزة أندرويد فقط", + style: TextStyle(fontSize: 16)), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () => Get.back(), + child: const Text("رجوع"), + ), + ], + ), + ), + ), + ); + } + return Scaffold( backgroundColor: const Color(0xFFF7F8FC), appBar: AppBar( @@ -188,57 +220,48 @@ class _DeviceCompatibilityPageState extends State { ); } - /// ## Corrected Score Header Widget - /// This widget now uses a `Stack` to correctly place the text over the `PieChart`. + /// الهيدر Widget _buildScoreHeader() { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), - height: 220, // Give the container a fixed height + height: 220, child: Stack( alignment: Alignment.center, children: [ - // Layer 1: The Pie Chart PieChart( PieChartData( sectionsSpace: 4, - // This creates the "hole" in the middle. centerSpaceRadius: 80, startDegreeOffset: -90, sections: [ PieChartSectionData( - color: _getColorForScore(score), - value: score.toDouble(), - title: '', - radius: 25, - ), + color: _getColorForScore(score), + value: score.toDouble(), + title: '', + radius: 25), PieChartSectionData( - color: Colors.grey.shade200, - value: (100 - score).toDouble().clamp(0, 100), - title: '', - radius: 25, - ), + color: Colors.grey.shade200, + value: (100 - score).toDouble().clamp(0, 100), + title: '', + radius: 25), ], ), ), - // Layer 2: The text and message, centered on top of the chart Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - "$score%", - style: TextStyle( - fontSize: 52, - fontWeight: FontWeight.bold, - color: _getColorForScore(score)), - ), + Text("$score%", + style: TextStyle( + fontSize: 52, + fontWeight: FontWeight.bold, + color: _getColorForScore(score))), const SizedBox(height: 4), - Text( - _getScoreMessage(score), - style: TextStyle( - color: Colors.grey.shade700, - fontSize: 16, - fontWeight: FontWeight.w500), - ), + Text(_getScoreMessage(score), + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 16, + fontWeight: FontWeight.w500)), ], ), ], diff --git a/lib/main.dart b/lib/main.dart index 5a3d0e9..8b28dd6 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:intl/date_symbol_data_local.dart'; import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -17,7 +18,9 @@ import 'constant/api_key.dart'; import 'constant/info.dart'; import 'controller/firebase/firbase_messge.dart'; import 'controller/firebase/local_notification.dart'; +import 'controller/functions/add_error.dart'; import 'controller/functions/battery_status.dart'; +import 'controller/functions/crud.dart'; import 'controller/functions/encrypt_decrypt.dart'; import 'controller/functions/secure_storage.dart'; import 'controller/local/local_controller.dart'; @@ -134,13 +137,22 @@ void main() async { await WakelockPlus.enable(); await GetStorage.init(); + await initializeDateFormatting(); Stripe.publishableKey = AK.publishableKeyStripe; SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); - - runApp(const MyApp()); + runZonedGuarded>(() async { + // ... الكود الحالي الموجود في دالة main ... + runApp(const MyApp()); + }, (error, stack) { + // أي خطأ غير متوقع في التطبيق سيتم التقاطه هنا CRUD. + print("Caught Dart error: $error"); + print(stack); + // أرسل الخطأ إلى السيرفر + CRUD.addError(error.toString(), stack.toString(), 'main'); + }); } class MyApp extends StatefulWidget { diff --git a/lib/views/Rate/rate_passenger.dart b/lib/views/Rate/rate_passenger.dart index ae1e7f6..4a38926 100755 --- a/lib/views/Rate/rate_passenger.dart +++ b/lib/views/Rate/rate_passenger.dart @@ -240,7 +240,7 @@ class RatePassenger extends StatelessWidget { RatingBar.builder( initialRating: 0, itemCount: 5, - itemSize: 50, + itemSize: 40, itemPadding: const EdgeInsets.symmetric(horizontal: 4), itemBuilder: (context, index) { switch (index) { diff --git a/lib/views/auth/captin/contact_us_page.dart b/lib/views/auth/captin/contact_us_page.dart index 6e5abc9..9ec2230 100755 --- a/lib/views/auth/captin/contact_us_page.dart +++ b/lib/views/auth/captin/contact_us_page.dart @@ -35,7 +35,7 @@ class ContactUsPage extends StatelessWidget { IconButton( onPressed: () async { Get.put(TextToSpeechController()).speakText( - 'Tripz 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.' + 'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, 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), @@ -43,7 +43,7 @@ class ContactUsPage extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'Tripz 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.' + 'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, 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, diff --git a/lib/views/auth/captin/login_captin.dart b/lib/views/auth/captin/login_captin.dart index 1867f10..9816f2b 100755 --- a/lib/views/auth/captin/login_captin.dart +++ b/lib/views/auth/captin/login_captin.dart @@ -8,10 +8,12 @@ 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'; @@ -356,7 +358,12 @@ class LoginCaptin extends StatelessWidget { decoration: TextDecoration.underline, color: AppColor.blueColor, fontWeight: FontWeight.bold), - recognizer: TapGestureRecognizer()..onTap = () {}), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse('${AppLink.server}/privacy_policy.php'), + mode: LaunchMode.externalApplication); + }), TextSpan(text: " and acknowledge our Privacy Policy.".tr), ], ), @@ -369,7 +376,9 @@ class LoginCaptin extends StatelessWidget { ), child: SingleChildScrollView( padding: const EdgeInsets.all(12), - child: HtmlWidget(AppInformation.privacyPolicyArabic), + child: HtmlWidget(box.read(BoxName.lang).toString() == 'ar' + ? AppInformation.privacyPolicyArabic + : AppInformation.privacyPolicy), ), ), ), diff --git a/lib/views/auth/syria/pending_driver_page.dart b/lib/views/auth/syria/pending_driver_page.dart new file mode 100644 index 0000000..84a0f1f --- /dev/null +++ b/lib/views/auth/syria/pending_driver_page.dart @@ -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 createState() => + _DriverVerificationScreenState(); +} + +class _DriverVerificationScreenState extends State + 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, + // ), + // ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/auth/syria/registration_view.dart b/lib/views/auth/syria/registration_view.dart index 88a23bc..2418509 100644 --- a/lib/views/auth/syria/registration_view.dart +++ b/lib/views/auth/syria/registration_view.dart @@ -72,29 +72,53 @@ class RegistrationView extends StatelessWidget { TextFormField( controller: c.firstNameController, decoration: InputDecoration( - labelText: 'First Name'.tr, - border: const OutlineInputBorder()), - validator: (v) => - (v?.isEmpty ?? true) ? 'Required field'.tr : null, + 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) => - (v?.isEmpty ?? true) ? 'Required field'.tr : null, + 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()), + labelText: 'National ID Number'.tr, + border: const OutlineInputBorder(), + ), keyboardType: TextInputType.number, - validator: (v) => - (v?.isEmpty ?? true) ? 'Required field'.tr : null, + 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( @@ -296,6 +320,15 @@ class RegistrationView extends StatelessWidget { ); } + 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, File? img, VoidCallback onTap) { return Card( margin: const EdgeInsets.only(bottom: 16), diff --git a/lib/views/home/Captin/About Us/about_us.dart b/lib/views/home/Captin/About Us/about_us.dart index 52a8f2e..7e20ba8 100755 --- a/lib/views/home/Captin/About Us/about_us.dart +++ b/lib/views/home/Captin/About Us/about_us.dart @@ -26,7 +26,7 @@ class AboutPage extends StatelessWidget { Padding( padding: const EdgeInsets.all(16.0), child: Text( - 'SEFER LLC\n${box.read(BoxName.countryCode).toString().tr}', + 'Intaleq LLC\n${'Syria'.tr}', style: AppStyle.headTitle2, textAlign: TextAlign.center, ), @@ -34,7 +34,7 @@ class AboutPage extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( - 'SEFER 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:' + '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, diff --git a/lib/views/home/Captin/driver_map_page.dart b/lib/views/home/Captin/driver_map_page.dart index c4d1070..af058df 100755 --- a/lib/views/home/Captin/driver_map_page.dart +++ b/lib/views/home/Captin/driver_map_page.dart @@ -10,6 +10,7 @@ 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'; @@ -44,12 +45,13 @@ class PassengerLocationMapPage extends StatelessWidget { // 2. شريط تعليمات الطريق في الأعلى const InstructionsOfRoads(), - // 3. زر إلغاء الرحلة في الأعلى يسارًا - CancelWidget(mapDriverController: mapDriverController), - // 4. نافذة معلومات الراكب في الأسفل (تظهر قبل بدء الرحلة) - const PassengerInfoWindow(), + const PassengerInfoWindow(), + // 3. زر إلغاء الرحلة في الأعلى يسارًا + + CancelWidget(mapDriverController: mapDriverController), + // Changed: تم تعديل تصميم زر الإلغاء ليكون أيقونة بسيطة في الأعلى // 5. شريط معلومات وإنهاء الرحلة (يظهر بعد بدء الرحلة) driverEndRideBar(), @@ -58,7 +60,7 @@ class PassengerLocationMapPage extends StatelessWidget { // 7. دائرة عرض السرعة speedCircle(), - + GoogleMapApp(), // 8. نافذة عرض السعر النهائي (تظهر بعد انتهاء الرحلة) const PricesWindow(), ], @@ -131,7 +133,7 @@ class CancelWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Positioned( - top: 10, + top: 70, left: 10, child: GetBuilder( builder: (controller) { diff --git a/lib/views/home/Captin/history/history_captain.dart b/lib/views/home/Captin/history/history_captain.dart index 20e4a5e..87ee127 100755 --- a/lib/views/home/Captin/history/history_captain.dart +++ b/lib/views/home/Captin/history/history_captain.dart @@ -1,12 +1,9 @@ -import 'package:sefer_driver/views/widgets/mydialoug.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import '../../../../controller/auth/captin/history_captain.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:get/get.dart'; - -import '../../../../controller/functions/encrypt_decrypt.dart'; +import 'package:sefer_driver/views/widgets/mydialoug.dart'; class HistoryCaptain extends StatelessWidget { const HistoryCaptain({super.key}); @@ -14,136 +11,221 @@ class HistoryCaptain extends StatelessWidget { @override Widget build(BuildContext context) { Get.put(HistoryCaptainController()); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text('History Page'.tr), - leading: CupertinoNavigationBarBackButton( - onPressed: () => Get.back(), - ), + + return Scaffold( + backgroundColor: Colors.grey[100], // A softer background color + appBar: AppBar( + title: Text('Ride History'.tr), + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 1, ), - child: SafeArea( - child: GetBuilder( - builder: (historyCaptainController) => historyCaptainController - .isloading - ? const Center(child: CupertinoActivityIndicator()) - : historyCaptainController.historyData['message'].length < 1 - ? Center( - child: Text( - 'No ride Yet.'.tr, - style: CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle, + body: GetBuilder( + builder: (controller) { + if (controller.isloading) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.historyData['message'].isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.history_toggle_off, + size: 80, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No Rides Yet'.tr, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(color: Colors.grey[600]), + ), + ], + ), + ); + } + + // 动画: Wrap ListView with AnimationLimiter for staggered animations + return AnimationLimiter( + child: ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: controller.historyData['message'].length, + itemBuilder: (BuildContext context, int index) { + var trip = controller.historyData['message'][index]; + + // 动画: Apply animation to each list item + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 375), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: _AnimatedHistoryCard( + trip: trip, + onTap: () { + // Your original logic is preserved here + if (trip['status'] != 'Cancel') { + controller.getHistoryDetails(trip['order_id']); + } else { + MyDialog().getDialog( + 'This Trip Was Cancelled'.tr, + 'This Trip Was Cancelled'.tr, + () => Get.back(), + ); + } + }, ), - ) - : ListView.builder( - itemCount: historyCaptainController - .historyData['message'].length, - itemBuilder: (BuildContext context, int index) { - var list = historyCaptainController - .historyData['message'][index]; - return Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: CupertinoColors.systemGrey, width: 1), - borderRadius: - const BorderRadius.all(Radius.circular(8.0)), - ), - child: CupertinoButton( - onPressed: () { - if (list['status'] != 'Cancel') { - historyCaptainController - .getHistoryDetails(list['order_id']); - } else { - MyDialog().getDialog( - 'This Trip Cancelled'.tr, - 'This Trip Cancelled'.tr, - () => Get.back(), - ); - } - }, - child: Container( - margin: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'OrderId'.tr, - style: CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle, - ), - Text( - EncryptionHelper.instance - .decryptData(list['order_id']), - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - ], - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'created time'.tr, - style: CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle, - ), - Text( - list['created_at'], - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - ], - ), - Text( - list['status'], - style: EncryptionHelper.instance - .decryptData( - list['status']) == - 'Apply' - ? CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle - .copyWith( - color: CupertinoColors - .systemGreen) - : EncryptionHelper.instance.decryptData( - list['status']) == - 'Refused' - ? CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle - .copyWith( - color: CupertinoColors - .systemRed) - : CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle - .copyWith( - color: CupertinoColors - .systemYellow), - ), - ], - ), - ), - ), - ), - ); - }, ), + ), + ); + }, + ), + ); + }, + ), + ); + } +} + +// 动画: A new stateful widget to handle the tap animation +class _AnimatedHistoryCard extends StatefulWidget { + final Map trip; + final VoidCallback onTap; + + const _AnimatedHistoryCard({required this.trip, required this.onTap}); + + @override + __AnimatedHistoryCardState createState() => __AnimatedHistoryCardState(); +} + +class __AnimatedHistoryCardState extends State<_AnimatedHistoryCard> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 150), + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _onTapDown(TapDownDetails details) { + _controller.forward(); + } + + void _onTapUp(TapUpDetails details) { + _controller.reverse(); + widget.onTap(); + } + + void _onTapCancel() { + _controller.reverse(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: ScaleTransition( + scale: _scaleAnimation, + child: Card( + elevation: 4, + shadowColor: Colors.black.withOpacity(0.1), + margin: const EdgeInsets.only(bottom: 16.0), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon(Icons.receipt_long, + color: Theme.of(context).primaryColor, size: 40), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${'OrderId'.tr}: ${widget.trip['order_id']}', + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + widget.trip['created_at'], + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.grey[600], + ), + ), + ], + ), + ), + const SizedBox(width: 16), + _buildStatusChip(widget.trip['status']), + ], + ), + ), ), ), ); } } + +// 🎨 A separate function for the status chip, slightly restyled for Material +Widget _buildStatusChip(String status) { + Color chipColor; + Color textColor; + String statusText = status; + IconData iconData; + + switch (status) { + case 'Apply': + chipColor = Colors.green.shade50; + textColor = Colors.green.shade800; + iconData = Icons.check_circle; + 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; + statusText = '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( + statusText.tr, + style: TextStyle(color: textColor, fontWeight: FontWeight.w600), + ), + backgroundColor: chipColor, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + side: BorderSide.none, + ); +} diff --git a/lib/views/home/Captin/history/history_details_page.dart b/lib/views/home/Captin/history/history_details_page.dart index 0115b81..50982ca 100755 --- a/lib/views/home/Captin/history/history_details_page.dart +++ b/lib/views/home/Captin/history/history_details_page.dart @@ -1,252 +1,369 @@ -import 'package:sefer_driver/controller/functions/location_controller.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; + import 'package:sefer_driver/controller/auth/captin/history_captain.dart'; import 'package:sefer_driver/controller/functions/launch.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:flutter/cupertino.dart'; +class HistoryDetailsPage extends StatefulWidget { + const HistoryDetailsPage({super.key}); -import '../../../../controller/functions/encrypt_decrypt.dart'; + @override + State createState() => _HistoryDetailsPageState(); +} -class HistoryDetailsPage extends StatelessWidget { - HistoryDetailsPage({super.key}); - HistoryCaptainController historyCaptainController = - Get.put(HistoryCaptainController()); +class _HistoryDetailsPageState extends State { + // Get the controller instance + final HistoryCaptainController controller = + Get.find(); + + // 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 CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text('Trip Detail'.tr), - leading: CupertinoButton( - padding: EdgeInsets.zero, - child: const Icon(CupertinoIcons.back), - onPressed: () => Navigator.pop(context), - ), + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + title: Text('Trip Details'.tr), + backgroundColor: Colors.white, + elevation: 1, ), - child: GetBuilder( - builder: (historyCaptainController) { - var res = historyCaptainController.historyDetailsData['data']; - return historyCaptainController.isloading - ? const Center( - child: CupertinoActivityIndicator(), - ) - : CupertinoScrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox( - height: 20, - ), - CupertinoButton( - onPressed: () { - String mapUrl = - 'https://www.google.com/maps/dir/${EncryptionHelper.instance.decryptData(res['start_location'])}/${EncryptionHelper.instance.decryptData(res['end_location'])}/'; - showInBrowser(mapUrl); - }, - child: Container( - width: MediaQuery.of(context).size.width * 0.9, - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - border: Border.all( - color: CupertinoColors.activeBlue, - width: 2), - ), - child: Column( - children: [ - const SizedBox( - height: 20, - ), - SizedBox( - height: MediaQuery.of(context).size.height * - 0.3, - child: GoogleMap( - initialCameraPosition: CameraPosition( - target: Get.find() - .myLocation, - tilt: 80, - zoom: 13, - ), - zoomControlsEnabled: true, - polylines: { - Polyline( - polylineId: const PolylineId('route'), - points: [ - LatLng( - double.parse(EncryptionHelper - .instance - .decryptData( - res['start_location']) - .toString() - .split(',')[0]), - double.parse(EncryptionHelper - .instance - .decryptData( - res['start_location']) - .toString() - .split(',')[1]), - ), - LatLng( - double.parse(EncryptionHelper - .instance - .decryptData( - res['end_location']) - .toString() - .split(',')[0]), - double.parse(EncryptionHelper - .instance - .decryptData( - res['end_location']) - .toString() - .split(',')[1]), - ) - ], - color: CupertinoColors.activeGreen, - width: 5, - ), - }, - ), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - '${'Order ID'.tr} ${EncryptionHelper.instance.decryptData(res['id'])}', - style: CupertinoTheme.of(context) - .textTheme - .navActionTextStyle, - ), - Text( - res['date'].toString(), - style: CupertinoTheme.of(context) - .textTheme - .navActionTextStyle, - ), - ], - ), - ], - ), - ), - ), - const SizedBox(height: 20), - Container( - width: MediaQuery.of(context).size.width * 0.9, - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - border: Border.all( - color: CupertinoColors.activeGreen, width: 2), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${'Price is'.tr} ${EncryptionHelper.instance.decryptData(res['price_for_driver'])}', - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - Text( - '${'Distance is'.tr} ${EncryptionHelper.instance.decryptData(res['distance'])} KM', - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - ], - ), - ), - const SizedBox(height: 20), - Text( - 'Times of Trip'.tr, - style: CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle, - ), - const SizedBox(height: 10), - Container( - width: MediaQuery.of(context).size.width * 0.9, - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - border: Border.all( - color: CupertinoColors.destructiveRed, - width: 2), - ), - child: Column( - children: [ - Text( - '${'Time to Passenger is'.tr} ${res['DriverIsGoingToPassenger']}', - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - Text( - '${'TimeStart is'.tr} ${res['rideTimeStart']}', - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - Text( - '${'Time Finish is'.tr} ${res['rideTimeFinish']}', - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - ], - ), - ), - const SizedBox(height: 20), - Container( - width: MediaQuery.of(context).size.width * 0.9, - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - border: Border.all( - color: CupertinoColors.systemGreen, width: 2), - ), - child: Center( - child: Text( - '${'Passenger Name is'.tr} ${EncryptionHelper.instance.decryptData(res['first_name'])} ${EncryptionHelper.instance.decryptData(res['last_name'])}', - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - ), - ), - const SizedBox(height: 20), - Container( - width: MediaQuery.of(context).size.width * 0.9, - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - border: Border.all( - color: CupertinoColors.systemYellow, - width: 2), - ), - child: Center( - child: Text( - '${'Status is'.tr} ${EncryptionHelper.instance.decryptData(res['status'])}', - style: CupertinoTheme.of(context) - .textTheme - .textStyle, - ), - ), - ), - ], - ), + body: GetBuilder( + 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 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 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.withOpacity(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: GoogleMap( + initialCameraPosition: initialCameraPosition, + markers: markers, + polylines: { + if (startLocation != null && endLocation != null) + Polyline( + polylineId: const PolylineId('route'), + points: [startLocation, endLocation], + color: Colors.deepPurple, + width: 5, + ), + }, + onMapCreated: (GoogleMapController 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, 60.0)); + } + }, + ), + ), + 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.withOpacity(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, + ); } diff --git a/lib/views/home/Captin/home_captain/drawer_captain.dart b/lib/views/home/Captin/home_captain/drawer_captain.dart index e99024f..2c08a27 100755 --- a/lib/views/home/Captin/home_captain/drawer_captain.dart +++ b/lib/views/home/Captin/home_captain/drawer_captain.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -20,6 +22,7 @@ import 'package:sefer_driver/views/home/Captin/About Us/settings_captain.dart'; import 'package:sefer_driver/views/home/my_wallet/walet_captain.dart'; import 'package:sefer_driver/views/home/profile/profile_captain.dart'; import 'package:sefer_driver/views/notification/notification_captain.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../About Us/video_page.dart'; import '../assurance_health_page.dart'; import '../maintain_center_page.dart'; @@ -47,7 +50,7 @@ class AppDrawer extends StatelessWidget { // 2. تعريف بيانات القائمة بشكل مركزي ومنظم final List drawerItems = [ DrawerItem( - title: 'Wallet'.tr, + title: 'Balance'.tr, icon: Icons.account_balance_wallet, color: Colors.green, onTap: () => Get.to(() => WalletCaptainRefactored())), @@ -81,16 +84,16 @@ class AppDrawer extends StatelessWidget { icon: Icons.share, 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: '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, @@ -111,6 +114,12 @@ class AppDrawer extends StatelessWidget { 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, diff --git a/lib/views/home/Captin/home_captain/home_captin.dart b/lib/views/home/Captin/home_captain/home_captin.dart index 015312c..68a79fc 100755 --- a/lib/views/home/Captin/home_captain/home_captin.dart +++ b/lib/views/home/Captin/home_captain/home_captin.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:ui'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:sefer_driver/constant/box_name.dart'; @@ -26,18 +27,19 @@ import 'widget/connect.dart'; import 'widget/left_menu_map_captain.dart'; import '../../../../main.dart'; -// الويدجت الرئيسية للصفحة بعد تنظيمها +// ================================================================== +// Redesigned Main Widget (V3) +// ================================================================== class HomeCaptain extends StatelessWidget { HomeCaptain({super.key}); - // تم الإبقاء على تعريف الـ Controllers كما هو في الكود الأصلي final LocationController locationController = Get.put(LocationController()); final HomeCaptainController homeCaptainController = Get.put(HomeCaptainController()); @override Widget build(BuildContext context) { - // لم يتم تغيير أي شيء في هذه الأوامر + // Initial calls remain the same. Get.put(HomeCaptainController()); WidgetsBinding.instance.addPostFrameCallback((_) async { closeOverlayIfFound(); @@ -46,19 +48,20 @@ class HomeCaptain extends StatelessWidget { showDriverGiftClaim(context); }); - // التصميم الجديد: أصبح الـ build الرئيسي نظيفاً وواضحاً جداً + // The stack is now even simpler. return Scaffold( - appBar: const _HomeAppBar(), // 1. تم فصل الـ AppBar في ويدجت خاصة + appBar: const _HomeAppBar(), drawer: AppDrawer(), body: Stack( children: [ - // كل جزء من الواجهة أصبح ويدجت منفصلة - const _MapView(), // 2. تم فصل الخريطة - const _DriverStatsOverlay(), // 3. تم فصل كارت الإحصائيات العلوي - const _DriverDurationOverlay(), // 4. تم فصل كارت مدة العمل - const _FloatingActionButtons(), // 5. تم فصل الأزرار الجانبية العائمة - const _ConnectButtonOverlay(), // 6. تم فصل زر الاتصال السفلي - leftMainMenuCaptainIcons(), // هذه بقيت كما هي + // 1. The Map View is the base layer. + const _MapView(), + + // 2. The new floating "Status Pod" at the bottom. + const _StatusPodOverlay(), + + // This widget from the original code remains. + leftMainMenuCaptainIcons(), ], ), ); @@ -66,64 +69,51 @@ class HomeCaptain extends StatelessWidget { } // ================================================================== -// الأجزاء الصغيرة التي تم فصلها (Helper Widgets) -// هذه الويدجتس تحتوي على نفس كود التصميم الأصلي الخاص بك تماماً، ولكنها منظمة بشكل أفضل +// Redesigned Helper Widgets (V3) // ================================================================== -/// 1. ويدجت الـ AppBar +/// 1. The AppBar now contains the map actions in a PopupMenuButton. class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget { const _HomeAppBar(); @override Widget build(BuildContext context) { - // نفس الكود الأصلي للـ AppBar final homeCaptainController = Get.find(); return AppBar( - elevation: 2, - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.white, Colors.grey.shade50], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - spreadRadius: 1, - blurRadius: 4, - ), - ], - ), - ), + backgroundColor: Colors.white, + elevation: 1, + shadowColor: Colors.black.withOpacity(0.1), title: Row( children: [ Image.asset( 'assets/images/logo.gif', - height: 32, - width: 35, + height: 35, ), - const SizedBox(width: 8), + const SizedBox(width: 10), Text( AppInformation.appName.split(' ')[0].toString().tr, style: AppStyle.title.copyWith( - fontSize: 22, - fontWeight: FontWeight.w600, + fontSize: 24, + fontWeight: FontWeight.bold, color: AppColor.blueColor, ), ), ], ), actions: [ + // Refuse count indicator Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: MyCircleContainer( - child: Text( - homeCaptainController.countRefuse.toString(), - style: AppStyle.title, + padding: const EdgeInsets.only(right: 8.0), + child: Center( + child: MyCircleContainer( + child: Text( + homeCaptainController.countRefuse.toString(), + style: AppStyle.title.copyWith(fontWeight: FontWeight.bold), + ), ), ), ), + // The new PopupMenuButton for all map and ride actions. Container( margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( @@ -165,28 +155,45 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget { ], ), ), - const SizedBox(width: 8), ], ); } + PopupMenuItem _buildPopupMenuItem({ + required String value, + IconData? icon, + Widget? iconWidget, + required String text, + Color? iconColor, + }) { + return PopupMenuItem( + value: value, + child: Row( + children: [ + iconWidget ?? Icon(icon, color: iconColor ?? Colors.grey.shade600), + const SizedBox(width: 16), + Text(text), + ], + ), + ); + } + @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } -/// 2. ويدجت الخريطة +/// 2. The Map View is unchanged functionally. class _MapView extends StatelessWidget { const _MapView(); @override Widget build(BuildContext context) { final locationController = Get.find(); - // نفس الكود الأصلي للخريطة return GetBuilder(builder: (controller) { return controller.isLoading ? const MyCircularProgressIndicator() : GoogleMap( - padding: EdgeInsets.only(bottom: 50, top: 300), + padding: const EdgeInsets.only(bottom: 110, top: 300), fortyFiveDegreeImageryEnabled: true, onMapCreated: controller.onMapCreated, minMaxZoomPreference: const MinMaxZoomPreference(6, 18), @@ -217,7 +224,7 @@ class _MapView extends StatelessWidget { myLocationEnabled: false, trafficEnabled: controller.mapTrafficON, buildingsEnabled: true, - mapToolbarEnabled: true, + mapToolbarEnabled: false, compassEnabled: true, zoomControlsEnabled: false, ); @@ -225,353 +232,213 @@ class _MapView extends StatelessWidget { } } -/// 3. ويدجت كارت الإحصائيات العلوي -class _DriverStatsOverlay extends StatelessWidget { - const _DriverStatsOverlay(); +/// 3. The floating "Status Pod" at the bottom of the screen. +class _StatusPodOverlay extends StatelessWidget { + const _StatusPodOverlay(); - @override - Widget build(BuildContext context) { - // نفس الكود الأصلي لكارت الإحصائيات - return Positioned( - top: 5, - right: Get.width * .05, - left: Get.width * .05, - child: GetBuilder( - builder: (homeCaptainController) { - return Container( - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Colors.white, Colors.white70], - ), - borderRadius: BorderRadius.circular(15), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - spreadRadius: 2, - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - width: Get.width * .8, - height: 120, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColor.greenColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - const Icon( - Entypo.wallet, - color: AppColor.greenColor, - size: 20, - ), - const SizedBox(width: 8), - Text( - '${"Today".tr}: ${(homeCaptainController.totalMoneyToday)}', - style: AppStyle.title.copyWith( - color: AppColor.greenColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColor.yellowColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - const Icon( - Entypo.wallet, - color: AppColor.yellowColor, - size: 20, - ), - const SizedBox(width: 8), - Text( - '${AppInformation.appName}: ${(homeCaptainController.totalMoneyInSEFER)}', - style: AppStyle.title.copyWith( - color: AppColor.yellowColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${'Total Points is'.tr}: ${(homeCaptainController.totalPoints)}', - style: AppStyle.title.copyWith( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: int.parse( - (homeCaptainController.countRideToday)) < - 5 - ? AppColor.accentColor - : int.parse((homeCaptainController - .countRideToday)) > - 5 && - int.parse((homeCaptainController - .countRideToday)) < - 10 - ? AppColor.yellowColor - : AppColor.greenColor, - ), - child: Row( - children: [ - const Icon( - Icons.directions_car_rounded, - color: Colors.white, - size: 18, - ), - const SizedBox(width: 4), - Text( - '${"Ride Today : ".tr}: ${(homeCaptainController.countRideToday)}', - style: AppStyle.title.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], - ), - ]), - ); - }, - ), + void _showDetailsDialog(BuildContext context) { + Get.dialog( + const _DriverDetailsDialog(), + barrierColor: Colors.black.withOpacity(0.3), ); } -} - -/// 4. ويدجت كارت مدة العمل -class _DriverDurationOverlay extends StatelessWidget { - const _DriverDurationOverlay(); @override Widget build(BuildContext context) { - // نفس الكود الأصلي لكارت المدة + final homeCaptainController = Get.find(); return Positioned( - bottom: 65, - right: Get.width * .1, - left: Get.width * .1, - child: GetBuilder( - builder: (homeCaptainController) => Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - spreadRadius: 2, - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.timer_outlined, color: AppColor.greenColor), - const SizedBox(width: 8), - Text( - 'Active Duration:'.tr, - style: AppStyle.title, - ), - const SizedBox(width: 4), - Text( - (homeCaptainController.stringActiveDuration), - style: AppStyle.title.copyWith( - fontWeight: FontWeight.bold, - color: AppColor.greenColor, - ), - ), + bottom: 16, + left: 16, + right: 16, + child: GestureDetector( + onTap: () => _showDetailsDialog(context), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.85), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + spreadRadius: -5, + ) ], ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Row( children: [ - const Icon(Icons.access_time, color: AppColor.accentColor), + const ConnectWidget(), + const Spacer(), + _buildQuickStat( + icon: Icons.directions_car_rounded, + value: homeCaptainController.countRideToday, + label: 'Rides'.tr, + color: AppColor.blueColor, + ), + const SizedBox(width: 16), + _buildQuickStat( + icon: Entypo.wallet, + value: homeCaptainController.totalMoneyToday.toString(), + label: 'Today'.tr, + color: AppColor.greenColor, + ), const SizedBox(width: 8), - Text( - 'Total Connection Duration:'.tr, - style: AppStyle.title, - ), - const SizedBox(width: 4), - Text( - (homeCaptainController.totalDurationToday), - style: AppStyle.title.copyWith( - fontWeight: FontWeight.bold, - color: AppColor.accentColor, - ), - ), ], ), - ], + ), ), ), ), ); } -} -/// 5. ويدجت الأزرار الجانبية العائمة -class _FloatingActionButtons extends StatelessWidget { - const _FloatingActionButtons(); - - @override - Widget build(BuildContext context) { - // نفس الكود الأصلي للأزرار - return Positioned( - bottom: Get.height * .2, - right: 6, - child: - GetBuilder(builder: (homeCaptainController) { - return Column( + Widget _buildQuickStat( + {required IconData icon, + required String value, + required String label, + required Color color}) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( children: [ - Platform.isAndroid - ? AnimatedContainer( - duration: const Duration(microseconds: 200), - width: homeCaptainController.widthMapTypeAndTraffic, - decoration: BoxDecoration( - border: Border.all(color: AppColor.blueColor), - color: AppColor.secondaryColor, - borderRadius: BorderRadius.circular(15)), - child: IconButton( - onPressed: () async { - Bubble().startBubbleHead(sendAppToBackground: true); - }, - icon: Image.asset( - 'assets/images/logo1.png', - fit: BoxFit.cover, - width: 35, - height: 35, - ), - ), - ) - : const SizedBox(), - const SizedBox( - height: 5, - ), - AnimatedContainer( - duration: const Duration(microseconds: 200), - width: homeCaptainController.widthMapTypeAndTraffic, - decoration: BoxDecoration( - border: Border.all(color: AppColor.blueColor), - color: AppColor.secondaryColor, - borderRadius: BorderRadius.circular(15)), - child: IconButton( - onPressed: () { - Get.to(() => const AvailableRidesPage()); - }, - icon: const Icon( - Icons.train_sharp, - size: 29, - color: AppColor.blueColor, - ), - ), - ), - const SizedBox( - height: 5, - ), - box.read(BoxName.rideStatus) == 'Applied' || - box.read(BoxName.rideStatus) == 'Begin' - ? Positioned( - bottom: Get.height * .2, - right: 6, - child: AnimatedContainer( - duration: const Duration(microseconds: 200), - width: homeCaptainController.widthMapTypeAndTraffic, - decoration: BoxDecoration( - border: Border.all(color: AppColor.blueColor), - color: AppColor.secondaryColor, - borderRadius: BorderRadius.circular(15)), - child: GestureDetector( - onLongPress: () { - box.write(BoxName.rideStatus, 'delete'); - homeCaptainController.update(); - }, - child: IconButton( - onPressed: () { - box.read(BoxName.rideStatus) == 'Applied' - ? { - Get.to(() => PassengerLocationMapPage(), - arguments: - box.read(BoxName.rideArguments)), - Get.put(MapDriverController()) - .changeRideToBeginToPassenger() - } - : { - Get.to(() => PassengerLocationMapPage(), - arguments: - box.read(BoxName.rideArguments)), - Get.put(MapDriverController()) - .startRideFromStartApp() - }; - }, - icon: const Icon( - Icons.directions_rounded, - size: 29, - color: AppColor.blueColor, - ), - ), - ), - ), - ) - : const SizedBox() + Icon(icon, color: color, size: 20), + const SizedBox(width: 4), + Text(value, + style: AppStyle.title + .copyWith(fontSize: 16, fontWeight: FontWeight.bold)), ], - ); - }), + ), + Text(label, + style: AppStyle.title + .copyWith(fontSize: 12, color: Colors.grey.shade700)), + ], ); } } -/// 6. ويدجت زر الاتصال السفلي -class _ConnectButtonOverlay extends StatelessWidget { - const _ConnectButtonOverlay(); +/// 4. The Dialog that shows detailed driver stats. +class _DriverDetailsDialog extends StatelessWidget { + const _DriverDetailsDialog(); @override Widget build(BuildContext context) { - // نفس الكود الأصلي لزر الاتصال - return Positioned( - bottom: 10, - right: Get.width * .1, - left: Get.width * .1, - child: const ConnectWidget()); + final homeCaptainController = Get.find(); + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: AlertDialog( + backgroundColor: Colors.white.withOpacity(0.95), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + titlePadding: const EdgeInsets.only(top: 20), + title: Center( + child: Text( + 'Your Activity'.tr, + style: AppStyle.title + .copyWith(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 20), + _buildStatRow( + icon: Entypo.wallet, + color: AppColor.greenColor, + label: 'Today'.tr, + value: homeCaptainController.totalMoneyToday.toString(), + ), + const SizedBox(height: 12), + _buildStatRow( + icon: Entypo.wallet, + color: AppColor.yellowColor, + label: AppInformation.appName, + value: homeCaptainController.totalMoneyInSEFER.toString(), + ), + const Divider(height: 24), + _buildDurationRow( + icon: Icons.timer_outlined, + label: 'Active Duration:'.tr, + value: homeCaptainController.stringActiveDuration, + color: AppColor.greenColor, + ), + const SizedBox(height: 12), + _buildDurationRow( + icon: Icons.access_time, + label: 'Total Connection Duration:'.tr, + value: homeCaptainController.totalDurationToday, + color: AppColor.accentColor, + ), + const Divider(height: 24), + _buildStatRow( + icon: Icons.star_border_rounded, + color: AppColor.blueColor, + label: 'Total Points'.tr, + value: homeCaptainController.totalPoints.toString(), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('Close'.tr, + style: AppStyle.title.copyWith( + color: AppColor.blueColor, fontWeight: FontWeight.bold)), + ) + ], + ), + ); + } + + Widget _buildStatRow( + {required IconData icon, + required Color color, + required String label, + required String value}) { + return Row( + children: [ + Icon(icon, color: color, size: 22), + const SizedBox(width: 12), + Text('$label:', style: AppStyle.title), + const Spacer(), + Text( + value, + style: AppStyle.title.copyWith( + color: color, fontWeight: FontWeight.bold, fontSize: 18), + ), + ], + ); + } + + Widget _buildDurationRow( + {required IconData icon, + required String label, + required String value, + required Color color}) { + return Row( + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(width: 12), + Text(label, style: AppStyle.title), + const Spacer(), + Text( + value, + style: AppStyle.title.copyWith( + fontWeight: FontWeight.bold, color: color, fontSize: 16), + ), + ], + ); } } -// هذه الويدجت المساعدة بقيت كما هي class _MapControlButton extends StatelessWidget { final IconData icon; final VoidCallback onPressed; @@ -606,7 +473,44 @@ class _MapControlButton extends StatelessWidget { } } -// الدوال المساعدة الأخرى تبقى كما هي في ملفك... -// showFirstTimeOfferNotification(BuildContext context) async { ... } -// bool _checkIfFirstTime() { ... } -// void _markAsNotFirstTime() { ... } +/// NOTE: The _FloatingActionButtons and _MapControlButton widgets have been removed +/// as their functionality is now integrated into the _HomeAppBar. +/// +/// You will still need to modify your existing `ConnectWidget` +/// to accept an `isCompact` boolean flag as mentioned in the previous design. +/* +class ConnectWidget extends StatelessWidget { + final bool isCompact; + const ConnectWidget({super.key, this.isCompact = false}); + + @override + Widget build(BuildContext context) { + // ... your existing controller logic + + if (isCompact) { + // Return a smaller version for the pod + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: controller.isConnect ? AppColor.greenColor : AppColor.accentColor, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(controller.isConnect ? Icons.wifi_tethering_rounded : Icons.wifi_tethering_off_rounded, color: Colors.white, size: 20), + const SizedBox(width: 8), + Text( + controller.isConnect ? 'Online'.tr : 'Offline'.tr, + style: AppStyle.title.copyWith(color: Colors.white, fontSize: 14), + ), + ], + ), + ); + } + + // Return the original, larger button + return ElevatedButton.icon(...) + } +} +*/ diff --git a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart index ce8de89..cfe17c5 100755 --- a/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart +++ b/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart @@ -1,6 +1,7 @@ import 'package:sefer_driver/constant/box_name.dart'; import 'package:sefer_driver/controller/firebase/local_notification.dart'; import 'package:sefer_driver/main.dart'; +import 'package:sefer_driver/views/auth/captin/login_captin.dart'; import 'package:sefer_driver/views/home/Captin/driver_map_page.dart'; import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart'; import 'package:flutter/material.dart'; @@ -14,6 +15,7 @@ import '../../../../../constant/links.dart'; import '../../../../../controller/firebase/firbase_messge.dart'; import '../../../../../controller/functions/crud.dart'; import '../../../../../controller/home/captin/order_request_controller.dart'; +import '../../../../../controller/home/navigation/navigation_view.dart'; import '../../../../Rate/ride_calculate_driver.dart'; import '../../../../auth/syria/registration_view.dart'; @@ -156,18 +158,42 @@ GetBuilder leftMainMenuCaptainIcons() { child: Builder(builder: (context) { return IconButton( onPressed: () async { - Get.to(() => const RegistrationView()); + box.remove(BoxName.agreeTerms); + Get.to(() => const NavigationView()); // box.write(BoxName.statusDriverLocation, 'off'); }, icon: const Icon( - FontAwesome5.grin_tears, + FontAwesome5.map, size: 29, color: AppColor.blueColor, ), ); }), ), + // AnimatedContainer( + // duration: const Duration(microseconds: 200), + // width: controller.widthMapTypeAndTraffic, + // decoration: BoxDecoration( + // color: AppColor.secondaryColor, + // border: Border.all(color: AppColor.blueColor), + // borderRadius: BorderRadius.circular(15)), + // child: Builder(builder: (context) { + // return IconButton( + // onPressed: () async { + // box.remove(BoxName.agreeTerms); + // Get.to(() => const NavigationView()); + + // // box.write(BoxName.statusDriverLocation, 'off'); + // }, + // icon: const Icon( + // FontAwesome5.grin_tears, + // size: 29, + // color: AppColor.blueColor, + // ), + // ); + // }), + // ), const SizedBox( height: 5, diff --git a/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart b/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart index 32f12d3..0078f8f 100755 --- a/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart +++ b/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart @@ -204,7 +204,7 @@ GetBuilder speedCircle() { ? Positioned( // New: تم وضع دائرة السرعة في الأسفل يمينًا bottom: 25, - left: 16, + left: 3, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, diff --git a/lib/views/home/Captin/mapDriverWidgets/google_driver_map_page.dart b/lib/views/home/Captin/mapDriverWidgets/google_driver_map_page.dart index 3e9c585..beae0a9 100755 --- a/lib/views/home/Captin/mapDriverWidgets/google_driver_map_page.dart +++ b/lib/views/home/Captin/mapDriverWidgets/google_driver_map_page.dart @@ -1,117 +1,3 @@ -// import 'package:flutter/material.dart'; -// import 'package:get/get.dart'; -// import 'package:google_maps_flutter/google_maps_flutter.dart'; - -// import '../../../../controller/functions/location_controller.dart'; -// import '../../../../controller/home/captin/map_driver_controller.dart'; - -// class GoogleDriverMap extends StatelessWidget { -// const GoogleDriverMap({ -// super.key, -// required this.locationController, -// }); - -// final LocationController locationController; - -// @override -// Widget build(BuildContext context) { -// Get.put(MapDriverController()); -// return Padding( -// padding: const EdgeInsets.all(8.0), -// child: GetBuilder( -// builder: (controller) => Column( -// children: [ -// SizedBox( -// height: Get.height * .92, -// child: GoogleMap( -// onMapCreated: controller.onMapCreated, -// zoomControlsEnabled: true, -// // initialCameraPosition: CameraPosition( -// // target: locationController.myLocation, -// // zoom: 13, -// // bearing: locationController.heading, -// // tilt: 40, -// // ), -// initialCameraPosition: CameraPosition( -// target: locationController.myLocation, -// zoom: 17, -// bearing: locationController.heading, // استخدام اتجاه السائق -// tilt: 60, // زاوية ميل -// ), -// cameraTargetBounds: -// CameraTargetBounds.unbounded, // Allow unrestricted movement -// onCameraMove: (position) { -// CameraPosition( -// target: locationController.myLocation, -// zoom: 13, -// bearing: locationController.heading, -// tilt: 40, -// ); -// //todo -// // locationController.myLocation = position.target; -// // -// // controller.mapController -// // ?.animateCamera(CameraUpdate.newCameraPosition(position)); -// }, -// minMaxZoomPreference: const MinMaxZoomPreference(8, 15), -// myLocationEnabled: true, -// myLocationButtonEnabled: true, -// compassEnabled: true, -// mapType: MapType.terrain, -// rotateGesturesEnabled: true, -// scrollGesturesEnabled: true, -// trafficEnabled: false, -// buildingsEnabled: true, -// mapToolbarEnabled: true, -// fortyFiveDegreeImageryEnabled: true, -// zoomGesturesEnabled: true, -// polylines: { -// Polyline( -// zIndex: 2, -// geodesic: true, -// polylineId: const PolylineId('route1'), -// points: controller.polylineCoordinates, -// color: const Color.fromARGB(255, 163, 81, 246), -// width: 5, -// ), -// Polyline( -// zIndex: 2, -// geodesic: true, -// polylineId: const PolylineId('route'), -// points: controller.polylineCoordinatesDestination, -// color: const Color.fromARGB(255, 10, 29, 126), -// width: 5, -// ), -// }, -// markers: { -// Marker( -// markerId: MarkerId('MyLocation'.tr), -// position: locationController.myLocation, -// draggable: true, -// icon: controller.carIcon, -// rotation: locationController.heading, -// ), -// Marker( -// markerId: MarkerId('start'.tr), -// position: controller.latLngPassengerLocation, -// draggable: true, -// icon: controller.startIcon, -// ), -// Marker( -// markerId: MarkerId('end'.tr), -// position: controller.latLngPassengerDestination, -// draggable: true, -// icon: controller.endIcon, -// ), -// }, -// ), -// ), -// ], -// ), -// ), -// ); -// } -// } import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -179,15 +65,30 @@ class GoogleDriverMap extends StatelessWidget { startCap: Cap.roundCap, endCap: Cap.roundCap, ), - Polyline( - zIndex: 2, + // Polyline( + // zIndex: 2, - polylineId: const PolylineId('route'), - points: controller.polylineCoordinatesDestination, - color: const Color.fromARGB(255, 10, 29, 126), - width: 6, // Changed: زيادة عرض الخط - startCap: Cap.roundCap, - endCap: Cap.roundCap, + // polylineId: const PolylineId('route'), + // points: controller.polylineCoordinatesDestination, + // color: const Color.fromARGB(255, 10, 29, 126), + // width: 6, // Changed: زيادة عرض الخط + // startCap: Cap.roundCap, + // endCap: Cap.roundCap, + // ), + Polyline( + polylineId: const PolylineId('upcoming_route'), + points: controller.upcomingPathPoints, + color: Colors.blue, // أو أي لون آخر تختاره للمسار + width: 8, + zIndex: 2, + ), + // 2. الخط المقطوع (تحت) + Polyline( + polylineId: const PolylineId('traveled_route'), + points: controller.traveledPathPoints, + color: Colors.grey.withOpacity(0.8), + width: 7, + zIndex: 1, ), }, markers: { diff --git a/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart b/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart index 7481fd2..db55580 100755 --- a/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart +++ b/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart @@ -14,7 +14,7 @@ class GoogleMapApp extends StatelessWidget { return GetBuilder( builder: (mapDriverController) => mapDriverController.isRideStarted ? Positioned( - left: 150, + right: 3, bottom: 20, child: Container( decoration: AppStyle.boxDecoration, diff --git a/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart b/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart index 6006016..037fa39 100755 --- a/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart +++ b/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart @@ -144,6 +144,11 @@ class PassengerInfoWindow extends StatelessWidget { borderRadius: BorderRadius.circular(10)), ), onPressed: () async { + controller.getRoute( + origin: controller.latLngPassengerLocation, + destination: controller.latLngPassengerDestination, + routeColor: Colors.blue // أو أي لون + ); if (await controller .calculateDistanceBetweenDriverAndPassengerLocation() < 140) { diff --git a/lib/views/home/Captin/orderCaptin/order_request_page.dart b/lib/views/home/Captin/orderCaptin/order_request_page.dart index d6f2142..ce5c27d 100755 --- a/lib/views/home/Captin/orderCaptin/order_request_page.dart +++ b/lib/views/home/Captin/orderCaptin/order_request_page.dart @@ -274,13 +274,18 @@ class _OrderRequestPageState extends State { controller.myList[8].toString(), controller.myList[9].toString(), ]; - FirebaseMessagesController() - .sendNotificationToPassengerToken( - "Accepted Ride".tr, - 'your ride is Accepted'.tr, - controller.myList[9].toString(), - bodyToPassenger, - 'start.wav'); + final fmc = + Get.isRegistered() + ? Get.find() + : Get.put(FirebaseMessagesController()); + + fmc.sendNotificationToDriverMAP( + "Accepted Ride".tr, + 'your ride is Accepted'.tr, + controller.myList[9].toString(), + bodyToPassenger, + 'start.wav', + ); Get.back(); box.write(BoxName.rideArguments, { 'passengerLocation': diff --git a/lib/views/home/my_wallet/card_wallet_widget.dart b/lib/views/home/my_wallet/card_wallet_widget.dart index 67e609f..627bb6c 100755 --- a/lib/views/home/my_wallet/card_wallet_widget.dart +++ b/lib/views/home/my_wallet/card_wallet_widget.dart @@ -95,7 +95,7 @@ class CardSeferWalletDriver extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - 'محفظة انطلق', + 'رصيد انطلق', style: AppStyle.headTitle.copyWith( fontFamily: 'Amiri', // خط يوحي بالفخامة color: Colors.white, diff --git a/lib/views/home/my_wallet/payment_history_driver_page.dart b/lib/views/home/my_wallet/payment_history_driver_page.dart index a442eec..b92aeac 100755 --- a/lib/views/home/my_wallet/payment_history_driver_page.dart +++ b/lib/views/home/my_wallet/payment_history_driver_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:sefer_driver/constant/colors.dart'; -import 'package:sefer_driver/constant/style.dart'; -import 'package:sefer_driver/views/widgets/my_scafold.dart'; -import 'package:sefer_driver/views/widgets/mycircular.dart'; +import 'package:sefer_driver/constant/style.dart'; // Assuming this has your text styles +import 'package:sefer_driver/views/widgets/mycircular.dart'; // Assuming this is your loading widget import '../../../controller/payment/driver_payment_controller.dart'; @@ -12,43 +12,133 @@ class PaymentHistoryDriverPage extends StatelessWidget { @override Widget build(BuildContext context) { + // Initialize your controller Get.put(DriverWalletHistoryController()); - return MyScafolld( - title: 'Payment History'.tr, - body: [ - GetBuilder( - builder: (controller) => controller.isLoading - ? const MyCircularProgressIndicator() - : ListView.builder( - itemCount: controller.archive.length, - itemBuilder: (BuildContext context, int index) { - var list = controller.archive[index]; - return Padding( - padding: const EdgeInsets.all(4), - child: Container( - decoration: BoxDecoration( - color: double.parse(list['amount']) < 0 - ? AppColor.redColor.withOpacity(.4) - : AppColor.greenColor.withOpacity(.4)), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - list['amount'], - style: AppStyle.title, - ), - Text( - list['created_at'], - style: AppStyle.title, - ), - ], - ), - ), - ); - }, + + return Scaffold( + appBar: AppBar( + title: Text('Payment History'.tr), + backgroundColor: Colors.white, + elevation: 1, + ), + backgroundColor: Colors.grey[100], + body: GetBuilder( + builder: (controller) { + if (controller.isLoading) { + // Using your custom loading indicator + return const Center(child: MyCircularProgressIndicator()); + } + + if (controller.archive.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.account_balance_wallet_outlined, + size: 80, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No transactions yet'.tr, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(color: Colors.grey[600]), ), - ) - ], - isleading: true); + ], + ), + ); + } + + return AnimationLimiter( + child: ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: controller.archive.length, + itemBuilder: (BuildContext context, int index) { + var transaction = controller.archive[index]; + + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 375), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: _TransactionCard(transaction: transaction), + ), + ), + ); + }, + ), + ); + }, + ), + ); + } +} + +// A dedicated widget for displaying a single transaction with a modern UI. +class _TransactionCard extends StatelessWidget { + final Map transaction; + + const _TransactionCard({required this.transaction}); + + @override + Widget build(BuildContext context) { + // Safely parse the amount to avoid errors + final double amount = + double.tryParse(transaction['amount']?.toString() ?? '0') ?? 0; + + final bool isCredit = amount >= 0; + + final Color indicatorColor = + isCredit ? AppColor.greenColor : AppColor.redColor; + final IconData iconData = + isCredit ? Icons.arrow_upward_rounded : Icons.arrow_downward_rounded; + final String transactionType = (isCredit ? 'Credit'.tr : 'Debit'.tr).tr; + + return Card( + elevation: 2, + shadowColor: Colors.black.withOpacity(0.05), + margin: const EdgeInsets.only(bottom: 12.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + clipBehavior: Clip.antiAlias, // Ensures the color bar is clipped neatly + child: IntrinsicHeight( + // Ensures the color bar and content have the same height + child: Row( + children: [ + // Left-side color indicator bar + Container(width: 6, color: indicatorColor), + + Expanded( + child: ListTile( + leading: Icon(iconData, color: indicatorColor, size: 30), + title: Text( + // Use .abs() to remove the negative sign from the display + '${amount.abs().toStringAsFixed(2)} ${'SYP'.tr}', + style: AppStyle.title.copyWith( + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + subtitle: Text( + transaction['created_at'] ?? 'No date', + style: AppStyle.title.copyWith( + fontSize: 12, + color: Colors.grey[600], + ), + ), + trailing: Text( + transactionType, + style: AppStyle.title.copyWith( + fontSize: 14, + color: indicatorColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); } } diff --git a/lib/views/home/my_wallet/walet_captain.dart b/lib/views/home/my_wallet/walet_captain.dart index 11250a8..e92f5cc 100755 --- a/lib/views/home/my_wallet/walet_captain.dart +++ b/lib/views/home/my_wallet/walet_captain.dart @@ -47,7 +47,7 @@ class WalletCaptainRefactored extends StatelessWidget { Widget build(BuildContext context) { captainWalletController.refreshCaptainWallet(); return MyScafolld( - title: 'Driver Wallet'.tr, + title: 'Driver Balance'.tr, isleading: true, action: IconButton( icon: const Icon(Icons.refresh), @@ -173,7 +173,7 @@ class WalletCaptainRefactored extends StatelessWidget { 'This amount for all trip I get from Passengers' .tr), child: const Icon(Icons.headphones)), - '${'Total Amount:'.tr} ${controller.totalAmount} ${'S.P'.tr}', + '${'Total Amount:'.tr} ${controller.totalAmount} ${'SYP'.tr}', 'This amount for all trip I get from Passengers'.tr, duration: const Duration(seconds: 6), backgroundColor: AppColor.yellowColor, @@ -195,7 +195,7 @@ class WalletCaptainRefactored extends StatelessWidget { ' Intaleq Wallet'.tr), child: const Icon(Icons.headphones), ), - '${'Total Amount:'.tr} ${controller.totalAmountVisa} ${'S.P'.tr}', + '${'Total Amount:'.tr} ${controller.totalAmountVisa} ${'SYP'.tr}', 'This amount for all trip I get from Passengers and Collected For me in' .tr + ' ${AppInformation.appName} Wallet'.tr, diff --git a/lib/views/home/my_wallet/weekly_payment_page.dart b/lib/views/home/my_wallet/weekly_payment_page.dart index 6e9245b..bdbac34 100755 --- a/lib/views/home/my_wallet/weekly_payment_page.dart +++ b/lib/views/home/my_wallet/weekly_payment_page.dart @@ -1,11 +1,11 @@ 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 'package:sefer_driver/constant/colors.dart'; import 'package:sefer_driver/constant/style.dart'; -import 'package:sefer_driver/views/widgets/my_scafold.dart'; import 'package:sefer_driver/views/widgets/mycircular.dart'; -import 'package:intl/intl.dart'; - import '../../../controller/payment/driver_payment_controller.dart'; class WeeklyPaymentPage extends StatelessWidget { @@ -14,128 +14,210 @@ class WeeklyPaymentPage extends StatelessWidget { @override Widget build(BuildContext context) { Get.put(DriverWalletHistoryController()); - return MyScafolld( - title: 'Payment History'.tr, - body: [ - GetBuilder( - builder: (controller) => controller.isLoading - ? const MyCircularProgressIndicator() - : Column( - children: [ - Container( - width: Get.width * .8, - decoration: AppStyle.boxDecoration1, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - decoration: AppStyle.boxDecoration1, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - controller.weeklyList.isEmpty - ? '0' - : controller.weeklyList[0] - ['totalAmount'] - .toString(), - style: AppStyle.number, - ), + + return Scaffold( + appBar: AppBar( + title: Text('Weekly Summary'.tr), + backgroundColor: Colors.white, + elevation: 1, + ), + backgroundColor: Colors.grey[100], + body: GetBuilder( + builder: (controller) { + if (controller.isLoading) { + return const Center(child: MyCircularProgressIndicator()); + } + + return Column( + children: [ + // 1. Prominent Summary Card at the top + _buildSummaryCard(controller), + + // 2. A title for the transactions list + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 10), + child: Row( + children: [ + Icon(Icons.list_alt, color: Colors.grey[600]), + const SizedBox(width: 8), + Text( + 'Transactions this week'.tr, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.grey[700], + ), + ), + ], + ), + ), + + // 3. The animated list of transactions + Expanded( + child: controller.weeklyList.isEmpty + ? _buildEmptyState(context) + : AnimationLimiter( + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: controller.weeklyList.length, + itemBuilder: (BuildContext context, int index) { + var transaction = controller.weeklyList[index]; + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 375), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: _TransactionListItem( + transaction: transaction), ), ), - ), - Text( - ' Total weekly is '.tr, - style: AppStyle.title, - ), - ], + ); + }, ), ), - const SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - child: SizedBox( - height: Get.height * .75, - child: controller.weeklyList.isNotEmpty - ? ListView.builder( - itemCount: controller.weeklyList.length, - itemBuilder: - (BuildContext context, int index) { - var list = controller.weeklyList[index]; - return Padding( - padding: const EdgeInsets.all(2.0), - child: Container( - decoration: AppStyle.boxDecoration1, - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - Card( - elevation: 2, - color: list['paymentMethod'] == - 'visa' - ? AppColor.blueColor - : AppColor.secondaryColor, - child: Padding( - padding: - const EdgeInsets.all(8.0), - child: Text( - list['paymentMethod'] == - 'Remainder' - ? 'Remainder'.tr - : list['paymentMethod'] == - 'fromBudget' - ? 'fromBudget'.tr - : list[ - 'paymentMethod'], - style: AppStyle.title, - ), - ), - ), - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Card( - child: Padding( - padding: - const EdgeInsets.all( - 8.0), - child: Text( - list['amount'], - style: AppStyle.number, - ), - ), - ), - Text( - DateFormat( - 'yyyy-MM-dd hh:mm a') - .format(DateTime.parse( - list[ - 'dateUpdated'])), - style: AppStyle.number, - ), - ], - ), - ], - ), - ), - ), - ); - }, - ) - : const SizedBox(), - ), - ), - ], + ), + ], + ); + }, + ), + ); + } + + // A widget for the top summary card. + Widget _buildSummaryCard(DriverWalletHistoryController controller) { + final String totalAmount = controller.weeklyList.isEmpty + ? '0.00' + : controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00'; + + return Card( + margin: const EdgeInsets.all(16.0), + elevation: 4, + shadowColor: AppColor.primaryColor.withOpacity(0.2), + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Container( + padding: const EdgeInsets.all(20.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [AppColor.primaryColor, AppColor.greenColor], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Row( + children: [ + const Icon(Icons.account_balance_wallet, + color: Colors.white, size: 40), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Total Weekly Earnings'.tr, + style: AppStyle.title + .copyWith(color: Colors.white70, fontSize: 16), ), - ) + const SizedBox(height: 4), + Text( + '$totalAmount ${'SYP'.tr}', + style: AppStyle.number + .copyWith(color: Colors.white, fontSize: 32), + ), + ], + ), + ), + ], + ), + ), + ); + } + + // A dedicated widget for the list item. + Widget _TransactionListItem({required Map transaction}) { + final String paymentMethod = transaction['paymentMethod'] ?? 'Unknown'; + final String amount = transaction['amount']?.toString() ?? '0'; + final DateTime? date = DateTime.tryParse(transaction['dateUpdated'] ?? ''); + + return Card( + elevation: 2, + shadowColor: Colors.black.withOpacity(0.05), + margin: const EdgeInsets.only(bottom: 12.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: ListTile( + leading: CircleAvatar( + backgroundColor: AppColor.primaryColor.withOpacity(0.1), + child: Icon(_getPaymentIcon(paymentMethod), + color: AppColor.primaryColor, size: 22), + ), + title: Text( + '$amount ${'SYP'.tr}', + style: AppStyle.title.copyWith(fontWeight: FontWeight.bold), + ), + subtitle: Text( + date != null + ? DateFormat('EEEE, hh:mm a', Get.locale?.toString()) + .format(date) // e.g., Tuesday, 10:11 AM + : 'Invalid Date', + style: AppStyle.title.copyWith(fontSize: 12, color: Colors.grey[600]), + ), + trailing: Chip( + label: Text( + _getTranslatedPaymentMethod(paymentMethod), + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + ), + backgroundColor: Colors.grey.shade200, + padding: const EdgeInsets.symmetric(horizontal: 6), + side: BorderSide.none, + ), + ), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.receipt_long_outlined, size: 80, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No transactions this week'.tr, + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(color: Colors.grey[600]), + ), ], - isleading: true); + ), + ); + } + + // Helper to get a specific icon for each payment method. + IconData _getPaymentIcon(String paymentMethod) { + switch (paymentMethod.toLowerCase()) { + case 'visa': + return Icons.credit_card; + case 'frombudget': + return Icons.account_balance_wallet_outlined; + case 'remainder': + return Icons.receipt_long; + case 'cash': + return Icons.money; + default: + return Icons.payment; + } + } + + // Helper to get translated or formatted payment method names. + String _getTranslatedPaymentMethod(String paymentMethod) { + switch (paymentMethod) { + case 'Remainder': + return 'Remainder'.tr; + case 'fromBudget': + return 'From Budget'.tr; + default: + return paymentMethod; + } } } diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 59d072f..533c095 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES">