refactor of usecases

This commit is contained in:
2026-02-23 17:18:50 +05:30
parent 56666ece30
commit 13f8003bda
37 changed files with 1563 additions and 105 deletions

View File

@@ -257,10 +257,83 @@ class _ClockInPageState extends State<ClockInPage> {
],
),
)
else
else ...<Widget>[
// Attire Photo Section
if (!isCheckedIn) ...<Widget>[
Container(
padding: const EdgeInsets.all(UiConstants.space4),
margin: const EdgeInsets.only(bottom: UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: Row(
children: <Widget>[
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: UiColors.bgSecondary,
borderRadius: UiConstants.radiusMd,
),
child: const Icon(UiIcons.camera, color: UiColors.primary),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(i18n.attire_photo_label, style: UiTypography.body2b),
Text(i18n.attire_photo_desc, style: UiTypography.body3r.textSecondary),
],
),
),
UiButton.secondary(
text: i18n.take_attire_photo,
onPressed: () {
UiSnackbar.show(
context,
message: i18n.attire_captured,
type: UiSnackbarType.success,
);
},
),
],
),
),
],
if (!isCheckedIn && (!state.isLocationVerified || state.currentLocation == null)) ...<Widget>[
Container(
width: double.infinity,
padding: const EdgeInsets.all(UiConstants.space4),
margin: const EdgeInsets.only(bottom: UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.tagError,
borderRadius: UiConstants.radiusLg,
),
child: Row(
children: [
const Icon(UiIcons.error, color: UiColors.textError, size: 20),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(
state.currentLocation == null
? i18n.location_verifying
: i18n.not_in_range(distance: '500'),
style: UiTypography.body3m.textError,
),
),
],
),
),
],
SwipeToCheckIn(
isCheckedIn: isCheckedIn,
mode: state.checkInMode,
isDisabled: !isCheckedIn && !state.isLocationVerified,
isLoading:
state.status ==
ClockInStatus.actionInProgress,
@@ -293,6 +366,7 @@ class _ClockInPageState extends State<ClockInPage> {
);
},
),
],
] else if (selectedShift != null &&
checkOutTime != null) ...<Widget>[
// Shift Completed State

View File

@@ -11,12 +11,14 @@ class SwipeToCheckIn extends StatefulWidget {
this.isLoading = false,
this.mode = 'swipe',
this.isCheckedIn = false,
this.isDisabled = false,
});
final VoidCallback? onCheckIn;
final VoidCallback? onCheckOut;
final bool isLoading;
final String mode; // 'swipe' or 'nfc'
final bool isCheckedIn;
final bool isDisabled;
@override
State<SwipeToCheckIn> createState() => _SwipeToCheckInState();
@@ -40,7 +42,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
}
void _onDragUpdate(DragUpdateDetails details, double maxWidth) {
if (_isComplete || widget.isLoading) return;
if (_isComplete || widget.isLoading || widget.isDisabled) return;
setState(() {
_dragValue = (_dragValue + details.delta.dx).clamp(
0.0,
@@ -50,7 +52,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
}
void _onDragEnd(DragEndDetails details, double maxWidth) {
if (_isComplete || widget.isLoading) return;
if (_isComplete || widget.isLoading || widget.isDisabled) return;
final double threshold = (maxWidth - _handleSize - 8) * 0.8;
if (_dragValue > threshold) {
setState(() {
@@ -81,7 +83,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
if (widget.mode == 'nfc') {
return GestureDetector(
onTap: () {
if (widget.isLoading) return;
if (widget.isLoading || widget.isDisabled) return;
// Simulate completion for NFC tap
Future.delayed(const Duration(milliseconds: 300), () {
if (widget.isCheckedIn) {
@@ -94,9 +96,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
child: Container(
height: 56,
decoration: BoxDecoration(
color: baseColor,
color: widget.isDisabled ? UiColors.bgSecondary : baseColor,
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
boxShadow: widget.isDisabled ? [] : <BoxShadow>[
BoxShadow(
color: baseColor.withValues(alpha: 0.4),
blurRadius: 25,
@@ -116,7 +118,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
? i18n.checking_out
: i18n.checking_in)
: (widget.isCheckedIn ? i18n.nfc_checkout : i18n.nfc_checkin),
style: UiTypography.body1b.white,
style: UiTypography.body1b.copyWith(
color: widget.isDisabled ? UiColors.textDisabled : UiColors.white,
),
),
],
),
@@ -137,8 +141,10 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
final Color endColor = widget.isCheckedIn
? UiColors.primary
: UiColors.success;
final Color currentColor =
Color.lerp(startColor, endColor, progress) ?? startColor;
final Color currentColor = widget.isDisabled
? UiColors.bgSecondary
: (Color.lerp(startColor, endColor, progress) ?? startColor);
return Container(
height: 56,
@@ -162,7 +168,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
widget.isCheckedIn
? i18n.swipe_checkout
: i18n.swipe_checkin,
style: UiTypography.body1b,
style: UiTypography.body1b.copyWith(
color: widget.isDisabled ? UiColors.textDisabled : UiColors.white,
),
),
),
),
@@ -170,7 +178,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
Center(
child: Text(
widget.isCheckedIn ? i18n.checkout_complete : i18n.checkin_complete,
style: UiTypography.body1b,
style: UiTypography.body1b.copyWith(
color: widget.isDisabled ? UiColors.textDisabled : UiColors.white,
),
),
),
Positioned(
@@ -198,7 +208,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
child: Center(
child: Icon(
_isComplete ? UiIcons.check : UiIcons.arrowRight,
color: startColor,
color: widget.isDisabled ? UiColors.iconDisabled : startColor,
),
),
),
@@ -211,4 +221,3 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
);
}
}

View File

@@ -19,26 +19,61 @@ class EmptyStateWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(UiConstants.space4),
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: UiColors.bgSecondary,
color: UiColors.bgSecondary.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: UiColors.border.withValues(alpha: 0.5),
style: BorderStyle.solid,
),
),
alignment: Alignment.center,
child: Column(
children: [
Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: UiColors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Icon(
UiIcons.info,
size: 20,
color: UiColors.mutedForeground.withValues(alpha: 0.5),
),
),
const SizedBox(height: UiConstants.space3),
Text(
message,
style: UiTypography.body2r.copyWith(color: UiColors.mutedForeground),
style: UiTypography.body2m.copyWith(color: UiColors.mutedForeground),
textAlign: TextAlign.center,
),
if (actionLink != null)
GestureDetector(
onTap: onAction,
child: Padding(
padding: const EdgeInsets.only(top: UiConstants.space2),
child: Text(
actionLink!,
style: UiTypography.body2m.copyWith(color: UiColors.primary),
padding: const EdgeInsets.only(top: UiConstants.space3),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space2,
),
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1),
borderRadius: UiConstants.radiusFull,
),
child: Text(
actionLink!,
style: UiTypography.body3m.copyWith(color: UiColors.primary),
),
),
),
),

View File

@@ -125,7 +125,7 @@ class _ShiftCardState extends State<ShiftCard> {
),
Text.rich(
TextSpan(
text: '\$${widget.shift.hourlyRate}',
text: '\$${widget.shift.hourlyRate % 1 == 0 ? widget.shift.hourlyRate.toInt() : widget.shift.hourlyRate.toStringAsFixed(2)}',
style: UiTypography.body1b.textPrimary,
children: [
TextSpan(text: '/h', style: UiTypography.body3r),
@@ -247,7 +247,7 @@ class _ShiftCardState extends State<ShiftCard> {
),
Text.rich(
TextSpan(
text: '\$${widget.shift.hourlyRate}',
text: '\$${widget.shift.hourlyRate % 1 == 0 ? widget.shift.hourlyRate.toInt() : widget.shift.hourlyRate.toStringAsFixed(2)}',
style: UiTypography.headline3m.textPrimary,
children: [
TextSpan(text: '/h', style: UiTypography.body1r),

View File

@@ -7,6 +7,7 @@ import 'domain/usecases/get_payment_history_usecase.dart';
import 'data/repositories/payments_repository_impl.dart';
import 'presentation/blocs/payments/payments_bloc.dart';
import 'presentation/pages/payments_page.dart';
import 'presentation/pages/early_pay_page.dart';
class StaffPaymentsModule extends Module {
@override
@@ -28,5 +29,9 @@ class StaffPaymentsModule extends Module {
StaffPaths.childRoute(StaffPaths.payments, StaffPaths.payments),
child: (BuildContext context) => const PaymentsPage(),
);
r.child(
'/early-pay',
child: (BuildContext context) => const EarlyPayPage(),
);
}
}

View File

@@ -0,0 +1,110 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:core_localization/core_localization.dart';
class EarlyPayPage extends StatelessWidget {
const EarlyPayPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.t.staff_payments.early_pay.title),
elevation: 0,
backgroundColor: UiColors.white,
foregroundColor: UiColors.primary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.05),
borderRadius: UiConstants.radius2xl,
border: Border.all(color: UiColors.primary.withValues(alpha: 0.1)),
),
child: Column(
children: [
Text(
context.t.staff_payments.early_pay.available_label,
style: UiTypography.body2m.textSecondary,
),
const SizedBox(height: 8),
Text(
'\$340.00',
style: UiTypography.secondaryDisplay1b.primary,
),
],
),
),
const SizedBox(height: 32),
Text(
context.t.staff_payments.early_pay.select_amount,
style: UiTypography.headline4m.textPrimary,
),
const SizedBox(height: 16),
UiTextField(
hintText: context.t.staff_payments.early_pay.hint_amount,
keyboardType: TextInputType.number,
prefixIcon: UiIcons.chart, // Currency icon if available
),
const SizedBox(height: 32),
Text(
context.t.staff_payments.early_pay.deposit_to,
style: UiTypography.body2b.textPrimary,
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.separatorPrimary),
),
child: Row(
children: [
const Icon(UiIcons.bank, size: 24, color: UiColors.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Chase Bank', style: UiTypography.body2b.textPrimary),
Text('Ending in 4321', style: UiTypography.footnote2r.textSecondary),
],
),
),
const Icon(UiIcons.chevronRight, size: 18, color: UiColors.iconSecondary),
],
),
),
const SizedBox(height: 40),
UiButton.primary(
text: context.t.staff_payments.early_pay.confirm_button,
fullWidth: true,
onPressed: () {
UiSnackbar.show(
context,
message: context.t.staff_payments.early_pay.success_message,
type: UiSnackbarType.success,
);
Navigator.pop(context);
},
),
const SizedBox(height: 16),
Center(
child: Text(
context.t.staff_payments.early_pay.fee_notice,
style: UiTypography.footnote2r.textSecondary,
textAlign: TextAlign.center,
),
),
],
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:design_system/design_system.dart';
import 'package:krow_core/core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
@@ -178,7 +179,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
PendingPayCard(
amount: state.summary.pendingEarnings,
onCashOut: () {
Modular.to.pushNamed('/early-pay');
Modular.to.pushNamed('${StaffPaths.payments}early-pay');
},
),
const SizedBox(height: UiConstants.space6),

View File

@@ -120,8 +120,17 @@ class EarningsGraph extends StatelessWidget {
}
List<FlSpot> _generateSpots(List<StaffPayment> data) {
if (data.isEmpty) return [];
// If only one data point, add a dummy point at the start to create a horizontal line
if (data.length == 1) {
return [
FlSpot(0, data[0].amount),
FlSpot(1, data[0].amount),
];
}
// Generate spots based on index in the list for simplicity in this demo
// Real implementation would map to actual dates on X-axis
return List<FlSpot>.generate(data.length, (int index) {
return FlSpot(index.toDouble(), data[index].amount);
});

View File

@@ -60,6 +60,15 @@ class PendingPayCard extends StatelessWidget {
),
],
),
UiButton.secondary(
text: 'Early Pay',
onPressed: onCashOut,
size: UiButtonSize.small,
style: OutlinedButton.styleFrom(
backgroundColor: UiColors.white,
foregroundColor: UiColors.primary,
),
),
],
),
);

View File

@@ -101,11 +101,17 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
);
} else if (state is ShiftDetailsError) {
if (_isApplying) {
UiSnackbar.show(
context,
message: translateErrorKey(state.message),
type: UiSnackbarType.error,
);
final String errorMessage = state.message.toUpperCase();
if (errorMessage.contains('ELIGIBILITY') ||
errorMessage.contains('COMPLIANCE')) {
_showEligibilityErrorDialog(context);
} else {
UiSnackbar.show(
context,
message: translateErrorKey(state.message),
type: UiSnackbarType.error,
);
}
}
_isApplying = false;
}
@@ -300,4 +306,38 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
Navigator.of(context, rootNavigator: true).pop();
_actionDialogOpen = false;
}
void _showEligibilityErrorDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext ctx) => AlertDialog(
backgroundColor: UiColors.bgPopup,
shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg),
title: Row(
children: [
const Icon(UiIcons.warning, color: UiColors.error),
const SizedBox(width: UiConstants.space2),
Expanded(child: Text("Eligibility Requirements")),
],
),
content: Text(
"You are missing required certifications or documents to claim this shift. Please upload them to continue.",
style: UiTypography.body2r.textSecondary,
),
actions: [
UiButton.secondary(
text: "Cancel",
onPressed: () => Navigator.of(ctx).pop(),
),
UiButton.primary(
text: "Go to Certificates",
onPressed: () {
Navigator.of(ctx).pop();
Modular.to.pushNamed(StaffPaths.certificates);
},
),
],
),
);
}
}

View File

@@ -27,6 +27,8 @@ class MyShiftCard extends StatefulWidget {
}
class _MyShiftCardState extends State<MyShiftCard> {
bool _isSubmitted = false;
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
@@ -477,6 +479,37 @@ class _MyShiftCardState extends State<MyShiftCard> {
),
],
),
if (status == 'completed') ...[
const SizedBox(height: UiConstants.space4),
const Divider(),
const SizedBox(height: UiConstants.space2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_isSubmitted ? 'SUBMITTED' : 'READY TO SUBMIT',
style: UiTypography.footnote2b.copyWith(
color: _isSubmitted ? UiColors.textSuccess : UiColors.textSecondary,
),
),
if (!_isSubmitted)
UiButton.secondary(
text: 'Submit for Approval',
size: UiButtonSize.small,
onPressed: () {
setState(() => _isSubmitted = true);
UiSnackbar.show(
context,
message: 'Timesheet submitted for client approval',
type: UiSnackbarType.success,
);
},
)
else
const Icon(UiIcons.success, color: UiColors.iconSuccess, size: 20),
],
),
],
],
),
),

View File

@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../blocs/shifts/shifts_bloc.dart';
import '../my_shift_card.dart';
import '../shared/empty_state_view.dart';
import 'package:geolocator/geolocator.dart';
class FindShiftsTab extends StatefulWidget {
final List<Shift> availableJobs;
@@ -20,6 +21,109 @@ class FindShiftsTab extends StatefulWidget {
class _FindShiftsTabState extends State<FindShiftsTab> {
String _searchQuery = '';
String _jobType = 'all';
double? _maxDistance; // miles
Position? _currentPosition;
@override
void initState() {
super.initState();
_initLocation();
}
Future<void> _initLocation() async {
try {
final LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.always ||
permission == LocationPermission.whileInUse) {
final Position pos = await Geolocator.getCurrentPosition();
if (mounted) {
setState(() => _currentPosition = pos);
}
}
} catch (_) {}
}
double _calculateDistance(double lat, double lng) {
if (_currentPosition == null) return -1;
final double distMeters = Geolocator.distanceBetween(
_currentPosition!.latitude,
_currentPosition!.longitude,
lat,
lng,
);
return distMeters / 1609.34; // meters to miles
}
void _showDistanceFilter() {
showModalBottomSheet<void>(
context: context,
backgroundColor: UiColors.bgPopup,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setModalState) {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
context.t.staff_shifts.find_shifts.radius_filter_title,
style: UiTypography.headline4m.textPrimary,
),
const SizedBox(height: 16),
Text(
_maxDistance == null
? context.t.staff_shifts.find_shifts.unlimited_distance
: context.t.staff_shifts.find_shifts.within_miles(
miles: _maxDistance!.round().toString(),
),
style: UiTypography.body2m.textSecondary,
),
Slider(
value: _maxDistance ?? 100,
min: 5,
max: 100,
divisions: 19,
activeColor: UiColors.primary,
onChanged: (double val) {
setModalState(() => _maxDistance = val);
setState(() => _maxDistance = val);
},
),
const SizedBox(height: 24),
Row(
children: <Widget>[
Expanded(
child: UiButton.secondary(
text: context.t.staff_shifts.find_shifts.clear,
onPressed: () {
setModalState(() => _maxDistance = null);
setState(() => _maxDistance = null);
Navigator.pop(context);
},
),
),
const SizedBox(width: 12),
Expanded(
child: UiButton.primary(
text: context.t.staff_shifts.find_shifts.apply,
onPressed: () => Navigator.pop(context),
),
),
],
),
],
),
);
},
);
},
);
}
bool _isRecurring(Shift shift) =>
(shift.orderType ?? '').toUpperCase() == 'RECURRING';
@@ -178,6 +282,11 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
if (!matchesSearch) return false;
if (_maxDistance != null && s.latitude != null && s.longitude != null) {
final double dist = _calculateDistance(s.latitude!, s.longitude!);
if (dist > _maxDistance!) return false;
}
if (_jobType == 'all') return true;
if (_jobType == 'one-day') {
if (_isRecurring(s) || _isPermanent(s)) return false;
@@ -248,20 +357,31 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
),
),
const SizedBox(width: UiConstants.space2),
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
GestureDetector(
onTap: _showDistanceFilter,
child: Container(
height: 48,
width: 48,
decoration: BoxDecoration(
color: _maxDistance != null
? UiColors.primary.withValues(alpha: 0.1)
: UiColors.white,
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
border: Border.all(
color: _maxDistance != null
? UiColors.primary
: UiColors.border,
),
),
child: Icon(
UiIcons.filter,
size: 18,
color: _maxDistance != null
? UiColors.primary
: UiColors.textSecondary,
),
border: Border.all(color: UiColors.border),
),
child: const Icon(
UiIcons.filter,
size: 18,
color: UiColors.textSecondary,
),
),
],