diff --git a/.gitignore b/.gitignore index 9dd0e50a..c3c5a87f 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,8 @@ node_modules/ dist/ dist-ssr/ coverage/ +!**/lib/**/coverage/ +!**/src/**/coverage/ .nyc_output/ .vite/ .temp/ diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/coverage/data/repositories/coverage_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/coverage/data/repositories/coverage_connector_repository_impl.dart new file mode 100644 index 00000000..5305fd16 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/connectors/coverage/data/repositories/coverage_connector_repository_impl.dart @@ -0,0 +1,155 @@ +import 'package:intl/intl.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/repositories/coverage_connector_repository.dart'; + +/// Implementation of [CoverageConnectorRepository]. +class CoverageConnectorRepositoryImpl implements CoverageConnectorRepository { + CoverageConnectorRepositoryImpl({ + dc.DataConnectService? service, + }) : _service = service ?? dc.DataConnectService.instance; + + final dc.DataConnectService _service; + + @override + Future> getShiftsForDate({ + required String businessId, + required DateTime date, + }) async { + return _service.run(() async { + final DateTime start = DateTime(date.year, date.month, date.day); + final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999); + + final shiftRolesResult = await _service.connector + .listShiftRolesByBusinessAndDateRange( + businessId: businessId, + start: _service.toTimestamp(start), + end: _service.toTimestamp(end), + ) + .execute(); + + final applicationsResult = await _service.connector + .listStaffsApplicationsByBusinessForDay( + businessId: businessId, + dayStart: _service.toTimestamp(start), + dayEnd: _service.toTimestamp(end), + ) + .execute(); + + return _mapCoverageShifts( + shiftRolesResult.data.shiftRoles, + applicationsResult.data.applications, + date, + ); + }); + } + + List _mapCoverageShifts( + List shiftRoles, + List applications, + DateTime date, + ) { + if (shiftRoles.isEmpty && applications.isEmpty) return []; + + final Map groups = {}; + + for (final sr in shiftRoles) { + final String key = '${sr.shiftId}:${sr.roleId}'; + final startTime = _service.toDateTime(sr.startTime); + + groups[key] = _CoverageGroup( + shiftId: sr.shiftId, + roleId: sr.roleId, + title: sr.role.name, + location: sr.shift.location ?? sr.shift.locationAddress ?? '', + startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '00:00', + workersNeeded: sr.count, + date: _service.toDateTime(sr.shift.date) ?? date, + workers: [], + ); + } + + for (final app in applications) { + final String key = '${app.shiftId}:${app.roleId}'; + if (!groups.containsKey(key)) { + final startTime = _service.toDateTime(app.shiftRole.startTime); + groups[key] = _CoverageGroup( + shiftId: app.shiftId, + roleId: app.roleId, + title: app.shiftRole.role.name, + location: app.shiftRole.shift.location ?? app.shiftRole.shift.locationAddress ?? '', + startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '00:00', + workersNeeded: app.shiftRole.count, + date: _service.toDateTime(app.shiftRole.shift.date) ?? date, + workers: [], + ); + } + + final checkIn = _service.toDateTime(app.checkInTime); + groups[key]!.workers.add( + CoverageWorker( + name: app.staff.fullName, + status: _mapWorkerStatus(app.status.stringValue), + checkInTime: checkIn != null ? DateFormat('HH:mm').format(checkIn) : null, + ), + ); + } + + return groups.values + .map((g) => CoverageShift( + id: '${g.shiftId}:${g.roleId}', + title: g.title, + location: g.location, + startTime: g.startTime, + workersNeeded: g.workersNeeded, + date: g.date, + workers: g.workers, + )) + .toList(); + } + + CoverageWorkerStatus _mapWorkerStatus(String status) { + switch (status) { + case 'PENDING': + return CoverageWorkerStatus.pending; + case 'REJECTED': + return CoverageWorkerStatus.rejected; + case 'CONFIRMED': + return CoverageWorkerStatus.confirmed; + case 'CHECKED_IN': + return CoverageWorkerStatus.checkedIn; + case 'CHECKED_OUT': + return CoverageWorkerStatus.checkedOut; + case 'LATE': + return CoverageWorkerStatus.late; + case 'NO_SHOW': + return CoverageWorkerStatus.noShow; + case 'COMPLETED': + return CoverageWorkerStatus.completed; + default: + return CoverageWorkerStatus.pending; + } + } +} + +class _CoverageGroup { + _CoverageGroup({ + required this.shiftId, + required this.roleId, + required this.title, + required this.location, + required this.startTime, + required this.workersNeeded, + required this.date, + required this.workers, + }); + + final String shiftId; + final String roleId; + final String title; + final String location; + final String startTime; + final int workersNeeded; + final DateTime date; + final List workers; +} diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/coverage/domain/repositories/coverage_connector_repository.dart b/apps/mobile/packages/data_connect/lib/src/connectors/coverage/domain/repositories/coverage_connector_repository.dart new file mode 100644 index 00000000..abb993c1 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/connectors/coverage/domain/repositories/coverage_connector_repository.dart @@ -0,0 +1,12 @@ +import 'package:krow_domain/krow_domain.dart'; + +/// Repository interface for coverage connector operations. +/// +/// This acts as a buffer layer between the domain repository and the Data Connect SDK. +abstract interface class CoverageConnectorRepository { + /// Fetches coverage data for a specific date and business. + Future> getShiftsForDate({ + required String businessId, + required DateTime date, + }); +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_bloc.dart new file mode 100644 index 00000000..4f2ea984 --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_bloc.dart @@ -0,0 +1,31 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../domain/repositories/reports_repository.dart'; +import 'coverage_event.dart'; +import 'coverage_state.dart'; + +class CoverageBloc extends Bloc { + final ReportsRepository _reportsRepository; + + CoverageBloc({required ReportsRepository reportsRepository}) + : _reportsRepository = reportsRepository, + super(CoverageInitial()) { + on(_onLoadCoverageReport); + } + + Future _onLoadCoverageReport( + LoadCoverageReport event, + Emitter emit, + ) async { + emit(CoverageLoading()); + try { + final report = await _reportsRepository.getCoverageReport( + businessId: event.businessId, + startDate: event.startDate, + endDate: event.endDate, + ); + emit(CoverageLoaded(report)); + } catch (e) { + emit(CoverageError(e.toString())); + } + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_event.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_event.dart new file mode 100644 index 00000000..6b6dc7cb --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_event.dart @@ -0,0 +1,23 @@ +import 'package:equatable/equatable.dart'; + +abstract class CoverageEvent extends Equatable { + const CoverageEvent(); + + @override + List get props => []; +} + +class LoadCoverageReport extends CoverageEvent { + final String? businessId; + final DateTime startDate; + final DateTime endDate; + + const LoadCoverageReport({ + this.businessId, + required this.startDate, + required this.endDate, + }); + + @override + List get props => [businessId, startDate, endDate]; +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_state.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_state.dart new file mode 100644 index 00000000..cef85e0f --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +abstract class CoverageState extends Equatable { + const CoverageState(); + + @override + List get props => []; +} + +class CoverageInitial extends CoverageState {} + +class CoverageLoading extends CoverageState {} + +class CoverageLoaded extends CoverageState { + final CoverageReport report; + + const CoverageLoaded(this.report); + + @override + List get props => [report]; +} + +class CoverageError extends CoverageState { + final String message; + + const CoverageError(this.message); + + @override + List get props => [message]; +}