diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart index 28644dec..6a293ac8 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart +++ b/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 2 /// 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 // ignore_for_file: type=lint, unused_import diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 8bab1718..df3a825c 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -29,6 +29,7 @@ export 'src/entities/events/work_session.dart'; // Shifts export 'src/entities/shifts/shift.dart'; +export 'src/adapters/shifts/shift_adapter.dart'; // Orders & Requests export 'src/entities/orders/order_type.dart'; diff --git a/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart new file mode 100644 index 00000000..07cab44a --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/shifts/shift_adapter.dart @@ -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. +} diff --git a/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart b/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart index d2db5ae7..e7cbf17d 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/payments_module.dart @@ -10,10 +10,7 @@ import 'presentation/pages/payments_page.dart'; class StaffPaymentsModule extends Module { @override void binds(Injector i) { - // Data Connect Mocks - i.add(FinancialRepositoryMock.new); - - // Repositories + // Repositories i.add(PaymentsRepositoryImpl.new); // Use Cases diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index f2cf4d74..b94a748f 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -1,22 +1,15 @@ -import 'package:krow_data_connect/krow_data_connect.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/src/session/staff_session_store.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:intl/intl.dart'; +import 'package:firebase_auth/firebase_auth.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 { - ShiftsRepositoryImpl(); + final dc.ExampleConnector _dataConnect; + final FirebaseAuth _auth = FirebaseAuth.instance; + + ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; // Cache: ShiftID -> ApplicationID (For Accept/Decline) final Map _shiftToAppIdMap = {}; @@ -24,193 +17,205 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { final Map _appToRoleIdMap = {}; String get _currentStaffId { - final session = StaffSessionStore.instance.session; - if (session?.staff?.id == null) throw Exception('User not logged in'); - return session!.staff!.id; + final StaffSession? session = StaffSessionStore.instance.session; + if (session?.staff?.id != null) { + 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 Future> getMyShifts() async { - return _fetchApplications(ApplicationStatus.ACCEPTED); + return _fetchApplications(dc.ApplicationStatus.ACCEPTED); } - + @override Future> getPendingAssignments() async { - // Fetch both PENDING (User applied) and OFFERED (Business offered) if schema supports - // For now assuming PENDING covers invitations/offers. - return _fetchApplications(ApplicationStatus.PENDING); + return _fetchApplications(dc.ApplicationStatus.PENDING); } - Future> _fetchApplications(ApplicationStatus status) async { + Future> _fetchApplications(dc.ApplicationStatus status) async { try { - final response = await ExampleConnector.instance + final response = await _dataConnect .getApplicationsByStaffId(staffId: _currentStaffId) .execute(); - return response.data.applications - .where((app) => app.status is Known && (app.status as Known).value == status) - .map((app) { - // Cache IDs for actions - _shiftToAppIdMap[app.shift.id] = app.id; - _appToRoleIdMap[app.id] = app.shiftRole.roleId; + final apps = response.data.applications.where((app) => app.status == status); + final List shifts = []; - return _mapApplicationToShift(app); - }) - .toList(); + for (final app in apps) { + _shiftToAppIdMap[app.shift.id] = app.id; + _appToRoleIdMap[app.id] = app.shiftRole.id; + + final shiftTuple = await _getShiftDetails(app.shift.id); + if (shiftTuple != null) { + shifts.add(shiftTuple); + } + } + return shifts; } catch (e) { - return []; + return []; } } @override Future> getAvailableShifts(String query, String type) async { - try { - final response = await ExampleConnector.instance.listShifts().execute(); - - var shifts = response.data.shifts - .where((s) => s.status is Known && (s.status as Known).value == ShiftStatus.OPEN) - .map((s) => _mapConnectorShiftToDomain(s)) - .toList(); - - // Client-side filtering - if (query.isNotEmpty) { - shifts = shifts.where((s) => - s.title.toLowerCase().contains(query.toLowerCase()) || - s.clientName.toLowerCase().contains(query.toLowerCase()) - ).toList(); - } - - if (type != 'all') { - if (type == 'one-day') { - shifts = shifts.where((s) => !s.title.contains('Multi-Day')).toList(); - } else if (type == 'multi-day') { - shifts = shifts.where((s) => s.title.contains('Multi-Day')).toList(); + try { + final result = await _dataConnect.listShifts().execute(); + final allShifts = result.data.shifts; + + final List mappedShifts = []; + + for (final s in allShifts) { + // For each shift, map to Domain Shift + // Note: date fields in generated code might be specific types + final startDt = _toDateTime(s.startTime); + final endDt = _toDateTime(s.endTime); + final createdDt = _toDateTime(s.createdAt); + + mappedShifts.add(Shift( + id: s.id, + title: s.title, + clientName: s.order.business.businessName, + logoUrl: null, + hourlyRate: s.cost ?? 0.0, + location: s.location ?? '', + 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) { - return []; - } + if (query.isNotEmpty) { + return mappedShifts.where((s) => + s.title.toLowerCase().contains(query.toLowerCase()) || + s.clientName.toLowerCase().contains(query.toLowerCase()) + ).toList(); + } + + return mappedShifts; + + } catch (e) { + return []; + } } @override Future getShiftDetails(String shiftId) async { - try { - final response = await ExampleConnector.instance.getShiftById(id: shiftId).execute(); - final s = response.data.shift; - if (s == null) return null; - - // Map to domain Shift - 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, - ); - } catch (e) { - return null; - } + return _getShiftDetails(shiftId); + } + + Future _getShiftDetails(String shiftId) async { + try { + final result = await _dataConnect.getShiftById(id: shiftId).execute(); + final s = result.data.shift; + if (s == null) return null; + + final startDt = _toDateTime(s.startTime); + final endDt = _toDateTime(s.endTime); + final createdDt = _toDateTime(s.createdAt); + + return Shift( + id: s.id, + title: s.title, + clientName: s.order.business.businessName, + logoUrl: null, + hourlyRate: s.cost ?? 0.0, + location: s.location ?? '', + 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, + ); + } catch (e) { + return null; + } } @override Future applyForShift(String shiftId) async { - // API LIMITATION: 'createApplication' requires roleId. - // 'listShifts' / 'getShiftById' does not currently return the Shift's available Roles. - // We cannot reliably apply for a shift without knowing the Role ID. - // Falling back to Mock delay for now. - await Future.delayed(const Duration(milliseconds: 500)); - - // In future: - // 1. Fetch Shift Roles - // 2. Select Role - // 3. createApplication(shiftId, roleId, staffId, status: PENDING, origin: MOBILE) + final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute(); + if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift'); + + final role = rolesResult.data.shiftRoles.first; + + await _dataConnect.createApplication( + shiftId: shiftId, + staffId: _currentStaffId, + roleId: role.id, + status: dc.ApplicationStatus.PENDING, + origin: dc.ApplicationOrigin.STAFF, + ).execute(); } - + @override Future acceptShift(String shiftId) async { - await _updateApplicationStatus(shiftId, ApplicationStatus.ACCEPTED); + await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED); } @override Future declineShift(String shiftId) async { - await _updateApplicationStatus(shiftId, ApplicationStatus.REJECTED); + await _updateApplicationStatus(shiftId, dc.ApplicationStatus.REJECTED); } - Future _updateApplicationStatus(String shiftId, ApplicationStatus newStatus) async { + Future _updateApplicationStatus(String shiftId, dc.ApplicationStatus newStatus) async { String? appId = _shiftToAppIdMap[shiftId]; String? roleId; - // Refresh if missing from cache if (appId == null) { - await getPendingAssignments(); - appId = _shiftToAppIdMap[shiftId]; + // Try to find it in pending + 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) { throw Exception("Application not found for shift $shiftId"); } - await ExampleConnector.instance.updateApplicationStatus( + await _dataConnect.updateApplicationStatus( id: appId, roleId: roleId, ) .status(newStatus) .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: [], - ); - } } - diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart index e89ded58..0458fd33 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart @@ -157,19 +157,6 @@ class _ShiftsPageState extends State { 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),