import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:supplier_new/orders/unloading_inprogress.dart'; import '../common/settings.dart'; import 'all_orders.dart'; class UnloadArrivalScreen extends StatefulWidget { var details; UnloadArrivalScreen({ this.details }); @override State createState() => _UnloadArrivalScreenState(); } class _UnloadArrivalScreenState extends State with TickerProviderStateMixin{ final _c1 = TextEditingController(); final _c2 = TextEditingController(); final _c3 = TextEditingController(); final _c4 = TextEditingController(); final _f1 = FocusNode(); final _f2 = FocusNode(); final _f3 = FocusNode(); final _f4 = FocusNode(); bool get _otpReady => _c1.text.isNotEmpty && _c2.text.isNotEmpty && _c3.text.isNotEmpty && _c4.text.isNotEmpty; late AnimationController _shakeController; late Animation _offsetAnimation; bool _isOtpComplete = false; void _checkOtpFilled() { setState(() { _isOtpComplete = _c1.text.isNotEmpty && _c2.text.isNotEmpty && _c3.text.isNotEmpty && _c4.text.isNotEmpty; }); } @override void dispose() { _c1.dispose(); _c2.dispose(); _c3.dispose(); _c4.dispose(); _f1.dispose(); _f2.dispose(); _f3.dispose(); _f4.dispose(); _shakeController.dispose(); super.dispose(); } @override void initState() { super.initState(); _shakeController = AnimationController( duration: const Duration(milliseconds: 450), vsync: this, ); _offsetAnimation = Tween(begin: 0.0, end: 15.0) .chain(CurveTween(curve: Curves.elasticIn)) .animate(_shakeController); } void _onDigit({ required String value, required FocusNode current, FocusNode? next, FocusNode? prev, }) { if (value.length == 1 && next != null) { next.requestFocus(); } else if (value.isEmpty && prev != null) { prev.requestFocus(); } setState(() {}); } InputDecoration _otpDecoration(bool focused) => InputDecoration( counterText: '', contentPadding: const EdgeInsets.symmetric(vertical: 14), filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFFE0E0E0)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Colors.black, width: 1.2), ), ); @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => AllOrders(navigationFrom:"")), (route) => false, ); return false;}, child:Scaffold( backgroundColor: Colors.white, appBar: AppSettings.appBarWithNotificationIcon(widget.details.building_name, widget.details.type_of_water, widget.details.capacity, context), body: Stack( children: [ // Map background Positioned.fill( child: Image.asset( 'images/google_maps.png', // make sure this exists & is declared in pubspec.yaml fit: BoxFit.cover, ), ), // Success text Positioned( left: 16, right: 16, top: 8, child: Padding( padding: const EdgeInsets.only(top: 24, left: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('You have reached!', style: fontTextStyle( 20, const Color(0xFF101214), FontWeight.w600)), const SizedBox(height: 4), Text('Hurray! You are on-time.', style: fontTextStyle( 16, const Color(0xFF0A9E04), FontWeight.w500)), ], ), ), ), // Center card (nudged up) Align( alignment: const Alignment(0, -0.6), child: Container( width: MediaQuery.of(context).size.width * 0.86, padding: const EdgeInsets.fromLTRB(16, 18, 16, 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 10, offset: Offset(0, 4)) ], ), child: widget.details.tank_name!=''?Column( mainAxisSize: MainAxisSize.min, children: [ Text(widget.details.tank_name!=''?"Unload Water in":'The user hasn’t selected a tank. Please ask the user to select one.', textAlign: TextAlign.center, style: fontTextStyle( 16, const Color(0xFF939495), FontWeight.w500)), const SizedBox(height: 4), Visibility( visible: widget.details.tank_name!='', child: Text(widget.details.tank_name, style: fontTextStyle( 20, const Color(0xFF101214), FontWeight.w600)),), const SizedBox(height: 14), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Enter OTP to Start Unloading', style: fontTextStyle(14, const Color(0xFF646566), FontWeight.w500)), const SizedBox(width: 6), Icon(Icons.help_outline, size: 16, color: Colors.grey.shade600), ], ), const SizedBox(height: 14), // OTP boxes AnimatedBuilder( animation: _shakeController, builder: (context, child) { return Transform.translate( offset: Offset(_offsetAnimation.value, 0), child: child, ); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _OtpBox( controller: _c1, focusNode: _f1, onChanged: (v){ _onDigit(value: v, current: _f1, next: _f2); _checkOtpFilled(); }, //onChanged: (v) => _onDigit(value: v, current: _f1, next: _f2), decoration: _otpDecoration(_f1.hasFocus), ), _OtpBox( controller: _c2, focusNode: _f2, onChanged: (v){ _onDigit(value: v, current: _f2, next: _f3, prev: _f1); _checkOtpFilled(); }, decoration: _otpDecoration(_f2.hasFocus), ), _OtpBox( controller: _c3, focusNode: _f3, onChanged: (v){ _onDigit(value: v, current: _f3, next: _f4, prev: _f2); _checkOtpFilled(); }, decoration: _otpDecoration(_f3.hasFocus), ), _OtpBox( controller: _c4, focusNode: _f4, onChanged: (v){ _onDigit(value: v, current: _f4, prev: _f3); _checkOtpFilled(); }, decoration: _otpDecoration(_f4.hasFocus), ), ], ), ), const SizedBox(height: 16), // Continue button -> navigate to UnloadingInProgressScreen SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: _isOtpComplete ? () async { if (!_otpReady) return; AppSettings.preLoaderDialog(context); bool isOnline = await AppSettings.internetConnectivity(); if (!isOnline) { Navigator.of(context, rootNavigator: true).pop(); AppSettings.longFailedToast("Please Check Internet"); return; } // Combine OTP final otp = "${_c1.text}${_c2.text}${_c3.text}${_c4.text}"; var payload = { "action": "start", "percentage": "100", "otp": otp, }; // 🔥 Call API var response = await AppSettings.verifyUnloadStartOTP( widget.details.bookingid, payload ); Navigator.of(context, rootNavigator: true).pop(); // CLOSE LOADER if (response.isNotEmpty) { // Decode JSON var json = jsonDecode(response); if (json["status_code"] == 200) { // OTP VERIFIED → NAVIGATE FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 100)); Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => UnloadingInProgressScreen(details: widget.details)), (route) => false, ); } else { _shakeController.forward(from: 0); // 🔥 SHAKES OTP BOXES Future.delayed(const Duration(milliseconds: 300), () { _c1.clear(); _c2.clear(); _c3.clear(); _c4.clear(); _f1.requestFocus(); // Move focus back to first box _checkOtpFilled(); // Disable button again }); AppSettings.longFailedToast(json["msg"] ?? "Invalid OTP"); return; } } else { AppSettings.longFailedToast("Something went wrong"); } } : null, style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith((_) => Colors.black), foregroundColor: MaterialStateProperty.resolveWith((_) => Colors.white), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(28)), ), ), child: Text('Continue', style: fontTextStyle( 16, const Color(0xFFFFFFFF), FontWeight.w400)), ), ), ], ): Text(widget.details.tank_name!=''?"Unload Water in":'The user hasn’t selected a tank. Please ask the user to select one.', textAlign: TextAlign.center, style: fontTextStyle( 16, const Color(0xFF939495), FontWeight.w500)), ), ), ], ), )); } } class _OtpBox extends StatelessWidget { final TextEditingController controller; final FocusNode focusNode; final ValueChanged onChanged; final InputDecoration decoration; const _OtpBox({ required this.controller, required this.focusNode, required this.onChanged, required this.decoration, }); @override Widget build(BuildContext context) { return SizedBox( width: 54, child: TextField( controller: controller, focusNode: focusNode, textAlign: TextAlign.center, keyboardType: TextInputType.number, maxLength: 1, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: decoration, onChanged: onChanged, ), ); } }