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:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
avoid_print: true
|
||||
prefer_single_quotes: true
|
||||
always_use_package_imports: true
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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/');
|
||||
}
|
||||
}
|
||||
@@ -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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
library;
|
||||
|
||||
export 'src/presentation/navigation/staff_main_navigator.dart';
|
||||
export 'src/staff_main_module.dart';
|
||||
43
apps/mobile/packages/features/staff/staff_main/pubspec.yaml
Normal file
43
apps/mobile/packages/features/staff/staff_main/pubspec.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user