feat: implement staff availability, clock-in, payments and fix UI navigation

This commit is contained in:
Suriya
2026-01-30 21:46:44 +05:30
parent 56aab9e1f6
commit ac7874c634
55 changed files with 1373 additions and 463 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ class StaffHomeModule extends Module {
// Repository
i.addLazySingleton<HomeRepository>(
() => HomeRepositoryImpl(i.get<MockService>()),
() => HomeRepositoryImpl(),
);
// Presentation layer - Cubit

View File

@@ -28,6 +28,9 @@ dependencies:
path: ../../../core
krow_domain:
path: ../../../domain
krow_data_connect:
path: ../../../data_connect
firebase_data_connect:
dev_dependencies:
flutter_test: