|
|
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<UnloadArrivalScreen> createState() => _UnloadArrivalScreenState();
|
|
|
}
|
|
|
|
|
|
class _UnloadArrivalScreenState extends State<UnloadArrivalScreen> 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<double> _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<String> 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,
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
}
|