refactor(staff_home): refactor worker home page

- Extracted widgets to smaller components in presentation/widgets/home_page
- Converted WorkerHomePage to StatelessWidget
- Moved local state (autoMatchEnabled) to HomeCubit
- Improved code organization and readability
This commit is contained in:
Achintha Isuru
2026-01-24 12:34:56 -05:00
parent 7cea81e99e
commit 67749ba1a8
10 changed files with 791 additions and 677 deletions

View File

@@ -25,6 +25,8 @@ class HomeCubit extends Cubit<HomeState> {
todayShifts: result.today, todayShifts: result.today,
tomorrowShifts: result.tomorrow, tomorrowShifts: result.tomorrow,
recommendedShifts: result.recommended, recommendedShifts: result.recommended,
// Mock profile status for now, ideally fetched from a user repository
isProfileComplete: false,
), ),
); );
} catch (e) { } catch (e) {
@@ -33,4 +35,8 @@ class HomeCubit extends Cubit<HomeState> {
); );
} }
} }
void toggleAutoMatch(bool enabled) {
emit(state.copyWith(autoMatchEnabled: enabled));
}
} }

View File

@@ -7,6 +7,8 @@ class HomeState extends Equatable {
final List<Shift> todayShifts; final List<Shift> todayShifts;
final List<Shift> tomorrowShifts; final List<Shift> tomorrowShifts;
final List<Shift> recommendedShifts; final List<Shift> recommendedShifts;
final bool autoMatchEnabled;
final bool isProfileComplete;
final String? errorMessage; final String? errorMessage;
const HomeState({ const HomeState({
@@ -14,6 +16,8 @@ class HomeState extends Equatable {
this.todayShifts = const [], this.todayShifts = const [],
this.tomorrowShifts = const [], this.tomorrowShifts = const [],
this.recommendedShifts = const [], this.recommendedShifts = const [],
this.autoMatchEnabled = false,
this.isProfileComplete = false,
this.errorMessage, this.errorMessage,
}); });
@@ -24,6 +28,8 @@ class HomeState extends Equatable {
List<Shift>? todayShifts, List<Shift>? todayShifts,
List<Shift>? tomorrowShifts, List<Shift>? tomorrowShifts,
List<Shift>? recommendedShifts, List<Shift>? recommendedShifts,
bool? autoMatchEnabled,
bool? isProfileComplete,
String? errorMessage, String? errorMessage,
}) { }) {
return HomeState( return HomeState(
@@ -31,16 +37,20 @@ class HomeState extends Equatable {
todayShifts: todayShifts ?? this.todayShifts, todayShifts: todayShifts ?? this.todayShifts,
tomorrowShifts: tomorrowShifts ?? this.tomorrowShifts, tomorrowShifts: tomorrowShifts ?? this.tomorrowShifts,
recommendedShifts: recommendedShifts ?? this.recommendedShifts, recommendedShifts: recommendedShifts ?? this.recommendedShifts,
autoMatchEnabled: autoMatchEnabled ?? this.autoMatchEnabled,
isProfileComplete: isProfileComplete ?? this.isProfileComplete,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
); );
} }
@override @override
List<Object?> get props => [ List<Object?> get props => [
status, status,
todayShifts, todayShifts,
tomorrowShifts, tomorrowShifts,
recommendedShifts, recommendedShifts,
errorMessage, autoMatchEnabled,
]; isProfileComplete,
errorMessage,
];
} }

View File

@@ -1,30 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:core_localization/core_localization.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
import 'package:staff_home/src/presentation/navigation/home_navigator.dart'; import 'package:staff_home/src/presentation/navigation/home_navigator.dart';
import 'package:staff_home/src/theme.dart'; import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart';
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
import 'package:staff_home/src/presentation/widgets/home_page/pending_payment_card.dart';
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
import 'package:staff_home/src/presentation/widgets/home_page/quick_action_item.dart';
import 'package:staff_home/src/presentation/widgets/home_page/recommended_shift_card.dart';
import 'package:staff_home/src/presentation/widgets/home_page/section_header.dart';
import 'package:staff_home/src/presentation/widgets/shift_card.dart'; import 'package:staff_home/src/presentation/widgets/shift_card.dart';
import 'package:staff_home/src/presentation/widgets/worker/auto_match_toggle.dart'; import 'package:staff_home/src/presentation/widgets/worker/auto_match_toggle.dart';
import 'package:staff_home/src/presentation/widgets/worker/benefits_widget.dart'; import 'package:staff_home/src/presentation/widgets/worker/benefits_widget.dart';
import 'package:staff_home/src/presentation/widgets/worker/improve_yourself_widget.dart'; import 'package:staff_home/src/presentation/widgets/worker/improve_yourself_widget.dart';
import 'package:staff_home/src/presentation/widgets/worker/more_ways_widget.dart'; import 'package:staff_home/src/presentation/widgets/worker/more_ways_widget.dart';
import 'package:staff_home/src/domain/models/shift.dart'; import 'package:staff_home/src/theme.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
class WorkerHomePage extends StatefulWidget { class WorkerHomePage extends StatelessWidget {
const WorkerHomePage({super.key}); const WorkerHomePage({super.key});
@override
State<WorkerHomePage> createState() => _WorkerHomePageState();
}
class _WorkerHomePageState extends State<WorkerHomePage> {
bool _autoMatchEnabled = false;
final bool _isProfileComplete = false; // Added for mock profile completion
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final i18n = t.staff.home; final i18n = t.staff.home;
@@ -32,7 +30,7 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
final quickI18n = i18n.quick_actions; final quickI18n = i18n.quick_actions;
final sectionsI18n = i18n.sections; final sectionsI18n = i18n.sections;
final emptyI18n = i18n.empty_states; final emptyI18n = i18n.empty_states;
final recI18n = i18n.recommended_card;
return BlocProvider( return BlocProvider(
create: (_) => Modular.get<HomeCubit>()..loadShifts(), create: (_) => Modular.get<HomeCubit>()..loadShifts(),
child: Scaffold( child: Scaffold(
@@ -43,37 +41,50 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildHeader(), const HomeHeader(),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: Column(
children: [ children: [
if (!_isProfileComplete) BlocBuilder<HomeCubit, HomeState>(
_buildPlaceholderBanner( buildWhen: (previous, current) =>
bannersI18n.complete_profile_title, previous.isProfileComplete !=
bannersI18n.complete_profile_subtitle, current.isProfileComplete,
Colors.blue[50]!, builder: (context, state) {
Colors.blue, if (state.isProfileComplete) return const SizedBox();
onTap: () { return PlaceholderBanner(
Modular.to.pushWorkerProfile(); title: bannersI18n.complete_profile_title,
}, subtitle: bannersI18n.complete_profile_subtitle,
), bg: Colors.blue[50]!,
accent: Colors.blue,
onTap: () {
Modular.to.pushWorkerProfile();
},
);
},
),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildPlaceholderBanner( PlaceholderBanner(
bannersI18n.availability_title, title: bannersI18n.availability_title,
bannersI18n.availability_subtitle, subtitle: bannersI18n.availability_subtitle,
Colors.orange[50]!, bg: Colors.orange[50]!,
Colors.orange, accent: Colors.orange,
onTap: () => Modular.to.pushAvailability(), onTap: () => Modular.to.pushAvailability(),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Auto Match Toggle // Auto Match Toggle
AutoMatchToggle( BlocBuilder<HomeCubit, HomeState>(
enabled: _autoMatchEnabled, buildWhen: (previous, current) =>
onToggle: (val) => previous.autoMatchEnabled != current.autoMatchEnabled,
setState(() => _autoMatchEnabled = val), builder: (context, state) {
return AutoMatchToggle(
enabled: state.autoMatchEnabled,
onToggle: (val) => context
.read<HomeCubit>()
.toggleAutoMatch(val),
);
},
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -82,35 +93,31 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: _buildQuickAction( child: QuickActionItem(
context, icon: LucideIcons.search,
LucideIcons.search, label: quickI18n.find_shifts,
quickI18n.find_shifts, onTap: () => Modular.to.pushShifts(),
() => Modular.to.pushShifts(),
), ),
), ),
Expanded( Expanded(
child: _buildQuickAction( child: QuickActionItem(
context, icon: LucideIcons.calendar,
LucideIcons.calendar, label: quickI18n.availability,
quickI18n.availability, onTap: () => Modular.to.pushAvailability(),
() => Modular.to.pushAvailability(),
), ),
), ),
Expanded( Expanded(
child: _buildQuickAction( child: QuickActionItem(
context, icon: LucideIcons.messageSquare,
LucideIcons.messageSquare, label: quickI18n.messages,
quickI18n.messages, onTap: () => Modular.to.pushMessages(),
() => Modular.to.pushMessages(),
), ),
), ),
Expanded( Expanded(
child: _buildQuickAction( child: QuickActionItem(
context, icon: LucideIcons.dollarSign,
LucideIcons.dollarSign, label: quickI18n.earnings,
quickI18n.earnings, onTap: () => Modular.to.pushPayments(),
() => Modular.to.pushPayments(),
), ),
), ),
], ],
@@ -123,9 +130,9 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
final shifts = state.todayShifts; final shifts = state.todayShifts;
return Column( return Column(
children: [ children: [
_buildSectionHeader( SectionHeader(
sectionsI18n.todays_shift, title: sectionsI18n.todays_shift,
shifts.isNotEmpty action: shifts.isNotEmpty
? sectionsI18n.scheduled_count( ? sectionsI18n.scheduled_count(
count: shifts.length, count: shifts.length,
) )
@@ -140,10 +147,11 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
), ),
) )
else if (shifts.isEmpty) else if (shifts.isEmpty)
_buildEmptyState( EmptyStateWidget(
emptyI18n.no_shifts_today, message: emptyI18n.no_shifts_today,
emptyI18n.find_shifts_cta, actionLink: emptyI18n.find_shifts_cta,
() => Modular.to.pushShifts(tab: 'find'), onAction: () =>
Modular.to.pushShifts(tab: 'find'),
) )
else else
Column( Column(
@@ -168,11 +176,12 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
final shifts = state.tomorrowShifts; final shifts = state.tomorrowShifts;
return Column( return Column(
children: [ children: [
_buildSectionHeader(sectionsI18n.tomorrow, null), SectionHeader(
title: sectionsI18n.tomorrow,
),
if (shifts.isEmpty) if (shifts.isEmpty)
_buildEmptyState( EmptyStateWidget(
emptyI18n.no_shifts_tomorrow, message: emptyI18n.no_shifts_tomorrow,
null,
) )
else else
Column( Column(
@@ -192,21 +201,20 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Pending Payment Card // Pending Payment Card
_buildPendingPaymentCard(), const PendingPaymentCard(),
const SizedBox(height: 24), const SizedBox(height: 24),
// Recommended Shifts // Recommended Shifts
_buildSectionHeader( SectionHeader(
sectionsI18n.recommended_for_you, title: sectionsI18n.recommended_for_you,
sectionsI18n.view_all, action: sectionsI18n.view_all,
onAction: () => Modular.to.pushShifts(tab: 'find'), onAction: () => Modular.to.pushShifts(tab: 'find'),
), ),
BlocBuilder<HomeCubit, HomeState>( BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) { builder: (context, state) {
if (state.recommendedShifts.isEmpty) { if (state.recommendedShifts.isEmpty) {
return _buildEmptyState( return EmptyStateWidget(
emptyI18n.no_recommended_shifts, message: emptyI18n.no_recommended_shifts,
null,
); );
} }
return SizedBox( return SizedBox(
@@ -217,8 +225,8 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
clipBehavior: Clip.none, clipBehavior: Clip.none,
itemBuilder: (context, index) => Padding( itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 12), padding: const EdgeInsets.only(right: 12),
child: _buildRecommendedCard( child: RecommendedShiftCard(
state.recommendedShifts[index], shift: state.recommendedShifts[index],
), ),
), ),
), ),
@@ -244,598 +252,4 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
), ),
); );
} }
Widget _buildSectionHeader(
String title,
String? action, {
VoidCallback? onAction,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
),
if (action != null)
if (onAction != null)
GestureDetector(
onTap: onAction,
child: Row(
children: [
Text(
action,
style: const TextStyle(
color: AppColors.krowBlue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const Icon(
LucideIcons.chevronRight,
size: 16,
color: AppColors.krowBlue,
),
],
),
)
else
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppColors.krowBlue.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.krowBlue.withValues(alpha: 0.2),
),
),
child: Text(
action,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.krowBlue,
),
),
),
],
),
);
}
Widget _buildEmptyState(
String message,
String? actionLink, [
VoidCallback? onAction,
]) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF1F3F5),
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Column(
children: [
Text(
message,
style: const TextStyle(color: AppColors.krowMuted, fontSize: 14),
),
if (actionLink != null)
GestureDetector(
onTap: onAction,
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
actionLink,
style: const TextStyle(
color: AppColors.krowBlue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}
Widget _buildHeader() {
final headerI18n = t.staff.home.header;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.krowBlue.withValues(alpha: 0.2),
width: 2,
),
),
child: CircleAvatar(
backgroundColor: AppColors.krowBlue.withValues(alpha: 0.1),
child: const Text(
'K',
style: TextStyle(
color: AppColors.krowBlue,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
headerI18n.welcome_back,
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 14,
),
),
Text(
headerI18n.user_name_placeholder,
style: const TextStyle(
color: AppColors.krowCharcoal,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
Row(
children: [
GestureDetector(
onTap: () => Modular.to.pushMessages(),
child: Stack(
children: [
_buildHeaderIcon(LucideIcons.bell),
const Positioned(
top: -2,
right: -2,
child: CircleAvatar(
radius: 8,
backgroundColor: Color(0xFFF04444),
child: Text(
'2',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => Modular.to.pushWorkerProfile(),
child: _buildHeaderIcon(LucideIcons.settings),
),
],
),
],
),
);
}
Widget _buildHeaderIcon(IconData icon) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Icon(icon, color: AppColors.krowMuted, size: 20),
);
}
Widget _buildPlaceholderBanner(
String title,
String subtitle,
Color bg,
Color accent, {
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: accent.withValues(alpha: 0.3)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Icon(LucideIcons.star, color: accent, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
),
Icon(LucideIcons.chevronRight, color: accent),
],
),
),
);
}
Widget _buildQuickAction(
BuildContext context,
IconData icon,
String label,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 50,
height: 50,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Icon(icon, color: AppColors.krowBlue, size: 24),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColors.krowCharcoal,
),
),
],
),
);
}
Widget _buildPendingPaymentCard() {
final pendingI18n = t.staff.home.pending_payment;
return GestureDetector(
onTap: () => Modular.to.pushPayments(),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[50]!.withValues(alpha: 0.5), Colors.blue[50]!],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.blue[100]!.withValues(alpha: 0.5)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFFE8F0FF),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.dollarSign,
color: Color(0xFF0047FF),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
pendingI18n.title,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: AppColors.krowCharcoal,
),
overflow: TextOverflow.ellipsis,
),
Text(
pendingI18n.subtitle,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
const Row(
children: [
Text(
'\$285.00',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Color(0xFF0047FF),
),
),
SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
color: Color(0xFF94A3B8),
size: 20,
),
],
),
],
),
),
);
}
Widget _buildRecommendedCard(Shift shift) {
final recI18n = t.staff.home.recommended_card;
final duration = 8;
final totalPay = duration * shift.hourlyRate;
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
recI18n.applied_for(title: shift.title),
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
},
child: Container(
width: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.krowBorder),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.02),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
recI18n.act_now,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Color(0xFFDC2626),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFE8F0FF),
borderRadius: BorderRadius.circular(999),
),
child: Text(
recI18n.one_day,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xFF0047FF),
),
),
),
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFE8F0FF),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
LucideIcons.calendar,
color: Color(0xFF0047FF),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
shift.title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: AppColors.krowCharcoal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
'\$${totalPay.round()}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
],
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
shift.clientName,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
Text(
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr • ${duration}h',
style: const TextStyle(
fontSize: 10,
color: AppColors.krowMuted,
),
),
],
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(
LucideIcons.calendar,
size: 14,
color: AppColors.krowMuted,
),
const SizedBox(width: 4),
Text(
recI18n.today,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
const SizedBox(width: 12),
const Icon(
LucideIcons.clock,
size: 14,
color: AppColors.krowMuted,
),
const SizedBox(width: 4),
Text(
recI18n.time_range(start: shift.startTime, end: shift.endTime),
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
LucideIcons.mapPin,
size: 14,
color: AppColors.krowMuted,
),
const SizedBox(width: 4),
Expanded(
child: Text(
shift.locationAddress ?? shift.location,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
);
}
} }

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:staff_home/src/theme.dart';
class EmptyStateWidget extends StatelessWidget {
final String message;
final String? actionLink;
final VoidCallback? onAction;
const EmptyStateWidget({
super.key,
required this.message,
this.actionLink,
this.onAction,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF1F3F5),
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Column(
children: [
Text(
message,
style: const TextStyle(color: AppColors.krowMuted, fontSize: 14),
),
if (actionLink != null)
GestureDetector(
onTap: onAction,
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
actionLink!,
style: const TextStyle(
color: AppColors.krowBlue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:staff_home/src/theme.dart';
import 'package:core_localization/core_localization.dart';
import 'package:staff_home/src/presentation/navigation/home_navigator.dart';
class HomeHeader extends StatelessWidget {
const HomeHeader({super.key});
@override
Widget build(BuildContext context) {
final headerI18n = t.staff.home.header;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.krowBlue.withValues(alpha: 0.2),
width: 2,
),
),
child: CircleAvatar(
backgroundColor: AppColors.krowBlue.withValues(alpha: 0.1),
child: const Text(
'K',
style: TextStyle(
color: AppColors.krowBlue,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
headerI18n.welcome_back,
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 14,
),
),
Text(
headerI18n.user_name_placeholder,
style: const TextStyle(
color: AppColors.krowCharcoal,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
Row(
children: [
GestureDetector(
onTap: () => Modular.to.pushMessages(),
child: Stack(
children: [
_buildHeaderIcon(LucideIcons.bell),
const Positioned(
top: -2,
right: -2,
child: CircleAvatar(
radius: 8,
backgroundColor: Color(0xFFF04444),
child: Text(
'2',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => Modular.to.pushWorkerProfile(),
child: _buildHeaderIcon(LucideIcons.settings),
),
],
),
],
),
);
}
Widget _buildHeaderIcon(IconData icon) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Icon(icon, color: AppColors.krowMuted, size: 20),
);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:staff_home/src/theme.dart';
import 'package:core_localization/core_localization.dart';
import 'package:staff_home/src/presentation/navigation/home_navigator.dart';
class PendingPaymentCard extends StatelessWidget {
const PendingPaymentCard({super.key});
@override
Widget build(BuildContext context) {
final pendingI18n = t.staff.home.pending_payment;
return GestureDetector(
onTap: () => Modular.to.pushPayments(),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue[50]!.withValues(alpha: 0.5), Colors.blue[50]!],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.blue[100]!.withValues(alpha: 0.5)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFFE8F0FF),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.dollarSign,
color: Color(0xFF0047FF),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
pendingI18n.title,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: AppColors.krowCharcoal,
),
overflow: TextOverflow.ellipsis,
),
Text(
pendingI18n.subtitle,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
const Row(
children: [
Text(
'\$285.00',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Color(0xFF0047FF),
),
),
SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
color: Color(0xFF94A3B8),
size: 20,
),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:staff_home/src/theme.dart';
class PlaceholderBanner extends StatelessWidget {
final String title;
final String subtitle;
final Color bg;
final Color accent;
final VoidCallback? onTap;
const PlaceholderBanner({
super.key,
required this.title,
required this.subtitle,
required this.bg,
required this.accent,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: accent.withValues(alpha: 0.3)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Icon(LucideIcons.star, color: accent, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
),
Icon(LucideIcons.chevronRight, color: accent),
],
),
),
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:staff_home/src/theme.dart';
class QuickActionItem extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const QuickActionItem({
super.key,
required this.icon,
required this.label,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 50,
height: 50,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFF1F5F9)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Icon(icon, color: AppColors.krowBlue, size: 24),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColors.krowCharcoal,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,214 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:staff_home/src/domain/models/shift.dart';
import 'package:staff_home/src/theme.dart';
import 'package:core_localization/core_localization.dart';
class RecommendedShiftCard extends StatelessWidget {
final Shift shift;
const RecommendedShiftCard({super.key, required this.shift});
@override
Widget build(BuildContext context) {
final recI18n = t.staff.home.recommended_card;
final duration = 8;
final totalPay = duration * shift.hourlyRate;
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
recI18n.applied_for(title: shift.title),
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
},
child: Container(
width: 300,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.krowBorder),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.02),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
recI18n.act_now,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Color(0xFFDC2626),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFE8F0FF),
borderRadius: BorderRadius.circular(999),
),
child: Text(
recI18n.one_day,
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xFF0047FF),
),
),
),
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFFE8F0FF),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
LucideIcons.calendar,
color: Color(0xFF0047FF),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
shift.title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: AppColors.krowCharcoal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text(
'\$${totalPay.round()}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
],
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
shift.clientName,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
Text(
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr • ${duration}h',
style: const TextStyle(
fontSize: 10,
color: AppColors.krowMuted,
),
),
],
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(
LucideIcons.calendar,
size: 14,
color: AppColors.krowMuted,
),
const SizedBox(width: 4),
Text(
recI18n.today,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
const SizedBox(width: 12),
const Icon(
LucideIcons.clock,
size: 14,
color: AppColors.krowMuted,
),
const SizedBox(width: 4),
Text(
recI18n.time_range(
start: shift.startTime,
end: shift.endTime,
),
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
LucideIcons.mapPin,
size: 14,
color: AppColors.krowMuted,
),
const SizedBox(width: 4),
Expanded(
child: Text(
shift.locationAddress ?? shift.location,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:staff_home/src/theme.dart';
class SectionHeader extends StatelessWidget {
final String title;
final String? action;
final VoidCallback? onAction;
const SectionHeader({
super.key,
required this.title,
this.action,
this.onAction,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
),
if (action != null)
if (onAction != null)
GestureDetector(
onTap: onAction,
child: Row(
children: [
Text(
action!,
style: const TextStyle(
color: AppColors.krowBlue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const Icon(
LucideIcons.chevronRight,
size: 16,
color: AppColors.krowBlue,
),
],
),
)
else
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppColors.krowBlue.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.krowBlue.withValues(alpha: 0.2),
),
),
child: Text(
action!,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.krowBlue,
),
),
),
],
),
);
}
}