diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 9ce4cea3..b45d7c93 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -365,4 +365,59 @@ "pending_badge": "PENDING APPROVAL", "paid_badge": "PAID" } + , + "staff": { + "home": { + "header": { + "welcome_back": "Welcome back", + "user_name_placeholder": "Krower" + }, + "banners": { + "complete_profile_title": "Complete Your Profile", + "complete_profile_subtitle": "Get verified to see more shifts", + "availability_title": "Availability", + "availability_subtitle": "Update your availability for next week" + }, + "quick_actions": { + "find_shifts": "Find Shifts", + "availability": "Availability", + "messages": "Messages", + "earnings": "Earnings" + }, + "sections": { + "todays_shift": "Today's Shift", + "scheduled_count": "$count scheduled", + "tomorrow": "Tomorrow", + "recommended_for_you": "Recommended for You", + "view_all": "View all" + }, + "empty_states": { + "no_shifts_today": "No shifts scheduled for today", + "find_shifts_cta": "Find shifts →", + "no_shifts_tomorrow": "No shifts for tomorrow", + "no_recommended_shifts": "No recommended shifts" + }, + "pending_payment": { + "title": "Pending Payment", + "subtitle": "Payment processing", + "amount": "$amount" + }, + "recommended_card": { + "act_now": "• ACT NOW", + "one_day": "One Day", + "today": "Today", + "applied_for": "Applied for $title", + "time_range": "$start - $end" + } + } + }, + "staff_main": { + "tabs": { + "shifts": "Shifts", + "payments": "Payments", + "home": "Home", + "clock_in": "Clock In", + "profile": "Profile" + } + } } diff --git a/apps/mobile/packages/features/staff/staff_main/analysis_options.yaml b/apps/mobile/packages/features/staff/staff_main/analysis_options.yaml new file mode 100644 index 00000000..03ea3cc1 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/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/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart new file mode 100644 index 00000000..ae3dc5f8 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_cubit.dart @@ -0,0 +1,62 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:staff_main/src/presentation/blocs/staff_main_state.dart'; + +class StaffMainCubit extends Cubit implements Disposable { + StaffMainCubit() : super(const StaffMainState()) { + Modular.to.addListener(_onRouteChanged); + _onRouteChanged(); + } + + void _onRouteChanged() { + final String path = Modular.to.path; + int newIndex = state.currentIndex; + + // Detect which tab is active based on the route path + // Using contains() to handle child routes and trailing slashes + if (path.contains('/staff-main/shifts')) { + newIndex = 0; + } else if (path.contains('/staff-main/payments')) { + newIndex = 1; + } else if (path.contains('/staff-main/home')) { + newIndex = 2; + } else if (path.contains('/staff-main/clock-in')) { + newIndex = 3; + } else if (path.contains('/staff-main/profile')) { + newIndex = 4; + } + + if (newIndex != state.currentIndex) { + emit(state.copyWith(currentIndex: newIndex)); + } + } + + void navigateToTab(int index) { + if (index == state.currentIndex) return; + + switch (index) { + case 0: + Modular.to.navigate('/staff-main/shifts'); + break; + case 1: + Modular.to.navigate('/staff-main/payments'); + break; + case 2: + Modular.to.navigate('/staff-main/home'); + break; + case 3: + Modular.to.navigate('/staff-main/clock-in'); + break; + case 4: + Modular.to.navigate('/staff-main/profile'); + break; + } + // State update will happen via _onRouteChanged + } + + @override + void dispose() { + Modular.to.removeListener(_onRouteChanged); + close(); + } +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_state.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_state.dart new file mode 100644 index 00000000..68175302 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/blocs/staff_main_state.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +class StaffMainState extends Equatable { + const StaffMainState({ + this.currentIndex = 2, // Default to Home + }); + + final int currentIndex; + + StaffMainState copyWith({int? currentIndex}) { + return StaffMainState(currentIndex: currentIndex ?? this.currentIndex); + } + + @override + List get props => [currentIndex]; +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/navigation/staff_main_navigator.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/navigation/staff_main_navigator.dart new file mode 100644 index 00000000..a904d5dc --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/navigation/staff_main_navigator.dart @@ -0,0 +1,10 @@ +import 'package:flutter_modular/flutter_modular.dart'; + +/// Extension to provide typed navigation for the Staff Main feature. +extension StaffMainNavigator on IModularNavigator { + /// Navigates to the Staff Main Shell (Home). + /// This replaces the current navigation stack. + void navigateStaffMain() { + navigate('/staff-main/'); + } +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/placeholder_page.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/placeholder_page.dart new file mode 100644 index 00000000..b9d993d6 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/placeholder_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class PlaceholderPage extends StatelessWidget { + const PlaceholderPage({required this.title, super.key}); + + final String title; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(title)), + body: Center(child: Text('$title Page')), + ); + } +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/staff_main_page.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/staff_main_page.dart new file mode 100644 index 00000000..53cad7c8 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/pages/staff_main_page.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart'; +import 'package:staff_main/src/presentation/blocs/staff_main_state.dart'; +import 'package:staff_main/src/presentation/widgets/staff_main_bottom_bar.dart'; + +/// The main page for the Staff app, acting as a shell for the bottom navigation. +/// +/// It follows KROW Clean Architecture by: +/// - Being a [StatelessWidget]. +/// - Delegating state management to [StaffMainCubit]. +/// - Using [RouterOutlet] for nested navigation. +class StaffMainPage extends StatelessWidget { + const StaffMainPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => Modular.get(), + child: Scaffold( + extendBody: true, + body: const RouterOutlet(), + bottomNavigationBar: BlocBuilder( + builder: (BuildContext context, StaffMainState state) { + return StaffMainBottomBar( + currentIndex: state.currentIndex, + onTap: (int index) { + BlocProvider.of(context).navigateToTab(index); + }, + ); + }, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart new file mode 100644 index 00000000..ab9427d5 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/presentation/widgets/staff_main_bottom_bar.dart @@ -0,0 +1,157 @@ +import 'dart:ui'; + +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A custom bottom navigation bar for the Staff app. +/// +/// This widget provides a glassmorphic bottom navigation bar with blur effect +/// and follows the KROW Design System guidelines. It displays five tabs: +/// Shifts, Payments, Home, Clock In, and Profile. +/// +/// The widget uses: +/// - [UiColors] for all color values +/// - [UiTypography] for text styling +/// - [UiIcons] for icon assets +/// - [UiConstants] for spacing and sizing +class StaffMainBottomBar extends StatelessWidget { + /// Creates a [StaffMainBottomBar]. + /// + /// The [currentIndex] indicates which tab is currently selected. + /// The [onTap] callback is invoked when a tab is tapped. + const StaffMainBottomBar({ + required this.currentIndex, + required this.onTap, + super.key, + }); + + /// The index of the currently selected tab. + final int currentIndex; + + /// Callback invoked when a tab is tapped. + /// + /// The callback receives the index of the tapped tab. + final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + // Staff App colors from design system + // Using primary (Blue) for active as per prototype + const Color activeColor = UiColors.primary; + const Color inactiveColor = UiColors.textInactive; + + return Stack( + clipBehavior: Clip.none, + children: [ + // Glassmorphic background with blur effect + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.85), + border: Border( + top: BorderSide( + color: UiColors.black.withValues(alpha: 0.1), + ), + ), + ), + ), + ), + ), + ), + // Navigation items + Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + UiConstants.space2, + top: UiConstants.space4, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildNavItem( + index: 0, + icon: UiIcons.briefcase, + label: t.staff_main.tabs.shifts, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 1, + icon: UiIcons.dollar, + label: t.staff_main.tabs.payments, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 2, + icon: UiIcons.home, + label: t.staff_main.tabs.home, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 3, + icon: UiIcons.clock, + label: t.staff_main.tabs.clock_in, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 4, + icon: UiIcons.users, + label: t.staff_main.tabs.profile, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + ], + ), + ), + ], + ); + } + + /// Builds a single navigation item. + /// + /// Uses design system tokens for all styling: + /// - Icon size uses a standard value (24px is acceptable for navigation icons) + /// - Spacing uses [UiConstants.space1] + /// - Typography uses [UiTypography.footnote2m] + /// - Colors are passed as parameters from design system + Widget _buildNavItem({ + required int index, + required IconData icon, + required String label, + required Color activeColor, + required Color inactiveColor, + }) { + final bool isSelected = currentIndex == index; + return Expanded( + child: GestureDetector( + onTap: () => onTap(index), + behavior: HitTestBehavior.opaque, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + icon, + color: isSelected ? activeColor : inactiveColor, + size: 24, // Standard navigation icon size + ), + const SizedBox(height: UiConstants.space1), + Text( + label, + style: UiTypography.footnote2m.copyWith( + color: isSelected ? activeColor : inactiveColor, + ), + ), + ], + ), + ), + ); + } +} 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 new file mode 100644 index 00000000..74b5cec6 --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart'; +import 'package:staff_main/src/presentation/pages/placeholder_page.dart'; +import 'package:staff_main/src/presentation/pages/staff_main_page.dart'; + +class StaffMainModule extends Module { + @override + void binds(Injector i) { + i.addSingleton(StaffMainCubit.new); + } + + @override + void routes(RouteManager r) { + r.child( + '/', + child: (BuildContext context) => const StaffMainPage(), + children: >[ + ChildRoute( + '/shifts', + child: (BuildContext context) => + const PlaceholderPage(title: 'Shifts'), + ), + ChildRoute( + '/payments', + child: (BuildContext context) => + const PlaceholderPage(title: 'Payments'), + ), + ChildRoute( + '/home', + child: (BuildContext context) => const PlaceholderPage(title: 'Home'), + ), + ChildRoute( + '/clock-in', + child: (BuildContext context) => + const PlaceholderPage(title: 'Clock In'), + ), + ChildRoute( + '/profile', + child: (BuildContext context) => + const PlaceholderPage(title: 'Profile'), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/staff/staff_main/lib/staff_main.dart b/apps/mobile/packages/features/staff/staff_main/lib/staff_main.dart new file mode 100644 index 00000000..6f9aec7a --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/lib/staff_main.dart @@ -0,0 +1,4 @@ +library; + +export 'src/presentation/navigation/staff_main_navigator.dart'; +export 'src/staff_main_module.dart'; diff --git a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml new file mode 100644 index 00000000..7107fb1f --- /dev/null +++ b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml @@ -0,0 +1,43 @@ +name: staff_main +description: Main shell and navigation 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 + + # Architecture Packages + design_system: + path: ../../../design_system + core_localization: + path: ../../../core_localization + + # Features (Commented out until they are ready) + # staff_home: + # path: ../home + # staff_shifts: + # path: ../shifts + # staff_payments: + # path: ../payments + # staff_profile: + # path: ../profile + +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/pubspec.yaml b/apps/mobile/pubspec.yaml index 33599721..e416bb67 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/staff_main - packages/features/client/authentication - packages/features/client/home - packages/features/client/settings