feat: architecture overhaul, launchpad-style reports, and uber-style locations
- Strengthened Buffer Layer architecture to decouple Data Connect from Domain - Rewired Coverage, Performance, and Forecast reports to match Launchpad logic - Implemented Uber-style Preferred Locations search using Google Places API - Added session recovery logic to prevent crashes on app restart - Synchronized backend schemas & SDK for ShiftStatus enums - Fixed various build/compilation errors and localization duplicates
This commit is contained in:
@@ -1,68 +1,35 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/coverage_repository.dart';
|
||||
|
||||
/// Implementation of [CoverageRepository] in the Data layer.
|
||||
/// Implementation of [CoverageRepository] that delegates to [dc.CoverageConnectorRepository].
|
||||
///
|
||||
/// This class provides mock data for the coverage feature.
|
||||
/// In a production environment, this would delegate to `packages/data_connect`
|
||||
/// for real data access (e.g., Firebase Data Connect, REST API).
|
||||
///
|
||||
/// It strictly adheres to the Clean Architecture data layer responsibilities:
|
||||
/// - No business logic (except necessary data transformation).
|
||||
/// - Delegates to data sources (currently mock data, will be `data_connect`).
|
||||
/// - Returns domain entities from `domain/ui_entities`.
|
||||
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
|
||||
/// connector repository from the data_connect package.
|
||||
class CoverageRepositoryImpl implements CoverageRepository {
|
||||
/// Creates a [CoverageRepositoryImpl].
|
||||
CoverageRepositoryImpl({required dc.DataConnectService service}) : _service = service;
|
||||
|
||||
final dc.CoverageConnectorRepository _connectorRepository;
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
/// Fetches shifts for a specific date.
|
||||
CoverageRepositoryImpl({
|
||||
dc.CoverageConnectorRepository? connectorRepository,
|
||||
dc.DataConnectService? service,
|
||||
}) : _connectorRepository = connectorRepository ??
|
||||
dc.DataConnectService.instance.getCoverageRepository(),
|
||||
_service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
@override
|
||||
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
|
||||
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 fdc.QueryResult<dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
||||
await _service.connector
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
final fdc.QueryResult<dc.ListStaffsApplicationsByBusinessForDayData,
|
||||
dc.ListStaffsApplicationsByBusinessForDayVariables> 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,
|
||||
);
|
||||
});
|
||||
final businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.getShiftsForDate(
|
||||
businessId: businessId,
|
||||
date: date,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetches coverage statistics for a specific date.
|
||||
@override
|
||||
Future<CoverageStats> getCoverageStats({required DateTime date}) async {
|
||||
// Get shifts for the date
|
||||
final List<CoverageShift> shifts = await getShiftsForDate(date: date);
|
||||
|
||||
// Calculate statistics
|
||||
final int totalNeeded = shifts.fold<int>(
|
||||
0,
|
||||
(int sum, CoverageShift shift) => sum + shift.workersNeeded,
|
||||
@@ -90,129 +57,4 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
late: late,
|
||||
);
|
||||
}
|
||||
|
||||
List<CoverageShift> _mapCoverageShifts(
|
||||
List<dc.ListShiftRolesByBusinessAndDateRangeShiftRoles> shiftRoles,
|
||||
List<dc.ListStaffsApplicationsByBusinessForDayApplications> applications,
|
||||
DateTime date,
|
||||
) {
|
||||
if (shiftRoles.isEmpty && applications.isEmpty) {
|
||||
return <CoverageShift>[];
|
||||
}
|
||||
|
||||
final Map<String, _CoverageGroup> groups = <String, _CoverageGroup>{};
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
|
||||
in shiftRoles) {
|
||||
final String key = '${shiftRole.shiftId}:${shiftRole.roleId}';
|
||||
groups[key] = _CoverageGroup(
|
||||
shiftId: shiftRole.shiftId,
|
||||
roleId: shiftRole.roleId,
|
||||
title: shiftRole.role.name,
|
||||
location: shiftRole.shift.location ?? shiftRole.shift.locationAddress ?? '',
|
||||
startTime: _formatTime(shiftRole.startTime) ?? '00:00',
|
||||
workersNeeded: shiftRole.count,
|
||||
date: shiftRole.shift.date?.toDateTime() ?? date,
|
||||
workers: <CoverageWorker>[],
|
||||
);
|
||||
}
|
||||
|
||||
for (final dc.ListStaffsApplicationsByBusinessForDayApplications app
|
||||
in applications) {
|
||||
final String key = '${app.shiftId}:${app.roleId}';
|
||||
final _CoverageGroup existing = groups[key] ??
|
||||
_CoverageGroup(
|
||||
shiftId: app.shiftId,
|
||||
roleId: app.roleId,
|
||||
title: app.shiftRole.role.name,
|
||||
location: app.shiftRole.shift.location ??
|
||||
app.shiftRole.shift.locationAddress ??
|
||||
'',
|
||||
startTime: _formatTime(app.shiftRole.startTime) ?? '00:00',
|
||||
workersNeeded: app.shiftRole.count,
|
||||
date: app.shiftRole.shift.date?.toDateTime() ?? date,
|
||||
workers: <CoverageWorker>[],
|
||||
);
|
||||
|
||||
existing.workers.add(
|
||||
CoverageWorker(
|
||||
name: app.staff.fullName,
|
||||
status: _mapWorkerStatus(app.status),
|
||||
checkInTime: _formatTime(app.checkInTime),
|
||||
),
|
||||
);
|
||||
groups[key] = existing;
|
||||
}
|
||||
|
||||
return groups.values
|
||||
.map(
|
||||
(_CoverageGroup group) => CoverageShift(
|
||||
id: '${group.shiftId}:${group.roleId}',
|
||||
title: group.title,
|
||||
location: group.location,
|
||||
startTime: group.startTime,
|
||||
workersNeeded: group.workersNeeded,
|
||||
date: group.date,
|
||||
workers: group.workers,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
CoverageWorkerStatus _mapWorkerStatus(
|
||||
dc.EnumValue<dc.ApplicationStatus> status,
|
||||
) {
|
||||
if (status is dc.Known<dc.ApplicationStatus>) {
|
||||
switch (status.value) {
|
||||
case dc.ApplicationStatus.PENDING:
|
||||
return CoverageWorkerStatus.pending;
|
||||
case dc.ApplicationStatus.REJECTED:
|
||||
return CoverageWorkerStatus.rejected;
|
||||
case dc.ApplicationStatus.CONFIRMED:
|
||||
return CoverageWorkerStatus.confirmed;
|
||||
case dc.ApplicationStatus.CHECKED_IN:
|
||||
return CoverageWorkerStatus.checkedIn;
|
||||
case dc.ApplicationStatus.CHECKED_OUT:
|
||||
return CoverageWorkerStatus.checkedOut;
|
||||
case dc.ApplicationStatus.LATE:
|
||||
return CoverageWorkerStatus.late;
|
||||
case dc.ApplicationStatus.NO_SHOW:
|
||||
return CoverageWorkerStatus.noShow;
|
||||
case dc.ApplicationStatus.COMPLETED:
|
||||
return CoverageWorkerStatus.completed;
|
||||
}
|
||||
}
|
||||
return CoverageWorkerStatus.pending;
|
||||
}
|
||||
|
||||
String? _formatTime(fdc.Timestamp? timestamp) {
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
final DateTime date = timestamp.toDateTime().toLocal();
|
||||
final String hour = date.hour.toString().padLeft(2, '0');
|
||||
final String minute = date.minute.toString().padLeft(2, '0');
|
||||
return '$hour:$minute';
|
||||
}
|
||||
}
|
||||
|
||||
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<CoverageWorker> workers;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user