feat(staff): integrate staff_home feature

- Created staff_main package with module, bloc, and pages
- Integrated staff_home into staff_main
- Updated route constants to use /worker-main
- Fixed intl version conflict
This commit is contained in:
Achintha Isuru
2026-01-24 12:11:25 -05:00
parent faa2b2b0a6
commit 13265d844e
22 changed files with 2373 additions and 5 deletions

View File

@@ -0,0 +1,7 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_print: true
prefer_single_quotes: true
always_use_package_imports: true

View File

@@ -0,0 +1,18 @@
import 'package:staff_home/src/domain/models/shift.dart';
import 'package:staff_home/src/domain/repositories/home_repository.dart';
import 'package:staff_home/src/data/services/mock_service.dart';
class HomeRepositoryImpl implements HomeRepository {
final MockService _service;
HomeRepositoryImpl(this._service);
@override
Future<List<Shift>> getTodayShifts() => _service.getTodayShifts();
@override
Future<List<Shift>> getTomorrowShifts() => _service.getTomorrowShifts();
@override
Future<List<Shift>> getRecommendedShifts() => _service.getRecommendedShifts();
}

View File

@@ -0,0 +1,78 @@
import 'package:staff_home/src/domain/models/shift.dart';
class MockService {
static final Shift _sampleShift1 = Shift(
id: '1',
title: 'Line Cook',
clientName: 'The Burger Joint',
hourlyRate: 22.50,
location: 'Downtown, NY',
locationAddress: '123 Main St, New York, NY 10001',
date: DateTime.now().toIso8601String(),
startTime: '16:00',
endTime: '22:00',
createdDate: DateTime.now()
.subtract(const Duration(hours: 2))
.toIso8601String(),
tipsAvailable: true,
mealProvided: true,
managers: [ShiftManager(name: 'John Doe', phone: '+1 555 0101')],
description: 'Help with dinner service. Must be experienced with grill.',
);
static final Shift _sampleShift2 = Shift(
id: '2',
title: 'Dishwasher',
clientName: 'Pasta Place',
hourlyRate: 18.00,
location: 'Brooklyn, NY',
locationAddress: '456 Bedford Ave, Brooklyn, NY 11211',
date: DateTime.now().add(const Duration(days: 1)).toIso8601String(),
startTime: '18:00',
endTime: '23:00',
createdDate: DateTime.now()
.subtract(const Duration(hours: 5))
.toIso8601String(),
tipsAvailable: false,
mealProvided: true,
);
static final Shift _sampleShift3 = Shift(
id: '3',
title: 'Bartender',
clientName: 'Rooftop Bar',
hourlyRate: 25.00,
location: 'Manhattan, NY',
locationAddress: '789 5th Ave, New York, NY 10022',
date: DateTime.now().add(const Duration(days: 2)).toIso8601String(),
startTime: '19:00',
endTime: '02:00',
createdDate: DateTime.now()
.subtract(const Duration(hours: 1))
.toIso8601String(),
tipsAvailable: true,
parkingAvailable: true,
description: 'High volume bar. Mixology experience required.',
);
Future<List<Shift>> getTodayShifts() async {
await Future.delayed(const Duration(milliseconds: 500));
return [_sampleShift1];
}
Future<List<Shift>> getTomorrowShifts() async {
await Future.delayed(const Duration(milliseconds: 500));
return [_sampleShift2];
}
Future<List<Shift>> getRecommendedShifts() async {
await Future.delayed(const Duration(milliseconds: 500));
return [_sampleShift3, _sampleShift1, _sampleShift2];
}
Future<void> createWorkerProfile(Map<String, dynamic> data) async {
await Future.delayed(const Duration(seconds: 1));
}
}
final mockService = MockService();

View File

@@ -0,0 +1,59 @@
class Shift {
final String id;
final String title;
final String clientName;
final String? logoUrl;
final double hourlyRate;
final String location;
final String? locationAddress;
final String date;
final String startTime;
final String endTime;
final String createdDate;
final bool? tipsAvailable;
final bool? travelTime;
final bool? mealProvided;
final bool? parkingAvailable;
final bool? gasCompensation;
final String? description;
final String? instructions;
final List<ShiftManager>? managers;
final double? latitude;
final double? longitude;
final String? status;
final int? durationDays;
Shift({
required this.id,
required this.title,
required this.clientName,
this.logoUrl,
required this.hourlyRate,
required this.location,
this.locationAddress,
required this.date,
required this.startTime,
required this.endTime,
required this.createdDate,
this.tipsAvailable,
this.travelTime,
this.mealProvided,
this.parkingAvailable,
this.gasCompensation,
this.description,
this.instructions,
this.managers,
this.latitude,
this.longitude,
this.status,
this.durationDays,
});
}
class ShiftManager {
final String name;
final String phone;
final String? avatar;
ShiftManager({required this.name, required this.phone, this.avatar});
}

View File

@@ -0,0 +1,7 @@
import 'package:staff_home/src/domain/models/shift.dart';
abstract class HomeRepository {
Future<List<Shift>> getTodayShifts();
Future<List<Shift>> getTomorrowShifts();
Future<List<Shift>> getRecommendedShifts();
}

View File

@@ -0,0 +1,31 @@
import 'package:staff_home/src/domain/models/shift.dart';
import 'package:staff_home/src/domain/repositories/home_repository.dart';
class GetHomeShifts {
final HomeRepository repository;
GetHomeShifts(this.repository);
Future<HomeShifts> call() async {
final today = await repository.getTodayShifts();
final tomorrow = await repository.getTomorrowShifts();
final recommended = await repository.getRecommendedShifts();
return HomeShifts(
today: today,
tomorrow: tomorrow,
recommended: recommended,
);
}
}
class HomeShifts {
final List<Shift> today;
final List<Shift> tomorrow;
final List<Shift> recommended;
HomeShifts({
required this.today,
required this.tomorrow,
required this.recommended,
});
}

View File

@@ -0,0 +1,36 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:staff_home/src/domain/models/shift.dart';
import 'package:staff_home/src/domain/usecases/get_home_shifts.dart';
import 'package:staff_home/src/domain/repositories/home_repository.dart';
part 'home_state.dart';
/// Simple Cubit to manage home page state (shifts + loading/error).
class HomeCubit extends Cubit<HomeState> {
final GetHomeShifts _getHomeShifts;
HomeCubit(HomeRepository repository)
: _getHomeShifts = GetHomeShifts(repository),
super(const HomeState.initial());
Future<void> loadShifts() async {
emit(state.copyWith(status: HomeStatus.loading));
try {
final result = await _getHomeShifts.call();
emit(
state.copyWith(
status: HomeStatus.loaded,
todayShifts: result.today,
tomorrowShifts: result.tomorrow,
recommendedShifts: result.recommended,
),
);
} catch (e) {
emit(
state.copyWith(status: HomeStatus.error, errorMessage: e.toString()),
);
}
}
}

View File

@@ -0,0 +1,46 @@
part of 'home_cubit.dart';
enum HomeStatus { initial, loading, loaded, error }
class HomeState extends Equatable {
final HomeStatus status;
final List<Shift> todayShifts;
final List<Shift> tomorrowShifts;
final List<Shift> recommendedShifts;
final String? errorMessage;
const HomeState({
required this.status,
this.todayShifts = const [],
this.tomorrowShifts = const [],
this.recommendedShifts = const [],
this.errorMessage,
});
const HomeState.initial() : this(status: HomeStatus.initial);
HomeState copyWith({
HomeStatus? status,
List<Shift>? todayShifts,
List<Shift>? tomorrowShifts,
List<Shift>? recommendedShifts,
String? errorMessage,
}) {
return HomeState(
status: status ?? this.status,
todayShifts: todayShifts ?? this.todayShifts,
tomorrowShifts: tomorrowShifts ?? this.tomorrowShifts,
recommendedShifts: recommendedShifts ?? this.recommendedShifts,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
status,
todayShifts,
tomorrowShifts,
recommendedShifts,
errorMessage,
];
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter_modular/flutter_modular.dart';
/// Extension on [IModularNavigator] providing typed navigation helpers
/// for the Staff Home feature (worker home screen).
///
/// Keep routes as small wrappers around `pushNamed` / `navigate` so callers
/// don't need to rely on literal paths throughout the codebase.
extension HomeNavigator on IModularNavigator {
/// Navigates to the worker profile page.
void pushWorkerProfile() {
pushNamed('/worker-profile');
}
/// Navigates to the availability page.
void pushAvailability() {
pushNamed('/availability');
}
/// Navigates to the messages page.
void pushMessages() {
pushNamed('/messages');
}
/// Navigates to the payments page.
void pushPayments() {
pushNamed('/payments');
}
/// Navigates to the shifts listing.
/// Optionally provide a [tab] query param (e.g. `find`).
void pushShifts({String? tab}) {
if (tab == null) {
pushNamed('/shifts');
} else {
pushNamed('/shifts?tab=$tab');
}
}
}

View File

@@ -0,0 +1,850 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:core_localization/core_localization.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:staff_home/src/theme.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/data/services/mock_service.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/data/repositories/home_repository_impl.dart';
class WorkerHomePage extends ConsumerStatefulWidget {
const WorkerHomePage({super.key});
@override
ConsumerState<WorkerHomePage> createState() => _WorkerHomePageState();
}
class _WorkerHomePageState extends ConsumerState<WorkerHomePage> {
bool _autoMatchEnabled = false;
final bool _isProfileComplete = false; // Added for mock profile completion
@override
Widget build(BuildContext context) {
final i18n = t.staff.home;
final headerI18n = i18n.header;
final bannersI18n = i18n.banners;
final quickI18n = i18n.quick_actions;
final sectionsI18n = i18n.sections;
final emptyI18n = i18n.empty_states;
final pendingI18n = i18n.pending_payment;
final recI18n = i18n.recommended_card;
return BlocProvider(
create: (_) => HomeCubit(
// provide repository implementation backed by mock service for now
// later this should be wired from a DI container
HomeRepositoryImpl(mockService),
)..loadShifts(),
child: Scaffold(
backgroundColor: AppColors.krowBackground,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
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();
},
),
const SizedBox(height: 20),
_buildPlaceholderBanner(
bannersI18n.availability_title,
bannersI18n.availability_subtitle,
Colors.orange[50]!,
Colors.orange,
onTap: () => Modular.to.pushAvailability(),
),
const SizedBox(height: 20),
// Auto Match Toggle
AutoMatchToggle(
enabled: _autoMatchEnabled,
onToggle: (val) =>
setState(() => _autoMatchEnabled = val),
),
const SizedBox(height: 20),
// Quick Actions
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: _buildQuickAction(
context,
LucideIcons.search,
quickI18n.find_shifts,
() => Modular.to.pushShifts(),
),
),
Expanded(
child: _buildQuickAction(
context,
LucideIcons.calendar,
quickI18n.availability,
() => Modular.to.pushAvailability(),
),
),
Expanded(
child: _buildQuickAction(
context,
LucideIcons.messageSquare,
quickI18n.messages,
() => Modular.to.pushMessages(),
),
),
Expanded(
child: _buildQuickAction(
context,
LucideIcons.dollarSign,
quickI18n.earnings,
() => Modular.to.pushPayments(),
),
),
],
),
const SizedBox(height: 24),
// Today's Shifts
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
final shifts = state.todayShifts;
return Column(
children: [
_buildSectionHeader(
sectionsI18n.todays_shift,
shifts.isNotEmpty
? sectionsI18n.scheduled_count.replaceAll(
r'$count',
'${shifts.length}',
)
: null,
),
if (state.status == HomeStatus.loading)
const Center(
child: SizedBox(
height: 40,
width: 40,
child: CircularProgressIndicator(),
),
)
else if (shifts.isEmpty)
_buildEmptyState(
emptyI18n.no_shifts_today,
emptyI18n.find_shifts_cta,
() => Modular.to.pushShifts(tab: 'find'),
)
else
Column(
children: shifts
.map(
(shift) => ShiftCard(
shift: shift,
compact: true,
),
)
.toList(),
),
],
);
},
),
const SizedBox(height: 24),
// Tomorrow's Shifts
BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
final shifts = state.tomorrowShifts;
return Column(
children: [
_buildSectionHeader(sectionsI18n.tomorrow, null),
if (shifts.isEmpty)
_buildEmptyState(
emptyI18n.no_shifts_tomorrow,
null,
)
else
Column(
children: shifts
.map(
(shift) => ShiftCard(
shift: shift,
compact: true,
),
)
.toList(),
),
],
);
},
),
const SizedBox(height: 24),
// Pending Payment Card
_buildPendingPaymentCard(),
const SizedBox(height: 24),
// Recommended Shifts
_buildSectionHeader(
sectionsI18n.recommended_for_you,
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 SizedBox(
height: 160,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: state.recommendedShifts.length,
clipBehavior: Clip.none,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 12),
child: _buildRecommendedCard(
state.recommendedShifts[index],
),
),
),
);
},
),
const SizedBox(height: 24),
const BenefitsWidget(),
const SizedBox(height: 24),
const ImproveYourselfWidget(),
const SizedBox(height: 24),
const MoreWaysToUseKrowWidget(),
],
),
),
],
),
),
),
),
);
}
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() {
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() {
return GestureDetector(
onTap: () => context.go('/payments'),
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 duration = 8;
final totalPay = duration * shift.hourlyRate;
return GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
recI18n.applied_for.replaceAll(r'$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
.replaceAll(r'$start', shift.startTime)
.replaceAll(r'$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,497 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:intl/intl.dart';
import 'package:staff_home/src/domain/models/shift.dart';
import 'package:staff_home/src/theme.dart';
class ShiftCard extends StatefulWidget {
final Shift shift;
final VoidCallback? onApply;
final VoidCallback? onDecline;
final bool compact;
final bool disableTapNavigation; // Added property
const ShiftCard({
super.key,
required this.shift,
this.onApply,
this.onDecline,
this.compact = false,
this.disableTapNavigation = false,
});
@override
State<ShiftCard> createState() => _ShiftCardState();
}
class _ShiftCardState extends State<ShiftCard> {
bool isExpanded = false;
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mma').format(dt).toLowerCase();
} catch (e) {
return time;
}
}
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
return DateFormat('MMMM d').format(date);
} catch (e) {
return dateStr;
}
}
String _getTimeAgo(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
final diff = DateTime.now().difference(date);
if (diff.inHours < 1) return 'Just now';
if (diff.inHours < 24) return 'Pending ${diff.inHours}h ago';
return 'Pending ${diff.inDays}d ago';
} catch (e) {
return '';
}
}
Map<String, dynamic> _calculateDuration() {
if (widget.shift.startTime.isEmpty || widget.shift.endTime.isEmpty) {
return {'hours': 0, 'breakTime': '1 hour'};
}
try {
final startParts = widget.shift.startTime
.split(':')
.map(int.parse)
.toList();
final endParts = widget.shift.endTime.split(':').map(int.parse).toList();
double hours =
(endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60;
if (hours < 0) hours += 24;
return {'hours': hours.round(), 'breakTime': '1 hour'};
} catch (e) {
return {'hours': 0, 'breakTime': '1 hour'};
}
}
@override
Widget build(BuildContext context) {
if (widget.compact) {
return GestureDetector(
onTap: widget.disableTapNavigation
? null
: () {
setState(() => isExpanded = !isExpanded);
GoRouter.of(context).push(
'/shift-details/${widget.shift.id}',
extra: widget.shift,
);
},
child: Container(
margin: const EdgeInsets.only(bottom: 12),
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.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: widget.shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.shift.logoUrl!,
fit: BoxFit.contain,
),
)
: const Icon(
LucideIcons.building2,
color: AppColors.krowMuted,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
widget.shift.title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
overflow: TextOverflow.ellipsis,
),
),
Text.rich(
TextSpan(
text: '\$${widget.shift.hourlyRate}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppColors.krowCharcoal,
),
children: const [
TextSpan(
text: '/h',
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
),
),
],
),
),
],
),
Text(
widget.shift.clientName,
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 13,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${_formatTime(widget.shift.startTime)}${widget.shift.location}',
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 12,
),
),
],
),
),
],
),
),
);
}
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.krowBorder),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: widget.shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
widget.shift.logoUrl!,
fit: BoxFit.contain,
),
)
: const Icon(
LucideIcons.building2,
size: 28,
color: AppColors.krowBlue,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
color: AppColors.krowBlue,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Assigned ${_getTimeAgo(widget.shift.createdDate).replaceAll('Pending ', '')}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 16),
// Title and Rate
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.shift.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
Text(
widget.shift.clientName,
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 14,
),
),
],
),
),
Text.rich(
TextSpan(
text: '\$${widget.shift.hourlyRate}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
color: AppColors.krowCharcoal,
),
children: const [
TextSpan(
text: '/h',
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 16,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Location and Date
Row(
children: [
const Icon(
LucideIcons.mapPin,
size: 16,
color: AppColors.krowMuted,
),
const SizedBox(width: 6),
Expanded(
child: Text(
widget.shift.location,
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 16),
const Icon(
LucideIcons.calendar,
size: 16,
color: AppColors.krowMuted,
),
const SizedBox(width: 6),
Text(
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)}',
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 14,
),
),
],
),
const SizedBox(height: 16),
// Tags
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildTag(
LucideIcons.zap,
'Immediate start',
AppColors.krowYellow.withValues(alpha: 0.3),
AppColors.krowCharcoal,
),
_buildTag(
LucideIcons.timer,
'No experience',
const Color(0xFFFEE2E2),
const Color(0xFFDC2626),
),
],
),
const SizedBox(height: 16),
],
),
),
// Actions
if (!widget.compact)
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Column(
children: [
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: widget.onApply,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.krowCharcoal,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Accept shift',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton(
onPressed: widget.onDecline,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFEF4444),
side: const BorderSide(color: Color(0xFFFCA5A5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Decline shift',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
],
),
),
],
),
);
}
Widget _buildTag(IconData icon, String label, Color bg, Color text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: text),
const SizedBox(width: 4),
Flexible(
child: Text(
label,
style: TextStyle(
color: text,
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildDetailRow(IconData icon, String label, bool? value) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: AppColors.krowBorder)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(icon, size: 16, color: AppColors.krowMuted),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
color: AppColors.krowMuted,
fontSize: 14,
),
),
],
),
Text(
value == true ? 'Yes' : 'No',
style: TextStyle(
color: value == true
? const Color(0xFF10B981)
: AppColors.krowMuted,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:core_localization/core_localization.dart';
class AutoMatchToggle extends StatefulWidget {
final bool enabled;
final ValueChanged<bool> onToggle;
const AutoMatchToggle({
super.key,
required this.enabled,
required this.onToggle,
});
@override
State<AutoMatchToggle> createState() => _AutoMatchToggleState();
}
class _AutoMatchToggleState extends State<AutoMatchToggle>
with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final i18n = t.staff.home.auto_match;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: widget.enabled
? const LinearGradient(
colors: [Color(0xFF0032A0), Color(0xFF0047CC)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
)
: null,
color: widget.enabled ? null : Colors.white,
border: widget.enabled ? null : Border.all(color: Colors.grey.shade200),
boxShadow: widget.enabled
? [
BoxShadow(
color: const Color(0xFF0032A0).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: null,
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: widget.enabled
? Colors.white.withOpacity(0.2)
: const Color(0xFF0032A0).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
LucideIcons.zap,
color: widget.enabled
? Colors.white
: const Color(0xFF0032A0),
size: 20,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
i18n.title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: widget.enabled
? Colors.white
: const Color(0xFF0F172A),
),
),
Text(
widget.enabled ? i18n.finding_shifts : i18n.get_matched,
style: TextStyle(
fontSize: 12,
color: widget.enabled
? const Color(0xFFF8E08E)
: Colors.grey.shade500,
),
),
],
),
],
),
Switch(
value: widget.enabled,
onChanged: widget.onToggle,
activeThumbColor: Colors.white,
activeTrackColor: Colors.white.withOpacity(0.3),
inactiveThumbColor: Colors.white,
inactiveTrackColor: Colors.grey.shade300,
),
],
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: widget.enabled
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Container(
height: 1,
color: Colors.white.withOpacity(0.2),
),
const SizedBox(height: 16),
Text(
i18n.matching_based_on,
style: const TextStyle(
color: Color(0xFFF8E08E),
fontSize: 12,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
_buildChip(LucideIcons.mapPin, i18n.chips.location),
_buildChip(
LucideIcons.clock,
i18n.chips.availability,
),
_buildChip(LucideIcons.briefcase, i18n.chips.skills),
],
),
],
)
: const SizedBox.shrink(),
),
],
),
);
}
Widget _buildChip(IconData icon, String label) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: Colors.white),
const SizedBox(width: 4),
Text(
label,
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
);
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:go_router/go_router.dart';
import 'dart:math' as math;
import 'package:core_localization/core_localization.dart';
class BenefitsWidget extends StatelessWidget {
const BenefitsWidget({super.key});
@override
Widget build(BuildContext context) {
final i18n = t.staff.home.benefits;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
i18n.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFF0F172A),
),
),
GestureDetector(
onTap: () => context.push('/benefits'),
child: Row(
children: [
Text(
i18n.view_all,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF0032A0),
),
),
const Icon(
LucideIcons.chevronRight,
size: 16,
color: Color(0xFF0032A0),
),
],
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_BenefitItem(
label: i18n.items.sick_days,
current: 10,
total: 40,
color: const Color(0xFF0A39DF),
),
_BenefitItem(
label: i18n.items.vacation,
current: 40,
total: 40,
color: const Color(0xFF0A39DF),
),
_BenefitItem(
label: i18n.items.holidays,
current: 24,
total: 24,
color: const Color(0xFF0A39DF),
),
],
),
],
),
);
}
}
class _BenefitItem extends StatelessWidget {
final String label;
final double current;
final double total;
final Color color;
const _BenefitItem({
required this.label,
required this.current,
required this.total,
required this.color,
});
@override
Widget build(BuildContext context) {
final i18n = t.staff.home.benefits;
return Column(
children: [
SizedBox(
width: 56,
height: 56,
child: CustomPaint(
painter: _CircularProgressPainter(
progress: current / total,
color: color,
backgroundColor: const Color(0xFFE5E7EB),
strokeWidth: 4,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${current.toInt()}/${total.toInt()}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
Text(
i18n.hours_label,
style: const TextStyle(
fontSize: 8,
color: Color(0xFF94A3B8),
),
),
],
),
),
),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF475569),
),
),
],
);
}
}
class _CircularProgressPainter extends CustomPainter {
final double progress;
final Color color;
final Color backgroundColor;
final double strokeWidth;
_CircularProgressPainter({
required this.progress,
required this.color,
required this.backgroundColor,
required this.strokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width - strokeWidth) / 2;
final backgroundPaint = Paint()
..color = backgroundColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth;
canvas.drawCircle(center, radius, backgroundPaint);
final progressPaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
final sweepAngle = 2 * math.pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2,
sweepAngle,
false,
progressPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:core_localization/core_localization.dart';
class ImproveYourselfWidget extends StatelessWidget {
const ImproveYourselfWidget({super.key});
@override
Widget build(BuildContext context) {
final i18n = t.staff.home.improve;
final items = [
{
'id': 'training',
'title': i18n.items.training.title,
'description': i18n.items.training.description,
'image':
'https://images.unsplash.com/photo-1524995997946-a1c2e315a42f?w=400&h=300&fit=crop',
'page': i18n.items.training.page,
},
{
'id': 'podcast',
'title': i18n.items.podcast.title,
'description': i18n.items.podcast.description,
'image':
'https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=400&h=300&fit=crop',
'page': i18n.items.podcast.page,
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
i18n.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
clipBehavior: Clip.none,
child: Row(
children: items.map((item) => _buildCard(context, item)).toList(),
),
),
],
);
}
Widget _buildCard(BuildContext context, Map<String, String> item) {
return GestureDetector(
onTap: () => context.push(item['page']!),
child: Container(
width: 160,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 96,
width: double.infinity,
child: Image.network(
item['image']!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
color: Colors.grey.shade200,
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['title']!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 2),
Text(
item['description']!,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF64748B),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:core_localization/core_localization.dart';
class MoreWaysToUseKrowWidget extends StatelessWidget {
const MoreWaysToUseKrowWidget({super.key});
@override
Widget build(BuildContext context) {
final i18n = t.staff.home.more_ways;
final items = [
{
'id': 'benefits',
'title': i18n.items.benefits.title,
'image':
'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=400&h=300&fit=crop',
'page': i18n.items.benefits.page,
},
{
'id': 'refer',
'title': i18n.items.refer.title,
'image':
'https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=400&h=300&fit=crop',
'page': i18n.items.refer.page,
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
i18n.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF0F172A),
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
clipBehavior: Clip.none,
child: Row(
children: items.map((item) => _buildCard(context, item)).toList(),
),
),
],
);
}
Widget _buildCard(BuildContext context, Map<String, String> item) {
return GestureDetector(
onTap: () => context.push(item['page']!),
child: Container(
width: 160,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 96,
width: double.infinity,
child: Image.network(
item['image']!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
color: Colors.grey.shade200,
child: const Icon(
Icons.image_not_supported,
color: Colors.grey,
),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Text(
item['title']!,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF0F172A),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
import 'package:staff_home/src/presentation/pages/worker_home_page.dart';
class StaffHomeModule extends Module {
@override
void binds(Injector i) {
i.addSingleton(HomeCubit.new);
}
@override
void routes(RouteManager r) {
r.child('/', child: (BuildContext context) => const WorkerHomePage());
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppColors {
static const Color krowBlue = Color(0xFF0A39DF);
static const Color krowYellow = Color(0xFFFFED4A);
static const Color krowCharcoal = Color(0xFF121826);
static const Color krowMuted = Color(0xFF6A7382);
static const Color krowBorder = Color(0xFFE3E6E9);
static const Color krowBackground = Color(0xFFFAFBFC);
static const Color white = Colors.white;
static const Color black = Colors.black;
// helpers used by prototype (withValues extension in prototype); keep simple aliases
static Color withAlpha(Color c, double alpha) => c.withOpacity(alpha);
}
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: AppColors.krowBackground,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.krowBlue,
primary: AppColors.krowBlue,
secondary: AppColors.krowYellow,
surface: AppColors.white,
background: AppColors.krowBackground,
),
textTheme: GoogleFonts.instrumentSansTextTheme().apply(
bodyColor: AppColors.krowCharcoal,
displayColor: AppColors.krowCharcoal,
),
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.krowBackground,
elevation: 0,
iconTheme: IconThemeData(color: AppColors.krowCharcoal),
titleTextStyle: TextStyle(
color: AppColors.krowCharcoal,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
);
}
}
extension ColorWithValues on Color {
Color withValues({double alpha = 1.0}) => withOpacity(alpha);
}

View File

@@ -0,0 +1,3 @@
library;
export 'src/staff_home_module.dart';

View File

@@ -0,0 +1,38 @@
name: staff_home
description: Home feature for the staff application.
version: 0.0.1
publish_to: none
resolution: workspace
environment:
sdk: '>=3.10.0 <4.0.0'
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.0
flutter_modular: ^6.3.0
equatable: ^2.0.5
lucide_icons: ^0.257.0
intl: ^0.20.0
# Architecture Packages
design_system:
path: ../../../design_system
core_localization:
path: ../../../core_localization
krow_core:
path: ../../../core
krow_domain:
path: ../../../domain
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^9.1.0
mocktail: ^1.0.0
flutter_lints: ^6.0.0
flutter:
uses-material-design: true

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:staff_home/staff_home.dart';
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
@@ -28,9 +29,9 @@ class StaffMainModule extends Module {
child: (BuildContext context) =>
const PlaceholderPage(title: 'Payments'),
),
ChildRoute<dynamic>(
ModuleRoute<dynamic>(
StaffMainRoutes.home,
child: (BuildContext context) => const PlaceholderPage(title: 'Home'),
module: StaffHomeModule(),
),
ChildRoute<dynamic>(
StaffMainRoutes.clockIn,

View File

@@ -22,9 +22,9 @@ dependencies:
core_localization:
path: ../../../core_localization
# Features (Commented out until they are ready)
# staff_home:
# path: ../home
# Features
staff_home:
path: ../home
# staff_shifts:
# path: ../shifts
# staff_payments:

View File

@@ -10,6 +10,7 @@ workspace:
- packages/data_connect
- packages/core_localization
- packages/features/staff/authentication
- packages/features/staff/home
- packages/features/staff/staff_main
- packages/features/client/authentication
- packages/features/client/home