feat(staff): integrate staff_home feature
- Created staff_main package with module, bloc, and pages - Integrated staff_home into staff_main - Updated route constants to use /worker-main - Fixed intl version conflict
This commit is contained in:
@@ -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,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<List<Shift>> getTodayShifts() => _service.getTodayShifts();
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getTomorrowShifts() => _service.getTomorrowShifts();
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getRecommendedShifts() => _service.getRecommendedShifts();
|
||||
}
|
||||
@@ -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<List<Shift>> getTodayShifts() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return [_sampleShift1];
|
||||
}
|
||||
|
||||
Future<List<Shift>> getTomorrowShifts() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return [_sampleShift2];
|
||||
}
|
||||
|
||||
Future<List<Shift>> getRecommendedShifts() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return [_sampleShift3, _sampleShift1, _sampleShift2];
|
||||
}
|
||||
|
||||
Future<void> createWorkerProfile(Map<String, dynamic> data) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
final mockService = MockService();
|
||||
@@ -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<ShiftManager>? 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});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:staff_home/src/domain/models/shift.dart';
|
||||
|
||||
abstract class HomeRepository {
|
||||
Future<List<Shift>> getTodayShifts();
|
||||
Future<List<Shift>> getTomorrowShifts();
|
||||
Future<List<Shift>> getRecommendedShifts();
|
||||
}
|
||||
@@ -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<HomeShifts> 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<Shift> today;
|
||||
final List<Shift> tomorrow;
|
||||
final List<Shift> recommended;
|
||||
|
||||
HomeShifts({
|
||||
required this.today,
|
||||
required this.tomorrow,
|
||||
required this.recommended,
|
||||
});
|
||||
}
|
||||
@@ -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<HomeState> {
|
||||
final GetHomeShifts _getHomeShifts;
|
||||
|
||||
HomeCubit(HomeRepository repository)
|
||||
: _getHomeShifts = GetHomeShifts(repository),
|
||||
super(const HomeState.initial());
|
||||
|
||||
Future<void> 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
part of 'home_cubit.dart';
|
||||
|
||||
enum HomeStatus { initial, loading, loaded, error }
|
||||
|
||||
class HomeState extends Equatable {
|
||||
final HomeStatus status;
|
||||
final List<Shift> todayShifts;
|
||||
final List<Shift> tomorrowShifts;
|
||||
final List<Shift> 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<Shift>? todayShifts,
|
||||
List<Shift>? tomorrowShifts,
|
||||
List<Shift>? 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<Object?> get props => [
|
||||
status,
|
||||
todayShifts,
|
||||
tomorrowShifts,
|
||||
recommendedShifts,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<WorkerHomePage> createState() => _WorkerHomePageState();
|
||||
}
|
||||
|
||||
class _WorkerHomePageState extends ConsumerState<WorkerHomePage> {
|
||||
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<HomeCubit, HomeState>(
|
||||
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<HomeCubit, HomeState>(
|
||||
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<HomeCubit, HomeState>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<ShiftCard> createState() => _ShiftCardState();
|
||||
}
|
||||
|
||||
class _ShiftCardState extends State<ShiftCard> {
|
||||
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<String, dynamic> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<bool> onToggle;
|
||||
|
||||
const AutoMatchToggle({
|
||||
super.key,
|
||||
required this.enabled,
|
||||
required this.onToggle,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AutoMatchToggle> createState() => _AutoMatchToggleState();
|
||||
}
|
||||
|
||||
class _AutoMatchToggleState extends State<AutoMatchToggle>
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String, String> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
51
apps/mobile/packages/features/staff/home/lib/src/theme.dart
Normal file
51
apps/mobile/packages/features/staff/home/lib/src/theme.dart
Normal file
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
library;
|
||||
|
||||
export 'src/staff_home_module.dart';
|
||||
38
apps/mobile/packages/features/staff/home/pubspec.yaml
Normal file
38
apps/mobile/packages/features/staff/home/pubspec.yaml
Normal file
@@ -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
|
||||
@@ -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<dynamic>(
|
||||
ModuleRoute<dynamic>(
|
||||
StaffMainRoutes.home,
|
||||
child: (BuildContext context) => const PlaceholderPage(title: 'Home'),
|
||||
module: StaffHomeModule(),
|
||||
),
|
||||
ChildRoute<dynamic>(
|
||||
StaffMainRoutes.clockIn,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user