feat(staff): implement staff home screen shell (staff_main)

- Created staff_main package structure
- Implemented StaffMainPage, StaffMainBottomBar, StaffMainCubit
- Added localization for staff_main tabs
- Added typed navigation extension
- Registered package in workspace
This commit is contained in:
Achintha Isuru
2026-01-24 11:43:09 -05:00
parent 084ef8b025
commit cdc0344280
12 changed files with 455 additions and 0 deletions

View File

@@ -365,4 +365,59 @@
"pending_badge": "PENDING APPROVAL", "pending_badge": "PENDING APPROVAL",
"paid_badge": "PAID" "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"
}
}
} }

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,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<StaffMainState> 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();
}
}

View File

@@ -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<Object> get props => <Object>[currentIndex];
}

View File

@@ -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/');
}
}

View File

@@ -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')),
);
}
}

View File

@@ -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<StaffMainCubit>(
create: (BuildContext context) => Modular.get<StaffMainCubit>(),
child: Scaffold(
extendBody: true,
body: const RouterOutlet(),
bottomNavigationBar: BlocBuilder<StaffMainCubit, StaffMainState>(
builder: (BuildContext context, StaffMainState state) {
return StaffMainBottomBar(
currentIndex: state.currentIndex,
onTap: (int index) {
BlocProvider.of<StaffMainCubit>(context).navigateToTab(index);
},
);
},
),
),
);
}
}

View File

@@ -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<int> 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: <Widget>[
// 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: <Widget>[
_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: <Widget>[
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,
),
),
],
),
),
);
}
}

View File

@@ -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: <ParallelRoute<dynamic>>[
ChildRoute<dynamic>(
'/shifts',
child: (BuildContext context) =>
const PlaceholderPage(title: 'Shifts'),
),
ChildRoute<dynamic>(
'/payments',
child: (BuildContext context) =>
const PlaceholderPage(title: 'Payments'),
),
ChildRoute<dynamic>(
'/home',
child: (BuildContext context) => const PlaceholderPage(title: 'Home'),
),
ChildRoute<dynamic>(
'/clock-in',
child: (BuildContext context) =>
const PlaceholderPage(title: 'Clock In'),
),
ChildRoute<dynamic>(
'/profile',
child: (BuildContext context) =>
const PlaceholderPage(title: 'Profile'),
),
],
);
}
}

View File

@@ -0,0 +1,4 @@
library;
export 'src/presentation/navigation/staff_main_navigator.dart';
export 'src/staff_main_module.dart';

View File

@@ -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

View File

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