feat: implement staff availability, clock-in, payments and fix UI navigation
This commit is contained in:
@@ -1,18 +1,120 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||
import 'package:staff_home/src/domain/entities/shift.dart';
|
||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||
import 'package:staff_home/src/data/services/mock_service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
extension TimestampExt on Timestamp {
|
||||
DateTime toDate() {
|
||||
return DateTime.fromMillisecondsSinceEpoch(seconds.toInt() * 1000 + nanoseconds ~/ 1000000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class HomeRepositoryImpl implements HomeRepository {
|
||||
final MockService _service;
|
||||
HomeRepositoryImpl();
|
||||
|
||||
HomeRepositoryImpl(this._service);
|
||||
String get _currentStaffId {
|
||||
final session = StaffSessionStore.instance.session;
|
||||
if (session?.staff?.id == null) throw Exception('User not logged in');
|
||||
return session!.staff!.id;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getTodayShifts() => _service.getTodayShifts();
|
||||
Future<List<Shift>> getTodayShifts() async {
|
||||
return _getShiftsForDate(DateTime.now());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getTomorrowShifts() => _service.getTomorrowShifts();
|
||||
Future<List<Shift>> getTomorrowShifts() async {
|
||||
return _getShiftsForDate(DateTime.now().add(const Duration(days: 1)));
|
||||
}
|
||||
|
||||
Future<List<Shift>> _getShiftsForDate(DateTime date) async {
|
||||
try {
|
||||
final response = await ExampleConnector.instance
|
||||
.getApplicationsByStaffId(staffId: _currentStaffId)
|
||||
.execute();
|
||||
|
||||
final targetYmd = DateFormat('yyyy-MM-dd').format(date);
|
||||
|
||||
return response.data.applications
|
||||
.where((app) {
|
||||
final shiftDate = app.shift.date?.toDate();
|
||||
if (shiftDate == null) return false;
|
||||
|
||||
final isDateMatch = DateFormat('yyyy-MM-dd').format(shiftDate) == targetYmd;
|
||||
final isAssigned = app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED;
|
||||
|
||||
return isDateMatch && isAssigned;
|
||||
})
|
||||
.map((app) => _mapApplicationToShift(app))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getRecommendedShifts() => _service.getRecommendedShifts();
|
||||
Future<List<Shift>> getRecommendedShifts() async {
|
||||
try {
|
||||
// Logic: List ALL open shifts (simple recommendation engine)
|
||||
// Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED.
|
||||
final response = await ExampleConnector.instance.listShifts().execute();
|
||||
|
||||
return response.data.shifts
|
||||
.where((s) => s.status is Known && (s.status as Known).value == ShiftStatus.OPEN)
|
||||
.take(10)
|
||||
.map((s) => _mapConnectorShiftToDomain(s))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Mappers specific to Home's Domain Entity 'Shift'
|
||||
// Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift.
|
||||
|
||||
Shift _mapApplicationToShift(GetApplicationsByStaffIdApplications app) {
|
||||
final s = app.shift;
|
||||
final r = app.shiftRole;
|
||||
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: r.role.name,
|
||||
clientName: s.order.business.businessName,
|
||||
hourlyRate: r.role.costPerHour,
|
||||
location: s.location ?? 'Unknown',
|
||||
locationAddress: s.location ?? '',
|
||||
date: s.date?.toDate().toIso8601String() ?? '',
|
||||
startTime: DateFormat('HH:mm').format(r.startTime?.toDate() ?? DateTime.now()),
|
||||
endTime: DateFormat('HH:mm').format(r.endTime?.toDate() ?? DateTime.now()),
|
||||
createdDate: app.createdAt?.toDate().toIso8601String() ?? '',
|
||||
tipsAvailable: false, // Not in API
|
||||
mealProvided: false, // Not in API
|
||||
managers: [], // Not in this query
|
||||
description: null,
|
||||
);
|
||||
}
|
||||
|
||||
Shift _mapConnectorShiftToDomain(ListShiftsShifts s) {
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
clientName: s.order.business.businessName,
|
||||
hourlyRate: s.cost ?? 0.0,
|
||||
location: s.location ?? 'Unknown',
|
||||
locationAddress: s.locationAddress ?? '',
|
||||
date: s.date?.toDate().toIso8601String() ?? '',
|
||||
startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()),
|
||||
endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()),
|
||||
createdDate: s.createdAt?.toDate().toIso8601String() ?? '',
|
||||
tipsAvailable: false,
|
||||
mealProvided: false,
|
||||
managers: [],
|
||||
description: s.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ class HomeCubit extends Cubit<HomeState> {
|
||||
super(const HomeState.initial());
|
||||
|
||||
Future<void> loadShifts() async {
|
||||
if (isClosed) return;
|
||||
emit(state.copyWith(status: HomeStatus.loading));
|
||||
try {
|
||||
final result = await _getHomeShifts.call();
|
||||
if (isClosed) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: HomeStatus.loaded,
|
||||
@@ -30,6 +32,7 @@ class HomeCubit extends Cubit<HomeState> {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (isClosed) return;
|
||||
emit(
|
||||
state.copyWith(status: HomeStatus.error, errorMessage: e.toString()),
|
||||
);
|
||||
|
||||
@@ -44,8 +44,8 @@ class WorkerHomePage extends StatelessWidget {
|
||||
final sectionsI18n = i18n.sections;
|
||||
final emptyI18n = i18n.empty_states;
|
||||
|
||||
return BlocProvider<HomeCubit>(
|
||||
create: (context) => Modular.get<HomeCubit>()..loadShifts(),
|
||||
return BlocProvider<HomeCubit>.value(
|
||||
value: Modular.get<HomeCubit>()..loadShifts(),
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
|
||||
@@ -41,171 +41,173 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
recI18n.act_now,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF0047FF),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFDC2626),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE8F0FF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.calendar,
|
||||
color: Color(0xFF0047FF),
|
||||
size: 20,
|
||||
],
|
||||
),
|
||||
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,
|
||||
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: UiColors.foreground,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'\$${totalPay.round()}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.foreground,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'\$${totalPay.round()}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.foreground,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
shift.clientName,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
shift.clientName,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
Text(
|
||||
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr • ${duration}h',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr • ${duration}h',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.calendar,
|
||||
size: 14,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
recI18n.today,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.calendar,
|
||||
size: 14,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 14,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
recI18n.time_range(
|
||||
start: shift.startTime,
|
||||
end: shift.endTime,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.mapPin,
|
||||
size: 14,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
shift.locationAddress ?? shift.location,
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
recI18n.today,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 12),
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 14,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
recI18n.time_range(
|
||||
start: shift.startTime,
|
||||
end: shift.endTime,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.mapPin,
|
||||
size: 14,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
shift.locationAddress ?? shift.location,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ class StaffHomeModule extends Module {
|
||||
|
||||
// Repository
|
||||
i.addLazySingleton<HomeRepository>(
|
||||
() => HomeRepositoryImpl(i.get<MockService>()),
|
||||
() => HomeRepositoryImpl(),
|
||||
);
|
||||
|
||||
// Presentation layer - Cubit
|
||||
|
||||
@@ -28,6 +28,9 @@ dependencies:
|
||||
path: ../../../core
|
||||
krow_domain:
|
||||
path: ../../../domain
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
firebase_data_connect:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user