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,
tomorrowShifts: result.tomorrow,
recommendedShifts: result.recommended,
// Mock profile status for now, ideally fetched from a user repository
isProfileComplete: false,
),
);
} 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> tomorrowShifts;
final List<Shift> recommendedShifts;
final bool autoMatchEnabled;
final bool isProfileComplete;
final String? errorMessage;
const HomeState({
@@ -14,6 +16,8 @@ class HomeState extends Equatable {
this.todayShifts = const [],
this.tomorrowShifts = const [],
this.recommendedShifts = const [],
this.autoMatchEnabled = false,
this.isProfileComplete = false,
this.errorMessage,
});
@@ -24,6 +28,8 @@ class HomeState extends Equatable {
List<Shift>? todayShifts,
List<Shift>? tomorrowShifts,
List<Shift>? recommendedShifts,
bool? autoMatchEnabled,
bool? isProfileComplete,
String? errorMessage,
}) {
return HomeState(
@@ -31,16 +37,20 @@ class HomeState extends Equatable {
todayShifts: todayShifts ?? this.todayShifts,
tomorrowShifts: tomorrowShifts ?? this.tomorrowShifts,
recommendedShifts: recommendedShifts ?? this.recommendedShifts,
autoMatchEnabled: autoMatchEnabled ?? this.autoMatchEnabled,
isProfileComplete: isProfileComplete ?? this.isProfileComplete,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
status,
todayShifts,
tomorrowShifts,
recommendedShifts,
errorMessage,
];
status,
todayShifts,
tomorrowShifts,
recommendedShifts,
autoMatchEnabled,
isProfileComplete,
errorMessage,
];
}

View File

@@ -1,30 +1,28 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:core_localization/core_localization.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/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/worker/auto_match_toggle.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/more_ways_widget.dart';
import 'package:staff_home/src/domain/models/shift.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
import 'package:staff_home/src/theme.dart';
class WorkerHomePage extends StatefulWidget {
class WorkerHomePage extends StatelessWidget {
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
Widget build(BuildContext context) {
final i18n = t.staff.home;
@@ -32,7 +30,7 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
final quickI18n = i18n.quick_actions;
final sectionsI18n = i18n.sections;
final emptyI18n = i18n.empty_states;
final recI18n = i18n.recommended_card;
return BlocProvider(
create: (_) => Modular.get<HomeCubit>()..loadShifts(),
child: Scaffold(
@@ -43,37 +41,50 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const HomeHeader(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
if (!_isProfileComplete)
_buildPlaceholderBanner(
bannersI18n.complete_profile_title,
bannersI18n.complete_profile_subtitle,
Colors.blue[50]!,
Colors.blue,
onTap: () {
Modular.to.pushWorkerProfile();
},
),
BlocBuilder<HomeCubit, HomeState>(
buildWhen: (previous, current) =>
previous.isProfileComplete !=
current.isProfileComplete,
builder: (context, state) {
if (state.isProfileComplete) return const SizedBox();
return PlaceholderBanner(
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),
_buildPlaceholderBanner(
bannersI18n.availability_title,
bannersI18n.availability_subtitle,
Colors.orange[50]!,
Colors.orange,
PlaceholderBanner(
title: bannersI18n.availability_title,
subtitle: bannersI18n.availability_subtitle,
bg: Colors.orange[50]!,
accent: Colors.orange,
onTap: () => Modular.to.pushAvailability(),
),
const SizedBox(height: 20),
// Auto Match Toggle
AutoMatchToggle(
enabled: _autoMatchEnabled,
onToggle: (val) =>
setState(() => _autoMatchEnabled = val),
BlocBuilder<HomeCubit, HomeState>(
buildWhen: (previous, current) =>
previous.autoMatchEnabled != current.autoMatchEnabled,
builder: (context, state) {
return AutoMatchToggle(
enabled: state.autoMatchEnabled,
onToggle: (val) => context
.read<HomeCubit>()
.toggleAutoMatch(val),
);
},
),
const SizedBox(height: 20),
@@ -82,35 +93,31 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: _buildQuickAction(
context,
LucideIcons.search,
quickI18n.find_shifts,
() => Modular.to.pushShifts(),
child: QuickActionItem(
icon: LucideIcons.search,
label: quickI18n.find_shifts,
onTap: () => Modular.to.pushShifts(),
),
),
Expanded(
child: _buildQuickAction(
context,
LucideIcons.calendar,
quickI18n.availability,
() => Modular.to.pushAvailability(),
child: QuickActionItem(
icon: LucideIcons.calendar,
label: quickI18n.availability,
onTap: () => Modular.to.pushAvailability(),
),
),
Expanded(
child: _buildQuickAction(
context,
LucideIcons.messageSquare,
quickI18n.messages,
() => Modular.to.pushMessages(),
child: QuickActionItem(
icon: LucideIcons.messageSquare,
label: quickI18n.messages,
onTap: () => Modular.to.pushMessages(),
),
),
Expanded(
child: _buildQuickAction(
context,
LucideIcons.dollarSign,
quickI18n.earnings,
() => Modular.to.pushPayments(),
child: QuickActionItem(
icon: LucideIcons.dollarSign,
label: quickI18n.earnings,
onTap: () => Modular.to.pushPayments(),
),
),
],
@@ -123,9 +130,9 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
final shifts = state.todayShifts;
return Column(
children: [
_buildSectionHeader(
sectionsI18n.todays_shift,
shifts.isNotEmpty
SectionHeader(
title: sectionsI18n.todays_shift,
action: shifts.isNotEmpty
? sectionsI18n.scheduled_count(
count: shifts.length,
)
@@ -140,10 +147,11 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
),
)
else if (shifts.isEmpty)
_buildEmptyState(
emptyI18n.no_shifts_today,
emptyI18n.find_shifts_cta,
() => Modular.to.pushShifts(tab: 'find'),
EmptyStateWidget(
message: emptyI18n.no_shifts_today,
actionLink: emptyI18n.find_shifts_cta,
onAction: () =>
Modular.to.pushShifts(tab: 'find'),
)
else
Column(
@@ -168,11 +176,12 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
final shifts = state.tomorrowShifts;
return Column(
children: [
_buildSectionHeader(sectionsI18n.tomorrow, null),
SectionHeader(
title: sectionsI18n.tomorrow,
),
if (shifts.isEmpty)
_buildEmptyState(
emptyI18n.no_shifts_tomorrow,
null,
EmptyStateWidget(
message: emptyI18n.no_shifts_tomorrow,
)
else
Column(
@@ -192,21 +201,20 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
const SizedBox(height: 24),
// Pending Payment Card
_buildPendingPaymentCard(),
const PendingPaymentCard(),
const SizedBox(height: 24),
// Recommended Shifts
_buildSectionHeader(
sectionsI18n.recommended_for_you,
sectionsI18n.view_all,
SectionHeader(
title: sectionsI18n.recommended_for_you,
action: sectionsI18n.view_all,
onAction: () => Modular.to.pushShifts(tab: 'find'),
),
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
if (state.recommendedShifts.isEmpty) {
return _buildEmptyState(
emptyI18n.no_recommended_shifts,
null,
return EmptyStateWidget(
message: emptyI18n.no_recommended_shifts,
);
}
return SizedBox(
@@ -217,8 +225,8 @@ class _WorkerHomePageState extends State<WorkerHomePage> {
clipBehavior: Clip.none,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 12),
child: _buildRecommendedCard(
state.recommendedShifts[index],
child: RecommendedShiftCard(
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,
),
),
),
],
),
);
}
}