comprehensive cases
This commit is contained in:
@@ -255,6 +255,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
|
||||
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||
shiftResult = await _dataConnect
|
||||
.createShift(title: shiftTitle, orderId: orderId)
|
||||
.description(_orderNameController.text)
|
||||
.date(orderTimestamp)
|
||||
.location(selectedHub.hubName)
|
||||
.locationAddress(selectedHub.address)
|
||||
|
||||
@@ -46,6 +46,7 @@ class _OneTimeOrderEventNameInputState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiTextField(
|
||||
semanticsIdentifier: 'order_name_input',
|
||||
label: widget.label,
|
||||
controller: _controller,
|
||||
onChanged: widget.onChanged,
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
import 'package:smart_auth/smart_auth.dart';
|
||||
import '../../../blocs/auth_event.dart';
|
||||
import '../../../blocs/auth_bloc.dart';
|
||||
|
||||
@@ -29,144 +31,98 @@ class OtpInputField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OtpInputFieldState extends State<OtpInputField> {
|
||||
final List<TextEditingController> _controllers = List<TextEditingController>.generate(
|
||||
6,
|
||||
(int _) => TextEditingController(),
|
||||
);
|
||||
final List<FocusNode> _focusNodes = List<FocusNode>.generate(6, (int _) => FocusNode());
|
||||
|
||||
/// Hidden field for E2E: Maestro inputText sends full OTP in one call;
|
||||
/// the 6 visible boxes have maxLength:1 and would truncate.
|
||||
late final FocusNode _hiddenFocusNode;
|
||||
late final TextEditingController _controller;
|
||||
late final SmartAuth _smartAuth;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hiddenFocusNode = FocusNode();
|
||||
_controller = TextEditingController();
|
||||
_smartAuth = SmartAuth();
|
||||
_listenForSmsCode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hiddenFocusNode.dispose();
|
||||
for (final TextEditingController controller in _controllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final FocusNode node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Distributes full OTP from hidden field to the 6 visible boxes and notifies Bloc.
|
||||
void _syncFromHidden(BuildContext context, String value) {
|
||||
final String raw = value.replaceAll(RegExp(r'\D'), '');
|
||||
final String digits = raw.length > 6 ? raw.substring(0, 6) : raw;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
final String digit = i < digits.length ? digits[i] : '';
|
||||
if (_controllers[i].text != digit) {
|
||||
_controllers[i].text = digit;
|
||||
}
|
||||
}
|
||||
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(digits));
|
||||
if (digits.length == 6) {
|
||||
widget.onCompleted(digits);
|
||||
Future<void> _listenForSmsCode() async {
|
||||
final res = await _smartAuth.getSmsCode();
|
||||
if (res.code != null && mounted) {
|
||||
_controller.text = res.code!;
|
||||
_onChanged(_controller.text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper getter to compute the current OTP code from all controllers.
|
||||
String get _otpCode => _controllers.map((TextEditingController c) => c.text).join();
|
||||
|
||||
/// Handles changes to the OTP input fields.
|
||||
void _onChanged({
|
||||
required BuildContext context,
|
||||
required int index,
|
||||
required String value,
|
||||
}) {
|
||||
if (value.length == 1 && index < 5) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
}
|
||||
|
||||
void _onChanged(String value) {
|
||||
// Notify the Bloc of the change
|
||||
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(_otpCode));
|
||||
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(value));
|
||||
|
||||
if (_otpCode.length == 6) {
|
||||
widget.onCompleted(_otpCode);
|
||||
if (value.length == 6) {
|
||||
widget.onCompleted(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double boxWidth = 45;
|
||||
const double boxHeight = 56;
|
||||
final defaultPinTheme = PinTheme(
|
||||
width: 45,
|
||||
height: 56,
|
||||
textStyle: UiTypography.headline3m,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: Border.all(
|
||||
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.border,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final focusedPinTheme = defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(
|
||||
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final submittedPinTheme = defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(
|
||||
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final errorPinTheme = defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(color: UiColors.textError, width: 2),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 300,
|
||||
height: boxHeight,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List<Widget>.generate(6, (int index) {
|
||||
final TextField field = TextField(
|
||||
controller: _controllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 1,
|
||||
style: UiTypography.headline3m,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: widget.error.isNotEmpty
|
||||
? UiColors.textError
|
||||
: (_controllers[index].text.isNotEmpty
|
||||
? UiColors.primary
|
||||
: UiColors.border),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
borderSide: BorderSide(
|
||||
color: widget.error.isNotEmpty
|
||||
? UiColors.textError
|
||||
: (_controllers[index].text.isNotEmpty
|
||||
? UiColors.primary
|
||||
: UiColors.border),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (String value) =>
|
||||
_onChanged(context: context, index: index, value: value),
|
||||
);
|
||||
return SizedBox(width: boxWidth, height: boxHeight, child: field);
|
||||
}),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Semantics(
|
||||
identifier: 'staff_otp_input',
|
||||
container: false,
|
||||
child: Opacity(
|
||||
opacity: 0.01,
|
||||
child: TextField(
|
||||
focusNode: _hiddenFocusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(6),
|
||||
],
|
||||
maxLength: 6,
|
||||
onChanged: (String value) =>
|
||||
_syncFromHidden(context, value),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Semantics(
|
||||
identifier: 'staff_otp_input',
|
||||
child: Pinput(
|
||||
length: 6,
|
||||
controller: _controller,
|
||||
defaultPinTheme: defaultPinTheme,
|
||||
focusedPinTheme: focusedPinTheme,
|
||||
submittedPinTheme: submittedPinTheme,
|
||||
errorPinTheme: errorPinTheme,
|
||||
followingPinTheme: defaultPinTheme,
|
||||
forceErrorState: widget.error.isNotEmpty,
|
||||
onChanged: _onChanged,
|
||||
onCompleted: widget.onCompleted,
|
||||
autofillHints: const <String>[AutofillHints.oneTimeCode],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.error.isNotEmpty)
|
||||
|
||||
@@ -18,6 +18,8 @@ dependencies:
|
||||
firebase_auth: ^6.1.2
|
||||
firebase_data_connect: ^0.2.2+1
|
||||
http: ^1.2.0
|
||||
pinput: ^5.0.0
|
||||
smart_auth: ^1.1.0
|
||||
|
||||
# Architecture Packages
|
||||
krow_domain:
|
||||
|
||||
@@ -277,85 +277,89 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}) {
|
||||
final isActive = _activeTab == type;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: !enabled
|
||||
? null
|
||||
: () {
|
||||
setState(() => _activeTab = type);
|
||||
if (type == ShiftTabType.history) {
|
||||
_bloc.add(LoadHistoryShiftsEvent());
|
||||
}
|
||||
if (type == ShiftTabType.find) {
|
||||
_bloc.add(LoadAvailableShiftsEvent());
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space2,
|
||||
horizontal: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? UiColors.white
|
||||
: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: !enabled
|
||||
? UiColors.white.withValues(alpha: 0.5)
|
||||
: isActive
|
||||
? UiColors.primary
|
||||
: UiColors.white,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style:
|
||||
(isActive
|
||||
? UiTypography.body3m.copyWith(
|
||||
color: UiColors.primary,
|
||||
)
|
||||
: UiTypography.body3m.white)
|
||||
.copyWith(
|
||||
color: !enabled
|
||||
? UiColors.white.withValues(alpha: 0.5)
|
||||
: null,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Semantics(
|
||||
identifier: 'shift_tab_${type.name}',
|
||||
label: label,
|
||||
child: GestureDetector(
|
||||
onTap: !enabled
|
||||
? null
|
||||
: () {
|
||||
setState(() => _activeTab = type);
|
||||
if (type == ShiftTabType.history) {
|
||||
_bloc.add(LoadHistoryShiftsEvent());
|
||||
}
|
||||
if (type == ShiftTabType.find) {
|
||||
_bloc.add(LoadAvailableShiftsEvent());
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space2,
|
||||
horizontal: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? UiColors.white
|
||||
: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: !enabled
|
||||
? UiColors.white.withValues(alpha: 0.5)
|
||||
: isActive
|
||||
? UiColors.primary
|
||||
: UiColors.white,
|
||||
),
|
||||
),
|
||||
if (showCount) ...[
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space1,
|
||||
vertical: 2,
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style:
|
||||
(isActive
|
||||
? UiTypography.body3m.copyWith(
|
||||
color: UiColors.primary,
|
||||
)
|
||||
: UiTypography.body3m.white)
|
||||
.copyWith(
|
||||
color: !enabled
|
||||
? UiColors.white.withValues(alpha: 0.5)
|
||||
: null,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? UiColors.primary.withValues(alpha: 0.1)
|
||||
: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$count",
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: isActive ? UiColors.primary : UiColors.white,
|
||||
),
|
||||
if (showCount) ...[
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space1,
|
||||
vertical: 2,
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? UiColors.primary.withValues(alpha: 0.1)
|
||||
: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$count",
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: isActive ? UiColors.primary : UiColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -246,8 +246,10 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo
|
||||
Container(
|
||||
width: 44,
|
||||
Semantics(
|
||||
identifier: 'shft_card_logo_placeholder',
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -282,6 +284,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
size: UiConstants.iconMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
String _jobType = 'all';
|
||||
double? _maxDistance; // miles
|
||||
Position? _currentPosition;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -40,6 +41,12 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
_initLocation();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initLocation() async {
|
||||
try {
|
||||
final LocationPermission permission = await Geolocator.checkPermission();
|
||||
@@ -289,6 +296,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
final matchesSearch =
|
||||
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(s.description ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
@@ -371,17 +379,21 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (v) =>
|
||||
setState(() => _searchQuery = v),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.search_hint,
|
||||
hintStyle: UiTypography.body2r.textPlaceholder,
|
||||
child: Semantics(
|
||||
identifier: 'find_shifts_search_input',
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (v) =>
|
||||
setState(() => _searchQuery = v),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.search_hint,
|
||||
hintStyle: UiTypography.body2r.textPlaceholder,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -123,26 +123,30 @@ class StaffMainBottomBar extends StatelessWidget {
|
||||
|
||||
final bool isSelected = currentIndex == item.index;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(item.index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
item.icon,
|
||||
color: isSelected ? activeColor : inactiveColor,
|
||||
size: UiConstants.iconLg,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
item.label,
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
child: Semantics(
|
||||
identifier: 'nav_${item.tabKey}',
|
||||
label: item.label,
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(item.index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
item.icon,
|
||||
color: isSelected ? activeColor : inactiveColor,
|
||||
size: UiConstants.iconLg,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
item.label,
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: isSelected ? activeColor : inactiveColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user