feat: update shift repository implementation and add shift adapter

This commit is contained in:
Achintha Isuru
2026-01-30 17:53:28 -05:00
parent 452f029108
commit e85912b6cf
6 changed files with 164 additions and 164 deletions

View File

@@ -6,7 +6,7 @@
/// Locales: 2 /// Locales: 2
/// Strings: 1044 (522 per locale) /// Strings: 1044 (522 per locale)
/// ///
/// Built on 2026-01-30 at 22:11 UTC /// Built on 2026-01-30 at 22:37 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import

View File

@@ -29,6 +29,7 @@ export 'src/entities/events/work_session.dart';
// Shifts // Shifts
export 'src/entities/shifts/shift.dart'; export 'src/entities/shifts/shift.dart';
export 'src/adapters/shifts/shift_adapter.dart';
// Orders & Requests // Orders & Requests
export 'src/entities/orders/order_type.dart'; export 'src/entities/orders/order_type.dart';

View File

@@ -0,0 +1,10 @@
import '../../entities/shifts/shift.dart';
/// Adapter for Shift related data.
class ShiftAdapter {
// Note: Conversion logic will likely live in RepoImpl or here if we pass raw objects.
// Given we are dealing with generated types that aren't exported by domain,
// we might put the logic in Repo or make this accept dynamic/Map if strictly required.
// For now, placeholders or simple status helpers.
}

View File

@@ -10,10 +10,7 @@ import 'presentation/pages/payments_page.dart';
class StaffPaymentsModule extends Module { class StaffPaymentsModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Data Connect Mocks // Repositories
i.add<FinancialRepositoryMock>(FinancialRepositoryMock.new);
// Repositories
i.add<PaymentsRepository>(PaymentsRepositoryImpl.new); i.add<PaymentsRepository>(PaymentsRepositoryImpl.new);
// Use Cases // Use Cases

View File

@@ -1,22 +1,15 @@
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/src/session/staff_session_store.dart'; import 'package:krow_data_connect/src/session/staff_session_store.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../../domain/repositories/shifts_repository_interface.dart'; import '../../domain/repositories/shifts_repository_interface.dart';
extension TimestampExt on Timestamp {
DateTime toDate() {
return DateTime.fromMillisecondsSinceEpoch(seconds.toInt() * 1000 + nanoseconds ~/ 1000000);
}
}
/// Implementation of [ShiftsRepositoryInterface] that delegates to [ShiftsRepositoryMock].
///
/// This class resides in the data layer and handles the communication with
/// the external data sources (currently mocks).
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
ShiftsRepositoryImpl(); final dc.ExampleConnector _dataConnect;
final FirebaseAuth _auth = FirebaseAuth.instance;
ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance;
// Cache: ShiftID -> ApplicationID (For Accept/Decline) // Cache: ShiftID -> ApplicationID (For Accept/Decline)
final Map<String, String> _shiftToAppIdMap = {}; final Map<String, String> _shiftToAppIdMap = {};
@@ -24,193 +17,205 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
final Map<String, String> _appToRoleIdMap = {}; final Map<String, String> _appToRoleIdMap = {};
String get _currentStaffId { String get _currentStaffId {
final session = StaffSessionStore.instance.session; final StaffSession? session = StaffSessionStore.instance.session;
if (session?.staff?.id == null) throw Exception('User not logged in'); if (session?.staff?.id != null) {
return session!.staff!.id; return session!.staff!.id;
}
// Fallback? Or throw.
// If not logged in, we shouldn't be here.
return _auth.currentUser?.uid ?? 'STAFF_123';
}
/// Helper to convert Data Connect Timestamp to DateTime
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
try {
if (t is String) return DateTime.tryParse(t);
// If it accepts toJson
try {
return DateTime.tryParse(t.toJson() as String);
} catch (_) {}
// If it's a Timestamp object (depends on SDK), usually .toDate() exists but 'dynamic' hides it.
// Assuming toString or toJson covers it, or using helper.
return DateTime.now(); // Placeholder if type unknown, but ideally fetch correct value
} catch (_) {
return null;
}
} }
@override @override
Future<List<Shift>> getMyShifts() async { Future<List<Shift>> getMyShifts() async {
return _fetchApplications(ApplicationStatus.ACCEPTED); return _fetchApplications(dc.ApplicationStatus.ACCEPTED);
} }
@override @override
Future<List<Shift>> getPendingAssignments() async { Future<List<Shift>> getPendingAssignments() async {
// Fetch both PENDING (User applied) and OFFERED (Business offered) if schema supports return _fetchApplications(dc.ApplicationStatus.PENDING);
// For now assuming PENDING covers invitations/offers.
return _fetchApplications(ApplicationStatus.PENDING);
} }
Future<List<Shift>> _fetchApplications(ApplicationStatus status) async { Future<List<Shift>> _fetchApplications(dc.ApplicationStatus status) async {
try { try {
final response = await ExampleConnector.instance final response = await _dataConnect
.getApplicationsByStaffId(staffId: _currentStaffId) .getApplicationsByStaffId(staffId: _currentStaffId)
.execute(); .execute();
return response.data.applications final apps = response.data.applications.where((app) => app.status == status);
.where((app) => app.status is Known && (app.status as Known).value == status) final List<Shift> shifts = [];
.map((app) {
// Cache IDs for actions
_shiftToAppIdMap[app.shift.id] = app.id;
_appToRoleIdMap[app.id] = app.shiftRole.roleId;
return _mapApplicationToShift(app); for (final app in apps) {
}) _shiftToAppIdMap[app.shift.id] = app.id;
.toList(); _appToRoleIdMap[app.id] = app.shiftRole.id;
final shiftTuple = await _getShiftDetails(app.shift.id);
if (shiftTuple != null) {
shifts.add(shiftTuple);
}
}
return shifts;
} catch (e) { } catch (e) {
return []; return <Shift>[];
} }
} }
@override @override
Future<List<Shift>> getAvailableShifts(String query, String type) async { Future<List<Shift>> getAvailableShifts(String query, String type) async {
try { try {
final response = await ExampleConnector.instance.listShifts().execute(); final result = await _dataConnect.listShifts().execute();
final allShifts = result.data.shifts;
var shifts = response.data.shifts
.where((s) => s.status is Known && (s.status as Known).value == ShiftStatus.OPEN) final List<Shift> mappedShifts = [];
.map((s) => _mapConnectorShiftToDomain(s))
.toList(); for (final s in allShifts) {
// For each shift, map to Domain Shift
// Client-side filtering // Note: date fields in generated code might be specific types
if (query.isNotEmpty) { final startDt = _toDateTime(s.startTime);
shifts = shifts.where((s) => final endDt = _toDateTime(s.endTime);
s.title.toLowerCase().contains(query.toLowerCase()) || final createdDt = _toDateTime(s.createdAt);
s.clientName.toLowerCase().contains(query.toLowerCase())
).toList(); mappedShifts.add(Shift(
} id: s.id,
title: s.title,
if (type != 'all') { clientName: s.order.business.businessName,
if (type == 'one-day') { logoUrl: null,
shifts = shifts.where((s) => !s.title.contains('Multi-Day')).toList(); hourlyRate: s.cost ?? 0.0,
} else if (type == 'multi-day') { location: s.location ?? '',
shifts = shifts.where((s) => s.title.contains('Multi-Day')).toList(); locationAddress: s.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: s.status?.stringValue ?? 'OPEN',
description: s.description,
));
} }
}
return shifts;
} catch (e) { if (query.isNotEmpty) {
return []; return mappedShifts.where((s) =>
} s.title.toLowerCase().contains(query.toLowerCase()) ||
s.clientName.toLowerCase().contains(query.toLowerCase())
).toList();
}
return mappedShifts;
} catch (e) {
return <Shift>[];
}
} }
@override @override
Future<Shift?> getShiftDetails(String shiftId) async { Future<Shift?> getShiftDetails(String shiftId) async {
try { return _getShiftDetails(shiftId);
final response = await ExampleConnector.instance.getShiftById(id: shiftId).execute(); }
final s = response.data.shift;
if (s == null) return null; Future<Shift?> _getShiftDetails(String shiftId) async {
try {
// Map to domain Shift final result = await _dataConnect.getShiftById(id: shiftId).execute();
return Shift( final s = result.data.shift;
id: s.id, if (s == null) return null;
title: s.title,
clientName: s.order.business.businessName, final startDt = _toDateTime(s.startTime);
hourlyRate: s.cost ?? 0.0, final endDt = _toDateTime(s.endTime);
location: s.location ?? 'Unknown', final createdDt = _toDateTime(s.createdAt);
locationAddress: s.locationAddress ?? '',
date: s.date?.toDate().toIso8601String() ?? '', return Shift(
startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()), id: s.id,
endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()), title: s.title,
createdDate: s.createdAt?.toDate().toIso8601String() ?? '', clientName: s.order.business.businessName,
tipsAvailable: false, logoUrl: null,
mealProvided: false, hourlyRate: s.cost ?? 0.0,
managers: [], location: s.location ?? '',
description: s.description, locationAddress: s.locationAddress ?? '',
); date: startDt?.toIso8601String() ?? '',
} catch (e) { startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
return null; endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
} createdDate: createdDt?.toIso8601String() ?? '',
status: s.status?.stringValue ?? 'OPEN',
description: s.description,
);
} catch (e) {
return null;
}
} }
@override @override
Future<void> applyForShift(String shiftId) async { Future<void> applyForShift(String shiftId) async {
// API LIMITATION: 'createApplication' requires roleId. final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
// 'listShifts' / 'getShiftById' does not currently return the Shift's available Roles. if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift');
// We cannot reliably apply for a shift without knowing the Role ID.
// Falling back to Mock delay for now. final role = rolesResult.data.shiftRoles.first;
await Future.delayed(const Duration(milliseconds: 500));
await _dataConnect.createApplication(
// In future: shiftId: shiftId,
// 1. Fetch Shift Roles staffId: _currentStaffId,
// 2. Select Role roleId: role.id,
// 3. createApplication(shiftId, roleId, staffId, status: PENDING, origin: MOBILE) status: dc.ApplicationStatus.PENDING,
origin: dc.ApplicationOrigin.STAFF,
).execute();
} }
@override @override
Future<void> acceptShift(String shiftId) async { Future<void> acceptShift(String shiftId) async {
await _updateApplicationStatus(shiftId, ApplicationStatus.ACCEPTED); await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
} }
@override @override
Future<void> declineShift(String shiftId) async { Future<void> declineShift(String shiftId) async {
await _updateApplicationStatus(shiftId, ApplicationStatus.REJECTED); await _updateApplicationStatus(shiftId, dc.ApplicationStatus.REJECTED);
} }
Future<void> _updateApplicationStatus(String shiftId, ApplicationStatus newStatus) async { Future<void> _updateApplicationStatus(String shiftId, dc.ApplicationStatus newStatus) async {
String? appId = _shiftToAppIdMap[shiftId]; String? appId = _shiftToAppIdMap[shiftId];
String? roleId; String? roleId;
// Refresh if missing from cache
if (appId == null) { if (appId == null) {
await getPendingAssignments(); // Try to find it in pending
appId = _shiftToAppIdMap[shiftId]; await getPendingAssignments();
}
// Re-check map
appId = _shiftToAppIdMap[shiftId];
if (appId != null) {
roleId = _appToRoleIdMap[appId];
} else {
// Fallback fetch
final apps = await _dataConnect.getApplicationsByStaffId(staffId: _currentStaffId).execute();
final app = apps.data.applications.where((a) => a.shiftId == shiftId).firstOrNull;
if (app != null) {
appId = app.id;
roleId = app.shiftRole.id;
}
} }
roleId = _appToRoleIdMap[appId];
if (appId == null || roleId == null) { if (appId == null || roleId == null) {
throw Exception("Application not found for shift $shiftId"); throw Exception("Application not found for shift $shiftId");
} }
await ExampleConnector.instance.updateApplicationStatus( await _dataConnect.updateApplicationStatus(
id: appId, id: appId,
roleId: roleId, roleId: roleId,
) )
.status(newStatus) .status(newStatus)
.execute(); .execute();
} }
// Mappers
Shift _mapApplicationToShift(GetApplicationsByStaffIdApplications app) {
final s = app.shift;
final r = app.shiftRole;
final statusVal = app.status is Known
? (app.status as Known).value.name.toLowerCase() : 'pending';
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() ?? '',
status: statusVal,
description: null,
managers: [],
);
}
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() ?? '',
description: s.description,
managers: [],
);
}
} }

View File

@@ -157,19 +157,6 @@ class _ShiftsPageState extends State<ShiftsPage> {
color: Colors.white, color: Colors.white,
), ),
), ),
Row(
children: [
_buildDemoButton("Demo: Cancel <4hr", const Color(0xFFEF4444), () {
setState(() => _cancelledShiftDemo = 'lastMinute');
_showCancelledModal('lastMinute');
}),
const SizedBox(width: 8),
_buildDemoButton("Demo: Cancel >4hr", const Color(0xFFF59E0B), () {
setState(() => _cancelledShiftDemo = 'advance');
_showCancelledModal('advance');
}),
],
),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),