diff --git a/apps/mobile/packages/features/staff/home/analysis_options.yaml b/apps/mobile/packages/features/staff/home/analysis_options.yaml new file mode 100644 index 00000000..03ea3cc1 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: true + prefer_single_quotes: true + always_use_package_imports: true diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart new file mode 100644 index 00000000..44e2d0e8 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -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> getTodayShifts() => _service.getTodayShifts(); + + @override + Future> getTomorrowShifts() => _service.getTomorrowShifts(); + + @override + Future> getRecommendedShifts() => _service.getRecommendedShifts(); +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/services/mock_service.dart b/apps/mobile/packages/features/staff/home/lib/src/data/services/mock_service.dart new file mode 100644 index 00000000..3b09a607 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/data/services/mock_service.dart @@ -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> getTodayShifts() async { + await Future.delayed(const Duration(milliseconds: 500)); + return [_sampleShift1]; + } + + Future> getTomorrowShifts() async { + await Future.delayed(const Duration(milliseconds: 500)); + return [_sampleShift2]; + } + + Future> getRecommendedShifts() async { + await Future.delayed(const Duration(milliseconds: 500)); + return [_sampleShift3, _sampleShift1, _sampleShift2]; + } + + Future createWorkerProfile(Map data) async { + await Future.delayed(const Duration(seconds: 1)); + } +} + +final mockService = MockService(); diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/models/shift.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/models/shift.dart new file mode 100644 index 00000000..8a16d579 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/models/shift.dart @@ -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? 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}); +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart new file mode 100644 index 00000000..611fa64f --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/repositories/home_repository.dart @@ -0,0 +1,7 @@ +import 'package:staff_home/src/domain/models/shift.dart'; + +abstract class HomeRepository { + Future> getTodayShifts(); + Future> getTomorrowShifts(); + Future> getRecommendedShifts(); +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart new file mode 100644 index 00000000..4f88cb84 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/domain/usecases/get_home_shifts.dart @@ -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 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 today; + final List tomorrow; + final List recommended; + + HomeShifts({ + required this.today, + required this.tomorrow, + required this.recommended, + }); +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart new file mode 100644 index 00000000..bba00dae --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_cubit.dart @@ -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 { + final GetHomeShifts _getHomeShifts; + + HomeCubit(HomeRepository repository) + : _getHomeShifts = GetHomeShifts(repository), + super(const HomeState.initial()); + + Future 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()), + ); + } + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart new file mode 100644 index 00000000..156180a2 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/blocs/home_state.dart @@ -0,0 +1,46 @@ +part of 'home_cubit.dart'; + +enum HomeStatus { initial, loading, loaded, error } + +class HomeState extends Equatable { + final HomeStatus status; + final List todayShifts; + final List tomorrowShifts; + final List 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? todayShifts, + List? tomorrowShifts, + List? 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 get props => [ + status, + todayShifts, + tomorrowShifts, + recommendedShifts, + errorMessage, + ]; +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart new file mode 100644 index 00000000..a96546f5 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart @@ -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'); + } + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart new file mode 100644 index 00000000..5b5550d6 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -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 createState() => _WorkerHomePageState(); +} + +class _WorkerHomePageState extends ConsumerState { + 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( + 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( + 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( + 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, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart new file mode 100644 index 00000000..52a87d49 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/shift_card.dart @@ -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 createState() => _ShiftCardState(); +} + +class _ShiftCardState extends State { + 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 _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, + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/auto_match_toggle.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/auto_match_toggle.dart new file mode 100644 index 00000000..7cf637ee --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/auto_match_toggle.dart @@ -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 onToggle; + + const AutoMatchToggle({ + super.key, + required this.enabled, + required this.onToggle, + }); + + @override + State createState() => _AutoMatchToggleState(); +} + +class _AutoMatchToggleState extends State + 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), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/benefits_widget.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/benefits_widget.dart new file mode 100644 index 00000000..9911b968 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/benefits_widget.dart @@ -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; +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/improve_yourself_widget.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/improve_yourself_widget.dart new file mode 100644 index 00000000..53cd16ea --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/improve_yourself_widget.dart @@ -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 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, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/more_ways_widget.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/more_ways_widget.dart new file mode 100644 index 00000000..2286c76a --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/worker/more_ways_widget.dart @@ -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 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), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart new file mode 100644 index 00000000..f1f8fe2a --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/staff_home_module.dart @@ -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()); + } +} diff --git a/apps/mobile/packages/features/staff/home/lib/src/theme.dart b/apps/mobile/packages/features/staff/home/lib/src/theme.dart new file mode 100644 index 00000000..4bf11480 --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/theme.dart @@ -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); +} diff --git a/apps/mobile/packages/features/staff/home/lib/staff_home.dart b/apps/mobile/packages/features/staff/home/lib/staff_home.dart new file mode 100644 index 00000000..26110f8f --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/staff_home.dart @@ -0,0 +1,3 @@ +library; + +export 'src/staff_home_module.dart'; diff --git a/apps/mobile/packages/features/staff/home/pubspec.yaml b/apps/mobile/packages/features/staff/home/pubspec.yaml new file mode 100644 index 00000000..7c1f9aee --- /dev/null +++ b/apps/mobile/packages/features/staff/home/pubspec.yaml @@ -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 diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index c922e6bf..5a8a355e 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -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( + ModuleRoute( StaffMainRoutes.home, - child: (BuildContext context) => const PlaceholderPage(title: 'Home'), + module: StaffHomeModule(), ), ChildRoute( StaffMainRoutes.clockIn, diff --git a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml index 7107fb1f..dfa57fa3 100644 --- a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml +++ b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml @@ -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: diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index e416bb67..7bf83636 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -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