You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
329 lines
12 KiB
329 lines
12 KiB
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
import '../common/settings.dart';
|
|
import 'collect_money.dart';
|
|
|
|
TextStyle fts(double s, Color c, FontWeight w) =>
|
|
GoogleFonts.inter(fontSize: s, color: c, fontWeight: w);
|
|
|
|
class UnloadingCompleteScreen extends StatefulWidget {
|
|
var details;
|
|
|
|
UnloadingCompleteScreen({
|
|
this.details,
|
|
});
|
|
|
|
@override
|
|
State<UnloadingCompleteScreen> createState() => _UnloadingCompleteScreenState();
|
|
}
|
|
|
|
class _UnloadingCompleteScreenState extends State<UnloadingCompleteScreen> 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 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);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_c1.dispose();
|
|
_c2.dispose();
|
|
_c3.dispose();
|
|
_c4.dispose();
|
|
_f1.dispose();
|
|
_f2.dispose();
|
|
_f3.dispose();
|
|
_f4.dispose();
|
|
_shakeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
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 Scaffold(
|
|
backgroundColor: Colors.white,
|
|
appBar: AppSettings.appBarWithNotificationIcon(widget.details.building_name, widget.details.type_of_water, widget.details.capacity, context),
|
|
body: SafeArea(
|
|
child: Align(
|
|
alignment: const Alignment(0, -0.25),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Container(
|
|
width: double.infinity,
|
|
constraints: const BoxConstraints(maxWidth: 520),
|
|
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))],
|
|
border: Border.all(color: const Color(0xFFEDEDED)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Green check
|
|
Container(
|
|
width: 28,
|
|
height: 28,
|
|
decoration: const BoxDecoration(color: Color(0xFF2FAE22), shape: BoxShape.circle),
|
|
child: const Icon(Icons.check, color: Colors.white, size: 18),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
Text('Unloading Complete', style: fts(20, const Color(0xFF101214), FontWeight.w700)),
|
|
const SizedBox(height: 12),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text('Enter OTP to finish delivery', style: fts(12, const Color(0xFF7E7F80), FontWeight.w500)),
|
|
const SizedBox(width: 6),
|
|
Icon(Icons.help_outline, size: 16, color: Colors.grey.shade600),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
|
|
// OTP row
|
|
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 -> go to CollectMoneyScreen
|
|
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": "stop",
|
|
"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.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => CollectMoney(details:widget.details)),
|
|
);
|
|
} else {
|
|
// OTP WRONG
|
|
/*FocusScope.of(context).unfocus();
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => UnloadingInProgressScreen(details: widget.details),
|
|
),
|
|
);*/
|
|
_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), // always black
|
|
foregroundColor: MaterialStateProperty.resolveWith((_) => Colors.white),
|
|
shape: MaterialStateProperty.all(
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
|
),
|
|
),
|
|
child: Text('Continue', style: fts(14, Colors.white, FontWeight.w600)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
),
|
|
);
|
|
}
|
|
}
|