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",
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/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
|
||||||
|
|||||||
Reference in New Issue
Block a user