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

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,
),
);
}
}