diff --git a/.gitignore b/.gitignore index 33036db1..9dd0e50a 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,7 @@ internal/launchpad/prototypes-src/ # Temporary migration artifacts _legacy/ krow-workforce-export-latest/ + +# Data Connect Generated SDKs (Explicit) +apps/mobile/packages/data_connect/lib/src/dataconnect_generated/ +apps/web/src/dataconnect-generated/ diff --git a/apps/mobile/apps/client/test/widget_test.dart b/apps/mobile/apps/client/test/widget_test.dart deleted file mode 100644 index 1888d2ab..00000000 --- a/apps/mobile/apps/client/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:client/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/apps/mobile/apps/design_system_viewer/lib/main.dart b/apps/mobile/apps/design_system_viewer/lib/main.dart index 7bd967d4..78c4dc89 100644 --- a/apps/mobile/apps/design_system_viewer/lib/main.dart +++ b/apps/mobile/apps/design_system_viewer/lib/main.dart @@ -28,7 +28,7 @@ class MyApp extends StatelessWidget { // // This works for code too, not just values: Most code changes can be // tested with just a hot reload. - colorScheme: .fromSeed(seedColor: Colors.deepPurple), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); @@ -102,7 +102,7 @@ class _MyHomePageState extends State { // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" // action in the IDE, or press "p" in the console), to see the // wireframe for each widget. - mainAxisAlignment: .center, + mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('You have pushed the button this many times:'), Text( diff --git a/apps/mobile/apps/design_system_viewer/test/widget_test.dart b/apps/mobile/apps/design_system_viewer/test/widget_test.dart deleted file mode 100644 index 59ec1d1b..00000000 --- a/apps/mobile/apps/design_system_viewer/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:design_system_viewer/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/apps/mobile/apps/staff/test/widget_test.dart b/apps/mobile/apps/staff/test/widget_test.dart deleted file mode 100644 index 1e60e4a3..00000000 --- a/apps/mobile/apps/staff/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:staff/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart b/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart index f3bd8d69..086cb954 100644 --- a/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart +++ b/apps/mobile/packages/core_localization/lib/src/data/repositories_impl/locale_repository_impl.dart @@ -22,12 +22,6 @@ class LocaleRepositoryImpl implements LocaleRepositoryInterface { @override Future getSavedLocale() async { return getDefaultLocale(); - - /// TODO: FEATURE_NOT_IMPLEMENTED: Implement saved locale retrieval later - final String? languageCode = await localDataSource.getLanguageCode(); - if (languageCode != null) { - return Locale(languageCode); - } } @override diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 2aa6c54f..9eaf5902 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -369,8 +369,7 @@ "export_button": "Export All Invoices", "pending_badge": "PENDING APPROVAL", "paid_badge": "PAID" - } - , + }, "staff": { "main": { "tabs": { @@ -422,8 +421,7 @@ "today": "Today", "applied_for": "Applied for $title", "time_range": "$start - $end" - } - , + }, "benefits": { "title": "Your Benefits", "view_all": "View all", @@ -463,8 +461,14 @@ "more_ways": { "title": "More Ways To Use Krow", "items": { - "benefits": { "title": "Krow Benefits", "page": "/benefits" }, - "refer": { "title": "Refer a Friend", "page": "/worker-profile" } + "benefits": { + "title": "Krow Benefits", + "page": "/benefits" + }, + "refer": { + "title": "Refer a Friend", + "page": "/worker-profile" + } } } }, @@ -598,8 +602,8 @@ "progress": "$completed/$total Complete" }, "list": { - "empty": "No documents found", - "error": "Error: $message" + "empty": "No documents found", + "error": "Error: $message" }, "card": { "view": "View", @@ -675,43 +679,43 @@ "staff_shifts": { "title": "Shifts", "tabs": { - "my_shifts": "My Shifts", - "find_work": "Find Work" + "my_shifts": "My Shifts", + "find_work": "Find Work" }, "list": { - "no_shifts": "No shifts found", - "pending_offers": "PENDING OFFERS", - "available_jobs": "$count AVAILABLE JOBS", - "search_hint": "Search jobs..." + "no_shifts": "No shifts found", + "pending_offers": "PENDING OFFERS", + "available_jobs": "$count AVAILABLE JOBS", + "search_hint": "Search jobs..." }, "filter": { - "all": "All Jobs", - "one_day": "One Day", - "multi_day": "Multi Day", - "long_term": "Long Term" + "all": "All Jobs", + "one_day": "One Day", + "multi_day": "Multi Day", + "long_term": "Long Term" }, "status": { - "confirmed": "CONFIRMED", - "act_now": "ACT NOW", - "swap_requested": "SWAP REQUESTED", - "completed": "COMPLETED", - "no_show": "NO SHOW", - "pending_warning": "Please confirm assignment" + "confirmed": "CONFIRMED", + "act_now": "ACT NOW", + "swap_requested": "SWAP REQUESTED", + "completed": "COMPLETED", + "no_show": "NO SHOW", + "pending_warning": "Please confirm assignment" }, "action": { - "decline": "Decline", - "confirm": "Confirm", - "request_swap": "Request Swap" + "decline": "Decline", + "confirm": "Confirm", + "request_swap": "Request Swap" }, "details": { - "additional": "ADDITIONAL DETAILS", - "days": "$days Days", - "exp_total": "(exp.total \\$$amount)", - "pending_time": "Pending $time ago" + "additional": "ADDITIONAL DETAILS", + "days": "$days Days", + "exp_total": "(exp.total \\$$amount)", + "pending_time": "Pending $time ago" }, "tags": { - "immediate_start": "Immediate start", - "no_experience": "No experience" + "immediate_start": "Immediate start", + "no_experience": "No experience" } }, "staff_time_card": { @@ -767,7 +771,9 @@ }, "generic": { "unknown": "Something went wrong. Please try again.", - "no_connection": "No internet connection. Please check your network and try again." + "no_connection": "No internet connection. Please check your network and try again.", + "server_error": "Server error. Please try again later.", + "service_unavailable": "Service is currently unavailable." } }, "success": { @@ -783,5 +789,4 @@ "updated": "Profile updated successfully!" } } -} - +} \ No newline at end of file diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 6e7e8f1c..d25040bc 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -369,8 +369,7 @@ "export_button": "Exportar Todas las Facturas", "pending_badge": "PENDIENTE APROBACIÓN", "paid_badge": "PAGADO" - } - , + }, "staff": { "main": { "tabs": { @@ -462,8 +461,14 @@ "more_ways": { "title": "More Ways To Use Krow", "items": { - "benefits": { "title": "Krow Benefits", "page": "/benefits" }, - "refer": { "title": "Refer a Friend", "page": "/worker-profile" } + "benefits": { + "title": "Krow Benefits", + "page": "/benefits" + }, + "refer": { + "title": "Refer a Friend", + "page": "/worker-profile" + } } } }, @@ -674,43 +679,43 @@ "staff_shifts": { "title": "Shifts", "tabs": { - "my_shifts": "My Shifts", - "find_work": "Find Work" + "my_shifts": "My Shifts", + "find_work": "Find Work" }, "list": { - "no_shifts": "No shifts found", - "pending_offers": "PENDING OFFERS", - "available_jobs": "$count AVAILABLE JOBS", - "search_hint": "Search jobs..." + "no_shifts": "No shifts found", + "pending_offers": "PENDING OFFERS", + "available_jobs": "$count AVAILABLE JOBS", + "search_hint": "Search jobs..." }, "filter": { - "all": "All Jobs", - "one_day": "One Day", - "multi_day": "Multi Day", - "long_term": "Long Term" + "all": "All Jobs", + "one_day": "One Day", + "multi_day": "Multi Day", + "long_term": "Long Term" }, "status": { - "confirmed": "CONFIRMED", - "act_now": "ACT NOW", - "swap_requested": "SWAP REQUESTED", - "completed": "COMPLETED", - "no_show": "NO SHOW", - "pending_warning": "Please confirm assignment" + "confirmed": "CONFIRMED", + "act_now": "ACT NOW", + "swap_requested": "SWAP REQUESTED", + "completed": "COMPLETED", + "no_show": "NO SHOW", + "pending_warning": "Please confirm assignment" }, "action": { - "decline": "Decline", - "confirm": "Confirm", - "request_swap": "Request Swap" + "decline": "Decline", + "confirm": "Confirm", + "request_swap": "Request Swap" }, "details": { - "additional": "ADDITIONAL DETAILS", - "days": "$days Days", - "exp_total": "(exp.total \\$$amount)", - "pending_time": "Pending $time ago" + "additional": "ADDITIONAL DETAILS", + "days": "$days Days", + "exp_total": "(exp.total \\$$amount)", + "pending_time": "Pending $time ago" }, "tags": { - "immediate_start": "Immediate start", - "no_experience": "No experience" + "immediate_start": "Immediate start", + "no_experience": "No experience" } }, "staff_time_card": { @@ -766,7 +771,9 @@ }, "generic": { "unknown": "Algo salió mal. Por favor, intenta de nuevo.", - "no_connection": "Sin conexión a internet. Por favor, verifica tu red e intenta de nuevo." + "no_connection": "Sin conexión a internet. Por favor, verifica tu red e intenta de nuevo.", + "server_error": "Error del servidor. Inténtalo de nuevo más tarde.", + "service_unavailable": "El servicio no está disponible actualmente." } }, "success": { @@ -782,4 +789,4 @@ "updated": "¡Perfil actualizado exitosamente!" } } -} +} \ No newline at end of file diff --git a/apps/mobile/packages/core_localization/test/localization_test.dart b/apps/mobile/packages/core_localization/test/localization_test.dart deleted file mode 100644 index 0d7a0e1e..00000000 --- a/apps/mobile/packages/core_localization/test/localization_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:localization/localization.dart'; - -void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); - }); -} diff --git a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart index b57a46be..277ad737 100644 --- a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart +++ b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart @@ -7,21 +7,12 @@ /// They will implement interfaces defined in feature packages once those are created. library; -export 'src/mocks/auth_repository_mock.dart'; -export 'src/mocks/shifts_repository_mock.dart'; -export 'src/mocks/staff_repository_mock.dart'; -export 'src/mocks/profile_repository_mock.dart'; -export 'src/mocks/event_repository_mock.dart'; -export 'src/mocks/skill_repository_mock.dart'; -export 'src/mocks/financial_repository_mock.dart'; -export 'src/mocks/rating_repository_mock.dart'; -export 'src/mocks/support_repository_mock.dart'; -export 'src/mocks/home_repository_mock.dart'; -export 'src/mocks/business_repository_mock.dart'; -export 'src/mocks/order_repository_mock.dart'; + export 'src/data_connect_module.dart'; export 'src/session/client_session_store.dart'; // Export the generated Data Connect SDK export 'src/dataconnect_generated/generated.dart'; + export 'src/session/staff_session_store.dart'; +export 'src/mixins/data_error_handler.dart'; diff --git a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart index 984db50d..8f7aa678 100644 --- a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart +++ b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart @@ -1,19 +1,10 @@ import 'package:flutter_modular/flutter_modular.dart'; -import 'mocks/auth_repository_mock.dart'; -import 'mocks/business_repository_mock.dart'; -import 'mocks/home_repository_mock.dart'; -import 'mocks/order_repository_mock.dart'; -import 'mocks/profile_repository_mock.dart'; -/// A module that provides Data Connect dependencies, including mocks. +/// A module that provides Data Connect dependencies. class DataConnectModule extends Module { @override void exportedBinds(Injector i) { - // Make these mocks available to any module that imports this one. - i.addLazySingleton(AuthRepositoryMock.new); - i.addLazySingleton(ProfileRepositoryMock.new); - i.addLazySingleton(HomeRepositoryMock.new); - i.addLazySingleton(BusinessRepositoryMock.new); - i.addLazySingleton(OrderRepositoryMock.new); + // No mock bindings anymore. + // Real repositories are instantiated in their feature modules. } } diff --git a/apps/mobile/packages/data_connect/lib/src/mixins/data_error_handler.dart b/apps/mobile/packages/data_connect/lib/src/mixins/data_error_handler.dart new file mode 100644 index 00000000..d84b24e1 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/mixins/data_error_handler.dart @@ -0,0 +1,44 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Mixin to handle Data Layer errors and map them to Domain Failures. +/// +/// Use this in Repositories to wrap remote calls. +/// It catches [SocketException], [FirebaseException], etc., and throws [AppException]. +mixin DataErrorHandler { + /// Executes a Future and maps low-level exceptions to [AppException]. + /// + /// [timeout] defaults to 30 seconds. + Future executeProtected( + Future Function() action, { + Duration timeout = const Duration(seconds: 30), + }) async { + try { + return await action().timeout(timeout); + } on TimeoutException { + throw ServiceUnavailableException( + technicalMessage: 'Request timed out after ${timeout.inSeconds}s'); + } on SocketException catch (e) { + throw NetworkException(technicalMessage: 'SocketException: ${e.message}'); + } on FirebaseException catch (e) { + if (e.code == 'unavailable' || e.code == 'network-request-failed') { + throw NetworkException( + technicalMessage: 'Firebase ${e.code}: ${e.message}'); + } + if (e.code == 'deadline-exceeded') { + throw ServiceUnavailableException( + technicalMessage: 'Firebase ${e.code}: ${e.message}'); + } + // Fallback for other Firebase errors + throw ServerException( + technicalMessage: 'Firebase ${e.code}: ${e.message}'); + } catch (e) { + // If it's already an AppException, rethrow it + if (e is AppException) rethrow; + throw UnknownException(technicalMessage: e.toString()); + } + } +} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/auth_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/auth_repository_mock.dart deleted file mode 100644 index 1b571cb3..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/auth_repository_mock.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement AuthRepositoryInterface once defined in a feature package. -class AuthRepositoryMock { - Stream get currentUser => Stream.value( - const User(id: 'mock_user_1', email: 'test@krow.com', role: 'staff'), - ); - - Future signInWithPhone(String phoneNumber) async { - await Future.delayed(const Duration(milliseconds: 500)); - return 'mock_verification_id'; - } - - Future verifyOtp(String verificationId, String smsCode) async { - await Future.delayed(const Duration(milliseconds: 500)); - return const User(id: 'mock_user_1', email: 'test@krow.com', role: 'staff'); - } - - Future signOut() async { - await Future.delayed(const Duration(milliseconds: 200)); - } - - /// Signs in a user with email and password (Mock). - Future signInWithEmail(String email, String password) async { - await Future.delayed(const Duration(milliseconds: 500)); - return User(id: 'mock_client_1', email: email, role: 'client_admin'); - } - - /// Registers a new user with email and password (Mock). - Future signUpWithEmail( - String email, - String password, - String companyName, - ) async { - await Future.delayed(const Duration(milliseconds: 500)); - return User(id: 'mock_client_new', email: email, role: 'client_admin'); - } - - /// Authenticates using a social provider (Mock). - Future signInWithSocial(String provider) async { - await Future.delayed(const Duration(milliseconds: 500)); - return const User( - id: 'mock_social_user', - email: 'social@example.com', - role: 'client_admin', - ); - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart deleted file mode 100644 index 6ed624ef..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement BusinessRepositoryInterface once defined in a feature package. -class BusinessRepositoryMock { - Future getBusiness(String id) async { - await Future.delayed(const Duration(milliseconds: 300)); - return const Business( - id: 'biz_1', - name: 'Acme Events Ltd', - registrationNumber: 'REG123456', - status: BusinessStatus.active, - avatar: 'https://via.placeholder.com/150', - ); - } - - Future> getHubs(String businessId) async { - await Future.delayed(const Duration(milliseconds: 300)); - return [ - const Hub( - id: 'hub_1', - businessId: 'biz_1', - name: 'London HQ', - address: '123 Oxford Street, London', - status: HubStatus.active, - ), - ]; - } - - Future createHub({ - required String businessId, - required String name, - required String address, - }) async { - await Future.delayed(const Duration(milliseconds: 500)); - return Hub( - id: 'hub_${DateTime.now().millisecondsSinceEpoch}', - businessId: businessId, - name: name, - address: address, - status: HubStatus.active, - ); - } - - Future deleteHub(String id) async { - await Future.delayed(const Duration(milliseconds: 300)); - } - - Future assignNfcTag({ - required String hubId, - required String nfcTagId, - }) async { - await Future.delayed(const Duration(milliseconds: 500)); - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart deleted file mode 100644 index 4bbfb69e..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement EventRepositoryInterface once defined in a feature package. -class EventRepositoryMock { - Future applyForPosition(String positionId, String staffId) async { - await Future.delayed(const Duration(milliseconds: 600)); - return Assignment( - id: 'assign_1', - positionId: positionId, - staffId: staffId, - status: AssignmentStatus.assigned, - ); - } - - Future getEvent(String id) async { - await Future.delayed(const Duration(milliseconds: 300)); - return _mockEvent; - } - - Future> getEventShifts(String eventId) async { - await Future.delayed(const Duration(milliseconds: 300)); - return [ - const EventShift( - id: 'shift_1', - eventId: 'event_1', - name: 'Morning Setup', - address: 'Hyde Park, London', - ), - ]; - } - - Future> getStaffAssignments(String staffId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - const Assignment( - id: 'assign_1', - positionId: 'pos_1', - staffId: 'staff_1', - status: AssignmentStatus.confirmed, - ), - ]; - } - - Future> getUpcomingEvents() async { - await Future.delayed(const Duration(milliseconds: 800)); - return [_mockEvent]; - } - - static final Event _mockEvent = Event( - id: 'event_1', - businessId: 'biz_1', - hubId: 'hub_1', - name: 'Summer Festival 2026', - date: DateTime.now().add(const Duration(days: 10)), - status: EventStatus.active, - contractType: 'freelance', - ); -} \ No newline at end of file diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart deleted file mode 100644 index 9da38f9f..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement FinancialRepositoryInterface once defined in a feature package. -class FinancialRepositoryMock { - Future> getInvoices(String businessId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - const Invoice( - id: 'inv_1', - eventId: 'event_1', - businessId: 'biz_1', - status: InvoiceStatus.paid, - totalAmount: 1500.0, - workAmount: 1400.0, - addonsAmount: 100.0, - invoiceNumber: 'INV-1', - issueDate: null, - ), - ]; - } - - Future> getStaffPayments(String staffId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - StaffPayment( - id: 'pay_1', - staffId: staffId, - assignmentId: 'assign_1', - amount: 120.0, - status: PaymentStatus.paid, - paidAt: DateTime.now().subtract(const Duration(days: 2)), - ), - ]; - } - - Future> getInvoiceItems(String invoiceId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - const InvoiceItem( - id: 'item_1', - invoiceId: 'inv_1', - staffId: 'staff_1', - workHours: 8.0, - rate: 25.0, - amount: 200.0, - ), - const InvoiceItem( - id: 'item_2', - invoiceId: 'inv_1', - staffId: 'staff_2', - workHours: 6.0, - rate: 30.0, - amount: 180.0, - ), - ]; - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/home_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/home_repository_mock.dart deleted file mode 100644 index 3da2699a..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/home_repository_mock.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; -import '../session/client_session_store.dart'; - -/// Mock implementation of data source for Home dashboard data. -/// -/// This mock simulates backend responses for dashboard-related queries. -class HomeRepositoryMock { - /// Returns a mock [HomeDashboardData]. - Future getDashboardData() async { - // Simulate network delay - await Future.delayed(const Duration(milliseconds: 500)); - - return const HomeDashboardData( - weeklySpending: 4250.0, - next7DaysSpending: 6100.0, - weeklyShifts: 12, - next7DaysScheduled: 18, - totalNeeded: 10, - totalFilled: 8, - ); - } - - /// Returns the current user's session data. - /// - /// Returns a tuple of (businessName, photoUrl). - (String, String?) getUserSession() { - final ClientSession? session = ClientSessionStore.instance.session; - final String businessName = session?.business?.businessName ?? 'Your Company'; - final String? photoUrl = session?.userPhotoUrl; - return (businessName, photoUrl); - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart deleted file mode 100644 index 3d856536..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -/// Mock implementation of order-related data operations. -/// -/// This class simulates backend responses for order types and order creation. -/// It is used by the feature-level repository implementations. -class OrderRepositoryMock { - /// Returns a list of available [OrderType]s. - Future> getOrderTypes() async { - await Future.delayed(const Duration(milliseconds: 500)); - return const [ - OrderType( - id: 'rapid', - titleKey: 'client_create_order.types.rapid', - descriptionKey: 'client_create_order.types.rapid_desc', - ), - OrderType( - id: 'one-time', - titleKey: 'client_create_order.types.one_time', - descriptionKey: 'client_create_order.types.one_time_desc', - ), - OrderType( - id: 'recurring', - titleKey: 'client_create_order.types.recurring', - descriptionKey: 'client_create_order.types.recurring_desc', - ), - OrderType( - id: 'permanent', - titleKey: 'client_create_order.types.permanent', - descriptionKey: 'client_create_order.types.permanent_desc', - ), - ]; - } - - /// Simulates creating a one-time order. - Future createOneTimeOrder(OneTimeOrder order) async { - await Future.delayed(const Duration(milliseconds: 800)); - } - - /// Simulates creating a rapid order. - Future createRapidOrder(String description) async { - await Future.delayed(const Duration(seconds: 1)); - } - - /// Returns a mock list of client orders. - Future> getOrders() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - OrderItem( - id: '1', - orderId: 'order_1', - title: 'Server - Wedding', - clientName: 'Grand Plaza Hotel', - status: 'filled', - date: DateTime.now() - .add(const Duration(days: 1)) - .toIso8601String() - .split('T')[0], - startTime: '16:00', - endTime: '23:00', - location: 'Grand Plaza Hotel, 123 Main St', - locationAddress: 'Grand Plaza Hotel, 123 Main St', - filled: 10, - workersNeeded: 10, - hourlyRate: 22.0, - confirmedApps: List>.generate( - 10, - (int index) => { - 'id': 'app_$index', - 'worker_id': 'w_$index', - 'worker_name': 'Worker ${String.fromCharCode(65 + index)}', - 'status': 'confirmed', - 'check_in_time': index < 5 ? '15:55' : null, - }, - ), - ), - OrderItem( - id: '2', - orderId: 'order_2', - title: 'Bartender - Private Event', - clientName: 'Taste of the Town', - status: 'open', - date: DateTime.now() - .add(const Duration(days: 1)) - .toIso8601String() - .split('T')[0], - startTime: '18:00', - endTime: '02:00', - location: 'Downtown Loft, 456 High St', - locationAddress: 'Downtown Loft, 456 High St', - filled: 4, - workersNeeded: 5, - hourlyRate: 28.0, - confirmedApps: List>.generate( - 4, - (int index) => { - 'id': 'app_b_$index', - 'worker_id': 'w_b_$index', - 'worker_name': 'Bartender ${index + 1}', - 'status': 'confirmed', - }, - ), - ), - OrderItem( - id: '3', - orderId: 'order_3', - title: 'Event Staff', - clientName: 'City Center', - status: 'in_progress', - date: DateTime.now().toIso8601String().split('T')[0], - startTime: '08:00', - endTime: '16:00', - location: 'Convention Center, 789 Blvd', - locationAddress: 'Convention Center, 789 Blvd', - filled: 15, - workersNeeded: 15, - hourlyRate: 20.0, - confirmedApps: List>.generate( - 15, - (int index) => { - 'id': 'app_c_$index', - 'worker_id': 'w_c_$index', - 'worker_name': 'Staff ${index + 1}', - 'status': 'confirmed', - 'check_in_time': '07:55', - }, - ), - ), - OrderItem( - id: '4', - orderId: 'order_4', - title: 'Coat Check', - clientName: 'The Met Museum', - status: 'completed', - date: DateTime.now() - .subtract(const Duration(days: 1)) - .toIso8601String() - .split('T')[0], - startTime: '17:00', - endTime: '22:00', - location: 'The Met Museum, 1000 5th Ave', - locationAddress: 'The Met Museum, 1000 5th Ave', - filled: 2, - workersNeeded: 2, - hourlyRate: 18.0, - confirmedApps: List>.generate( - 2, - (int index) => { - 'id': 'app_d_$index', - 'worker_id': 'w_d_$index', - 'worker_name': 'Checker ${index + 1}', - 'status': 'confirmed', - 'check_in_time': '16:50', - }, - ), - ), - ]; - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart deleted file mode 100644 index 2bfd79c1..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/profile_repository_mock.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -/// Mock implementation of profile-related data operations. -/// -/// This mock provides hardcoded staff profile data for development and testing. -/// It simulates the behavior of a real backend service without requiring -/// actual API calls or Firebase Data Connect. -/// -/// In production, this will be replaced with a real implementation that -/// interacts with Firebase Data Connect. -class ProfileRepositoryMock { - /// Fetches the staff profile for the given user ID. - /// - /// Returns a [Staff] entity with mock data. - /// Simulates a network delay to mirror real API behavior. - /// - /// Throws an exception if the profile cannot be retrieved. - Future getStaffProfile(String userId) async { - // Simulate network delay - await Future.delayed(const Duration(milliseconds: 500)); - - // Return mock staff profile data - return const Staff( - id: '93673c8f-91aa-405d-8647-f1aac29cc19b', - authProviderId: 't8P3fYh4y1cPoZbbVPXUhfQCsDo3', - name: 'Krower', - email: 'worker@krow.com', - phone: '555-123-4567', - status: StaffStatus.active, - avatar: null, - address: null, - ); - } - - /// Signs out the current user. - /// - /// Simulates the sign-out process with a delay. - /// In a real implementation, this would clear session tokens, - /// update Firebase auth state, etc. - Future signOut() async { - // Simulate processing delay - await Future.delayed(const Duration(milliseconds: 300)); - } - - /// Fetches emergency contacts for the given staff ID. - /// - /// Returns a list of [EmergencyContact]. - Future> getEmergencyContacts(String staffId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - const EmergencyContact( - name: 'Jane Doe', - phone: '555-987-6543', - relationship: RelationshipType.spouse, - id: 'contact_1', - ), - ]; - } - - /// Saves emergency contacts for the given staff ID. - Future saveEmergencyContacts( - String staffId, - List contacts, - ) async { - await Future.delayed(const Duration(seconds: 1)); - // Simulate save - } - - /// Fetches selected industries for the given staff ID. - Future> getStaffIndustries(String staffId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return ['hospitality', 'events']; - } - - /// Fetches selected skills for the given staff ID. - Future> getStaffSkills(String staffId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return ['Bartending', 'Server']; - } - - /// Saves experience (industries and skills) for the given staff ID. - Future saveExperience( - String staffId, - List industries, - List skills, - ) async { - await Future.delayed(const Duration(seconds: 1)); - // Simulate save - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart deleted file mode 100644 index f679fa11..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement RatingRepositoryInterface once defined in a feature package. -class RatingRepositoryMock { - Future> getStaffRatings(String staffId) async { - await Future.delayed(const Duration(milliseconds: 400)); - return [ - const StaffRating( - id: 'rate_1', - staffId: 'staff_1', - eventId: 'event_1', - businessId: 'biz_1', - rating: 5, - comment: 'Great work!', - ), - ]; - } - - Future submitRating(StaffRating rating) async { - await Future.delayed(const Duration(milliseconds: 500)); - } -} \ No newline at end of file diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/shifts_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/shifts_repository_mock.dart deleted file mode 100644 index eb0c3e92..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/shifts_repository_mock.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; -import 'package:intl/intl.dart'; - -// Mock Implementation for now. -class ShiftsRepositoryMock { - - Future> getMyShifts() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - Shift( - id: 'm1', - title: 'Warehouse Assistant', - clientName: 'Amazon', - logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Amazon_2024.svg/500px-Amazon_2024.svg.png', - hourlyRate: 22.5, - date: DateFormat('yyyy-MM-dd').format(DateTime.now().add(const Duration(days: 1))), - startTime: '09:00', - endTime: '17:00', - location: 'Logistics Park', - locationAddress: '456 Industrial Way', - status: 'confirmed', - createdDate: DateTime.now().toIso8601String(), - description: 'Standard warehouse duties. Safety boots required.', - ), - ]; - } - - Future> getAvailableShifts() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - Shift( - id: 'a1', - title: 'Bartender', - clientName: 'Club Luxe', - logoUrl: null, - hourlyRate: 30.0, - date: DateFormat('yyyy-MM-dd').format(DateTime.now().add(const Duration(days: 3))), - startTime: '20:00', - endTime: '02:00', - location: 'City Center', - locationAddress: '789 Nightlife Blvd', - status: 'open', - createdDate: DateTime.now().toIso8601String(), - description: 'Experience mixing cocktails required.', - ), - // Add more mocks if needed - ]; - } - - Future> getPendingAssignments() async { - await Future.delayed(const Duration(milliseconds: 500)); - return [ - Shift( - id: 'p1', - title: 'Event Server', - clientName: 'Grand Hotel', - logoUrl: null, - hourlyRate: 25.0, - date: DateFormat('yyyy-MM-dd').format(DateTime.now().add(const Duration(days: 2))), - startTime: '16:00', - endTime: '22:00', - location: 'Downtown', - locationAddress: '123 Main St', - status: 'pending', - createdDate: DateTime.now().toIso8601String(), - ), - ]; - } - - Future getShiftDetails(String shiftId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return Shift( - id: shiftId, - title: 'Event Server', - clientName: 'Grand Hotel', - logoUrl: null, - hourlyRate: 25.0, - date: DateFormat('yyyy-MM-dd').format(DateTime.now()), - startTime: '16:00', - endTime: '22:00', - location: 'Downtown', - locationAddress: '123 Main St, New York, NY', - status: 'open', - createdDate: DateTime.now().toIso8601String(), - description: 'Provide exceptional customer service. Respond to guest requests or concerns promptly and professionally.', - managers: [], - ); - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart deleted file mode 100644 index f60187da..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement SkillRepositoryInterface once defined in a feature package. -class SkillRepositoryMock { - Future addStaffSkill(StaffSkill skill) async { - await Future.delayed(const Duration(milliseconds: 500)); - } - - Future> getAllSkills() async { - await Future.delayed(const Duration(milliseconds: 300)); - return [ - const Skill( - id: 'skill_1', - categoryId: 'cat_1', - name: 'Bartender', - basePrice: 15.0, - ), - const Skill( - id: 'skill_2', - categoryId: 'cat_2', - name: 'Security Guard', - basePrice: 18.0, - ), - ]; - } - - Future> getStaffSkills(String staffId) async { - await Future.delayed(const Duration(milliseconds: 400)); - return [ - const StaffSkill( - id: 'staff_skill_1', - staffId: 'staff_1', - skillId: 'skill_1', - level: SkillLevel.skilled, - experienceYears: 3, - status: StaffSkillStatus.verified, - ), - ]; - } -} \ No newline at end of file diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart deleted file mode 100644 index f561ff11..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement StaffRepositoryInterface once defined in a feature package. -class StaffRepositoryMock { - Future createStaffProfile(Staff staff) async { - await Future.delayed(const Duration(milliseconds: 500)); - return staff; - } - - Future> getMemberships(String userId) async { - await Future.delayed(const Duration(milliseconds: 300)); - return [ - Membership( - id: 'mem_1', - userId: userId, - memberableId: 'biz_1', - memberableType: 'business', - role: 'staff', - ), - ]; - } - - Future getStaffProfile(String userId) async { - await Future.delayed(const Duration(milliseconds: 400)); - return Staff( - id: 'staff_1', - authProviderId: userId, - name: 'John Doe', - email: 'john@krow.com', - status: StaffStatus.active, - avatar: 'https://i.pravatar.cc/300', - ); - } - - Future updateStaffProfile(Staff staff) async { - await Future.delayed(const Duration(milliseconds: 500)); - return staff; - } - - // Mock Availability Data Store - final Map _mockAvailability = {}; - - Future> getAvailability(String userId, DateTime start, DateTime end) async { - await Future.delayed(const Duration(milliseconds: 300)); - // Return mock structure: Date ISO String -> { isAvailable: bool, slots: { id: bool } } - - // Auto-generate some data if empty - if (_mockAvailability.isEmpty) { - // Just return empty, let the caller handle defaults - } - return _mockAvailability; - } - - Future updateAvailability(String userId, String dateIso, Map data) async { - await Future.delayed(const Duration(milliseconds: 200)); - _mockAvailability[dateIso] = data; - } -} \ No newline at end of file diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart deleted file mode 100644 index 0722052e..00000000 --- a/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; - -// TODO: Implement SupportRepositoryInterface once defined in a feature package. -class SupportRepositoryMock { - Future> getTags() async { - await Future.delayed(const Duration(milliseconds: 200)); - return [ - const Tag(id: 'tag_1', label: 'Urgent'), - const Tag(id: 'tag_2', label: 'VIP Event'), - ]; - } - - Future> getWorkingAreas() async { - await Future.delayed(const Duration(milliseconds: 200)); - return [ - const WorkingArea( - id: 'area_1', - name: 'Central London', - centerLat: 51.5074, - centerLng: -0.1278, - radiusKm: 10.0, - ), - ]; - } -} \ No newline at end of file diff --git a/apps/mobile/packages/design_system/lib/src/ui_colors.dart b/apps/mobile/packages/design_system/lib/src/ui_colors.dart index 35c455cc..2462d0f3 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_colors.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_colors.dart @@ -57,6 +57,12 @@ class UiColors { /// Focus ring color (#0A39DF) static const Color ring = Color(0xFF0A39DF); + /// Success green color (#10B981) + static const Color success = Color(0xFF10B981); + + /// Error red color (#F04444) + static const Color error = destructive; + // --------------------------------------------------------------------------- // 2. Semantic Mappings // --------------------------------------------------------------------------- diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_error_snackbar.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_error_snackbar.dart index a296bcad..c35e36ae 100644 --- a/apps/mobile/packages/design_system/lib/src/widgets/ui_error_snackbar.dart +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_error_snackbar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:krow_core_localization/krow_core_localization.dart'; +import 'package:core_localization/core_localization.dart'; import '../ui_colors.dart'; import '../ui_typography.dart'; @@ -28,14 +28,15 @@ class UiErrorSnackbar { String? errorCode, Duration duration = const Duration(seconds: 4), }) { - final texts = Texts.of(context); - final message = _getMessageFromKey(texts, messageKey); + // 1. Added explicit type 'Translations' to satisfy the lint + final Translations texts = Translations.of(context); + final String message = _getMessageFromKey(texts, messageKey); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ - Icon(Icons.error_outline, color: UiColors.white), + const Icon(Icons.error_outline, color: UiColors.white), const SizedBox(width: 12), Expanded( child: Column( @@ -51,7 +52,8 @@ class UiErrorSnackbar { Text( 'Error Code: $errorCode', style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withOpacity(0.7), + // 3. Fixed deprecated member use + color: UiColors.white.withValues(alpha: 0.7), ), ), ], @@ -75,7 +77,7 @@ class UiErrorSnackbar { /// - errors.auth.invalid_credentials /// - errors.hub.has_orders /// - errors.generic.unknown - static String _getMessageFromKey(Texts texts, String key) { + static String _getMessageFromKey(Translations texts, String key) { // Parse key like "errors.auth.invalid_credentials" final parts = key.split('.'); if (parts.length < 2) return texts.errors.generic.unknown; @@ -102,7 +104,7 @@ class UiErrorSnackbar { } } - static String _getAuthError(Texts texts, String key) { + static String _getAuthError(Translations texts, String key) { switch (key) { case 'invalid_credentials': return texts.errors.auth.invalid_credentials; @@ -131,7 +133,7 @@ class UiErrorSnackbar { } } - static String _getHubError(Texts texts, String key) { + static String _getHubError(Translations texts, String key) { switch (key) { case 'has_orders': return texts.errors.hub.has_orders; @@ -144,7 +146,7 @@ class UiErrorSnackbar { } } - static String _getOrderError(Texts texts, String key) { + static String _getOrderError(Translations texts, String key) { switch (key) { case 'missing_hub': return texts.errors.order.missing_hub; @@ -161,7 +163,7 @@ class UiErrorSnackbar { } } - static String _getProfileError(Texts texts, String key) { + static String _getProfileError(Translations texts, String key) { switch (key) { case 'staff_not_found': return texts.errors.profile.staff_not_found; @@ -174,7 +176,7 @@ class UiErrorSnackbar { } } - static String _getShiftError(Texts texts, String key) { + static String _getShiftError(Translations texts, String key) { switch (key) { case 'no_open_roles': return texts.errors.shift.no_open_roles; @@ -187,7 +189,7 @@ class UiErrorSnackbar { } } - static String _getGenericError(Texts texts, String key) { + static String _getGenericError(Translations texts, String key) { switch (key) { case 'unknown': return texts.errors.generic.unknown; diff --git a/apps/mobile/packages/design_system/pubspec.yaml b/apps/mobile/packages/design_system/pubspec.yaml index ae3c4b60..6bd42bb3 100644 --- a/apps/mobile/packages/design_system/pubspec.yaml +++ b/apps/mobile/packages/design_system/pubspec.yaml @@ -1,6 +1,7 @@ name: design_system description: "A new Flutter package project." version: 0.0.1 +publish_to: none homepage: resolution: workspace @@ -14,6 +15,8 @@ dependencies: google_fonts: ^7.0.2 lucide_icons: ^0.257.0 font_awesome_flutter: ^10.7.0 + core_localization: + path: ../core_localization dev_dependencies: flutter_test: diff --git a/apps/mobile/packages/design_system/test/design_system_test.dart b/apps/mobile/packages/design_system/test/design_system_test.dart deleted file mode 100644 index c2beb8de..00000000 --- a/apps/mobile/packages/design_system/test/design_system_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:design_system/design_system.dart'; - -void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); - }); -} diff --git a/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart b/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart index bf9ae481..c9effdd4 100644 --- a/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart +++ b/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart @@ -304,6 +304,24 @@ class UnknownException extends AppException { String get messageKey => 'errors.generic.unknown'; } +/// Thrown when the server returns an error (500, etc.). +class ServerException extends AppException { + const ServerException({String? technicalMessage}) + : super(code: 'SRV_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.server_error'; +} + +/// Thrown when the service is unavailable (Data Connect down). +class ServiceUnavailableException extends AppException { + const ServiceUnavailableException({String? technicalMessage}) + : super(code: 'SRV_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.service_unavailable'; +} + /// Thrown when user is not authenticated. class NotAuthenticatedException extends AppException { const NotAuthenticatedException({String? technicalMessage}) diff --git a/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart b/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart index 035d7af8..12310fde 100644 --- a/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart +++ b/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart @@ -1,4 +1,4 @@ -library client_authentication; +library; import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:flutter_modular/flutter_modular.dart'; diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index f1ff2c80..1841794c 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -21,9 +21,9 @@ import '../../domain/repositories/auth_repository_interface.dart'; /// /// This implementation integrates with Firebase Authentication for user /// identity management and Krow's Data Connect SDK for storing user profile data. -class AuthRepositoryImpl implements AuthRepositoryInterface { - final firebase.FirebaseAuth _firebaseAuth; - final dc.ExampleConnector _dataConnect; +class AuthRepositoryImpl + with dc.DataErrorHandler + implements AuthRepositoryInterface { /// Creates an [AuthRepositoryImpl] with the real dependencies. AuthRepositoryImpl({ @@ -31,6 +31,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { required dc.ExampleConnector dataConnect, }) : _firebaseAuth = firebaseAuth, _dataConnect = dataConnect; + final firebase.FirebaseAuth _firebaseAuth; + final dc.ExampleConnector _dataConnect; @override Future signInWithEmail({ @@ -221,16 +223,12 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { } /// Checks if a user with BUSINESS role exists in PostgreSQL. + Future _checkBusinessUserExists(String firebaseUserId) async { - try { - final QueryResult response = - await _dataConnect.getUserById(id: firebaseUserId).execute(); - final dc.GetUserByIdUser? user = response.data?.user; - return user != null && user.userRole == 'BUSINESS'; - } catch (e) { - developer.log('Error checking business user: $e', name: 'AuthRepository'); - return false; - } + final QueryResult response = + await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute()); + final dc.GetUserByIdUser? user = response.data.user; + return user != null && user.userRole == 'BUSINESS'; } /// Creates Business and User entities in PostgreSQL for a Firebase user. @@ -241,38 +239,30 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { required void Function(String businessId) onBusinessCreated, }) async { // Create Business entity in PostgreSQL + final OperationResult createBusinessResponse = - await _dataConnect.createBusiness( + await executeProtected(() => _dataConnect.createBusiness( businessName: companyName, userId: firebaseUser.uid, rateGroup: dc.BusinessRateGroup.STANDARD, status: dc.BusinessStatus.PENDING, - ).execute(); + ).execute()); - final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert; - if (businessData == null) { - throw const SignUpFailedException( - technicalMessage: 'Business creation failed in PostgreSQL', - ); - } + final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert; onBusinessCreated(businessData.id); // Create User entity in PostgreSQL + final OperationResult createUserResponse = - await _dataConnect.createUser( + await executeProtected(() => _dataConnect.createUser( id: firebaseUser.uid, role: dc.UserBaseRole.USER, ) .email(email) .userRole('BUSINESS') - .execute(); + .execute()); - final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert; - if (newUserData == null) { - throw const SignUpFailedException( - technicalMessage: 'User profile creation failed in PostgreSQL', - ); - } + final dc.CreateUserUserInsert newUserData = createUserResponse.data.user_insert; return _getUserProfile( firebaseUserId: firebaseUser.uid, @@ -323,8 +313,9 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { required String? fallbackEmail, bool requireBusinessRole = false, }) async { - final QueryResult response = await _dataConnect.getUserById(id: firebaseUserId).execute(); - final dc.GetUserByIdUser? user = response.data?.user; + final QueryResult response = + await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute()); + final dc.GetUserByIdUser? user = response.data.user; if (user == null) { throw UserNotFoundException( technicalMessage: 'Firebase UID $firebaseUserId not found in users table', @@ -351,9 +342,10 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { role: user.role.stringValue, ); - final QueryResult businessResponse = await _dataConnect.getBusinessesByUserId( + final QueryResult businessResponse = + await executeProtected(() => _dataConnect.getBusinessesByUserId( userId: firebaseUserId, - ).execute(); + ).execute()); final dc.GetBusinessesByUserIdBusinesses? business = businessResponse.data.businesses.isNotEmpty ? businessResponse.data.businesses.first : null; diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_email_arguments.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_email_arguments.dart index 54904804..8e123c10 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_email_arguments.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_email_arguments.dart @@ -2,14 +2,14 @@ import 'package:krow_core/core.dart'; /// Arguments for the [SignInWithEmailUseCase]. class SignInWithEmailArguments extends UseCaseArgument { + + const SignInWithEmailArguments({required this.email, required this.password}); /// The user's email address. final String email; /// The user's password. final String password; - const SignInWithEmailArguments({required this.email, required this.password}); - @override List get props => [email, password]; } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_social_arguments.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_social_arguments.dart index 4ba83fc2..2300080b 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_social_arguments.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_in_with_social_arguments.dart @@ -2,10 +2,10 @@ import 'package:krow_core/core.dart'; /// Arguments for the [SignInWithSocialUseCase]. class SignInWithSocialArguments extends UseCaseArgument { - /// The social provider name (e.g. 'google' or 'apple'). - final String provider; const SignInWithSocialArguments({required this.provider}); + /// The social provider name (e.g. 'google' or 'apple'). + final String provider; @override List get props => [provider]; diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_up_with_email_arguments.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_up_with_email_arguments.dart index 7e0e127a..07539e87 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_up_with_email_arguments.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/arguments/sign_up_with_email_arguments.dart @@ -2,6 +2,12 @@ import 'package:krow_core/core.dart'; /// Arguments for the [SignUpWithEmailUseCase]. class SignUpWithEmailArguments extends UseCaseArgument { + + const SignUpWithEmailArguments({ + required this.companyName, + required this.email, + required this.password, + }); /// The name of the company. final String companyName; @@ -11,12 +17,6 @@ class SignUpWithEmailArguments extends UseCaseArgument { /// The user's password. final String password; - const SignUpWithEmailArguments({ - required this.companyName, - required this.email, - required this.password, - }); - @override List get props => [companyName, email, password]; } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_email_use_case.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_email_use_case.dart index a10358f7..71867638 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_email_use_case.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_email_use_case.dart @@ -9,9 +9,9 @@ import '../repositories/auth_repository_interface.dart'; /// via email/password credentials. class SignInWithEmailUseCase implements UseCase { - final AuthRepositoryInterface _repository; const SignInWithEmailUseCase(this._repository); + final AuthRepositoryInterface _repository; /// Executes the sign-in operation. @override diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_social_use_case.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_social_use_case.dart index dcbeab4a..c43d8f3b 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_social_use_case.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_in_with_social_use_case.dart @@ -6,9 +6,9 @@ import '../repositories/auth_repository_interface.dart'; /// Use case for signing in a client via social providers (Google/Apple). class SignInWithSocialUseCase implements UseCase { - final AuthRepositoryInterface _repository; const SignInWithSocialUseCase(this._repository); + final AuthRepositoryInterface _repository; /// Executes the social sign-in operation. @override diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_out_use_case.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_out_use_case.dart index 707cf75c..3f510883 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_out_use_case.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_out_use_case.dart @@ -6,9 +6,9 @@ import '../repositories/auth_repository_interface.dart'; /// This use case handles the termination of the user's session and /// clearing of any local authentication tokens. class SignOutUseCase implements NoInputUseCase { - final AuthRepositoryInterface _repository; const SignOutUseCase(this._repository); + final AuthRepositoryInterface _repository; /// Executes the sign-out operation. @override diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_up_with_email_use_case.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_up_with_email_use_case.dart index 60c53fde..8ff76885 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_up_with_email_use_case.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/usecases/sign_up_with_email_use_case.dart @@ -9,9 +9,9 @@ import '../repositories/auth_repository_interface.dart'; /// email, password, and company details. class SignUpWithEmailUseCase implements UseCase { - final AuthRepositoryInterface _repository; const SignUpWithEmailUseCase(this._repository); + final AuthRepositoryInterface _repository; /// Executes the sign-up operation. @override diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart index fb6dbe45..4d98faef 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart @@ -25,10 +25,6 @@ import 'client_auth_state.dart'; /// * Session Termination class ClientAuthBloc extends Bloc with BlocErrorHandler { - final SignInWithEmailUseCase _signInWithEmail; - final SignUpWithEmailUseCase _signUpWithEmail; - final SignInWithSocialUseCase _signInWithSocial; - final SignOutUseCase _signOut; /// Initializes the BLoC with the required use cases and initial state. ClientAuthBloc({ @@ -46,6 +42,10 @@ class ClientAuthBloc extends Bloc on(_onSocialSignInRequested); on(_onSignOutRequested); } + final SignInWithEmailUseCase _signInWithEmail; + final SignUpWithEmailUseCase _signUpWithEmail; + final SignInWithSocialUseCase _signInWithSocial; + final SignOutUseCase _signOut; /// Handles the [ClientSignInRequested] event. Future _onSignInRequested( @@ -57,12 +57,12 @@ class ClientAuthBloc extends Bloc await handleError( emit: emit, action: () async { - final user = await _signInWithEmail( + final User user = await _signInWithEmail( SignInWithEmailArguments(email: event.email, password: event.password), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientAuthStatus.error, errorMessage: errorKey, ), @@ -79,7 +79,7 @@ class ClientAuthBloc extends Bloc await handleError( emit: emit, action: () async { - final user = await _signUpWithEmail( + final User user = await _signUpWithEmail( SignUpWithEmailArguments( companyName: event.companyName, email: event.email, @@ -88,7 +88,7 @@ class ClientAuthBloc extends Bloc ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientAuthStatus.error, errorMessage: errorKey, ), @@ -105,12 +105,12 @@ class ClientAuthBloc extends Bloc await handleError( emit: emit, action: () async { - final user = await _signInWithSocial( + final User user = await _signInWithSocial( SignInWithSocialArguments(provider: event.provider), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientAuthStatus.error, errorMessage: errorKey, ), @@ -130,7 +130,7 @@ class ClientAuthBloc extends Bloc await _signOut(); emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null)); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientAuthStatus.error, errorMessage: errorKey, ), diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_event.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_event.dart index 5eb35db1..66b1f615 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_event.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_event.dart @@ -10,10 +10,10 @@ abstract class ClientAuthEvent extends Equatable { /// Event dispatched when a user attempts to sign in with email and password. class ClientSignInRequested extends ClientAuthEvent { - final String email; - final String password; const ClientSignInRequested({required this.email, required this.password}); + final String email; + final String password; @override List get props => [email, password]; @@ -21,15 +21,15 @@ class ClientSignInRequested extends ClientAuthEvent { /// Event dispatched when a user attempts to create a new business account. class ClientSignUpRequested extends ClientAuthEvent { - final String companyName; - final String email; - final String password; const ClientSignUpRequested({ required this.companyName, required this.email, required this.password, }); + final String companyName; + final String email; + final String password; @override List get props => [companyName, email, password]; @@ -37,9 +37,9 @@ class ClientSignUpRequested extends ClientAuthEvent { /// Event dispatched for third-party authentication (Google/Apple). class ClientSocialSignInRequested extends ClientAuthEvent { - final String provider; const ClientSocialSignInRequested({required this.provider}); + final String provider; @override List get props => [provider]; diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_state.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_state.dart index 47573991..02e07aa3 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_state.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_state.dart @@ -21,6 +21,12 @@ enum ClientAuthStatus { /// Represents the state of the client authentication flow. class ClientAuthState extends Equatable { + + const ClientAuthState({ + this.status = ClientAuthStatus.initial, + this.user, + this.errorMessage, + }); /// Current status of the authentication process. final ClientAuthStatus status; @@ -30,12 +36,6 @@ class ClientAuthState extends Equatable { /// Optional error message when status is [ClientAuthStatus.error]. final String? errorMessage; - const ClientAuthState({ - this.status = ClientAuthStatus.initial, - this.user, - this.errorMessage, - }); - /// Creates a copy of this state with the given fields replaced by the new values. ClientAuthState copyWith({ ClientAuthStatus? status, diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart index d8c297ae..e8453f17 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart @@ -11,7 +11,6 @@ import '../blocs/client_auth_event.dart'; import '../blocs/client_auth_state.dart'; import '../widgets/client_sign_up_page/client_sign_up_form.dart'; import '../widgets/common/auth_divider.dart'; -import '../widgets/common/auth_social_button.dart'; /// Page for client users to sign up for a new account. /// diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart index bfa0a737..129fc662 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart @@ -7,12 +7,6 @@ import 'package:flutter/material.dart'; /// This widget handles user input for email and password and delegates /// authentication events to the parent via callbacks. class ClientSignInForm extends StatefulWidget { - /// Callback when the sign-in button is pressed. - final void Function({required String email, required String password}) - onSignIn; - - /// Whether the authentication is currently loading. - final bool isLoading; /// Creates a [ClientSignInForm]. const ClientSignInForm({ @@ -20,6 +14,12 @@ class ClientSignInForm extends StatefulWidget { required this.onSignIn, this.isLoading = false, }); + /// Callback when the sign-in button is pressed. + final void Function({required String email, required String password}) + onSignIn; + + /// Whether the authentication is currently loading. + final bool isLoading; @override State createState() => _ClientSignInFormState(); diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart index 504d2db8..da3c215c 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart @@ -7,6 +7,13 @@ import 'package:flutter/material.dart'; /// This widget handles user input for company name, email, and password, /// and delegates registration events to the parent via callbacks. class ClientSignUpForm extends StatefulWidget { + + /// Creates a [ClientSignUpForm]. + const ClientSignUpForm({ + super.key, + required this.onSignUp, + this.isLoading = false, + }); /// Callback when the sign-up button is pressed. final void Function({ required String companyName, @@ -18,13 +25,6 @@ class ClientSignUpForm extends StatefulWidget { /// Whether the authentication is currently loading. final bool isLoading; - /// Creates a [ClientSignUpForm]. - const ClientSignUpForm({ - super.key, - required this.onSignUp, - this.isLoading = false, - }); - @override State createState() => _ClientSignUpFormState(); } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_divider.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_divider.dart index 66148a8e..e35f4849 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_divider.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_divider.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; /// /// Displays a horizontal line with text in the middle (e.g., "Or continue with"). class AuthDivider extends StatelessWidget { - /// The text to display in the center of the divider. - final String text; /// Creates an [AuthDivider]. const AuthDivider({super.key, required this.text}); + /// The text to display in the center of the divider. + final String text; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_social_button.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_social_button.dart index 35b8a4cc..d75919d8 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_social_button.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/auth_social_button.dart @@ -6,14 +6,6 @@ import 'package:flutter/material.dart'; /// This widget wraps [UiButton.secondary] to provide a consistent look and feel /// for social sign-in/sign-up buttons (e.g., Google, Apple). class AuthSocialButton extends StatelessWidget { - /// The localizable text to display on the button (e.g., "Continue with Google"). - final String text; - - /// The icon representing the social provider. - final IconData icon; - - /// Callback to execute when the button is tapped. - final VoidCallback onPressed; /// Creates an [AuthSocialButton]. /// @@ -24,6 +16,14 @@ class AuthSocialButton extends StatelessWidget { required this.icon, required this.onPressed, }); + /// The localizable text to display on the button (e.g., "Continue with Google"). + final String text; + + /// The icon representing the social provider. + final IconData icon; + + /// Callback to execute when the button is tapped. + final VoidCallback onPressed; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/section_titles.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/section_titles.dart index 67243de8..61ffd3f1 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/section_titles.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/common/section_titles.dart @@ -3,14 +3,14 @@ import 'package:flutter/material.dart'; /// A widget that displays a section title with a leading icon. class SectionTitle extends StatelessWidget { + + const SectionTitle({super.key, required this.title, required this.subtitle}); /// The title of the section. final String title; /// The subtitle of the section. final String subtitle; - const SectionTitle({super.key, required this.title, required this.subtitle}); - @override Widget build(BuildContext context) { return Column( diff --git a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart index 2db654ae..a8591d07 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart @@ -16,13 +16,11 @@ import 'presentation/pages/billing_page.dart'; class BillingModule extends Module { @override void binds(Injector i) { - // Mock repositories (TODO: Replace with real implementations) - i.addSingleton(FinancialRepositoryMock.new); + // Repositories i.addSingleton( () => BillingRepositoryImpl( - financialRepository: i.get(), dataConnect: ExampleConnector.instance, ), ); diff --git a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart index 9fa8d5cb..689ac4bf 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart @@ -12,28 +12,34 @@ import '../../domain/repositories/billing_repository.dart'; /// It strictly adheres to the Clean Architecture data layer responsibilities: /// - No business logic (except necessary data transformation/filtering). /// - Delegates to data sources. -class BillingRepositoryImpl implements BillingRepository { +class BillingRepositoryImpl + with data_connect.DataErrorHandler + implements BillingRepository { /// Creates a [BillingRepositoryImpl]. /// /// Requires the [financialRepository] to fetch financial data. BillingRepositoryImpl({ - required data_connect.FinancialRepositoryMock financialRepository, required data_connect.ExampleConnector dataConnect, - }) : _financialRepository = financialRepository, - _dataConnect = dataConnect; + }) : _dataConnect = dataConnect; - final data_connect.FinancialRepositoryMock _financialRepository; final data_connect.ExampleConnector _dataConnect; /// Fetches the current bill amount by aggregating open invoices. @override + @override Future getCurrentBillAmount() async { - // In a real app, this might be an aggregate query. - // Simulating fetching invoices and summing up. - final List invoices = await _financialRepository.getInvoices( - 'current_business', - ); - return invoices + final String? businessId = + data_connect.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return 0.0; + } + + final fdc.QueryResult result = await executeProtected(() => _dataConnect + .listInvoicesByBusinessId(businessId: businessId) + .execute()); + + return result.data.invoices + .map(_mapInvoice) .where((Invoice i) => i.status == InvoiceStatus.open) .fold( 0.0, @@ -50,25 +56,32 @@ class BillingRepositoryImpl implements BillingRepository { return []; } - final fdc.QueryResult result = - await _dataConnect + final fdc.QueryResult result = await executeProtected(() => _dataConnect .listInvoicesByBusinessId( businessId: businessId, ) .limit(10) - .execute(); + .execute()); return result.data.invoices.map(_mapInvoice).toList(); } /// Fetches pending invoices (Open or Disputed). @override + @override Future> getPendingInvoices() async { - final List invoices = await _financialRepository.getInvoices( - 'current_business', - ); - return invoices + final String? businessId = + data_connect.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return []; + } + + final fdc.QueryResult result = await executeProtected(() => _dataConnect + .listInvoicesByBusinessId(businessId: businessId) + .execute()); + + return result.data.invoices + .map(_mapInvoice) .where( (Invoice i) => i.status == InvoiceStatus.open || @@ -111,16 +124,13 @@ class BillingRepositoryImpl implements BillingRepository { end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999); } - final fdc.QueryResult< - data_connect.ListShiftRolesByBusinessAndDatesSummaryData, - data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables> result = - await _dataConnect + final fdc.QueryResult result = await executeProtected(() => _dataConnect .listShiftRolesByBusinessAndDatesSummary( businessId: businessId, start: _toTimestamp(start), end: _toTimestamp(end), ) - .execute(); + .execute()); final List shiftRoles = result.data.shiftRoles; diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart index dc72ab2f..fa9ad332 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart @@ -1,6 +1,5 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_domain/krow_domain.dart'; -import '../../domain/models/billing_period.dart'; import '../../domain/usecases/get_current_bill_amount.dart'; import '../../domain/usecases/get_invoice_history.dart'; import '../../domain/usecases/get_pending_invoices.dart'; diff --git a/apps/mobile/packages/features/client/client_main/test/presentation/blocs/client_main_cubit_test.dart b/apps/mobile/packages/features/client/client_main/test/presentation/blocs/client_main_cubit_test.dart deleted file mode 100644 index 1ef6ab40..00000000 --- a/apps/mobile/packages/features/client/client_main/test/presentation/blocs/client_main_cubit_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:client_main/src/presentation/blocs/client_main_cubit.dart'; -import 'package:client_main/src/presentation/blocs/client_main_state.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockIModularNavigator extends Mock implements IModularNavigator {} - -void main() { - group('ClientMainCubit', () { - late MockIModularNavigator navigator; - - setUp(() { - navigator = MockIModularNavigator(); - when(() => navigator.path).thenReturn('/home'); - when(() => navigator.addListener(any())).thenReturn(null); - // Stub addListener to avoid errors when Cubit adds listener - // Note: addListener might be on Modular directly or via some other mechanic, - // but for this unit test we just want to suppress errors if possible or let the Cubit work. - // Actually Modular.to.addListener calls Modular.navigatorDelegate.addListener if it exists? - // Modular.to.addListener uses the internal RouterDelegate. - // Mocking Modular internals is hard. - - // Let's rely on the fact that we mocked navigatorDelegate. - Modular.navigatorDelegate = navigator; - }); - - test('initial state is correct', () { - final ClientMainCubit cubit = ClientMainCubit(); - expect(cubit.state, const ClientMainState(currentIndex: 2)); - cubit.close(); - }); - - // Note: Testing actual route changes requires more complex Modular mocking - // or integration tests, but the structure allows it. - }); -} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart index 25b962a4..f10115d6 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -14,6 +14,7 @@ import '../../domain/repositories/client_create_order_repository_interface.dart' /// It follows the KROW Clean Architecture by keeping the data layer focused /// on delegation and data mapping, without business logic. class ClientCreateOrderRepositoryImpl + with dc.DataErrorHandler implements ClientCreateOrderRepositoryInterface { ClientCreateOrderRepositoryImpl({ required firebase.FirebaseAuth firebaseAuth, @@ -74,22 +75,20 @@ class ClientCreateOrderRepositoryImpl order.date.day, ); final fdc.Timestamp orderTimestamp = _toTimestamp(orderDateOnly); - final fdc.OperationResult orderResult = await _dataConnect - .createOrder( - businessId: businessId, - orderType: dc.OrderType.ONE_TIME, - teamHubId: hub.id, - ) - .vendorId(vendorId) - .eventName(order.eventName) - .status(dc.OrderStatus.POSTED) - .date(orderTimestamp) - .execute(); + final fdc.OperationResult + orderResult = await executeProtected(() => _dataConnect + .createOrder( + businessId: businessId, + orderType: dc.OrderType.ONE_TIME, + teamHubId: hub.id, + ) + .vendorId(vendorId) + .eventName(order.eventName) + .status(dc.OrderStatus.POSTED) + .date(orderTimestamp) + .execute()); - final String? orderId = orderResult.data?.order_insert.id; - if (orderId == null) { - throw Exception('Order creation failed.'); - } + final String orderId = orderResult.data.order_insert.id; final int workersNeeded = order.positions.fold( 0, @@ -98,29 +97,27 @@ class ClientCreateOrderRepositoryImpl final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}'; final double shiftCost = _calculateShiftCost(order); - final fdc.OperationResult shiftResult = await _dataConnect - .createShift(title: shiftTitle, orderId: orderId) - .date(orderTimestamp) - .location(hub.name) - .locationAddress(hub.address) - .latitude(hub.latitude) - .longitude(hub.longitude) - .placeId(hub.placeId) - .city(hub.city) - .state(hub.state) - .street(hub.street) - .country(hub.country) - .status(dc.ShiftStatus.PENDING) - .workersNeeded(workersNeeded) - .filled(0) - .durationDays(1) - .cost(shiftCost) - .execute(); + final fdc.OperationResult + shiftResult = await executeProtected(() => _dataConnect + .createShift(title: shiftTitle, orderId: orderId) + .date(orderTimestamp) + .location(hub.name) + .locationAddress(hub.address) + .latitude(hub.latitude) + .longitude(hub.longitude) + .placeId(hub.placeId) + .city(hub.city) + .state(hub.state) + .street(hub.street) + .country(hub.country) + .status(dc.ShiftStatus.PENDING) + .workersNeeded(workersNeeded) + .filled(0) + .durationDays(1) + .cost(shiftCost) + .execute()); - final String? shiftId = shiftResult.data?.shift_insert.id; - if (shiftId == null) { - throw Exception('Shift creation failed.'); - } + final String shiftId = shiftResult.data.shift_insert.id; for (final domain.OneTimeOrderPosition position in order.positions) { final DateTime start = _parseTime(order.date, position.startTime); @@ -135,7 +132,7 @@ class ClientCreateOrderRepositoryImpl 'CreateOneTimeOrder shiftRole: start=${start.toIso8601String()} end=${normalizedEnd.toIso8601String()}', ); - await _dataConnect + await executeProtected(() => _dataConnect .createShiftRole( shiftId: shiftId, roleId: position.role, @@ -146,13 +143,13 @@ class ClientCreateOrderRepositoryImpl .hours(hours) .breakType(_breakDurationFromValue(position.lunchBreak)) .totalValue(totalValue) - .execute(); + .execute()); } - await _dataConnect + await executeProtected(() => _dataConnect .updateOrder(id: orderId, teamHubId: hub.id) .shifts(fdc.AnyValue([shiftId])) - .execute(); + .execute()); } @override diff --git a/apps/mobile/packages/features/client/home/lib/client_home.dart b/apps/mobile/packages/features/client/home/lib/client_home.dart index 37bc4bc0..65415b46 100644 --- a/apps/mobile/packages/features/client/home/lib/client_home.dart +++ b/apps/mobile/packages/features/client/home/lib/client_home.dart @@ -25,7 +25,6 @@ class ClientHomeModule extends Module { // Repositories i.addLazySingleton( () => HomeRepositoryImpl( - i.get(), ExampleConnector.instance, ), ); diff --git a/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart b/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart index 7c014fc2..fb63d85b 100644 --- a/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart +++ b/apps/mobile/packages/features/client/home/lib/src/data/repositories_impl/home_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; -import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; import '../../domain/repositories/home_repository_interface.dart'; @@ -8,17 +8,14 @@ import '../../domain/repositories/home_repository_interface.dart'; /// This implementation resides in the data layer and acts as a bridge between the /// domain layer and the data source (in this case, a mock from data_connect). class HomeRepositoryImpl implements HomeRepositoryInterface { - final HomeRepositoryMock _mock; - final ExampleConnector _dataConnect; /// Creates a [HomeRepositoryImpl]. - /// - /// Requires a [HomeRepositoryMock] to perform data operations. - HomeRepositoryImpl(this._mock, this._dataConnect); + HomeRepositoryImpl(this._dataConnect); + final dc.ExampleConnector _dataConnect; @override Future getDashboardData() async { - final String? businessId = ClientSessionStore.instance.session?.business?.id; + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; if (businessId == null || businessId.isEmpty) { return const HomeDashboardData( weeklySpending: 0, @@ -38,8 +35,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { final DateTime weekRangeEnd = DateTime(monday.year, monday.month, monday.day + 13, 23, 59, 59, 999); final fdc.QueryResult< - GetCompletedShiftsByBusinessIdData, - GetCompletedShiftsByBusinessIdVariables> completedResult = + dc.GetCompletedShiftsByBusinessIdData, + dc.GetCompletedShiftsByBusinessIdVariables> completedResult = await _dataConnect .getCompletedShiftsByBusinessId( businessId: businessId, @@ -47,16 +44,13 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { dateTo: _toTimestamp(weekRangeEnd), ) .execute(); - print( - 'Home spending: businessId=$businessId dateFrom=${weekRangeStart.toIso8601String()} ' - 'dateTo=${weekRangeEnd.toIso8601String()} shifts=${completedResult.data.shifts.length}', - ); + double weeklySpending = 0.0; double next7DaysSpending = 0.0; int weeklyShifts = 0; int next7DaysScheduled = 0; - for (final GetCompletedShiftsByBusinessIdShifts shift + for (final dc.GetCompletedShiftsByBusinessIdShifts shift in completedResult.data.shifts) { final DateTime? shiftDate = shift.date?.toDateTime(); if (shiftDate == null) { @@ -80,8 +74,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { final DateTime end = DateTime(now.year, now.month, now.day, 23, 59, 59, 999); final fdc.QueryResult< - ListShiftRolesByBusinessAndDateRangeData, - ListShiftRolesByBusinessAndDateRangeVariables> result = + dc.ListShiftRolesByBusinessAndDateRangeData, + dc.ListShiftRolesByBusinessAndDateRangeVariables> result = await _dataConnect .listShiftRolesByBusinessAndDateRange( businessId: businessId, @@ -89,18 +83,11 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { end: _toTimestamp(end), ) .execute(); - print( - 'Home coverage: businessId=$businessId ' - 'startLocal=${start.toIso8601String()} ' - 'endLocal=${end.toIso8601String()} ' - 'startUtc=${_toTimestamp(start).toJson()} ' - 'endUtc=${_toTimestamp(end).toJson()} ' - 'shiftRoles=${result.data.shiftRoles.length}', - ); + int totalNeeded = 0; int totalFilled = 0; - for (final ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole + for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole in result.data.shiftRoles) { totalNeeded += shiftRole.count; totalFilled += shiftRole.assigned ?? 0; @@ -118,16 +105,16 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { @override UserSessionData getUserSessionData() { - final (String businessName, String? photoUrl) = _mock.getUserSession(); + final dc.ClientSession? session = dc.ClientSessionStore.instance.session; return UserSessionData( - businessName: businessName, - photoUrl: photoUrl, + businessName: session?.business?.businessName ?? '', + photoUrl: null, // Business photo isn't currently in session ); } @override Future> getRecentReorders() async { - final String? businessId = ClientSessionStore.instance.session?.business?.id; + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; if (businessId == null || businessId.isEmpty) { return const []; } @@ -138,27 +125,20 @@ class HomeRepositoryImpl implements HomeRepositoryInterface { final fdc.Timestamp endTimestamp = _toTimestamp(now); final fdc.QueryResult< - ListShiftRolesByBusinessDateRangeCompletedOrdersData, - ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result = + dc.ListShiftRolesByBusinessDateRangeCompletedOrdersData, + dc.ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result = await _dataConnect.listShiftRolesByBusinessDateRangeCompletedOrders( businessId: businessId, start: startTimestamp, end: endTimestamp, ).execute(); - print( - 'Home reorder: completed shiftRoles=${result.data.shiftRoles.length}', - ); + return result.data.shiftRoles.map(( - ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole, + dc.ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole, ) { - print( - 'Home reorder item: orderId=${shiftRole.shift.order.id} ' - 'shiftId=${shiftRole.shiftId} roleId=${shiftRole.roleId} ' - 'orderType=${shiftRole.shift.order.orderType.stringValue} ' - 'hours=${shiftRole.hours} count=${shiftRole.count}', - ); + final String location = shiftRole.shift.location ?? shiftRole.shift.locationAddress ?? diff --git a/apps/mobile/packages/features/client/home/lib/src/domain/repositories/home_repository_interface.dart b/apps/mobile/packages/features/client/home/lib/src/domain/repositories/home_repository_interface.dart index e0a5eef4..22b5a7f4 100644 --- a/apps/mobile/packages/features/client/home/lib/src/domain/repositories/home_repository_interface.dart +++ b/apps/mobile/packages/features/client/home/lib/src/domain/repositories/home_repository_interface.dart @@ -2,17 +2,17 @@ import 'package:krow_domain/krow_domain.dart'; /// User session data for the home page. class UserSessionData { - /// The business name of the logged-in user. - final String businessName; - - /// The photo URL of the logged-in user (optional). - final String? photoUrl; /// Creates a [UserSessionData]. const UserSessionData({ required this.businessName, this.photoUrl, }); + /// The business name of the logged-in user. + final String businessName; + + /// The photo URL of the logged-in user (optional). + final String? photoUrl; } /// Interface for the Client Home repository. diff --git a/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_dashboard_data_usecase.dart b/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_dashboard_data_usecase.dart index 142e4703..c421674b 100644 --- a/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_dashboard_data_usecase.dart +++ b/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_dashboard_data_usecase.dart @@ -7,10 +7,10 @@ import '../repositories/home_repository_interface.dart'; /// This use case coordinates with the [HomeRepositoryInterface] to retrieve /// the [HomeDashboardData] required for the dashboard display. class GetDashboardDataUseCase implements NoInputUseCase { - final HomeRepositoryInterface _repository; /// Creates a [GetDashboardDataUseCase]. GetDashboardDataUseCase(this._repository); + final HomeRepositoryInterface _repository; @override Future call() { diff --git a/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_recent_reorders_usecase.dart b/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_recent_reorders_usecase.dart index 170d485e..a8e3de6b 100644 --- a/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_recent_reorders_usecase.dart +++ b/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_recent_reorders_usecase.dart @@ -4,10 +4,10 @@ import '../repositories/home_repository_interface.dart'; /// Use case to fetch recent completed shift roles for reorder suggestions. class GetRecentReordersUseCase implements NoInputUseCase> { - final HomeRepositoryInterface _repository; /// Creates a [GetRecentReordersUseCase]. GetRecentReordersUseCase(this._repository); + final HomeRepositoryInterface _repository; @override Future> call() { diff --git a/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_user_session_data_usecase.dart b/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_user_session_data_usecase.dart index 9710b727..24d043c5 100644 --- a/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_user_session_data_usecase.dart +++ b/apps/mobile/packages/features/client/home/lib/src/domain/usecases/get_user_session_data_usecase.dart @@ -4,10 +4,10 @@ import '../repositories/home_repository_interface.dart'; /// /// Returns the user's business name and photo URL for display in the header. class GetUserSessionDataUseCase { - final HomeRepositoryInterface _repository; /// Creates a [GetUserSessionDataUseCase]. GetUserSessionDataUseCase(this._repository); + final HomeRepositoryInterface _repository; /// Executes the use case to get session data. UserSessionData call() { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart index 24e96bb3..fb844f76 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_bloc.dart @@ -9,9 +9,6 @@ import 'client_home_state.dart'; /// BLoC responsible for managing the state and business logic of the client home dashboard. class ClientHomeBloc extends Bloc { - final GetDashboardDataUseCase _getDashboardDataUseCase; - final GetRecentReordersUseCase _getRecentReordersUseCase; - final GetUserSessionDataUseCase _getUserSessionDataUseCase; ClientHomeBloc({ required GetDashboardDataUseCase getDashboardDataUseCase, @@ -29,6 +26,9 @@ class ClientHomeBloc extends Bloc { add(ClientHomeStarted()); } + final GetDashboardDataUseCase _getDashboardDataUseCase; + final GetRecentReordersUseCase _getRecentReordersUseCase; + final GetUserSessionDataUseCase _getUserSessionDataUseCase; Future _onStarted( ClientHomeStarted event, @@ -83,7 +83,7 @@ class ClientHomeBloc extends Bloc { Emitter emit, ) { final List newList = List.from(state.widgetOrder); - int oldIndex = event.oldIndex; + final int oldIndex = event.oldIndex; int newIndex = event.newIndex; if (oldIndex < newIndex) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_event.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_event.dart index 10eecaad..aa5baf1b 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_event.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_event.dart @@ -12,17 +12,17 @@ class ClientHomeStarted extends ClientHomeEvent {} class ClientHomeEditModeToggled extends ClientHomeEvent {} class ClientHomeWidgetVisibilityToggled extends ClientHomeEvent { - final String widgetId; const ClientHomeWidgetVisibilityToggled(this.widgetId); + final String widgetId; @override List get props => [widgetId]; } class ClientHomeWidgetReordered extends ClientHomeEvent { + const ClientHomeWidgetReordered(this.oldIndex, this.newIndex); final int oldIndex; final int newIndex; - const ClientHomeWidgetReordered(this.oldIndex, this.newIndex); @override List get props => [oldIndex, newIndex]; diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart index e86bd5f7..e229a36d 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/blocs/client_home_state.dart @@ -6,15 +6,6 @@ enum ClientHomeStatus { initial, loading, success, error } /// Represents the state of the client home dashboard. class ClientHomeState extends Equatable { - final ClientHomeStatus status; - final List widgetOrder; - final Map widgetVisibility; - final bool isEditMode; - final String? errorMessage; - final HomeDashboardData dashboardData; - final List reorderItems; - final String businessName; - final String? photoUrl; const ClientHomeState({ this.status = ClientHomeStatus.initial, @@ -46,6 +37,15 @@ class ClientHomeState extends Equatable { this.businessName = 'Your Company', this.photoUrl, }); + final ClientHomeStatus status; + final List widgetOrder; + final Map widgetVisibility; + final bool isEditMode; + final String? errorMessage; + final HomeDashboardData dashboardData; + final List reorderItems; + final String businessName; + final String? photoUrl; ClientHomeState copyWith({ ClientHomeStatus? status, diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart index c5384950..52562f6d 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart @@ -4,14 +4,6 @@ import 'package:flutter/material.dart'; /// A widget that displays quick actions for the client. class ActionsWidget extends StatelessWidget { - /// Callback when RAPID is pressed. - final VoidCallback onRapidPressed; - - /// Callback when Create Order is pressed. - final VoidCallback onCreateOrderPressed; - - /// Optional subtitle for the section. - final String? subtitle; /// Creates an [ActionsWidget]. const ActionsWidget({ @@ -20,6 +12,14 @@ class ActionsWidget extends StatelessWidget { required this.onCreateOrderPressed, this.subtitle, }); + /// Callback when RAPID is pressed. + final VoidCallback onRapidPressed; + + /// Callback when Create Order is pressed. + final VoidCallback onCreateOrderPressed; + + /// Optional subtitle for the section. + final String? subtitle; @override Widget build(BuildContext context) { @@ -69,16 +69,6 @@ class ActionsWidget extends StatelessWidget { } class _ActionCard extends StatelessWidget { - final String title; - final String subtitle; - final IconData icon; - final Color color; - final Color borderColor; - final Color iconBgColor; - final Color iconColor; - final Color textColor; - final Color subtitleColor; - final VoidCallback onTap; const _ActionCard({ required this.title, @@ -92,6 +82,16 @@ class _ActionCard extends StatelessWidget { required this.subtitleColor, required this.onTap, }); + final String title; + final String subtitle; + final IconData icon; + final Color color; + final Color borderColor; + final Color iconBgColor; + final Color iconColor; + final Color textColor; + final Color subtitleColor; + final VoidCallback onTap; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart index 7db658d7..9c2931d7 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_edit_banner.dart @@ -10,14 +10,14 @@ import '../blocs/client_home_state.dart'; /// Shows instructions for reordering widgets and provides a reset button /// to restore the default layout. class ClientHomeEditBanner extends StatelessWidget { - /// The internationalization object for localized strings. - final dynamic i18n; /// Creates a [ClientHomeEditBanner]. const ClientHomeEditBanner({ required this.i18n, super.key, }); + /// The internationalization object for localized strings. + final dynamic i18n; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_header.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_header.dart index 39c9af6c..aebf6e36 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_header.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_header.dart @@ -13,14 +13,14 @@ import 'header_icon_button.dart'; /// Displays the user's business name, avatar, and action buttons /// (edit mode, notifications, settings). class ClientHomeHeader extends StatelessWidget { - /// The internationalization object for localized strings. - final dynamic i18n; /// Creates a [ClientHomeHeader]. const ClientHomeHeader({ required this.i18n, super.key, }); + /// The internationalization object for localized strings. + final dynamic i18n; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart index 20272bdd..4e48e5cf 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_dashboard.dart @@ -3,11 +3,6 @@ import 'package:flutter/material.dart'; /// A dashboard widget that displays today's coverage status. class CoverageDashboard extends StatelessWidget { - /// The list of shifts for today. - final List shifts; - - /// The list of applications for today's shifts. - final List applications; /// Creates a [CoverageDashboard]. const CoverageDashboard({ @@ -15,6 +10,11 @@ class CoverageDashboard extends StatelessWidget { required this.shifts, required this.applications, }); + /// The list of shifts for today. + final List shifts; + + /// The list of applications for today's shifts. + final List applications; @override Widget build(BuildContext context) { @@ -145,12 +145,6 @@ class CoverageDashboard extends StatelessWidget { } class _StatusCard extends StatelessWidget { - final String label; - final String value; - final IconData icon; - final bool isWarning; - final bool isError; - final bool isInfo; const _StatusCard({ required this.label, @@ -160,6 +154,12 @@ class _StatusCard extends StatelessWidget { this.isError = false, this.isInfo = false, }); + final String label; + final String value; + final IconData icon; + final bool isWarning; + final bool isError; + final bool isInfo; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart index 3dfaf5f7..cc54f893 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/coverage_widget.dart @@ -3,6 +3,15 @@ import 'package:flutter/material.dart'; /// A widget that displays the daily coverage metrics. class CoverageWidget extends StatelessWidget { + + /// Creates a [CoverageWidget]. + const CoverageWidget({ + super.key, + this.totalNeeded = 0, + this.totalConfirmed = 0, + this.coveragePercent = 0, + this.subtitle, + }); /// The total number of shifts needed. final int totalNeeded; @@ -15,15 +24,6 @@ class CoverageWidget extends StatelessWidget { /// Optional subtitle for the section. final String? subtitle; - /// Creates a [CoverageWidget]. - const CoverageWidget({ - super.key, - this.totalNeeded = 0, - this.totalConfirmed = 0, - this.coveragePercent = 0, - this.subtitle, - }); - @override Widget build(BuildContext context) { Color backgroundColor; @@ -114,11 +114,6 @@ class CoverageWidget extends StatelessWidget { } class _MetricCard extends StatelessWidget { - final IconData icon; - final Color iconColor; - final String label; - final String value; - final Color? valueColor; const _MetricCard({ required this.icon, @@ -127,6 +122,11 @@ class _MetricCard extends StatelessWidget { required this.value, this.valueColor, }); + final IconData icon; + final Color iconColor; + final String label; + final String value; + final Color? valueColor; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart index 3da413cc..488a9bb3 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/dashboard_widget_builder.dart @@ -16,14 +16,6 @@ import 'client_home_sheets.dart'; /// This widget encapsulates the logic for rendering different dashboard /// widgets based on their unique identifiers and current state. class DashboardWidgetBuilder extends StatelessWidget { - /// The unique identifier for the widget to build. - final String id; - - /// The current dashboard state. - final ClientHomeState state; - - /// Whether the widget is in edit mode. - final bool isEditMode; /// Creates a [DashboardWidgetBuilder]. const DashboardWidgetBuilder({ @@ -32,6 +24,14 @@ class DashboardWidgetBuilder extends StatelessWidget { required this.isEditMode, super.key, }); + /// The unique identifier for the widget to build. + final String id; + + /// The current dashboard state. + final ClientHomeState state; + + /// Whether the widget is in edit mode. + final bool isEditMode; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/draggable_widget_wrapper.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/draggable_widget_wrapper.dart index 3f82a79e..fc819c78 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/draggable_widget_wrapper.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/draggable_widget_wrapper.dart @@ -9,6 +9,15 @@ import '../blocs/client_home_event.dart'; /// Displays drag handles, visibility toggles, and wraps the actual widget /// content with appropriate styling for the edit state. class DraggableWidgetWrapper extends StatelessWidget { + + /// Creates a [DraggableWidgetWrapper]. + const DraggableWidgetWrapper({ + required this.id, + required this.title, + required this.child, + required this.isVisible, + super.key, + }); /// The unique identifier for this widget. final String id; @@ -21,15 +30,6 @@ class DraggableWidgetWrapper extends StatelessWidget { /// Whether this widget is currently visible. final bool isVisible; - /// Creates a [DraggableWidgetWrapper]. - const DraggableWidgetWrapper({ - required this.id, - required this.title, - required this.child, - required this.isVisible, - super.key, - }); - @override Widget build(BuildContext context) { return Column( diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/header_icon_button.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/header_icon_button.dart index b093130d..a9917eae 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/header_icon_button.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/header_icon_button.dart @@ -6,6 +6,15 @@ import 'package:flutter/material.dart'; /// Supports an optional badge for notification counts and an active state /// for toggled actions. class HeaderIconButton extends StatelessWidget { + + /// Creates a [HeaderIconButton]. + const HeaderIconButton({ + required this.icon, + this.badgeText, + this.isActive = false, + required this.onTap, + super.key, + }); /// The icon to display. final IconData icon; @@ -18,15 +27,6 @@ class HeaderIconButton extends StatelessWidget { /// Callback invoked when the button is tapped. final VoidCallback onTap; - /// Creates a [HeaderIconButton]. - const HeaderIconButton({ - required this.icon, - this.badgeText, - this.isActive = false, - required this.onTap, - super.key, - }); - @override Widget build(BuildContext context) { return GestureDetector( diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart index 7efa461f..75b7793d 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/live_activity_widget.dart @@ -7,11 +7,6 @@ import 'coverage_dashboard.dart'; /// A widget that displays live activity information. class LiveActivityWidget extends StatefulWidget { - /// Callback when "View all" is pressed. - final VoidCallback onViewAllPressed; - - /// Optional subtitle for the section. - final String? subtitle; /// Creates a [LiveActivityWidget]. const LiveActivityWidget({ @@ -19,6 +14,11 @@ class LiveActivityWidget extends StatefulWidget { required this.onViewAllPressed, this.subtitle }); + /// Callback when "View all" is pressed. + final VoidCallback onViewAllPressed; + + /// Optional subtitle for the section. + final String? subtitle; @override State createState() => _LiveActivityWidgetState(); @@ -178,6 +178,16 @@ class _LiveActivityWidgetState extends State { } class _LiveActivityData { + + factory _LiveActivityData.empty() { + return const _LiveActivityData( + totalNeeded: 0, + totalAssigned: 0, + totalCost: 0, + checkedInCount: 0, + lateCount: 0, + ); + } const _LiveActivityData({ required this.totalNeeded, required this.totalAssigned, @@ -191,14 +201,4 @@ class _LiveActivityData { final double totalCost; final int checkedInCount; final int lateCount; - - factory _LiveActivityData.empty() { - return const _LiveActivityData( - totalNeeded: 0, - totalAssigned: 0, - totalCost: 0, - checkedInCount: 0, - lateCount: 0, - ); - } } diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart index 1dfa8353..b3544e48 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/reorder_widget.dart @@ -5,14 +5,6 @@ import 'package:krow_domain/krow_domain.dart'; /// A widget that allows clients to reorder recent shifts. class ReorderWidget extends StatelessWidget { - /// Recent completed orders for reorder. - final List orders; - - /// Callback when a reorder button is pressed. - final Function(Map shiftData) onReorderPressed; - - /// Optional subtitle for the section. - final String? subtitle; /// Creates a [ReorderWidget]. const ReorderWidget({ @@ -21,6 +13,14 @@ class ReorderWidget extends StatelessWidget { required this.onReorderPressed, this.subtitle, }); + /// Recent completed orders for reorder. + final List orders; + + /// Callback when a reorder button is pressed. + final Function(Map shiftData) onReorderPressed; + + /// Optional subtitle for the section. + final String? subtitle; @override Widget build(BuildContext context) { @@ -177,11 +177,6 @@ class ReorderWidget extends StatelessWidget { } class _Badge extends StatelessWidget { - final IconData icon; - final String text; - final Color color; - final Color bg; - final Color textColor; const _Badge({ required this.icon, @@ -190,6 +185,11 @@ class _Badge extends StatelessWidget { required this.bg, required this.textColor, }); + final IconData icon; + final String text; + final Color color; + final Color bg; + final Color textColor; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart index 5fbb81f0..fc15c480 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart @@ -29,14 +29,6 @@ class _VendorOption { /// This widget provides a comprehensive form matching the design patterns /// used in view_order_card.dart for consistency across the app. class ShiftOrderFormSheet extends StatefulWidget { - /// Initial data for the form (e.g. from a reorder action). - final Map? initialData; - - /// Callback when the form is submitted. - final Function(Map data) onSubmit; - - /// Whether the submission is loading. - final bool isLoading; /// Creates a [ShiftOrderFormSheet]. const ShiftOrderFormSheet({ @@ -45,6 +37,14 @@ class ShiftOrderFormSheet extends StatefulWidget { required this.onSubmit, this.isLoading = false, }); + /// Initial data for the form (e.g. from a reorder action). + final Map? initialData; + + /// Callback when the form is submitted. + final Function(Map data) onSubmit; + + /// Whether the submission is loading. + final bool isLoading; @override State createState() => _ShiftOrderFormSheetState(); @@ -222,10 +222,7 @@ class _ShiftOrderFormSheetState extends State { .date(orderTimestamp) .execute(); - final String? orderId = orderResult.data?.order_insert.id; - if (orderId == null) { - return; - } + final String orderId = orderResult.data.order_insert.id; final int workersNeeded = _positions.fold( 0, @@ -255,10 +252,7 @@ class _ShiftOrderFormSheetState extends State { .cost(shiftCost) .execute(); - final String? shiftId = shiftResult.data?.shift_insert.id; - if (shiftId == null) { - return; - } + final String shiftId = shiftResult.data.shift_insert.id; for (final Map pos in _positions) { final String roleId = pos['roleId']?.toString() ?? ''; @@ -415,12 +409,12 @@ class _ShiftOrderFormSheetState extends State { final dc.ListShiftRolesByBusinessAndOrderShiftRolesShift firstShift = shiftRoles.first.shift; - final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub? + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub teamHub = firstShift.order.teamHub; await _loadHubsAndSelect( - placeId: teamHub?.placeId, - hubName: teamHub?.hubName, - address: teamHub?.address, + placeId: teamHub.placeId, + hubName: teamHub.hubName, + address: teamHub.address, ); _orderNameController.text = firstShift.order.eventName ?? ''; diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart index 1d20ab63..846a774b 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/spending_widget.dart @@ -4,6 +4,16 @@ import 'package:flutter/material.dart'; /// A widget that displays spending insights for the client. class SpendingWidget extends StatelessWidget { + + /// Creates a [SpendingWidget]. + const SpendingWidget({ + super.key, + required this.weeklySpending, + required this.next7DaysSpending, + required this.weeklyShifts, + required this.next7DaysScheduled, + this.subtitle, + }); /// The spending this week. final double weeklySpending; @@ -19,16 +29,6 @@ class SpendingWidget extends StatelessWidget { /// Optional subtitle for the section. final String? subtitle; - /// Creates a [SpendingWidget]. - const SpendingWidget({ - super.key, - required this.weeklySpending, - required this.next7DaysSpending, - required this.weeklyShifts, - required this.next7DaysScheduled, - this.subtitle, - }); - @override Widget build(BuildContext context) { final TranslationsClientHomeEn i18n = t.client_home; diff --git a/apps/mobile/packages/features/client/home/pubspec.yaml b/apps/mobile/packages/features/client/home/pubspec.yaml index e75de091..8e75e141 100644 --- a/apps/mobile/packages/features/client/home/pubspec.yaml +++ b/apps/mobile/packages/features/client/home/pubspec.yaml @@ -23,7 +23,11 @@ dependencies: path: ../../../core_localization krow_domain: ^0.0.1 krow_data_connect: ^0.0.1 + krow_core: + path: ../../../core + firebase_data_connect: any + intl: any dev_dependencies: flutter_test: sdk: flutter diff --git a/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart b/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart index b3422228..088ec6d1 100644 --- a/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart +++ b/apps/mobile/packages/features/client/hubs/lib/client_hubs.dart @@ -1,4 +1,4 @@ -library client_hubs; +library; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; diff --git a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart index 5fb1f0ba..7d7a24f0 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart @@ -14,10 +14,11 @@ import 'package:krow_domain/krow_domain.dart' NotAuthenticatedException; import '../../domain/repositories/hub_repository_interface.dart'; -import '../../util/hubs_constants.dart'; /// Implementation of [HubRepositoryInterface] backed by Data Connect. -class HubRepositoryImpl implements HubRepositoryInterface { +class HubRepositoryImpl + with dc.DataErrorHandler + implements HubRepositoryInterface { HubRepositoryImpl({ required firebase.FirebaseAuth firebaseAuth, required dc.ExampleConnector dataConnect, @@ -57,27 +58,23 @@ class HubRepositoryImpl implements HubRepositoryInterface { final String? countryValue = country ?? placeAddress?.country; final String? zipCodeValue = zipCode ?? placeAddress?.zipCode; - final OperationResult result = await _dataConnect - .createTeamHub( - teamId: teamId, - hubName: name, - address: address, - ) - .placeId(placeId) - .latitude(latitude) - .longitude(longitude) - .city(cityValue?.isNotEmpty == true ? cityValue : '') - .state(stateValue) - .street(streetValue) - .country(countryValue) - .zipCode(zipCodeValue) - .execute(); - final String? createdId = result.data?.teamHub_insert.id; - if (createdId == null) { - throw HubCreationFailedException( - technicalMessage: 'teamHub_insert returned null for hub: $name', - ); - } + final OperationResult + result = await executeProtected(() => _dataConnect + .createTeamHub( + teamId: teamId, + hubName: name, + address: address, + ) + .placeId(placeId) + .latitude(latitude) + .longitude(longitude) + .city(cityValue?.isNotEmpty == true ? cityValue : '') + .state(stateValue) + .street(streetValue) + .country(countryValue) + .zipCode(zipCodeValue) + .execute()); + final String createdId = result.data.teamHub_insert.id; final List hubs = await _fetchHubsForTeam( teamId: teamId, @@ -111,14 +108,14 @@ class HubRepositoryImpl implements HubRepositoryInterface { ); } - final QueryResult< - dc.ListOrdersByBusinessAndTeamHubData, - dc.ListOrdersByBusinessAndTeamHubVariables> result = await _dataConnect - .listOrdersByBusinessAndTeamHub( - businessId: businessId, - teamHubId: id, - ) - .execute(); + final QueryResult result = + await executeProtected(() => _dataConnect + .listOrdersByBusinessAndTeamHub( + businessId: businessId, + teamHubId: id, + ) + .execute()); if (result.data.orders.isNotEmpty) { throw HubHasOrdersException( @@ -126,7 +123,7 @@ class HubRepositoryImpl implements HubRepositoryInterface { ); } - await _dataConnect.deleteTeamHub(id: id).execute(); + await executeProtected(() => _dataConnect.deleteTeamHub(id: id).execute()); } @override @@ -169,9 +166,11 @@ class HubRepositoryImpl implements HubRepositoryInterface { ); } - final QueryResult result = await _dataConnect.getBusinessesByUserId( - userId: user.uid, - ).execute(); + final QueryResult result = + await executeProtected(() => _dataConnect.getBusinessesByUserId( + userId: user.uid, + ).execute()); if (result.data.businesses.isEmpty) { await _firebaseAuth.signOut(); throw BusinessNotFoundException( @@ -203,9 +202,10 @@ class HubRepositoryImpl implements HubRepositoryInterface { Future _getOrCreateTeamId( dc.GetBusinessesByUserIdBusinesses business, ) async { - final QueryResult teamsResult = await _dataConnect.getTeamsByOwnerId( - ownerId: business.id, - ).execute(); + final QueryResult + teamsResult = await executeProtected(() => _dataConnect.getTeamsByOwnerId( + ownerId: business.id, + ).execute()); if (teamsResult.data.teams.isNotEmpty) { return teamsResult.data.teams.first.id; } @@ -220,13 +220,10 @@ class HubRepositoryImpl implements HubRepositoryInterface { createTeamBuilder.email(business.email); } - final OperationResult createTeamResult = await createTeamBuilder.execute(); - final String? teamId = createTeamResult.data?.team_insert.id; - if (teamId == null) { - throw HubCreationFailedException( - technicalMessage: 'Team creation failed for business ${business.id}', - ); - } + final OperationResult + createTeamResult = + await executeProtected(() => createTeamBuilder.execute()); + final String teamId = createTeamResult.data.team_insert.id; return teamId; } @@ -235,9 +232,11 @@ class HubRepositoryImpl implements HubRepositoryInterface { required String teamId, required String businessId, }) async { - final QueryResult hubsResult = await _dataConnect.getTeamHubsByTeamId( - teamId: teamId, - ).execute(); + final QueryResult hubsResult = + await executeProtected(() => _dataConnect.getTeamHubsByTeamId( + teamId: teamId, + ).execute()); return hubsResult.data.teamHubs .map( @@ -318,13 +317,13 @@ class HubRepositoryImpl implements HubRepositoryInterface { } } - final String? streetValue = [streetNumber, route] - .where((String? value) => value != null && value!.isNotEmpty) + final String streetValue = [streetNumber, route] + .where((String? value) => value != null && value.isNotEmpty) .join(' ') .trim(); return _PlaceAddress( - street: streetValue?.isEmpty == true ? null : streetValue, + street: streetValue.isEmpty == true ? null : streetValue, city: city, state: state, country: country, diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/assign_nfc_tag_arguments.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/assign_nfc_tag_arguments.dart index ded2d2fb..76f854ca 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/assign_nfc_tag_arguments.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/assign_nfc_tag_arguments.dart @@ -4,17 +4,17 @@ import 'package:krow_core/core.dart'; /// /// Encapsulates the hub ID and the NFC tag ID to be assigned. class AssignNfcTagArguments extends UseCaseArgument { + + /// Creates an [AssignNfcTagArguments] instance. + /// + /// Both [hubId] and [nfcTagId] are required. + const AssignNfcTagArguments({required this.hubId, required this.nfcTagId}); /// The unique identifier of the hub. final String hubId; /// The unique identifier of the NFC tag. final String nfcTagId; - /// Creates an [AssignNfcTagArguments] instance. - /// - /// Both [hubId] and [nfcTagId] are required. - const AssignNfcTagArguments({required this.hubId, required this.nfcTagId}); - @override List get props => [hubId, nfcTagId]; } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart index 8518d9f0..ad6199de 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart @@ -4,20 +4,6 @@ import 'package:krow_core/core.dart'; /// /// Encapsulates the name and address of the hub to be created. class CreateHubArguments extends UseCaseArgument { - /// The name of the hub. - final String name; - - /// The physical address of the hub. - final String address; - - final String? placeId; - final double? latitude; - final double? longitude; - final String? city; - final String? state; - final String? street; - final String? country; - final String? zipCode; /// Creates a [CreateHubArguments] instance. /// @@ -34,6 +20,20 @@ class CreateHubArguments extends UseCaseArgument { this.country, this.zipCode, }); + /// The name of the hub. + final String name; + + /// The physical address of the hub. + final String address; + + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; @override List get props => [ diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/delete_hub_arguments.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/delete_hub_arguments.dart index 6a08630d..07a34bbf 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/delete_hub_arguments.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/delete_hub_arguments.dart @@ -4,13 +4,13 @@ import 'package:krow_core/core.dart'; /// /// Encapsulates the hub ID of the hub to be deleted. class DeleteHubArguments extends UseCaseArgument { - /// The unique identifier of the hub to delete. - final String hubId; /// Creates a [DeleteHubArguments] instance. /// /// The [hubId] is required. const DeleteHubArguments({required this.hubId}); + /// The unique identifier of the hub to delete. + final String hubId; @override List get props => [hubId]; diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/assign_nfc_tag_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/assign_nfc_tag_usecase.dart index ed627c9c..dc3fe00a 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/assign_nfc_tag_usecase.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/assign_nfc_tag_usecase.dart @@ -7,12 +7,12 @@ import '../repositories/hub_repository_interface.dart'; /// This use case handles the association of a physical NFC tag with a specific /// hub by calling the [HubRepositoryInterface]. class AssignNfcTagUseCase implements UseCase { - final HubRepositoryInterface _repository; /// Creates an [AssignNfcTagUseCase]. /// /// Requires a [HubRepositoryInterface] to interact with the backend. AssignNfcTagUseCase(this._repository); + final HubRepositoryInterface _repository; @override Future call(AssignNfcTagArguments arguments) { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart index 50550bc1..9c55ed30 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart @@ -9,12 +9,12 @@ import '../repositories/hub_repository_interface.dart'; /// [HubRepositoryInterface]. It requires [CreateHubArguments] which includes /// the name and address of the hub. class CreateHubUseCase implements UseCase { - final HubRepositoryInterface _repository; /// Creates a [CreateHubUseCase]. /// /// Requires a [HubRepositoryInterface] to perform the actual creation. CreateHubUseCase(this._repository); + final HubRepositoryInterface _repository; @override Future call(CreateHubArguments arguments) { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/delete_hub_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/delete_hub_usecase.dart index ee8d14df..b89aa933 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/delete_hub_usecase.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/delete_hub_usecase.dart @@ -6,12 +6,12 @@ import '../repositories/hub_repository_interface.dart'; /// /// This use case removes a hub from the system via the [HubRepositoryInterface]. class DeleteHubUseCase implements UseCase { - final HubRepositoryInterface _repository; /// Creates a [DeleteHubUseCase]. /// /// Requires a [HubRepositoryInterface] to perform the deletion. DeleteHubUseCase(this._repository); + final HubRepositoryInterface _repository; @override Future call(DeleteHubArguments arguments) { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/get_hubs_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/get_hubs_usecase.dart index 7db32de4..450a090a 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/get_hubs_usecase.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/get_hubs_usecase.dart @@ -7,12 +7,12 @@ import '../repositories/hub_repository_interface.dart'; /// This use case retrieves all hubs associated with the current client /// by interacting with the [HubRepositoryInterface]. class GetHubsUseCase implements NoInputUseCase> { - final HubRepositoryInterface _repository; /// Creates a [GetHubsUseCase]. /// /// Requires a [HubRepositoryInterface] to fetch the data. GetHubsUseCase(this._repository); + final HubRepositoryInterface _repository; @override Future> call() { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart index 6fa6c573..2c2acb02 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart @@ -19,10 +19,6 @@ import 'client_hubs_state.dart'; class ClientHubsBloc extends Bloc with BlocErrorHandler implements Disposable { - final GetHubsUseCase _getHubsUseCase; - final CreateHubUseCase _createHubUseCase; - final DeleteHubUseCase _deleteHubUseCase; - final AssignNfcTagUseCase _assignNfcTagUseCase; ClientHubsBloc({ required GetHubsUseCase getHubsUseCase, @@ -42,6 +38,10 @@ class ClientHubsBloc extends Bloc on(_onAddDialogToggled); on(_onIdentifyDialogToggled); } + final GetHubsUseCase _getHubsUseCase; + final CreateHubUseCase _createHubUseCase; + final DeleteHubUseCase _deleteHubUseCase; + final AssignNfcTagUseCase _assignNfcTagUseCase; void _onAddDialogToggled( ClientHubsAddDialogToggled event, @@ -70,10 +70,10 @@ class ClientHubsBloc extends Bloc await handleError( emit: emit, action: () async { - final hubs = await _getHubsUseCase(); + final List hubs = await _getHubsUseCase(); emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs)); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientHubsStatus.failure, errorMessage: errorKey, ), @@ -103,7 +103,7 @@ class ClientHubsBloc extends Bloc zipCode: event.zipCode, ), ); - final hubs = await _getHubsUseCase(); + final List hubs = await _getHubsUseCase(); emit( state.copyWith( status: ClientHubsStatus.actionSuccess, @@ -113,7 +113,7 @@ class ClientHubsBloc extends Bloc ), ); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientHubsStatus.actionFailure, errorMessage: errorKey, ), @@ -130,7 +130,7 @@ class ClientHubsBloc extends Bloc emit: emit, action: () async { await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId)); - final hubs = await _getHubsUseCase(); + final List hubs = await _getHubsUseCase(); emit( state.copyWith( status: ClientHubsStatus.actionSuccess, @@ -139,7 +139,7 @@ class ClientHubsBloc extends Bloc ), ); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientHubsStatus.actionFailure, errorMessage: errorKey, ), @@ -158,7 +158,7 @@ class ClientHubsBloc extends Bloc await _assignNfcTagUseCase( AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId), ); - final hubs = await _getHubsUseCase(); + final List hubs = await _getHubsUseCase(); emit( state.copyWith( status: ClientHubsStatus.actionSuccess, @@ -168,7 +168,7 @@ class ClientHubsBloc extends Bloc ), ); }, - onError: (errorKey) => state.copyWith( + onError: (String errorKey) => state.copyWith( status: ClientHubsStatus.actionFailure, errorMessage: errorKey, ), diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart index 428eb774..9e539c8e 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart @@ -16,16 +16,6 @@ class ClientHubsFetched extends ClientHubsEvent { /// Event triggered to add a new hub. class ClientHubsAddRequested extends ClientHubsEvent { - final String name; - final String address; - final String? placeId; - final double? latitude; - final double? longitude; - final String? city; - final String? state; - final String? street; - final String? country; - final String? zipCode; const ClientHubsAddRequested({ required this.name, @@ -39,6 +29,16 @@ class ClientHubsAddRequested extends ClientHubsEvent { this.country, this.zipCode, }); + final String name; + final String address; + final String? placeId; + final double? latitude; + final double? longitude; + final String? city; + final String? state; + final String? street; + final String? country; + final String? zipCode; @override List get props => [ @@ -57,9 +57,9 @@ class ClientHubsAddRequested extends ClientHubsEvent { /// Event triggered to delete a hub. class ClientHubsDeleteRequested extends ClientHubsEvent { - final String hubId; const ClientHubsDeleteRequested(this.hubId); + final String hubId; @override List get props => [hubId]; @@ -67,13 +67,13 @@ class ClientHubsDeleteRequested extends ClientHubsEvent { /// Event triggered to assign an NFC tag to a hub. class ClientHubsNfcTagAssignRequested extends ClientHubsEvent { - final String hubId; - final String nfcTagId; const ClientHubsNfcTagAssignRequested({ required this.hubId, required this.nfcTagId, }); + final String hubId; + final String nfcTagId; @override List get props => [hubId, nfcTagId]; @@ -86,9 +86,9 @@ class ClientHubsMessageCleared extends ClientHubsEvent { /// Event triggered to toggle the visibility of the "Add Hub" dialog. class ClientHubsAddDialogToggled extends ClientHubsEvent { - final bool visible; const ClientHubsAddDialogToggled({required this.visible}); + final bool visible; @override List get props => [visible]; @@ -96,9 +96,9 @@ class ClientHubsAddDialogToggled extends ClientHubsEvent { /// Event triggered to toggle the visibility of the "Identify NFC" dialog. class ClientHubsIdentifyDialogToggled extends ClientHubsEvent { - final Hub? hub; const ClientHubsIdentifyDialogToggled({this.hub}); + final Hub? hub; @override List get props => [hub]; diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart index 4d592df8..1d1eea5d 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart @@ -14,6 +14,15 @@ enum ClientHubsStatus { /// State class for the ClientHubs BLoC. class ClientHubsState extends Equatable { + + const ClientHubsState({ + this.status = ClientHubsStatus.initial, + this.hubs = const [], + this.errorMessage, + this.successMessage, + this.showAddHubDialog = false, + this.hubToIdentify, + }); final ClientHubsStatus status; final List hubs; final String? errorMessage; @@ -26,15 +35,6 @@ class ClientHubsState extends Equatable { /// If null, the identification dialog is closed. final Hub? hubToIdentify; - const ClientHubsState({ - this.status = ClientHubsStatus.initial, - this.hubs = const [], - this.errorMessage, - this.successMessage, - this.showAddHubDialog = false, - this.hubToIdentify, - }); - ClientHubsState copyWith({ ClientHubsStatus? status, List? hubs, diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart index 7d95c749..a0be904f 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart @@ -7,6 +7,13 @@ import 'hub_address_autocomplete.dart'; /// A dialog for adding a new hub. class AddHubDialog extends StatefulWidget { + + /// Creates an [AddHubDialog]. + const AddHubDialog({ + required this.onCreate, + required this.onCancel, + super.key, + }); /// Callback when the "Create Hub" button is pressed. final void Function( String name, @@ -19,13 +26,6 @@ class AddHubDialog extends StatefulWidget { /// Callback when the dialog is cancelled. final VoidCallback onCancel; - /// Creates an [AddHubDialog]. - const AddHubDialog({ - required this.onCreate, - required this.onCancel, - super.key, - }); - @override State createState() => _AddHubDialogState(); } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart index 575f2cc6..138bb9e7 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart @@ -39,7 +39,7 @@ class HubAddressAutocomplete extends StatelessWidget { ); onSelected?.call(prediction); }, - itemBuilder: (_, _, Prediction prediction) { + itemBuilder: (BuildContext context, int index, Prediction prediction) { return Padding( padding: const EdgeInsets.all(UiConstants.space2), child: Row( diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart index 446620e6..812be35b 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_card.dart @@ -5,14 +5,6 @@ import 'package:core_localization/core_localization.dart'; /// A card displaying information about a single hub. class HubCard extends StatelessWidget { - /// The hub to display. - final Hub hub; - - /// Callback when the NFC button is pressed. - final VoidCallback onNfcPressed; - - /// Callback when the delete button is pressed. - final VoidCallback onDeletePressed; /// Creates a [HubCard]. const HubCard({ @@ -21,6 +13,14 @@ class HubCard extends StatelessWidget { required this.onDeletePressed, super.key, }); + /// The hub to display. + final Hub hub; + + /// Callback when the NFC button is pressed. + final VoidCallback onNfcPressed; + + /// Callback when the delete button is pressed. + final VoidCallback onDeletePressed; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart index e9b7f8c3..e3e18a91 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_empty_state.dart @@ -4,11 +4,11 @@ import 'package:core_localization/core_localization.dart'; /// Widget displayed when there are no hubs. class HubEmptyState extends StatelessWidget { - /// Callback when the add button is pressed. - final VoidCallback onAddPressed; /// Creates a [HubEmptyState]. const HubEmptyState({required this.onAddPressed, super.key}); + /// Callback when the add button is pressed. + final VoidCallback onAddPressed; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart index df815a6c..b902c707 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/identify_nfc_dialog.dart @@ -5,14 +5,6 @@ import 'package:core_localization/core_localization.dart'; /// A dialog for identifying and assigning an NFC tag to a hub. class IdentifyNfcDialog extends StatefulWidget { - /// The hub to assign the tag to. - final Hub hub; - - /// Callback when a tag is assigned. - final Function(String nfcTagId) onAssign; - - /// Callback when the dialog is cancelled. - final VoidCallback onCancel; /// Creates an [IdentifyNfcDialog]. const IdentifyNfcDialog({ @@ -21,6 +13,14 @@ class IdentifyNfcDialog extends StatefulWidget { required this.onCancel, super.key, }); + /// The hub to assign the tag to. + final Hub hub; + + /// Callback when a tag is assigned. + final Function(String nfcTagId) onAssign; + + /// Callback when the dialog is cancelled. + final VoidCallback onCancel; @override State createState() => _IdentifyNfcDialogState(); diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart index c7e8f653..82e730fc 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart @@ -6,7 +6,9 @@ import 'package:krow_domain/krow_domain.dart' as domain; import '../../domain/repositories/i_view_orders_repository.dart'; /// Implementation of [IViewOrdersRepository] using Data Connect. -class ViewOrdersRepositoryImpl implements IViewOrdersRepository { +class ViewOrdersRepositoryImpl + with dc.DataErrorHandler + implements IViewOrdersRepository { final firebase.FirebaseAuth _firebaseAuth; final dc.ExampleConnector _dataConnect; @@ -29,13 +31,15 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository { final fdc.Timestamp startTimestamp = _toTimestamp(_startOfDay(start)); final fdc.Timestamp endTimestamp = _toTimestamp(_endOfDay(end)); - final fdc.QueryResult result = await _dataConnect - .listShiftRolesByBusinessAndDateRange( - businessId: businessId, - start: startTimestamp, - end: endTimestamp, - ) - .execute(); + final fdc.QueryResult result = + await executeProtected(() => _dataConnect + .listShiftRolesByBusinessAndDateRange( + businessId: businessId, + start: startTimestamp, + end: endTimestamp, + ) + .execute()); print( 'ViewOrders range start=${start.toIso8601String()} end=${end.toIso8601String()} shiftRoles=${result.data.shiftRoles.length}', ); @@ -101,13 +105,15 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository { final fdc.Timestamp dayStart = _toTimestamp(_startOfDay(day)); final fdc.Timestamp dayEnd = _toTimestamp(_endOfDay(day)); - final fdc.QueryResult result = await _dataConnect - .listAcceptedApplicationsByBusinessForDay( - businessId: businessId, - dayStart: dayStart, - dayEnd: dayEnd, - ) - .execute(); + final fdc.QueryResult result = + await executeProtected(() => _dataConnect + .listAcceptedApplicationsByBusinessForDay( + businessId: businessId, + dayStart: dayStart, + dayEnd: dayEnd, + ) + .execute()); print( 'ViewOrders day=${day.toIso8601String()} applications=${result.data.applications.length}', diff --git a/apps/mobile/packages/features/client/view_orders/test/src/presentation/blocs/view_orders_cubit_test.dart b/apps/mobile/packages/features/client/view_orders/test/src/presentation/blocs/view_orders_cubit_test.dart deleted file mode 100644 index 27e68494..00000000 --- a/apps/mobile/packages/features/client/view_orders/test/src/presentation/blocs/view_orders_cubit_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:view_orders/src/presentation/blocs/view_orders_cubit.dart'; -import 'package:view_orders/src/presentation/blocs/view_orders_state.dart'; -import 'package:view_orders/src/domain/usecases/get_orders_use_case.dart'; -import 'package:view_orders/src/domain/usecases/get_accepted_applications_for_day_use_case.dart'; -import 'package:krow_domain/krow_domain.dart'; -import 'package:view_orders/src/domain/arguments/orders_range_arguments.dart'; -import 'package:view_orders/src/domain/arguments/orders_day_arguments.dart'; - -class MockGetOrdersUseCase extends Mock implements GetOrdersUseCase {} -class MockGetAcceptedAppsUseCase extends Mock implements GetAcceptedApplicationsForDayUseCase {} - -void main() { - group('ViewOrdersCubit', () { - late GetOrdersUseCase getOrdersUseCase; - late GetAcceptedApplicationsForDayUseCase getAcceptedAppsUseCase; - - setUp(() { - getOrdersUseCase = MockGetOrdersUseCase(); - getAcceptedAppsUseCase = MockGetAcceptedAppsUseCase(); - registerFallbackValue(OrdersRangeArguments(start: DateTime.now(), end: DateTime.now())); - registerFallbackValue(OrdersDayArguments(day: DateTime.now())); - }); - - test('initial state is correct', () { - final cubit = ViewOrdersCubit( - getOrdersUseCase: getOrdersUseCase, - getAcceptedAppsUseCase: getAcceptedAppsUseCase, - ); - expect(cubit.state.status, ViewOrdersStatus.initial); - cubit.close(); - }); - - blocTest( - 'calculates upNextCount based on ALL loaded orders, not just the selected day', - build: () { - final mockOrders = [ - // Order 1: Today (Matches selected date) - OrderItem( - id: '1', orderId: '1', title: 'Order 1', clientName: 'Client', - status: 'OPEN', date: '2026-02-04', startTime: '09:00', endTime: '17:00', - location: 'Loc', locationAddress: 'Addr', filled: 0, workersNeeded: 1, - hourlyRate: 20, hours: 8, totalValue: 160 - ), - // Order 2: Tomorrow (Different date) - OrderItem( - id: '2', orderId: '2', title: 'Order 2', clientName: 'Client', - status: 'OPEN', date: '2026-02-05', startTime: '09:00', endTime: '17:00', - location: 'Loc', locationAddress: 'Addr', filled: 0, workersNeeded: 1, - hourlyRate: 20, hours: 8, totalValue: 160 - ), - ]; - - when(() => getOrdersUseCase(any())).thenAnswer((_) async => mockOrders); - when(() => getAcceptedAppsUseCase(any())).thenAnswer((_) async => {}); - - return ViewOrdersCubit( - getOrdersUseCase: getOrdersUseCase, - getAcceptedAppsUseCase: getAcceptedAppsUseCase, - ); - }, - act: (cubit) async { - // Wait for init to trigger load - await Future.delayed(const Duration(milliseconds: 100)); - - // Select 'Today' (2026-02-04 matches Order 1) - cubit.selectDate(DateTime(2026, 02, 04)); - }, - verify: (cubit) { - // Assert: - // 1. filteredOrders should only have 1 order (the one for the selected date) - expect(cubit.state.filteredOrders.length, 1, reason: 'Should only show orders for selected filtered date'); - expect(cubit.state.filteredOrders.first.id, '1'); - - // 2. upNextCount should have 2 orders (Total for the loaded week) - expect(cubit.state.upNextCount, 2, reason: 'Up Next count should include ALL orders in the week range'); - }, - ); - }); -} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index 10dbffbe..e7aa6a97 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -10,7 +10,9 @@ import '../../domain/ui_entities/auth_mode.dart'; import '../../domain/repositories/auth_repository_interface.dart'; /// Implementation of [AuthRepositoryInterface]. -class AuthRepositoryImpl implements AuthRepositoryInterface { +class AuthRepositoryImpl + with DataErrorHandler + implements AuthRepositoryInterface { AuthRepositoryImpl({ required this.firebaseAuth, required this.dataConnect, @@ -112,31 +114,35 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { } final QueryResult response = - await dataConnect.getUserById( - id: firebaseUser.uid, - ).execute(); + await executeProtected(() => dataConnect + .getUserById( + id: firebaseUser.uid, + ) + .execute()); final GetUserByIdUser? user = response.data.user; GetStaffByUserIdStaffs? staffRecord; if (mode == AuthMode.signup) { if (user == null) { - await dataConnect + await executeProtected(() => dataConnect .createUser( id: firebaseUser.uid, role: UserBaseRole.USER, ) .userRole('STAFF') - .execute(); + .execute()); } else { if (user.userRole != 'STAFF') { await firebaseAuth.signOut(); throw Exception('User is not authorized for this app.'); } final QueryResult - staffResponse = await dataConnect.getStaffByUserId( - userId: firebaseUser.uid, - ).execute(); + staffResponse = await executeProtected(() => dataConnect + .getStaffByUserId( + userId: firebaseUser.uid, + ) + .execute()); if (staffResponse.data.staffs.isNotEmpty) { await firebaseAuth.signOut(); throw Exception( @@ -155,9 +161,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { } final QueryResult - staffResponse = await dataConnect.getStaffByUserId( - userId: firebaseUser.uid, - ).execute(); + staffResponse = await executeProtected(() => dataConnect + .getStaffByUserId( + userId: firebaseUser.uid, + ) + .execute()); if (staffResponse.data.staffs.isEmpty) { await firebaseAuth.signOut(); throw Exception( diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart index ba1f14e8..6caf9a50 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart @@ -1,22 +1,24 @@ -import 'package:firebase_data_connect/firebase_data_connect.dart'; +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 'package:krow_core/core.dart'; -import 'package:krow_data_connect/src/session/staff_session_store.dart'; + import '../../domain/repositories/clock_in_repository_interface.dart'; /// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect. -class ClockInRepositoryImpl implements ClockInRepositoryInterface { - final dc.ExampleConnector _dataConnect; - final Map _shiftToApplicationId = {}; - String? _activeApplicationId; +class ClockInRepositoryImpl + with dc.DataErrorHandler + implements ClockInRepositoryInterface { ClockInRepositoryImpl({ required dc.ExampleConnector dataConnect, }) : _dataConnect = dataConnect; + final dc.ExampleConnector _dataConnect; + final Map _shiftToApplicationId = {}; + String? _activeApplicationId; Future _getStaffId() async { - final StaffSession? session = StaffSessionStore.instance.session; + final dc.StaffSession? session = dc.StaffSessionStore.instance.session; final String? staffId = session?.staff?.id; if (staffId != null && staffId.isNotEmpty) { return staffId; @@ -24,7 +26,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { throw Exception('Staff session not found'); } - /// Helper to convert Data Connect Timestamp to DateTime + /// Helper to convert Data Connect fdc.Timestamp to DateTime DateTime? _toDateTime(dynamic t) { if (t == null) return null; DateTime? dt; @@ -34,7 +36,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { dt = DateTime.tryParse(t); } else { try { - if (t is Timestamp) { + if (t is fdc.Timestamp) { dt = t.toDateTime(); } } catch (_) {} @@ -46,9 +48,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { } catch (_) {} try { - if (dt == null) { - dt = DateTime.tryParse(t.toString()); - } + dt ??= DateTime.tryParse(t.toString()); } catch (_) {} } @@ -58,13 +58,13 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { return null; } - /// Helper to create Timestamp from DateTime - Timestamp _fromDateTime(DateTime d) { - // Assuming Timestamp.fromJson takes an ISO string - return Timestamp.fromJson(d.toUtc().toIso8601String()); + /// Helper to create fdc.Timestamp from DateTime + fdc.Timestamp _fromDateTime(DateTime d) { + // Assuming fdc.Timestamp.fromJson takes an ISO string + return fdc.Timestamp.fromJson(d.toUtc().toIso8601String()); } - ({Timestamp start, Timestamp end}) _utcDayRange(DateTime localDay) { + ({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) { final DateTime dayStartUtc = DateTime.utc( localDay.year, localDay.month, @@ -91,22 +91,24 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { String staffId, ) async { final DateTime now = DateTime.now(); - final range = _utcDayRange(now); - final QueryResult - result = await _dataConnect - .getApplicationsByStaffId(staffId: staffId) - .dayStart(range.start) - .dayEnd(range.end) - .execute(); + final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now); + final fdc.QueryResult result = await executeProtected( + () => _dataConnect + .getApplicationsByStaffId(staffId: staffId) + .dayStart(range.start) + .dayEnd(range.end) + .execute(), + ); - final apps = result.data.applications; - if (apps.isEmpty) return const []; + final List apps = result.data.applications; + if (apps.isEmpty) return const []; _shiftToApplicationId ..clear() - ..addEntries(apps.map((app) => MapEntry(app.shiftId, app.id))); + ..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) => MapEntry(app.shiftId, app.id))); - apps.sort((a, b) { + apps.sort((dc.GetApplicationsByStaffIdApplications a, dc.GetApplicationsByStaffIdApplications b) { final DateTime? aTime = _toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date); final DateTime? bTime = @@ -122,28 +124,17 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { return apps; } - dc.GetApplicationsByStaffIdApplications? _getActiveApplication( - List apps, - ) { - try { - return apps.firstWhere((app) { - final status = app.status.stringValue; - return status == 'CHECKED_IN' || status == 'LATE'; - }); - } catch (_) { - return null; - } - } + @override Future> getTodaysShifts() async { final String staffId = await _getStaffId(); final List apps = await _getTodaysApplications(staffId); - if (apps.isEmpty) return const []; + if (apps.isEmpty) return const []; - final List shifts = []; - for (final app in apps) { + final List shifts = []; + for (final dc.GetApplicationsByStaffIdApplications app in apps) { final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift; final DateTime? startDt = _toDateTime(app.shiftRole.startTime); final DateTime? endDt = _toDateTime(app.shiftRole.endTime); @@ -189,7 +180,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { } dc.GetApplicationsByStaffIdApplications? activeApp; - for (final app in apps) { + for (final dc.GetApplicationsByStaffIdApplications app in apps) { if (app.checkInTime != null && app.checkOutTime == null) { if (activeApp == null) { activeApp = app; @@ -209,7 +200,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { } _activeApplicationId = activeApp.id; - print('Active check-in appId=$_activeApplicationId'); + return AttendanceStatus( isCheckedIn: true, checkInTime: _toDateTime(activeApp.checkInTime), @@ -227,39 +218,22 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { dc.GetApplicationsByStaffIdApplications? app; if (cachedAppId != null) { try { - final apps = await _getTodaysApplications(staffId); - app = apps.firstWhere((a) => a.id == cachedAppId); + final List apps = await _getTodaysApplications(staffId); + app = apps.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.id == cachedAppId); } catch (_) {} } app ??= (await _getTodaysApplications(staffId)) - .firstWhere((a) => a.shiftId == shiftId); + .firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId); - final Timestamp checkInTs = _fromDateTime(DateTime.now()); - print( - 'ClockIn request: appId=${app.id} shiftId=$shiftId ' - 'checkInTime=${checkInTs.toJson()}', - ); - try { - await _dataConnect - .updateApplicationStatus( - id: app.id, - ) - .checkInTime(checkInTs) - .execute(); - _activeApplicationId = app.id; - } catch (e) { - print('ClockIn updateApplicationStatus error: $e'); - print('ClockIn error type: ${e.runtimeType}'); - try { - final dynamic err = e; - final dynamic details = - err.details ?? err.response ?? err.data ?? err.message; - if (details != null) { - print('ClockIn error details: $details'); - } - } catch (_) {} - rethrow; - } + final fdc.Timestamp checkInTs = _fromDateTime(DateTime.now()); + + await executeProtected(() => _dataConnect + .updateApplicationStatus( + id: app!.id, + ) + .checkInTime(checkInTs) + .execute()); + _activeApplicationId = app.id; return getAttendanceStatus(); } @@ -270,25 +244,18 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { int? breakTimeMinutes, String? applicationId, }) async { - final String staffId = await _getStaffId(); + await _getStaffId(); // Validate session - print( - 'ClockOut request: applicationId=$applicationId ' - 'activeApplicationId=$_activeApplicationId', - ); + final String? targetAppId = applicationId ?? _activeApplicationId; if (targetAppId == null || targetAppId.isEmpty) { throw Exception('No active application id for checkout'); } - final appResult = await _dataConnect + final fdc.QueryResult appResult = await executeProtected(() => _dataConnect .getApplicationById(id: targetAppId) - .execute(); - final app = appResult.data.application; - print( - 'ClockOut getApplicationById: id=${app?.id} ' - 'checkIn=${app?.checkInTime?.toJson()} ' - 'checkOut=${app?.checkOutTime?.toJson()}', - ); + .execute()); + final dc.GetApplicationByIdApplication? app = appResult.data.application; + if (app == null) { throw Exception('Application not found for checkout'); } @@ -296,12 +263,12 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface { throw Exception('No active shift found to clock out'); } - await _dataConnect + await executeProtected(() => _dataConnect .updateApplicationStatus( id: targetAppId, ) .checkOutTime(_fromDateTime(DateTime.now())) - .execute(); + .execute()); return getAttendanceStatus(); } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_in_arguments.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_in_arguments.dart index bf7cb9d2..e2b3724f 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_in_arguments.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_in_arguments.dart @@ -2,18 +2,18 @@ import 'package:krow_core/core.dart'; /// Represents the arguments required for the [ClockInUseCase]. class ClockInArguments extends UseCaseArgument { - /// The ID of the shift to clock in to. - final String shiftId; - - /// Optional notes provided by the user during clock-in. - final String? notes; /// Creates a [ClockInArguments] instance. const ClockInArguments({ required this.shiftId, this.notes, }); + /// The ID of the shift to clock in to. + final String shiftId; + + /// Optional notes provided by the user during clock-in. + final String? notes; @override - List get props => [shiftId, notes]; + List get props => [shiftId, notes]; } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart index 04cb55fc..f077eaf1 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/arguments/clock_out_arguments.dart @@ -2,6 +2,13 @@ import 'package:krow_core/core.dart'; /// Represents the arguments required for the [ClockOutUseCase]. class ClockOutArguments extends UseCaseArgument { + + /// Creates a [ClockOutArguments] instance. + const ClockOutArguments({ + this.notes, + this.breakTimeMinutes, + this.applicationId, + }); /// Optional notes provided by the user during clock-out. final String? notes; @@ -11,13 +18,6 @@ class ClockOutArguments extends UseCaseArgument { /// Optional application id for checkout. final String? applicationId; - /// Creates a [ClockOutArguments] instance. - const ClockOutArguments({ - this.notes, - this.breakTimeMinutes, - this.applicationId, - }); - @override - List get props => [notes, breakTimeMinutes, applicationId]; + List get props => [notes, breakTimeMinutes, applicationId]; } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart index c4535129..b99b27f5 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_in_usecase.dart @@ -5,9 +5,9 @@ import '../arguments/clock_in_arguments.dart'; /// Use case for clocking in a user. class ClockInUseCase implements UseCase { - final ClockInRepositoryInterface _repository; ClockInUseCase(this._repository); + final ClockInRepositoryInterface _repository; @override Future call(ClockInArguments arguments) { diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart index f5b0b14a..aa8ecdc4 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/clock_out_usecase.dart @@ -5,9 +5,9 @@ import '../arguments/clock_out_arguments.dart'; /// Use case for clocking out a user. class ClockOutUseCase implements UseCase { - final ClockInRepositoryInterface _repository; ClockOutUseCase(this._repository); + final ClockInRepositoryInterface _repository; @override Future call(ClockOutArguments arguments) { diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart index 1f80da69..1c78a836 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_attendance_status_usecase.dart @@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart'; /// Use case for getting the current attendance status (check-in/out times). class GetAttendanceStatusUseCase implements NoInputUseCase { - final ClockInRepositoryInterface _repository; GetAttendanceStatusUseCase(this._repository); + final ClockInRepositoryInterface _repository; @override Future call() { diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart index 3df3a14b..5cdf4862 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/domain/usecases/get_todays_shift_usecase.dart @@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart'; /// Use case for retrieving the user's scheduled shifts for today. class GetTodaysShiftUseCase implements NoInputUseCase> { - final ClockInRepositoryInterface _repository; GetTodaysShiftUseCase(this._repository); + final ClockInRepositoryInterface _repository; @override Future> call() { diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart index 98c9a078..270e296d 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_bloc.dart @@ -11,13 +11,6 @@ import 'clock_in_event.dart'; import 'clock_in_state.dart'; class ClockInBloc extends Bloc { - final GetTodaysShiftUseCase _getTodaysShift; - final GetAttendanceStatusUseCase _getAttendanceStatus; - final ClockInUseCase _clockIn; - final ClockOutUseCase _clockOut; - - // Mock Venue Location (e.g., Grand Hotel, NYC) - static const double allowedRadiusMeters = 500; ClockInBloc({ required GetTodaysShiftUseCase getTodaysShift, @@ -41,6 +34,13 @@ class ClockInBloc extends Bloc { add(ClockInPageLoaded()); } + final GetTodaysShiftUseCase _getTodaysShift; + final GetAttendanceStatusUseCase _getAttendanceStatus; + final ClockInUseCase _clockIn; + final ClockOutUseCase _clockOut; + + // Mock Venue Location (e.g., Grand Hotel, NYC) + static const double allowedRadiusMeters = 500; Future _onLoaded( ClockInPageLoaded event, @@ -48,8 +48,8 @@ class ClockInBloc extends Bloc { ) async { emit(state.copyWith(status: ClockInStatus.loading)); try { - final shifts = await _getTodaysShift(); - final status = await _getAttendanceStatus(); + final List shifts = await _getTodaysShift(); + final AttendanceStatus status = await _getAttendanceStatus(); // Check permissions silently on load? Maybe better to wait for user interaction or specific event // However, if shift exists, we might want to check permission state @@ -58,7 +58,7 @@ class ClockInBloc extends Bloc { if (status.activeShiftId != null) { try { selectedShift = - shifts.firstWhere((s) => s.id == status.activeShiftId); + shifts.firstWhere((Shift s) => s.id == status.activeShiftId); } catch (_) {} } selectedShift ??= shifts.last; @@ -93,7 +93,7 @@ class ClockInBloc extends Bloc { permission = await Geolocator.requestPermission(); } - final hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse; + final bool hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse; emit(state.copyWith(hasLocationConsent: hasConsent)); @@ -105,9 +105,9 @@ class ClockInBloc extends Bloc { } } - void _startLocationUpdates() async { + Future _startLocationUpdates() async { try { - final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); + final Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); double distance = 0; bool isVerified = false; // Require location match by default if shift has location @@ -195,7 +195,7 @@ class ClockInBloc extends Bloc { emit(state.copyWith(status: ClockInStatus.actionInProgress)); try { - final newStatus = await _clockIn( + final AttendanceStatus newStatus = await _clockIn( ClockInArguments(shiftId: event.shiftId, notes: event.notes), ); emit(state.copyWith( @@ -216,7 +216,7 @@ class ClockInBloc extends Bloc { ) async { emit(state.copyWith(status: ClockInStatus.actionInProgress)); try { - final newStatus = await _clockOut( + final AttendanceStatus newStatus = await _clockOut( ClockOutArguments( notes: event.notes, breakTimeMinutes: 0, // Should be passed from event if supported diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart index acc5c13c..85dd1614 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_event.dart @@ -6,75 +6,75 @@ abstract class ClockInEvent extends Equatable { const ClockInEvent(); @override - List get props => []; + List get props => []; } class ClockInPageLoaded extends ClockInEvent {} class ShiftSelected extends ClockInEvent { - final Shift shift; const ShiftSelected(this.shift); + final Shift shift; @override - List get props => [shift]; + List get props => [shift]; } class DateSelected extends ClockInEvent { - final DateTime date; const DateSelected(this.date); + final DateTime date; @override - List get props => [date]; + List get props => [date]; } class CheckInRequested extends ClockInEvent { + + const CheckInRequested({required this.shiftId, this.notes}); final String shiftId; final String? notes; - const CheckInRequested({required this.shiftId, this.notes}); - @override - List get props => [shiftId, notes]; + List get props => [shiftId, notes]; } class CheckOutRequested extends ClockInEvent { + + const CheckOutRequested({this.notes, this.breakTimeMinutes}); final String? notes; final int? breakTimeMinutes; - const CheckOutRequested({this.notes, this.breakTimeMinutes}); - @override - List get props => [notes, breakTimeMinutes]; + List get props => [notes, breakTimeMinutes]; } class CheckInModeChanged extends ClockInEvent { - final String mode; const CheckInModeChanged(this.mode); + final String mode; @override - List get props => [mode]; + List get props => [mode]; } class CommuteModeToggled extends ClockInEvent { - final bool isEnabled; const CommuteModeToggled(this.isEnabled); + final bool isEnabled; @override - List get props => [isEnabled]; + List get props => [isEnabled]; } class RequestLocationPermission extends ClockInEvent {} class LocationUpdated extends ClockInEvent { + + const LocationUpdated({required this.position, required this.distance, required this.isVerified}); final Position position; final double distance; final bool isVerified; - - const LocationUpdated({required this.position, required this.distance, required this.isVerified}); @override - List get props => [position, distance, isVerified]; + List get props => [position, distance, isVerified]; } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart index dc01582e..2474b519 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/bloc/clock_in_state.dart @@ -6,6 +6,22 @@ import 'package:geolocator/geolocator.dart'; enum ClockInStatus { initial, loading, success, failure, actionInProgress } class ClockInState extends Equatable { + + const ClockInState({ + this.status = ClockInStatus.initial, + this.todayShifts = const [], + this.selectedShift, + this.attendance = const AttendanceStatus(), + required this.selectedDate, + this.checkInMode = 'swipe', + this.errorMessage, + this.currentLocation, + this.distanceFromVenue, + this.isLocationVerified = false, + this.isCommuteModeOn = false, + this.hasLocationConsent = false, + this.etaMinutes, + }); final ClockInStatus status; final List todayShifts; final Shift? selectedShift; @@ -21,22 +37,6 @@ class ClockInState extends Equatable { final bool hasLocationConsent; final int? etaMinutes; - const ClockInState({ - this.status = ClockInStatus.initial, - this.todayShifts = const [], - this.selectedShift, - this.attendance = const AttendanceStatus(), - required this.selectedDate, - this.checkInMode = 'swipe', - this.errorMessage, - this.currentLocation, - this.distanceFromVenue, - this.isLocationVerified = false, - this.isCommuteModeOn = false, - this.hasLocationConsent = false, - this.etaMinutes, - }); - ClockInState copyWith({ ClockInStatus? status, List? todayShifts, @@ -70,7 +70,7 @@ class ClockInState extends Equatable { } @override - List get props => [ + List get props => [ status, todayShifts, selectedShift, diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/blocs/clock_in_cubit.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/blocs/clock_in_cubit.dart index 366b1652..7f0c228e 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/blocs/clock_in_cubit.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/blocs/clock_in_cubit.dart @@ -1,17 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:permission_handler/permission_handler.dart'; // --- State --- class ClockInState extends Equatable { - final bool isLoading; - final bool isLocationVerified; - final String? error; - final Position? currentLocation; - final double? distanceFromVenue; - final bool isClockedIn; - final DateTime? clockInTime; const ClockInState({ this.isLoading = false, @@ -22,6 +14,13 @@ class ClockInState extends Equatable { this.isClockedIn = false, this.clockInTime, }); + final bool isLoading; + final bool isLocationVerified; + final String? error; + final Position? currentLocation; + final double? distanceFromVenue; + final bool isClockedIn; + final DateTime? clockInTime; ClockInState copyWith({ bool? isLoading, @@ -44,7 +43,7 @@ class ClockInState extends Equatable { } @override - List get props => [ + List get props => [ isLoading, isLocationVerified, error, @@ -56,13 +55,13 @@ class ClockInState extends Equatable { } // --- Cubit --- -class ClockInCubit extends Cubit { +class ClockInCubit extends Cubit { // 500m radius + + ClockInCubit() : super(const ClockInState()); // Mock Venue Location (e.g., Grand Hotel, NYC) static const double venueLat = 40.7128; static const double venueLng = -74.0060; - static const double allowedRadiusMeters = 500; // 500m radius - - ClockInCubit() : super(const ClockInState()); + static const double allowedRadiusMeters = 500; Future checkLocationPermission() async { emit(state.copyWith(isLoading: true, error: null)); @@ -95,18 +94,18 @@ class ClockInCubit extends Cubit { Future _getCurrentLocation() async { try { - final position = await Geolocator.getCurrentPosition( + final Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); - final distance = Geolocator.distanceBetween( + final double distance = Geolocator.distanceBetween( position.latitude, position.longitude, venueLat, venueLng, ); - final isWithinRadius = distance <= allowedRadiusMeters; + final bool isWithinRadius = distance <= allowedRadiusMeters; emit(state.copyWith( isLoading: false, diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart index f04cc1a5..bd389ab6 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/pages/clock_in_page.dart @@ -36,7 +36,7 @@ class _ClockInPageState extends State { return BlocProvider.value( value: _bloc, child: BlocConsumer( - listener: (context, state) { + listener: (BuildContext context, ClockInState state) { if (state.status == ClockInStatus.failure && state.errorMessage != null) { ScaffoldMessenger.of( @@ -44,7 +44,7 @@ class _ClockInPageState extends State { ).showSnackBar(SnackBar(content: Text(state.errorMessage!))); } }, - builder: (context, state) { + builder: (BuildContext context, ClockInState state) { if (state.status == ClockInStatus.loading && state.todayShifts.isEmpty) { return const Scaffold( @@ -52,23 +52,23 @@ class _ClockInPageState extends State { ); } - final todayShifts = state.todayShifts; - final selectedShift = state.selectedShift; - final activeShiftId = state.attendance.activeShiftId; + final List todayShifts = state.todayShifts; + final Shift? selectedShift = state.selectedShift; + final String? activeShiftId = state.attendance.activeShiftId; final bool isActiveSelected = selectedShift != null && selectedShift.id == activeShiftId; - final checkInTime = + final DateTime? checkInTime = isActiveSelected ? state.attendance.checkInTime : null; - final checkOutTime = + final DateTime? checkOutTime = isActiveSelected ? state.attendance.checkOutTime : null; - final isCheckedIn = + final bool isCheckedIn = state.attendance.isCheckedIn && isActiveSelected; // Format times for display - final checkInStr = checkInTime != null + final String checkInStr = checkInTime != null ? DateFormat('h:mm a').format(checkInTime) : '--:-- --'; - final checkOutStr = checkOutTime != null + final String checkOutStr = checkOutTime != null ? DateFormat('h:mm a').format(checkOutTime) : '--:-- --'; @@ -94,7 +94,7 @@ class _ClockInPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ // Commute Tracker (shows before date selector when applicable) if (selectedShift != null) CommuteTracker( @@ -103,15 +103,15 @@ class _ClockInPageState extends State { isCommuteModeOn: state.isCommuteModeOn, distanceMeters: state.distanceFromVenue, etaMinutes: state.etaMinutes, - onCommuteToggled: (value) { + onCommuteToggled: (bool value) { _bloc.add(CommuteModeToggled(value)); }, ), // Date Selector DateSelector( selectedDate: state.selectedDate, - onSelect: (date) => _bloc.add(DateSelected(date)), - shiftDates: [ + onSelect: (DateTime date) => _bloc.add(DateSelected(date)), + shiftDates: [ DateFormat('yyyy-MM-dd').format(DateTime.now()), ], ), @@ -136,7 +136,7 @@ class _ClockInPageState extends State { Column( children: todayShifts .map( - (shift) => GestureDetector( + (Shift shift) => GestureDetector( onTap: () => _bloc.add(ShiftSelected(shift)), child: Container( @@ -162,12 +162,12 @@ class _ClockInPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Text( shift.id == selectedShift?.id @@ -208,7 +208,7 @@ class _ClockInPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.end, - children: [ + children: [ Text( "${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}", style: const TextStyle( @@ -236,7 +236,7 @@ class _ClockInPageState extends State { ), // Swipe To Check In / Checked Out State / No Shift State - if (selectedShift != null && checkOutTime == null) ...[ + if (selectedShift != null && checkOutTime == null) ...[ if (!isCheckedIn && !_isCheckInAllowed(selectedShift)) Container( @@ -247,7 +247,7 @@ class _ClockInPageState extends State { borderRadius: BorderRadius.circular(16), ), child: Column( - children: [ + children: [ const Icon( LucideIcons.clock, size: 48, @@ -296,7 +296,7 @@ class _ClockInPageState extends State { onCheckOut: () { showDialog( context: context, - builder: (context) => LunchBreakDialog( + builder: (BuildContext context) => LunchBreakDialog( onComplete: () { Navigator.of( context, @@ -308,7 +308,7 @@ class _ClockInPageState extends State { }, ), ] else if (selectedShift != null && - checkOutTime != null) ...[ + checkOutTime != null) ...[ // Shift Completed State Container( padding: const EdgeInsets.all(24), @@ -320,7 +320,7 @@ class _ClockInPageState extends State { ), // emerald-200 ), child: Column( - children: [ + children: [ Container( width: 48, height: 48, @@ -354,7 +354,7 @@ class _ClockInPageState extends State { ], ), ), - ] else ...[ + ] else ...[ // No Shift State Container( width: double.infinity, @@ -364,8 +364,8 @@ class _ClockInPageState extends State { borderRadius: BorderRadius.circular(16), ), child: const Column( - children: [ - const Text( + children: [ + Text( "No confirmed shifts for today", style: TextStyle( fontSize: 16, @@ -374,8 +374,8 @@ class _ClockInPageState extends State { ), textAlign: TextAlign.center, ), - const SizedBox(height: 4), - const Text( + SizedBox(height: 4), + Text( "Accept a shift to clock in", style: TextStyle( fontSize: 14, @@ -389,7 +389,7 @@ class _ClockInPageState extends State { ], // Checked In Banner - if (isCheckedIn && checkInTime != null) ...[ + if (isCheckedIn && checkInTime != null) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), @@ -403,11 +403,11 @@ class _ClockInPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ const Text( "Checked in at", style: TextStyle( @@ -468,7 +468,7 @@ class _ClockInPageState extends State { String value, String currentMode, ) { - final isSelected = currentMode == value; + final bool isSelected = currentMode == value; return Expanded( child: GestureDetector( onTap: () => _bloc.add(CheckInModeChanged(value)), @@ -478,18 +478,18 @@ class _ClockInPageState extends State { color: isSelected ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(8), boxShadow: isSelected - ? [ + ? [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, offset: const Offset(0, 1), ), ] - : [], + : [], ), child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ Icon( icon, size: 16, @@ -520,12 +520,12 @@ class _ClockInPageState extends State { barrierDismissible: false, builder: (BuildContext dialogContext) { return StatefulBuilder( - builder: (context, setState) { + builder: (BuildContext context, setState) { return AlertDialog( title: Text(scanned ? 'Tag Scanned!' : 'Scan NFC Tag'), content: Column( mainAxisSize: MainAxisSize.min, - children: [ + children: [ Container( width: 96, height: 96, @@ -559,7 +559,7 @@ class _ClockInPageState extends State { textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), - if (!scanned) ...[ + if (!scanned) ...[ const SizedBox(height: 24), SizedBox( width: double.infinity, @@ -620,14 +620,14 @@ class _ClockInPageState extends State { if (timeStr.isEmpty) return ''; try { // Try parsing as ISO string first (which contains date) - final dt = DateTime.parse(timeStr); + final DateTime dt = DateTime.parse(timeStr); return DateFormat('h:mm a').format(dt); } catch (_) { // Fallback for strict "HH:mm" or "HH:mm:ss" strings try { - final parts = timeStr.split(':'); + final List parts = timeStr.split(':'); if (parts.length >= 2) { - final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1])); + final DateTime dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1])); return DateFormat('h:mm a').format(dt); } return timeStr; @@ -638,12 +638,11 @@ class _ClockInPageState extends State { } bool _isCheckInAllowed(Shift shift) { - if (shift == null) return false; try { // Parse shift date (e.g. 2024-01-31T09:00:00) // The Shift entity has 'date' which is the start DateTime string - final shiftStart = DateTime.parse(shift.startTime); - final windowStart = shiftStart.subtract(const Duration(minutes: 15)); + final DateTime shiftStart = DateTime.parse(shift.startTime); + final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15)); return DateTime.now().isAfter(windowStart); } catch (e) { // Fallback: If parsing fails, allow check in to avoid blocking. @@ -652,10 +651,9 @@ class _ClockInPageState extends State { } String _getCheckInAvailabilityTime(Shift shift) { - if (shift == null) return ''; try { - final shiftStart = DateTime.parse(shift.startTime.trim()); - final windowStart = shiftStart.subtract(const Duration(minutes: 15)); + final DateTime shiftStart = DateTime.parse(shift.startTime.trim()); + final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15)); return DateFormat('h:mm a').format(windowStart); } catch (e) { return 'soon'; diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart index 5b67effe..9f5f07dd 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/attendance_card.dart @@ -4,11 +4,6 @@ import 'package:lucide_icons/lucide_icons.dart'; enum AttendanceType { checkin, checkout, breaks, days } class AttendanceCard extends StatelessWidget { - final AttendanceType type; - final String title; - final String value; - final String subtitle; - final String? scheduledTime; const AttendanceCard({ super.key, @@ -18,10 +13,15 @@ class AttendanceCard extends StatelessWidget { required this.subtitle, this.scheduledTime, }); + final AttendanceType type; + final String title; + final String value; + final String subtitle; + final String? scheduledTime; @override Widget build(BuildContext context) { - final styles = _getStyles(type); + final _AttendanceStyle styles = _getStyles(type); return Container( padding: const EdgeInsets.all(12), @@ -29,7 +29,7 @@ class AttendanceCard extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade100), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, @@ -40,7 +40,7 @@ class AttendanceCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [ + children: [ Container( width: 32, height: 32, @@ -72,7 +72,7 @@ class AttendanceCard extends StatelessWidget { ), ), ), - if (scheduledTime != null) ...[ + if (scheduledTime != null) ...[ const SizedBox(height: 2), Text( "Scheduled: $scheduledTime", @@ -123,13 +123,13 @@ class AttendanceCard extends StatelessWidget { } class _AttendanceStyle { - final IconData icon; - final Color bgColor; - final Color iconColor; _AttendanceStyle({ required this.icon, required this.bgColor, required this.iconColor, }); + final IconData icon; + final Color bgColor; + final Color iconColor; } diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart index f431b285..8f3726eb 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/commute_tracker.dart @@ -12,13 +12,6 @@ enum CommuteMode { } class CommuteTracker extends StatefulWidget { - final Shift? shift; - final Function(CommuteMode)? onModeChange; - final ValueChanged? onCommuteToggled; - final bool hasLocationConsent; - final bool isCommuteModeOn; - final double? distanceMeters; - final int? etaMinutes; const CommuteTracker({ super.key, @@ -30,6 +23,13 @@ class CommuteTracker extends StatefulWidget { this.distanceMeters, this.etaMinutes, }); + final Shift? shift; + final Function(CommuteMode)? onModeChange; + final ValueChanged? onCommuteToggled; + final bool hasLocationConsent; + final bool isCommuteModeOn; + final double? distanceMeters; + final int? etaMinutes; @override State createState() => _CommuteTrackerState(); @@ -65,7 +65,7 @@ class _CommuteTrackerState extends State { if (widget.shift == null) return CommuteMode.lockedNoShift; // For demo purposes, check if we're within 24 hours of shift - final now = DateTime.now(); + final DateTime now = DateTime.now(); DateTime shiftStart; try { // Try parsing startTime as full datetime first @@ -81,8 +81,8 @@ class _CommuteTrackerState extends State { ); } } - final hoursUntilShift = shiftStart.difference(now).inHours; - final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0; + final int hoursUntilShift = shiftStart.difference(now).inHours; + final bool inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0; if (_localIsCommuteOn) { // Check if arrived (mock: if distance < 200m) @@ -102,7 +102,7 @@ class _CommuteTrackerState extends State { } String _formatDistance(double meters) { - final miles = meters / 1609.34; + final double miles = meters / 1609.34; return miles < 0.1 ? '${meters.round()} m' : '${miles.toStringAsFixed(1)} mi'; @@ -110,7 +110,7 @@ class _CommuteTrackerState extends State { int _getMinutesUntilShift() { if (widget.shift == null) return 0; - final now = DateTime.now(); + final DateTime now = DateTime.now(); DateTime shiftStart; try { // Try parsing startTime as full datetime first @@ -131,7 +131,7 @@ class _CommuteTrackerState extends State { @override Widget build(BuildContext context) { - final mode = _getAppMode(); + final CommuteMode mode = _getAppMode(); // Notify parent of mode change WidgetsBinding.instance.addPostFrameCallback((_) { @@ -164,13 +164,13 @@ class _CommuteTrackerState extends State { gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ + colors: [ Color(0xFFEFF6FF), // blue-50 Color(0xFFECFEFF), // cyan-50 ], ), borderRadius: BorderRadius.circular(12), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, @@ -180,10 +180,10 @@ class _CommuteTrackerState extends State { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Container( width: 32, height: 32, @@ -198,11 +198,11 @@ class _CommuteTrackerState extends State { ), ), const SizedBox(width: 12), - Expanded( + const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( + children: [ + Text( 'Enable Commute Tracking?', style: TextStyle( fontSize: 14, @@ -210,7 +210,7 @@ class _CommuteTrackerState extends State { color: Color(0xFF0F172A), // slate-900 ), ), - const SizedBox(height: 4), + SizedBox(height: 4), Text( 'Share location 1hr before shift so your manager can see you\'re on the way.', style: TextStyle( @@ -225,7 +225,7 @@ class _CommuteTrackerState extends State { ), const SizedBox(height: 12), Row( - children: [ + children: [ Expanded( child: OutlinedButton( onPressed: () { @@ -268,7 +268,7 @@ class _CommuteTrackerState extends State { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, @@ -277,7 +277,7 @@ class _CommuteTrackerState extends State { ], ), child: Row( - children: [ + children: [ Container( width: 32, height: 32, @@ -295,9 +295,9 @@ class _CommuteTrackerState extends State { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Row( - children: [ + children: [ const Text( 'On My Way', style: TextStyle( @@ -308,7 +308,7 @@ class _CommuteTrackerState extends State { ), const SizedBox(width: 8), Row( - children: [ + children: [ const Icon( LucideIcons.clock, size: 12, @@ -338,11 +338,11 @@ class _CommuteTrackerState extends State { ), Switch( value: _localIsCommuteOn, - onChanged: (value) { + onChanged: (bool value) { setState(() => _localIsCommuteOn = value); widget.onCommuteToggled?.call(value); }, - activeColor: AppColors.krowBlue, + activeThumbColor: AppColors.krowBlue, ), ], ), @@ -356,7 +356,7 @@ class _CommuteTrackerState extends State { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ + colors: [ Color(0xFF2563EB), // blue-600 Color(0xFF0891B2), // cyan-600 ], @@ -364,19 +364,19 @@ class _CommuteTrackerState extends State { ), child: SafeArea( child: Column( - children: [ + children: [ Expanded( child: Center( child: Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ TweenAnimationBuilder( tween: Tween(begin: 1.0, end: 1.1), duration: const Duration(seconds: 1), curve: Curves.easeInOut, - builder: (context, double scale, child) { + builder: (BuildContext context, double scale, Widget? child) { return Transform.scale( scale: scale, child: Container( @@ -418,7 +418,7 @@ class _CommuteTrackerState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 32), - if (widget.distanceMeters != null) ...[ + if (widget.distanceMeters != null) ...[ Container( width: double.infinity, constraints: const BoxConstraints(maxWidth: 300), @@ -431,7 +431,7 @@ class _CommuteTrackerState extends State { ), ), child: Column( - children: [ + children: [ Text( 'Distance to Site', style: TextStyle( @@ -451,7 +451,7 @@ class _CommuteTrackerState extends State { ], ), ), - if (widget.etaMinutes != null) ...[ + if (widget.etaMinutes != null) ...[ const SizedBox(height: 12), Container( width: double.infinity, @@ -465,7 +465,7 @@ class _CommuteTrackerState extends State { ), ), child: Column( - children: [ + children: [ Text( 'Estimated Arrival', style: TextStyle( @@ -530,13 +530,13 @@ class _CommuteTrackerState extends State { gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [ + colors: [ Color(0xFFECFDF5), // emerald-50 Color(0xFFD1FAE5), // green-50 ], ), borderRadius: BorderRadius.circular(12), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, @@ -545,7 +545,7 @@ class _CommuteTrackerState extends State { ], ), child: Column( - children: [ + children: [ Container( width: 64, height: 64, diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/date_selector.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/date_selector.dart index 320ba176..3b732041 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/date_selector.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/date_selector.dart @@ -2,21 +2,21 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class DateSelector extends StatelessWidget { - final DateTime selectedDate; - final ValueChanged onSelect; - final List shiftDates; const DateSelector({ super.key, required this.selectedDate, required this.onSelect, - this.shiftDates = const [], + this.shiftDates = const [], }); + final DateTime selectedDate; + final ValueChanged onSelect; + final List shiftDates; @override Widget build(BuildContext context) { - final today = DateTime.now(); - final dates = List.generate(7, (index) { + final DateTime today = DateTime.now(); + final List dates = List.generate(7, (int index) { return today.add(Duration(days: index - 3)); }); @@ -24,10 +24,10 @@ class DateSelector extends StatelessWidget { height: 80, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: dates.map((date) { - final isSelected = _isSameDay(date, selectedDate); - final isToday = _isSameDay(date, today); - final hasShift = shiftDates.contains(_formatDateIso(date)); + children: dates.map((DateTime date) { + final bool isSelected = _isSameDay(date, selectedDate); + final bool isToday = _isSameDay(date, today); + final bool hasShift = shiftDates.contains(_formatDateIso(date)); return Expanded( child: GestureDetector( @@ -39,18 +39,18 @@ class DateSelector extends StatelessWidget { color: isSelected ? const Color(0xFF0032A0) : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: isSelected - ? [ + ? [ BoxShadow( color: const Color(0xFF0032A0).withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ] - : [], + : [], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ Text( DateFormat('d').format(date), style: TextStyle( diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/location_map_placeholder.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/location_map_placeholder.dart index f0b482a1..9f3d594d 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/location_map_placeholder.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/location_map_placeholder.dart @@ -3,14 +3,14 @@ import 'package:design_system/design_system.dart'; import 'package:lucide_icons/lucide_icons.dart'; class LocationMapPlaceholder extends StatelessWidget { - final bool isVerified; - final double? distance; const LocationMapPlaceholder({ super.key, required this.isVerified, this.distance, }); + final bool isVerified; + final double? distance; @override Widget build(BuildContext context) { @@ -31,12 +31,12 @@ class LocationMapPlaceholder extends StatelessWidget { ), ), child: Stack( - children: [ + children: [ // Fallback UI if image fails (which it will without key) const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ Icon(LucideIcons.mapPin, size: 48, color: UiColors.iconSecondary), SizedBox(height: 8), Text('Map View (GPS)', style: TextStyle(color: UiColors.textSecondary)), @@ -54,7 +54,7 @@ class LocationMapPlaceholder extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, @@ -63,7 +63,7 @@ class LocationMapPlaceholder extends StatelessWidget { ], ), child: Row( - children: [ + children: [ Icon( isVerified ? LucideIcons.checkCircle : LucideIcons.alertCircle, color: isVerified ? UiColors.textSuccess : UiColors.destructive, @@ -73,7 +73,7 @@ class LocationMapPlaceholder extends StatelessWidget { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Text( isVerified ? 'Location Verified' : 'Location Check', style: UiTypography.body1b.copyWith(color: UiColors.textPrimary), diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart index c0f1b897..f095f6a4 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/lunch_break_modal.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; class LunchBreakDialog extends StatefulWidget { - final VoidCallback onComplete; const LunchBreakDialog({super.key, required this.onComplete}); + final VoidCallback onComplete; @override State createState() => _LunchBreakDialogState(); @@ -23,7 +23,7 @@ class _LunchBreakDialogState extends State { String _additionalNotes = ''; final List _timeOptions = _generateTimeOptions(); - final List _noLunchReasons = [ + final List _noLunchReasons = [ 'Unpredictable Workflows', 'Poor Time Management', 'Lack of coverage or short Staff', @@ -32,12 +32,12 @@ class _LunchBreakDialogState extends State { ]; static List _generateTimeOptions() { - List options = []; + final List options = []; for (int h = 0; h < 24; h++) { for (int m = 0; m < 60; m += 15) { - final hour = h % 12 == 0 ? 12 : h % 12; - final ampm = h < 12 ? 'am' : 'pm'; - final timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm'; + final int hour = h % 12 == 0 ? 12 : h % 12; + final String ampm = h < 12 ? 'am' : 'pm'; + final String timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm'; options.add(timeStr); } } @@ -78,7 +78,7 @@ class _LunchBreakDialogState extends State { padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, - children: [ + children: [ Container( width: 80, height: 80, @@ -104,7 +104,7 @@ class _LunchBreakDialogState extends State { ), const SizedBox(height: 24), Row( - children: [ + children: [ Expanded( child: GestureDetector( onTap: () { @@ -171,7 +171,7 @@ class _LunchBreakDialogState extends State { padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, - children: [ + children: [ const Text( "When did you take lunch?", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), @@ -179,13 +179,13 @@ class _LunchBreakDialogState extends State { const SizedBox(height: 24), // Mock Inputs Row( - children: [ + children: [ Expanded( child: DropdownButtonFormField( isExpanded: true, - value: _breakStart, - items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(), - onChanged: (v) => setState(() => _breakStart = v), + initialValue: _breakStart, + items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(), + onChanged: (String? v) => setState(() => _breakStart = v), decoration: const InputDecoration( labelText: 'Start', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), @@ -196,9 +196,9 @@ class _LunchBreakDialogState extends State { Expanded( child: DropdownButtonFormField( isExpanded: true, - value: _breakEnd, - items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(), - onChanged: (v) => setState(() => _breakEnd = v), + initialValue: _breakEnd, + items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(), + onChanged: (String? v) => setState(() => _breakEnd = v), decoration: const InputDecoration( labelText: 'End', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), @@ -230,17 +230,17 @@ class _LunchBreakDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + children: [ const Text( "Why didn't you take lunch?", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), - ..._noLunchReasons.map((reason) => RadioListTile( + ..._noLunchReasons.map((String reason) => RadioListTile( title: Text(reason), value: reason, groupValue: _noLunchReason, - onChanged: (val) => setState(() => _noLunchReason = val), + onChanged: (String? val) => setState(() => _noLunchReason = val), )), const SizedBox(height: 24), @@ -264,14 +264,14 @@ class _LunchBreakDialogState extends State { padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, - children: [ + children: [ const Text( "Additional Notes", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), TextField( - onChanged: (v) => _additionalNotes = v, + onChanged: (String v) => _additionalNotes = v, decoration: const InputDecoration( hintText: 'Add any details...', border: OutlineInputBorder(), @@ -300,7 +300,7 @@ class _LunchBreakDialogState extends State { padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, - children: [ + children: [ const Icon(LucideIcons.checkCircle, size: 64, color: Colors.green), const SizedBox(height: 24), const Text( diff --git a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/swipe_to_check_in.dart b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/swipe_to_check_in.dart index fef53472..10315dd8 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/swipe_to_check_in.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/src/presentation/widgets/swipe_to_check_in.dart @@ -2,11 +2,6 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; class SwipeToCheckIn extends StatefulWidget { - final VoidCallback? onCheckIn; - final VoidCallback? onCheckOut; - final bool isLoading; - final String mode; // 'swipe' or 'nfc' - final bool isCheckedIn; const SwipeToCheckIn({ super.key, @@ -16,6 +11,11 @@ class SwipeToCheckIn extends StatefulWidget { this.mode = 'swipe', this.isCheckedIn = false, }); + final VoidCallback? onCheckIn; + final VoidCallback? onCheckOut; + final bool isLoading; + final String mode; // 'swipe' or 'nfc' + final bool isCheckedIn; @override State createState() => _SwipeToCheckInState(); @@ -50,7 +50,7 @@ class _SwipeToCheckInState extends State void _onDragEnd(DragEndDetails details, double maxWidth) { if (_isComplete || widget.isLoading) return; - final threshold = (maxWidth - _handleSize - 8) * 0.8; + final double threshold = (maxWidth - _handleSize - 8) * 0.8; if (_dragValue > threshold) { setState(() { _dragValue = maxWidth - _handleSize - 8; @@ -72,7 +72,7 @@ class _SwipeToCheckInState extends State @override Widget build(BuildContext context) { - final baseColor = widget.isCheckedIn + final Color baseColor = widget.isCheckedIn ? const Color(0xFF10B981) : const Color(0xFF0032A0); @@ -94,7 +94,7 @@ class _SwipeToCheckInState extends State decoration: BoxDecoration( color: baseColor, borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: [ BoxShadow( color: baseColor.withOpacity(0.4), blurRadius: 25, @@ -105,7 +105,7 @@ class _SwipeToCheckInState extends State ), child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ const Icon(LucideIcons.wifi, color: Colors.white), const SizedBox(width: 12), Text( @@ -127,19 +127,19 @@ class _SwipeToCheckInState extends State } return LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final maxDrag = maxWidth - _handleSize - 8; + builder: (BuildContext context, BoxConstraints constraints) { + final double maxWidth = constraints.maxWidth; + final double maxDrag = maxWidth - _handleSize - 8; // Calculate background color based on drag - final progress = _dragValue / maxDrag; - final startColor = widget.isCheckedIn + final double progress = _dragValue / maxDrag; + final Color startColor = widget.isCheckedIn ? const Color(0xFF10B981) : const Color(0xFF0032A0); - final endColor = widget.isCheckedIn + final Color endColor = widget.isCheckedIn ? const Color(0xFF0032A0) : const Color(0xFF10B981); - final currentColor = + final Color currentColor = Color.lerp(startColor, endColor, progress) ?? startColor; return Container( @@ -147,7 +147,7 @@ class _SwipeToCheckInState extends State decoration: BoxDecoration( color: currentColor, borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, @@ -156,7 +156,7 @@ class _SwipeToCheckInState extends State ], ), child: Stack( - children: [ + children: [ Center( child: Opacity( opacity: 1.0 - progress, @@ -187,15 +187,15 @@ class _SwipeToCheckInState extends State left: 4 + _dragValue, top: 4, child: GestureDetector( - onHorizontalDragUpdate: (d) => _onDragUpdate(d, maxWidth), - onHorizontalDragEnd: (d) => _onDragEnd(d, maxWidth), + onHorizontalDragUpdate: (DragUpdateDetails d) => _onDragUpdate(d, maxWidth), + onHorizontalDragEnd: (DragEndDetails d) => _onDragEnd(d, maxWidth), child: Container( width: _handleSize, height: _handleSize, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 2, diff --git a/apps/mobile/packages/features/staff/clock_in/lib/staff_clock_in.dart b/apps/mobile/packages/features/staff/clock_in/lib/staff_clock_in.dart index d9d93a80..60e7610d 100644 --- a/apps/mobile/packages/features/staff/clock_in/lib/staff_clock_in.dart +++ b/apps/mobile/packages/features/staff/clock_in/lib/staff_clock_in.dart @@ -1,4 +1,4 @@ -library staff_clock_in; +library; export 'src/staff_clock_in_module.dart'; export 'src/presentation/pages/clock_in_page.dart'; diff --git a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart index 6769036f..74f03d5c 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/data/repositories/home_repository_impl.dart @@ -11,7 +11,9 @@ extension TimestampExt on Timestamp { } } -class HomeRepositoryImpl implements HomeRepository { +class HomeRepositoryImpl + with DataErrorHandler + implements HomeRepository { HomeRepositoryImpl(); String get _currentStaffId { @@ -31,33 +33,32 @@ class HomeRepositoryImpl implements HomeRepository { } Future> _getShiftsForDate(DateTime date) async { - try { - final staffId = _currentStaffId; - - // Create start and end timestamps for the target date - 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 response = await ExampleConnector.instance - .getApplicationsByStaffId(staffId: staffId) - .dayStart(_toTimestamp(start)) - .dayEnd(_toTimestamp(end)) - .execute(); - - // Filter for ACCEPTED applications (same logic as shifts_repository_impl) - final apps = response.data.applications.where( - (app) => (app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED) || (app.status is Known && (app.status as Known).value == ApplicationStatus.CONFIRMED) - ); - - final List shifts = []; - for (final app in apps) { - shifts.add(_mapApplicationToShift(app)); - } - - return shifts; - } catch (e) { - return []; + final staffId = _currentStaffId; + + // Create start and end timestamps for the target date + 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 response = await executeProtected(() => ExampleConnector.instance + .getApplicationsByStaffId(staffId: staffId) + .dayStart(_toTimestamp(start)) + .dayEnd(_toTimestamp(end)) + .execute()); + + // Filter for ACCEPTED applications (same logic as shifts_repository_impl) + final apps = response.data.applications.where((app) => + (app.status is Known && + (app.status as Known).value == ApplicationStatus.ACCEPTED) || + (app.status is Known && + (app.status as Known).value == ApplicationStatus.CONFIRMED)); + + final List shifts = []; + for (final app in apps) { + shifts.add(_mapApplicationToShift(app)); } + + return shifts; } Timestamp _toTimestamp(DateTime dateTime) { @@ -69,27 +70,24 @@ class HomeRepositoryImpl implements HomeRepository { @override Future> 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(); + // Logic: List ALL open shifts (simple recommendation engine) + // Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED. + final response = await executeProtected(() => ExampleConnector.instance.listShifts().execute()); - return response.data.shifts - .where((s) { - final isOpen = s.status is Known && (s.status as Known).value == ShiftStatus.OPEN; - if (!isOpen) return false; + return response.data.shifts + .where((s) { + final isOpen = + s.status is Known && (s.status as Known).value == ShiftStatus.OPEN; + if (!isOpen) return false; - final start = s.startTime?.toDate(); - if (start == null) return false; + final start = s.startTime?.toDate(); + if (start == null) return false; - return start.isAfter(DateTime.now()); - }) - .take(10) - .map((s) => _mapConnectorShiftToDomain(s)) - .toList(); - } catch (e) { - return []; - } + return start.isAfter(DateTime.now()); + }) + .take(10) + .map((s) => _mapConnectorShiftToDomain(s)) + .toList(); } @override diff --git a/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart b/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart index f1b4afc4..25cdd03f 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/data/repositories/payments_repository_impl.dart @@ -1,22 +1,22 @@ 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:firebase_auth/firebase_auth.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:krow_core/core.dart'; import '../../domain/repositories/payments_repository.dart'; class PaymentsRepositoryImpl implements PaymentsRepository { - final dc.ExampleConnector _dataConnect; - final FirebaseAuth _auth = FirebaseAuth.instance; PaymentsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; + final dc.ExampleConnector _dataConnect; + final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance; String? _cachedStaffId; Future _getStaffId() async { // 1. Check Session Store - final StaffSession? session = StaffSessionStore.instance.session; + final dc.StaffSession? session = dc.StaffSessionStore.instance.session; if (session?.staff?.id != null) { return session!.staff!.id; } @@ -25,13 +25,13 @@ class PaymentsRepositoryImpl implements PaymentsRepository { if (_cachedStaffId != null) return _cachedStaffId!; // 3. Fetch from Data Connect using Firebase UID - final user = _auth.currentUser; + final firebase_auth.User? user = _auth.currentUser; if (user == null) { throw Exception('User is not authenticated'); } try { - final response = await _dataConnect.getStaffByUserId(userId: user.uid).execute(); + final QueryResult response = await _dataConnect.getStaffByUserId(userId: user.uid).execute(); if (response.data.staffs.isNotEmpty) { _cachedStaffId = response.data.staffs.first.id; return _cachedStaffId!; @@ -66,9 +66,7 @@ class PaymentsRepositoryImpl implements PaymentsRepository { } catch (_) {} try { - if (dt == null) { - dt = DateTime.tryParse(t.toString()); - } + dt ??= DateTime.tryParse(t.toString()); } catch (_) {} } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/domain/arguments/get_payment_history_arguments.dart b/apps/mobile/packages/features/staff/payments/lib/src/domain/arguments/get_payment_history_arguments.dart index a01acee4..e1f4d357 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/domain/arguments/get_payment_history_arguments.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/domain/arguments/get_payment_history_arguments.dart @@ -2,11 +2,11 @@ import 'package:krow_core/core.dart'; /// Arguments for getting payment history. class GetPaymentHistoryArguments extends UseCaseArgument { + + const GetPaymentHistoryArguments(this.period); /// The period to filter by (e.g., "monthly", "weekly"). final String period; - const GetPaymentHistoryArguments(this.period); - @override - List get props => [period]; + List get props => [period]; } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart index c43296f0..29b5c6e3 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_history_usecase.dart @@ -7,10 +7,10 @@ import '../repositories/payments_repository.dart'; /// /// This use case delegates the data retrieval to [PaymentsRepository]. class GetPaymentHistoryUseCase extends UseCase> { - final PaymentsRepository repository; /// Creates a [GetPaymentHistoryUseCase]. GetPaymentHistoryUseCase(this.repository); + final PaymentsRepository repository; @override Future> call(GetPaymentHistoryArguments arguments) async { diff --git a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart index 84c54d59..0f054097 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/domain/usecases/get_payment_summary_usecase.dart @@ -4,10 +4,10 @@ import '../repositories/payments_repository.dart'; /// Use case to retrieve payment summary information. class GetPaymentSummaryUseCase extends NoInputUseCase { - final PaymentsRepository repository; /// Creates a [GetPaymentSummaryUseCase]. GetPaymentSummaryUseCase(this.repository); + final PaymentsRepository repository; @override Future call() async { 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 f1b82f98..0225601a 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 @@ -1,6 +1,6 @@ +import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_core/core.dart'; -import 'package:krow_data_connect/krow_data_connect.dart'; import 'domain/repositories/payments_repository.dart'; import 'domain/usecases/get_payment_summary_usecase.dart'; import 'domain/usecases/get_payment_history_usecase.dart'; @@ -26,7 +26,7 @@ class StaffPaymentsModule extends Module { void routes(RouteManager r) { r.child( StaffPaths.childRoute(StaffPaths.payments, StaffPaths.payments), - child: (context) => const PaymentsPage(), + child: (BuildContext context) => const PaymentsPage(), ); } } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart index c25e98e8..233016a3 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_bloc.dart @@ -7,8 +7,6 @@ import 'payments_event.dart'; import 'payments_state.dart'; class PaymentsBloc extends Bloc { - final GetPaymentSummaryUseCase getPaymentSummary; - final GetPaymentHistoryUseCase getPaymentHistory; PaymentsBloc({ required this.getPaymentSummary, @@ -17,6 +15,8 @@ class PaymentsBloc extends Bloc { on(_onLoadPayments); on(_onChangePeriod); } + final GetPaymentSummaryUseCase getPaymentSummary; + final GetPaymentHistoryUseCase getPaymentHistory; Future _onLoadPayments( LoadPaymentsEvent event, diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_event.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_event.dart index 86aceffd..bf0cad93 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_event.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_event.dart @@ -1,20 +1,19 @@ -import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; abstract class PaymentsEvent extends Equatable { const PaymentsEvent(); @override - List get props => []; + List get props => []; } class LoadPaymentsEvent extends PaymentsEvent {} class ChangePeriodEvent extends PaymentsEvent { - final String period; const ChangePeriodEvent(this.period); + final String period; @override - List get props => [period]; + List get props => [period]; } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart index 6e100f83..edd2fb8c 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/blocs/payments/payments_state.dart @@ -5,7 +5,7 @@ abstract class PaymentsState extends Equatable { const PaymentsState(); @override - List get props => []; + List get props => []; } class PaymentsInitial extends PaymentsState {} @@ -13,15 +13,15 @@ class PaymentsInitial extends PaymentsState {} class PaymentsLoading extends PaymentsState {} class PaymentsLoaded extends PaymentsState { - final PaymentSummary summary; - final List history; - final String activePeriod; const PaymentsLoaded({ required this.summary, required this.history, this.activePeriod = 'week', }); + final PaymentSummary summary; + final List history; + final String activePeriod; PaymentsLoaded copyWith({ PaymentSummary? summary, @@ -36,14 +36,14 @@ class PaymentsLoaded extends PaymentsState { } @override - List get props => [summary, history, activePeriod]; + List get props => [summary, history, activePeriod]; } class PaymentsError extends PaymentsState { - final String message; const PaymentsError(this.message); + final String message; @override - List get props => [message]; + List get props => [message]; } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/models/payment_stats.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/models/payment_stats.dart index 41ddb91a..60eecc7b 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/models/payment_stats.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/models/payment_stats.dart @@ -1,10 +1,6 @@ import 'package:equatable/equatable.dart'; class PaymentStats extends Equatable { - final double weeklyEarnings; - final double monthlyEarnings; - final double pendingEarnings; - final double totalEarnings; const PaymentStats({ this.weeklyEarnings = 0.0, @@ -12,9 +8,13 @@ class PaymentStats extends Equatable { this.pendingEarnings = 0.0, this.totalEarnings = 0.0, }); + final double weeklyEarnings; + final double monthlyEarnings; + final double pendingEarnings; + final double totalEarnings; @override - List get props => [ + List get props => [ weeklyEarnings, monthlyEarnings, pendingEarnings, diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart index 0df77068..d8c16d86 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/pages/payments_page.dart @@ -177,7 +177,7 @@ class _PaymentsPageState extends State { // Recent Payments if (state.history.isNotEmpty) Column( - children: [ + children: [ const Text( "Recent Payments", style: TextStyle( diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/earnings_graph.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/earnings_graph.dart index 7a87df72..960bb7c7 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/earnings_graph.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/earnings_graph.dart @@ -4,21 +4,21 @@ import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; class EarningsGraph extends StatelessWidget { - final List payments; - final String period; const EarningsGraph({ super.key, required this.payments, required this.period, }); + final List payments; + final String period; @override Widget build(BuildContext context) { // Basic data processing for the graph // We'll aggregate payments by date - final validPayments = payments.where((p) => p.paidAt != null).toList() - ..sort((a, b) => a.paidAt!.compareTo(b.paidAt!)); + final List validPayments = payments.where((StaffPayment p) => p.paidAt != null).toList() + ..sort((StaffPayment a, StaffPayment b) => a.paidAt!.compareTo(b.paidAt!)); // If no data, show empty state or simple placeholder if (validPayments.isEmpty) { @@ -32,9 +32,9 @@ class EarningsGraph extends StatelessWidget { ); } - final spots = _generateSpots(validPayments); - final maxX = spots.isNotEmpty ? spots.last.x : 0.0; - final maxY = spots.isNotEmpty ? spots.map((s) => s.y).reduce((a, b) => a > b ? a : b) : 0.0; + final List spots = _generateSpots(validPayments); + final double maxX = spots.isNotEmpty ? spots.last.x : 0.0; + final double maxY = spots.isNotEmpty ? spots.map((FlSpot s) => s.y).reduce((double a, double b) => a > b ? a : b) : 0.0; return Container( height: 220, @@ -42,7 +42,7 @@ class EarningsGraph extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), offset: const Offset(0, 4), @@ -52,7 +52,7 @@ class EarningsGraph extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ const Text( "Earnings Trend", style: TextStyle( @@ -70,10 +70,10 @@ class EarningsGraph extends StatelessWidget { bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - getTitlesWidget: (value, meta) { + getTitlesWidget: (double value, TitleMeta meta) { // Simple logic to show a few dates if (value % 2 != 0) return const SizedBox(); - final index = value.toInt(); + final int index = value.toInt(); if (index >= 0 && index < validPayments.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), @@ -92,7 +92,7 @@ class EarningsGraph extends StatelessWidget { rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: false), - lineBarsData: [ + lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, @@ -121,7 +121,7 @@ class EarningsGraph extends StatelessWidget { List _generateSpots(List data) { // Generate spots based on index in the list for simplicity in this demo // Real implementation would map to actual dates on X-axis - return List.generate(data.length, (index) { + return List.generate(data.length, (int index) { return FlSpot(index.toDouble(), data[index].amount); }); } diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_history_item.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_history_item.dart index 9c49df1e..b64b78a9 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_history_item.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_history_item.dart @@ -2,15 +2,6 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; class PaymentHistoryItem extends StatelessWidget { - final double amount; - final String title; - final String location; - final String address; - final String date; - final String workedTime; - final int hours; - final double rate; - final String status; const PaymentHistoryItem({ super.key, @@ -24,6 +15,15 @@ class PaymentHistoryItem extends StatelessWidget { required this.rate, required this.status, }); + final double amount; + final String title; + final String location; + final String address; + final String date; + final String workedTime; + final int hours; + final double rate; + final String status; @override Widget build(BuildContext context) { @@ -32,7 +32,7 @@ class PaymentHistoryItem extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, @@ -42,10 +42,10 @@ class PaymentHistoryItem extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ // Status Badge Row( - children: [ + children: [ Container( width: 6, height: 6, @@ -70,7 +70,7 @@ class PaymentHistoryItem extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ // Icon Container( width: 44, @@ -90,15 +90,15 @@ class PaymentHistoryItem extends StatelessWidget { // Content Expanded( child: Column( - children: [ + children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Text( title, style: const TextStyle( @@ -119,7 +119,7 @@ class PaymentHistoryItem extends StatelessWidget { ), Column( crossAxisAlignment: CrossAxisAlignment.end, - children: [ + children: [ Text( "\$${amount.toStringAsFixed(0)}", style: const TextStyle( @@ -143,7 +143,7 @@ class PaymentHistoryItem extends StatelessWidget { // Date and Time Row( - children: [ + children: [ const Icon( LucideIcons.calendar, size: 12, @@ -177,7 +177,7 @@ class PaymentHistoryItem extends StatelessWidget { // Address Row( - children: [ + children: [ const Icon( LucideIcons.mapPin, size: 12, diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_stats_card.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_stats_card.dart index aad2cf9b..77673455 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_stats_card.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/payment_stats_card.dart @@ -1,11 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:lucide_icons/lucide_icons.dart'; class PaymentStatsCard extends StatelessWidget { - final IconData icon; - final Color iconColor; - final String label; - final String amount; const PaymentStatsCard({ super.key, @@ -14,6 +9,10 @@ class PaymentStatsCard extends StatelessWidget { required this.label, required this.amount, }); + final IconData icon; + final Color iconColor; + final String label; + final String amount; @override Widget build(BuildContext context) { @@ -22,7 +21,7 @@ class PaymentStatsCard extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, @@ -32,9 +31,9 @@ class PaymentStatsCard extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Row( - children: [ + children: [ Icon(icon, size: 16, color: iconColor), const SizedBox(width: 8), Text( diff --git a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart index 833a119e..54a56f39 100644 --- a/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart +++ b/apps/mobile/packages/features/staff/payments/lib/src/presentation/widgets/pending_pay_card.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; class PendingPayCard extends StatelessWidget { - final double amount; - final VoidCallback onCashOut; const PendingPayCard({ super.key, required this.amount, required this.onCashOut, }); + final double amount; + final VoidCallback onCashOut; @override Widget build(BuildContext context) { @@ -17,12 +17,12 @@ class PendingPayCard extends StatelessWidget { padding: const EdgeInsets.all(14), decoration: BoxDecoration( gradient: const LinearGradient( - colors: [Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50 + colors: [Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50 begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 2, @@ -32,9 +32,9 @@ class PendingPayCard extends StatelessWidget { ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + children: [ Row( - children: [ + children: [ Container( width: 40, height: 40, @@ -51,7 +51,7 @@ class PendingPayCard extends StatelessWidget { const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ const Text( "Pending", style: TextStyle( diff --git a/apps/mobile/packages/features/staff/payments/pubspec.yaml b/apps/mobile/packages/features/staff/payments/pubspec.yaml index 22435a8f..3712abe6 100644 --- a/apps/mobile/packages/features/staff/payments/pubspec.yaml +++ b/apps/mobile/packages/features/staff/payments/pubspec.yaml @@ -11,7 +11,8 @@ environment: dependencies: flutter: sdk: flutter - firebase_data_connect: + firebase_data_connect: ^0.2.2+2 + firebase_auth: ^6.1.4 flutter_modular: ^6.3.2 lucide_icons: ^0.257.0 intl: ^0.20.0 @@ -29,6 +30,8 @@ dependencies: krow_data_connect: path: ../../../data_connect + flutter_bloc: any + equatable: any dev_dependencies: flutter_test: sdk: flutter diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories/certificates_repository_mock.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories/certificates_repository_mock.dart deleted file mode 100644 index a5238a76..00000000 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/certificates/lib/src/data/repositories/certificates_repository_mock.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:krow_domain/krow_domain.dart'; -import '../../domain/repositories/certificates_repository.dart'; - -class CertificatesRepositoryMock implements CertificatesRepository { - @override - Future> getCertificates() async { - final DateTime now = DateTime.now(); - - // Create copies with dynamic dates - final List dynamicDocuments = [ - StaffDocument( - id: '1', - documentId: 'background', - staffId: 'current_user', - name: 'Background Check', - description: 'Required for all shifts', - status: DocumentStatus.verified, - expiryDate: now.add(const Duration(days: 365)), - ), - StaffDocument( - id: '2', - documentId: 'food_handler', - staffId: 'current_user', - name: 'Food Handler', - description: 'Required for food service', - status: DocumentStatus.verified, - expiryDate: now.add(const Duration(days: 15)), - ), - const StaffDocument( - id: '3', - documentId: 'rbs', - staffId: 'current_user', - name: 'RBS Alcohol', - description: 'Required for bar shifts', - status: DocumentStatus.missing, - expiryDate: null, - ), - ]; - - await Future.delayed(const Duration(seconds: 1)); // Simulate network delay - return dynamicDocuments; - } -} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart index d2cf2667..9a8045cf 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/data/repositories/personal_info_repository_impl.dart @@ -1,4 +1,4 @@ -import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_domain/krow_domain.dart'; @@ -14,21 +14,21 @@ import '../../domain/repositories/personal_info_repository_interface.dart'; /// - Mapping between data_connect DTOs and domain entities /// - Containing no business logic class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface { - final ExampleConnector _dataConnect; - final FirebaseAuth _firebaseAuth; /// Creates a [PersonalInfoRepositoryImpl]. /// /// Requires the Firebase Data Connect connector instance and Firebase Auth. PersonalInfoRepositoryImpl({ required ExampleConnector dataConnect, - required FirebaseAuth firebaseAuth, + required firebase_auth.FirebaseAuth firebaseAuth, }) : _dataConnect = dataConnect, _firebaseAuth = firebaseAuth; + final ExampleConnector _dataConnect; + final firebase_auth.FirebaseAuth _firebaseAuth; @override Future getStaffProfile() async { - final user = _firebaseAuth.currentUser; + final firebase_auth.User? user = _firebaseAuth.currentUser; if (user == null) { throw Exception('User not authenticated'); } @@ -41,7 +41,7 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface { throw Exception('Staff profile not found for User ID: ${user.uid}'); } - final rawStaff = result.data.staffs.first; + final GetStaffByUserIdStaffs rawStaff = result.data.staffs.first; // Map from data_connect DTO to domain entity return _mapToStaffEntity(rawStaff); @@ -50,7 +50,7 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface { @override Future updateStaffProfile({required String staffId, required Map data}) async { // Start building the update mutation - var updateBuilder = _dataConnect.updateStaff(id: staffId); + UpdateStaffVariablesBuilder updateBuilder = _dataConnect.updateStaff(id: staffId); // Apply updates from map if present if (data.containsKey('name')) { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/get_personal_info_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/get_personal_info_usecase.dart index 265e6f8b..76402f1c 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/get_personal_info_usecase.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/get_personal_info_usecase.dart @@ -8,12 +8,12 @@ import '../repositories/personal_info_repository_interface.dart'; /// which delegates to the data_connect layer for data access. class GetPersonalInfoUseCase implements NoInputUseCase { - final PersonalInfoRepositoryInterface _repository; /// Creates a [GetPersonalInfoUseCase]. /// /// Requires a [PersonalInfoRepositoryInterface] to fetch data. GetPersonalInfoUseCase(this._repository); + final PersonalInfoRepositoryInterface _repository; @override Future call() { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/update_personal_info_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/update_personal_info_usecase.dart index 5c5e5b27..5092e87e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/update_personal_info_usecase.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/domain/usecases/update_personal_info_usecase.dart @@ -4,19 +4,19 @@ import '../repositories/personal_info_repository_interface.dart'; /// Arguments for updating staff profile information. class UpdatePersonalInfoParams extends UseCaseArgument { + + const UpdatePersonalInfoParams({ + required this.staffId, + required this.data, + }); /// The staff member's ID. final String staffId; /// The fields to update. final Map data; - const UpdatePersonalInfoParams({ - required this.staffId, - required this.data, - }); - @override - List get props => [staffId, data]; + List get props => [staffId, data]; } /// Use case for updating staff profile information. @@ -25,12 +25,12 @@ class UpdatePersonalInfoParams extends UseCaseArgument { /// through the repository, which delegates to the data_connect layer. class UpdatePersonalInfoUseCase implements UseCase { - final PersonalInfoRepositoryInterface _repository; /// Creates an [UpdatePersonalInfoUseCase]. /// /// Requires a [PersonalInfoRepositoryInterface] to update data. UpdatePersonalInfoUseCase(this._repository); + final PersonalInfoRepositoryInterface _repository; @override Future call(UpdatePersonalInfoParams params) { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart index 47ba08f7..5359772d 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_bloc.dart @@ -14,8 +14,6 @@ import 'personal_info_state.dart'; /// use cases following Clean Architecture principles. class PersonalInfoBloc extends Bloc implements Disposable { - final GetPersonalInfoUseCase _getPersonalInfoUseCase; - final UpdatePersonalInfoUseCase _updatePersonalInfoUseCase; /// Creates a [PersonalInfoBloc]. /// @@ -33,6 +31,8 @@ class PersonalInfoBloc extends Bloc add(const PersonalInfoLoadRequested()); } + final GetPersonalInfoUseCase _getPersonalInfoUseCase; + final UpdatePersonalInfoUseCase _updatePersonalInfoUseCase; /// Handles loading staff profile information. Future _onLoadRequested( @@ -45,13 +45,13 @@ class PersonalInfoBloc extends Bloc // Initialize form values from staff entity // Note: Staff entity currently stores address as a string, but we want to map it to 'preferredLocations' - final Map initialValues = { + final Map initialValues = { 'name': staff.name, 'email': staff.email, 'phone': staff.phone, 'preferredLocations': staff.address != null - ? [staff.address] - : [], // TODO: Map correctly when Staff entity supports list + ? [staff.address] + : [], // TODO: Map correctly when Staff entity supports list 'avatar': staff.avatar, }; @@ -95,13 +95,13 @@ class PersonalInfoBloc extends Bloc ); // Update local state with the returned staff and keep form values in sync - final Map newValues = { + final Map newValues = { 'name': updatedStaff.name, 'email': updatedStaff.email, 'phone': updatedStaff.phone, 'preferredLocations': updatedStaff.address != null - ? [updatedStaff.address] - : [], + ? [updatedStaff.address] + : [], 'avatar': updatedStaff.avatar, }; diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart index b09d4860..a577287f 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_event.dart @@ -5,7 +5,7 @@ abstract class PersonalInfoEvent extends Equatable { const PersonalInfoEvent(); @override - List get props => []; + List get props => []; } /// Event to load personal information. @@ -15,16 +15,16 @@ class PersonalInfoLoadRequested extends PersonalInfoEvent { /// Event to update a field value. class PersonalInfoFieldChanged extends PersonalInfoEvent { - final String field; - final dynamic value; const PersonalInfoFieldChanged({ required this.field, required this.value, }); + final String field; + final dynamic value; @override - List get props => [field, value]; + List get props => [field, value]; } /// Event to submit the form. @@ -34,9 +34,9 @@ class PersonalInfoFormSubmitted extends PersonalInfoEvent { /// Event when an address is selected from autocomplete. class PersonalInfoAddressSelected extends PersonalInfoEvent { - final String address; const PersonalInfoAddressSelected(this.address); + final String address; @override - List get props => [address]; + List get props => [address]; } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart index cd0eabf8..0e7fbc52 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/blocs/personal_info_state.dart @@ -29,6 +29,21 @@ enum PersonalInfoStatus { /// /// Uses the shared [Staff] entity from the domain layer. class PersonalInfoState extends Equatable { + + /// Creates a [PersonalInfoState]. + const PersonalInfoState({ + this.status = PersonalInfoStatus.initial, + this.staff, + this.formValues = const {}, + this.errorMessage, + }); + + /// Initial state. + const PersonalInfoState.initial() + : status = PersonalInfoStatus.initial, + staff = null, + formValues = const {}, + errorMessage = null; /// The current status of the operation. final PersonalInfoStatus status; @@ -41,21 +56,6 @@ class PersonalInfoState extends Equatable { /// Error message if an error occurred. final String? errorMessage; - /// Creates a [PersonalInfoState]. - const PersonalInfoState({ - this.status = PersonalInfoStatus.initial, - this.staff, - this.formValues = const {}, - this.errorMessage, - }); - - /// Initial state. - const PersonalInfoState.initial() - : status = PersonalInfoStatus.initial, - staff = null, - formValues = const {}, - errorMessage = null; - /// Creates a copy of this state with the given fields replaced. PersonalInfoState copyWith({ PersonalInfoStatus? status, @@ -72,5 +72,5 @@ class PersonalInfoState extends Equatable { } @override - List get props => [status, staff, formValues, errorMessage]; + List get props => [status, staff, formValues, errorMessage]; } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart index ba71594e..41ed320d 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_content.dart @@ -18,14 +18,14 @@ import 'save_button.dart'; /// following Clean Architecture's separation of concerns principle and the design system guidelines. /// Works with the shared [Staff] entity from the domain layer. class PersonalInfoContent extends StatefulWidget { - /// The staff profile to display and edit. - final Staff staff; /// Creates a [PersonalInfoContent]. const PersonalInfoContent({ super.key, required this.staff, }); + /// The staff profile to display and edit. + final Staff staff; @override State createState() => _PersonalInfoContentState(); @@ -81,8 +81,8 @@ class _PersonalInfoContentState extends State { // The backend expects List (JSON/List) for preferredLocations final List locations = _locationsController.text .split(',') - .map((e) => e.trim()) - .where((e) => e.isNotEmpty) + .map((String e) => e.trim()) + .where((String e) => e.isNotEmpty) .toList(); context.read().add( diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart index 3f7a0af7..ee4e84b5 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_form.dart @@ -9,6 +9,17 @@ import 'package:design_system/design_system.dart'; /// and editable fields for phone and address. /// Uses only design system tokens for colors, typography, and spacing. class PersonalInfoForm extends StatelessWidget { + + /// Creates a [PersonalInfoForm]. + const PersonalInfoForm({ + super.key, + required this.fullName, + required this.email, + required this.emailController, + required this.phoneController, + required this.locationsController, + this.enabled = true, + }); /// The staff member's full name (read-only). final String fullName; @@ -27,17 +38,6 @@ class PersonalInfoForm extends StatelessWidget { /// Whether the form fields are enabled for editing. final bool enabled; - /// Creates a [PersonalInfoForm]. - const PersonalInfoForm({ - super.key, - required this.fullName, - required this.email, - required this.emailController, - required this.phoneController, - required this.locationsController, - this.enabled = true, - }); - @override Widget build(BuildContext context) { final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info; @@ -57,7 +57,7 @@ class PersonalInfoForm extends StatelessWidget { hint: i18n.email_label, enabled: enabled, keyboardType: TextInputType.emailAddress, - autofillHints: const [AutofillHints.email], + autofillHints: const [AutofillHints.email], ), const SizedBox(height: UiConstants.space4), @@ -85,9 +85,9 @@ class PersonalInfoForm extends StatelessWidget { /// A label widget for form fields. /// A label widget for form fields. class _FieldLabel extends StatelessWidget { - final String text; const _FieldLabel({required this.text}); + final String text; @override Widget build(BuildContext context) { @@ -101,9 +101,9 @@ class _FieldLabel extends StatelessWidget { /// A read-only field widget for displaying non-editable information. /// A read-only field widget for displaying non-editable information. class _ReadOnlyField extends StatelessWidget { - final String value; const _ReadOnlyField({required this.value}); + final String value; @override Widget build(BuildContext context) { @@ -129,11 +129,6 @@ class _ReadOnlyField extends StatelessWidget { /// An editable text field widget. /// An editable text field widget. class _EditableField extends StatelessWidget { - final TextEditingController controller; - final String hint; - final bool enabled; - final TextInputType? keyboardType; - final Iterable? autofillHints; const _EditableField({ required this.controller, @@ -142,6 +137,11 @@ class _EditableField extends StatelessWidget { this.keyboardType, this.autofillHints, }); + final TextEditingController controller; + final String hint; + final bool enabled; + final TextInputType? keyboardType; + final Iterable? autofillHints; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart index 528e7e4d..dc681266 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/profile_photo_widget.dart @@ -9,14 +9,6 @@ import 'package:design_system/design_system.dart'; /// Includes a camera icon button for changing the photo. /// Uses only design system tokens for colors, typography, and spacing. class ProfilePhotoWidget extends StatelessWidget { - /// The URL of the staff member's photo. - final String? photoUrl; - - /// The staff member's full name (used for initial avatar). - final String fullName; - - /// Callback when the photo/camera button is tapped. - final VoidCallback? onTap; /// Creates a [ProfilePhotoWidget]. const ProfilePhotoWidget({ @@ -25,6 +17,14 @@ class ProfilePhotoWidget extends StatelessWidget { required this.fullName, required this.onTap, }); + /// The URL of the staff member's photo. + final String? photoUrl; + + /// The staff member's full name (used for initial avatar). + final String fullName; + + /// Callback when the photo/camera button is tapped. + final VoidCallback? onTap; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart index 44b4d5c6..ea03339b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/save_button.dart @@ -7,14 +7,6 @@ import 'package:design_system/design_system.dart'; /// Displays a full-width button with a save icon and customizable label. /// Uses only design system tokens for colors, typography, and spacing. class SaveButton extends StatelessWidget { - /// Callback when the button is pressed. - final VoidCallback? onPressed; - - /// The button label text. - final String label; - - /// Whether to show a loading indicator. - final bool isLoading; /// Creates a [SaveButton]. const SaveButton({ @@ -23,6 +15,14 @@ class SaveButton extends StatelessWidget { required this.label, this.isLoading = false, }); + /// Callback when the button is pressed. + final VoidCallback? onPressed; + + /// The button label text. + final String label; + + /// Whether to show a loading indicator. + final bool isLoading; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/staff_profile_info.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/staff_profile_info.dart index 28387ab4..9f2d74f7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/staff_profile_info.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/staff_profile_info.dart @@ -1,2 +1,3 @@ /// Export the modular feature definition. +library; export 'src/staff_profile_info_module.dart'; diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/pubspec.yaml index 86592162..ef8602e7 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/pubspec.yaml +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/pubspec.yaml @@ -28,6 +28,8 @@ dependencies: krow_data_connect: path: ../../../../../data_connect + firebase_auth: any + firebase_data_connect: any dev_dependencies: flutter_test: sdk: flutter 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 2d769b91..b56b0c15 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,15 +1,16 @@ 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 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:krow_core/core.dart'; import '../../domain/repositories/shifts_repository_interface.dart'; -class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { +class ShiftsRepositoryImpl + with dc.DataErrorHandler + implements ShiftsRepositoryInterface { final dc.ExampleConnector _dataConnect; - final FirebaseAuth _auth = FirebaseAuth.instance; + final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance; ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; @@ -22,7 +23,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { Future _getStaffId() async { // 1. Check Session Store - final StaffSession? session = StaffSessionStore.instance.session; + final dc.StaffSession? session = dc.StaffSessionStore.instance.session; if (session?.staff?.id != null) { return session!.staff!.id; } @@ -31,15 +32,15 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { if (_cachedStaffId != null) return _cachedStaffId!; // 3. Fetch from Data Connect using Firebase UID - final user = _auth.currentUser; + final firebase_auth.User? user = _auth.currentUser; if (user == null) { throw Exception('User is not authenticated'); } try { - final response = await _dataConnect + final fdc.QueryResult response = await executeProtected(() => _dataConnect .getStaffByUserId(userId: user.uid) - .execute(); + .execute()); if (response.data.staffs.isNotEmpty) { _cachedStaffId = response.data.staffs.first.id; return _cachedStaffId!; @@ -52,10 +53,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { return user.uid; } - DateTime? _toDateTime(dynamic t, {String? debugKey}) { + DateTime? _toDateTime(dynamic t) { if (t == null) return null; DateTime? dt; - if (t is Timestamp) { + if (t is fdc.Timestamp) { dt = t.toDateTime(); } else if (t is String) { dt = DateTime.tryParse(t); @@ -73,11 +74,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { if (dt != null) { final local = DateTimeUtils.toDeviceTime(dt); - if (debugKey != null && debugKey.isNotEmpty) { - print( - 'ShiftDate convert: key=$debugKey raw=$t parsed=${dt.toIso8601String()} local=${local.toIso8601String()}', - ); - } + return local; } return null; @@ -103,135 +100,125 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { @override Future> getHistoryShifts() async { - try { - final staffId = await _getStaffId(); - final response = await _dataConnect - .listCompletedApplicationsByStaffId(staffId: staffId) - .execute(); - final List shifts = []; + final staffId = await _getStaffId(); + final fdc.QueryResult response = await executeProtected(() => _dataConnect + .listCompletedApplicationsByStaffId(staffId: staffId) + .execute()); + final List shifts = []; - for (final app in response.data.applications) { - _shiftToAppIdMap[app.shift.id] = app.id; - _appToRoleIdMap[app.id] = app.shiftRole.id; + for (final app in response.data.applications) { + _shiftToAppIdMap[app.shift.id] = app.id; + _appToRoleIdMap[app.id] = app.shiftRole.id; - final String roleName = app.shiftRole.role.name; - final String orderName = - (app.shift.order.eventName ?? '').trim().isNotEmpty - ? app.shift.order.eventName! - : app.shift.order.business.businessName; - final String title = '$roleName - $orderName'; - final DateTime? shiftDate = _toDateTime(app.shift.date); - final DateTime? startDt = _toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _toDateTime(app.createdAt); + final String roleName = app.shiftRole.role.name; + final String orderName = + (app.shift.order.eventName ?? '').trim().isNotEmpty + ? app.shift.order.eventName! + : app.shift.order.business.businessName; + final String title = '$roleName - $orderName'; + final DateTime? shiftDate = _toDateTime(app.shift.date); + final DateTime? startDt = _toDateTime(app.shiftRole.startTime); + final DateTime? endDt = _toDateTime(app.shiftRole.endTime); + final DateTime? createdDt = _toDateTime(app.createdAt); - shifts.add( - Shift( - id: app.shift.id, - roleId: app.shiftRole.roleId, - title: title, - clientName: app.shift.order.business.businessName, - logoUrl: app.shift.order.business.companyLogoUrl, - hourlyRate: app.shiftRole.role.costPerHour, - location: app.shift.location ?? '', - locationAddress: app.shift.order.teamHub.hubName, - date: shiftDate?.toIso8601String() ?? '', - startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', - endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', - createdDate: createdDt?.toIso8601String() ?? '', - status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT), - description: app.shift.description, - durationDays: app.shift.durationDays, - requiredSlots: app.shiftRole.count, - filledSlots: app.shiftRole.assigned ?? 0, - hasApplied: true, - ), - ); - } - return shifts; - } catch (e) { - return []; + shifts.add( + Shift( + id: app.shift.id, + roleId: app.shiftRole.roleId, + title: title, + clientName: app.shift.order.business.businessName, + logoUrl: app.shift.order.business.companyLogoUrl, + hourlyRate: app.shiftRole.role.costPerHour, + location: app.shift.location ?? '', + locationAddress: app.shift.order.teamHub.hubName, + date: shiftDate?.toIso8601String() ?? '', + startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', + endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', + createdDate: createdDt?.toIso8601String() ?? '', + status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT), + description: app.shift.description, + durationDays: app.shift.durationDays, + requiredSlots: app.shiftRole.count, + filledSlots: app.shiftRole.assigned ?? 0, + hasApplied: true, + ), + ); } + return shifts; } Future> _fetchApplications({ DateTime? start, DateTime? end, }) async { - try { - final staffId = await _getStaffId(); - var query = _dataConnect.getApplicationsByStaffId(staffId: staffId); - if (start != null && end != null) { - query = query - .dayStart(_toTimestamp(start)) - .dayEnd(_toTimestamp(end)); - } - final response = await query.execute(); - - final apps = response.data.applications; - final List shifts = []; - - for (final app in apps) { - _shiftToAppIdMap[app.shift.id] = app.id; - _appToRoleIdMap[app.id] = app.shiftRole.id; - - final String roleName = app.shiftRole.role.name; - final String orderName = - (app.shift.order.eventName ?? '').trim().isNotEmpty - ? app.shift.order.eventName! - : app.shift.order.business.businessName; - final String title = '$roleName - $orderName'; - final DateTime? shiftDate = _toDateTime(app.shift.date); - final DateTime? startDt = _toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _toDateTime(app.createdAt); - - // Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED) - final bool hasCheckIn = app.checkInTime != null; - final bool hasCheckOut = app.checkOutTime != null; - dc.ApplicationStatus? appStatus; - if (app.status is dc.Known) { - appStatus = (app.status as dc.Known).value; - } - final String mappedStatus = hasCheckOut - ? 'completed' - : hasCheckIn - ? 'checked_in' - : _mapStatus(appStatus ?? dc.ApplicationStatus.ACCEPTED); - shifts.add( - Shift( - id: app.shift.id, - roleId: app.shiftRole.roleId, - title: title, - clientName: app.shift.order.business.businessName, - logoUrl: app.shift.order.business.companyLogoUrl, - hourlyRate: app.shiftRole.role.costPerHour, - location: app.shift.location ?? '', - locationAddress: app.shift.order.teamHub.hubName, - date: shiftDate?.toIso8601String() ?? '', - startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', - endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', - createdDate: createdDt?.toIso8601String() ?? '', - status: mappedStatus, - description: app.shift.description, - durationDays: app.shift.durationDays, - requiredSlots: app.shiftRole.count, - filledSlots: app.shiftRole.assigned ?? 0, - hasApplied: true, - ), - ); - } - return shifts; - } catch (e) { - return []; + final staffId = await _getStaffId(); + var query = _dataConnect.getApplicationsByStaffId(staffId: staffId); + if (start != null && end != null) { + query = query.dayStart(_toTimestamp(start)).dayEnd(_toTimestamp(end)); } + final fdc.QueryResult response = await executeProtected(() => query.execute()); + + final apps = response.data.applications; + final List shifts = []; + + for (final app in apps) { + _shiftToAppIdMap[app.shift.id] = app.id; + _appToRoleIdMap[app.id] = app.shiftRole.id; + + final String roleName = app.shiftRole.role.name; + final String orderName = + (app.shift.order.eventName ?? '').trim().isNotEmpty + ? app.shift.order.eventName! + : app.shift.order.business.businessName; + final String title = '$roleName - $orderName'; + final DateTime? shiftDate = _toDateTime(app.shift.date); + final DateTime? startDt = _toDateTime(app.shiftRole.startTime); + final DateTime? endDt = _toDateTime(app.shiftRole.endTime); + final DateTime? createdDt = _toDateTime(app.createdAt); + + // Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED) + final bool hasCheckIn = app.checkInTime != null; + final bool hasCheckOut = app.checkOutTime != null; + dc.ApplicationStatus? appStatus; + if (app.status is dc.Known) { + appStatus = (app.status as dc.Known).value; + } + final String mappedStatus = hasCheckOut + ? 'completed' + : hasCheckIn + ? 'checked_in' + : _mapStatus(appStatus ?? dc.ApplicationStatus.ACCEPTED); + shifts.add( + Shift( + id: app.shift.id, + roleId: app.shiftRole.roleId, + title: title, + clientName: app.shift.order.business.businessName, + logoUrl: app.shift.order.business.companyLogoUrl, + hourlyRate: app.shiftRole.role.costPerHour, + location: app.shift.location ?? '', + locationAddress: app.shift.order.teamHub.hubName, + date: shiftDate?.toIso8601String() ?? '', + startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', + endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', + createdDate: createdDt?.toIso8601String() ?? '', + status: mappedStatus, + description: app.shift.description, + durationDays: app.shift.durationDays, + requiredSlots: app.shiftRole.count, + filledSlots: app.shiftRole.assigned ?? 0, + hasApplied: true, + ), + ); + } + return shifts; } - Timestamp _toTimestamp(DateTime dateTime) { + fdc.Timestamp _toTimestamp(DateTime dateTime) { final DateTime utc = dateTime.toUtc(); final int seconds = utc.millisecondsSinceEpoch ~/ 1000; final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000; - return Timestamp(nanoseconds, seconds); + return fdc.Timestamp(nanoseconds, seconds); } String _mapStatus(dc.ApplicationStatus status) { @@ -252,74 +239,60 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { @override Future> getAvailableShifts(String query, String type) async { - try { - final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId; - if (vendorId == null || vendorId.isEmpty) { - return []; - } - - final result = await _dataConnect - .listShiftRolesByVendorId(vendorId: vendorId) - .execute(); - final allShiftRoles = result.data.shiftRoles; - - final List mappedShifts = []; - for (final sr in allShiftRoles) { - print( - 'FindShifts raw: shiftId=${sr.shiftId} roleId=${sr.roleId} ' - 'start=${sr.startTime?.toJson()} end=${sr.endTime?.toJson()} ' - 'shiftDate=${sr.shift.date?.toJson()}', - ); - final DateTime? shiftDate = _toDateTime(sr.shift.date); - final startDt = _toDateTime(sr.startTime); - final endDt = _toDateTime(sr.endTime); - final createdDt = _toDateTime(sr.createdAt); - print( - 'FindShifts mapped: shiftId=${sr.shiftId} ' - 'origStart=${sr.startTime?.toJson()} ' - 'origEnd=${sr.endTime?.toJson()} ' - 'mappedStart=${startDt != null ? DateFormat('HH:mm').format(startDt) : ''} ' - 'mappedEnd=${endDt != null ? DateFormat('HH:mm').format(endDt) : ''}', - ); - mappedShifts.add( - Shift( - id: sr.shiftId, - roleId: sr.roleId, - title: sr.role.name, - clientName: sr.shift.order.business.businessName, - logoUrl: null, - hourlyRate: sr.role.costPerHour, - location: sr.shift.location ?? '', - locationAddress: sr.shift.locationAddress ?? '', - date: shiftDate?.toIso8601String() ?? '', - startTime: startDt != null - ? DateFormat('HH:mm').format(startDt) - : '', - endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', - createdDate: createdDt?.toIso8601String() ?? '', - status: sr.shift.status?.stringValue.toLowerCase() ?? 'open', - description: sr.shift.description, - durationDays: sr.shift.durationDays, - requiredSlots: sr.count, - filledSlots: sr.assigned ?? 0, - ), - ); - } - - if (query.isNotEmpty) { - return mappedShifts - .where( - (s) => - s.title.toLowerCase().contains(query.toLowerCase()) || - s.clientName.toLowerCase().contains(query.toLowerCase()), - ) - .toList(); - } - - return mappedShifts; - } catch (e) { + final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId; + if (vendorId == null || vendorId.isEmpty) { return []; } + + final fdc.QueryResult result = await executeProtected(() => _dataConnect + .listShiftRolesByVendorId(vendorId: vendorId) + .execute()); + final allShiftRoles = result.data.shiftRoles; + + final List mappedShifts = []; + for (final sr in allShiftRoles) { + + final DateTime? shiftDate = _toDateTime(sr.shift.date); + final startDt = _toDateTime(sr.startTime); + final endDt = _toDateTime(sr.endTime); + final createdDt = _toDateTime(sr.createdAt); + + mappedShifts.add( + Shift( + id: sr.shiftId, + roleId: sr.roleId, + title: sr.role.name, + clientName: sr.shift.order.business.businessName, + logoUrl: null, + hourlyRate: sr.role.costPerHour, + location: sr.shift.location ?? '', + locationAddress: sr.shift.locationAddress ?? '', + date: shiftDate?.toIso8601String() ?? '', + startTime: startDt != null + ? DateFormat('HH:mm').format(startDt) + : '', + endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', + createdDate: createdDt?.toIso8601String() ?? '', + status: sr.shift.status?.stringValue.toLowerCase() ?? 'open', + description: sr.shift.description, + durationDays: sr.shift.durationDays, + requiredSlots: sr.count, + filledSlots: sr.assigned ?? 0, + ), + ); + } + + if (query.isNotEmpty) { + return mappedShifts + .where( + (s) => + s.title.toLowerCase().contains(query.toLowerCase()) || + s.clientName.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + } + + return mappedShifts; } @override @@ -328,108 +301,101 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } Future _getShiftDetails(String shiftId, {String? roleId}) async { - try { - if (roleId != null && roleId.isNotEmpty) { - final roleResult = await _dataConnect - .getShiftRoleById(shiftId: shiftId, roleId: roleId) - .execute(); - final sr = roleResult.data.shiftRole; - if (sr == null) return null; + if (roleId != null && roleId.isNotEmpty) { + final roleResult = await executeProtected(() => _dataConnect + .getShiftRoleById(shiftId: shiftId, roleId: roleId) + .execute()); + final sr = roleResult.data.shiftRole; + if (sr == null) return null; - final DateTime? startDt = _toDateTime(sr.startTime); - final DateTime? endDt = _toDateTime(sr.endTime); - final DateTime? createdDt = _toDateTime(sr.createdAt); + final DateTime? startDt = _toDateTime(sr.startTime); + final DateTime? endDt = _toDateTime(sr.endTime); + final DateTime? createdDt = _toDateTime(sr.createdAt); - final String? staffId = await _getStaffId(); - bool hasApplied = false; - String status = 'open'; - if (staffId != null) { - final apps = await _dataConnect - .getApplicationsByStaffId(staffId: staffId) - .execute(); - final app = apps.data.applications - .where( - (a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId, - ) - .firstOrNull; - if (app != null) { - hasApplied = true; - if (app.status is dc.Known) { - final dc.ApplicationStatus s = - (app.status as dc.Known).value; - status = _mapStatus(s); - } - } + final String staffId = await _getStaffId(); + bool hasApplied = false; + String status = 'open'; + final apps = await executeProtected(() => + _dataConnect.getApplicationsByStaffId(staffId: staffId).execute()); + final app = apps.data.applications + .where( + (a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId, + ) + .firstOrNull; + if (app != null) { + hasApplied = true; + if (app.status is dc.Known) { + final dc.ApplicationStatus s = + (app.status as dc.Known).value; + status = _mapStatus(s); } - - return Shift( - id: sr.shiftId, - roleId: sr.roleId, - title: sr.shift.order.business.businessName, - clientName: sr.shift.order.business.businessName, - logoUrl: sr.shift.order.business.companyLogoUrl, - hourlyRate: sr.role.costPerHour, - location: sr.shift.location ?? sr.shift.order.teamHub.hubName, - locationAddress: sr.shift.locationAddress ?? '', - date: startDt?.toIso8601String() ?? '', - startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '', - endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', - createdDate: createdDt?.toIso8601String() ?? '', - status: status, - description: sr.shift.description, - durationDays: null, - requiredSlots: sr.count, - filledSlots: sr.assigned ?? 0, - hasApplied: hasApplied, - totalValue: sr.totalValue, - ); } - - final result = await _dataConnect.getShiftById(id: shiftId).execute(); - final s = result.data.shift; - if (s == null) return null; - - int? required; - int? filled; - try { - final rolesRes = await _dataConnect - .listShiftRolesByShiftId(shiftId: shiftId) - .execute(); - if (rolesRes.data.shiftRoles.isNotEmpty) { - required = 0; - filled = 0; - for (var r in rolesRes.data.shiftRoles) { - required = (required ?? 0) + r.count; - filled = (filled ?? 0) + (r.assigned ?? 0); - } - } - } catch (_) {} - - 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 ?? '', + id: sr.shiftId, + roleId: sr.roleId, + title: sr.shift.order.business.businessName, + clientName: sr.shift.order.business.businessName, + logoUrl: sr.shift.order.business.companyLogoUrl, + hourlyRate: sr.role.costPerHour, + location: sr.shift.location ?? sr.shift.order.teamHub.hubName, + locationAddress: sr.shift.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, - durationDays: s.durationDays, - requiredSlots: required, - filledSlots: filled, + status: status, + description: sr.shift.description, + durationDays: null, + requiredSlots: sr.count, + filledSlots: sr.assigned ?? 0, + hasApplied: hasApplied, + totalValue: sr.totalValue, ); - } catch (e) { - return null; } + + final fdc.QueryResult result = + await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute()); + final s = result.data.shift; + if (s == null) return null; + + int? required; + int? filled; + try { + final rolesRes = await executeProtected(() => + _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); + if (rolesRes.data.shiftRoles.isNotEmpty) { + required = 0; + filled = 0; + for (var r in rolesRes.data.shiftRoles) { + required = (required ?? 0) + r.count; + filled = (filled ?? 0) + (r.assigned ?? 0); + } + } + } catch (_) {} + + 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, + durationDays: s.durationDays, + requiredSlots: required, + filledSlots: filled, + ); } @override @@ -445,14 +411,15 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { throw Exception('Missing role id.'); } - final roleResult = await _dataConnect + final roleResult = await executeProtected(() => _dataConnect .getShiftRoleById(shiftId: shiftId, roleId: targetRoleId) - .execute(); + .execute()); final role = roleResult.data.shiftRole; if (role == null) { throw Exception('Shift role not found'); } - final shiftResult = await _dataConnect.getShiftById(id: shiftId).execute(); + final shiftResult = + await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute()); final shift = shiftResult.data.shift; if (shift == null) { throw Exception('Shift not found'); @@ -474,26 +441,23 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { 999, 999, ); - print( - 'Staff applyForShift: dayStartUtc=${_toTimestamp(dayStartUtc).toJson()} ' - 'dayEndUtc=${_toTimestamp(dayEndUtc).toJson()}', - ); - final dayApplications = await _dataConnect + + final dayApplications = await executeProtected(() => _dataConnect .vaidateDayStaffApplication(staffId: staffId) .dayStart(_toTimestamp(dayStartUtc)) .dayEnd(_toTimestamp(dayEndUtc)) - .execute(); + .execute()); if (dayApplications.data.applications.isNotEmpty) { throw Exception('The user already has a shift that day.'); } } - final existingApplicationResult = await _dataConnect + final existingApplicationResult = await executeProtected(() => _dataConnect .getApplicationByStaffShiftAndRole( staffId: staffId, shiftId: shiftId, roleId: targetRoleId, ) - .execute(); + .execute()); if (existingApplicationResult.data.applications.isNotEmpty) { throw Exception('Application already exists.'); } @@ -508,7 +472,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { bool updatedRole = false; bool updatedShift = false; try { - final appResult = await _dataConnect + final appResult = await executeProtected(() => _dataConnect .createApplication( shiftId: shiftId, staffId: staffId, @@ -517,29 +481,36 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { origin: dc.ApplicationOrigin.STAFF, ) // TODO: this should be PENDING so a vendor can accept it. - .execute(); + .execute()); appId = appResult.data.application_insert.id; - await _dataConnect + await executeProtected(() => _dataConnect .updateShiftRole(shiftId: shiftId, roleId: targetRoleId) .assigned(assigned + 1) - .execute(); + .execute()); updatedRole = true; - await _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute(); + await executeProtected( + () => _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute()); updatedShift = true; } catch (e) { if (updatedShift) { - await _dataConnect.updateShift(id: shiftId).filled(filled).execute(); + try { + await _dataConnect.updateShift(id: shiftId).filled(filled).execute(); + } catch (_) {} } if (updatedRole) { - await _dataConnect - .updateShiftRole(shiftId: shiftId, roleId: targetRoleId) - .assigned(assigned) - .execute(); + try { + await _dataConnect + .updateShiftRole(shiftId: shiftId, roleId: targetRoleId) + .assigned(assigned) + .execute(); + } catch (_) {} } if (appId != null) { - await _dataConnect.deleteApplication(id: appId).execute(); + try { + await _dataConnect.deleteApplication(id: appId).execute(); + } catch (_) {} } rethrow; } @@ -573,9 +544,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } else { // Fallback fetch final staffId = await _getStaffId(); - final apps = await _dataConnect - .getApplicationsByStaffId(staffId: staffId) - .execute(); + final apps = await executeProtected(() => + _dataConnect.getApplicationsByStaffId(staffId: staffId).execute()); final app = apps.data.applications .where((a) => a.shiftId == shiftId) .firstOrNull; @@ -588,13 +558,12 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { if (appId == null || roleId == null) { // If we are rejecting and can't find an application, create one as rejected (declining an available shift) if (newStatus == dc.ApplicationStatus.REJECTED) { - final rolesResult = await _dataConnect - .listShiftRolesByShiftId(shiftId: shiftId) - .execute(); + final rolesResult = await executeProtected(() => + _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); if (rolesResult.data.shiftRoles.isNotEmpty) { final role = rolesResult.data.shiftRoles.first; final staffId = await _getStaffId(); - await _dataConnect + await executeProtected(() => _dataConnect .createApplication( shiftId: shiftId, staffId: staffId, @@ -602,16 +571,16 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { status: dc.ApplicationStatus.REJECTED, origin: dc.ApplicationOrigin.STAFF, ) - .execute(); + .execute()); return; } } throw Exception("Application not found for shift $shiftId"); } - await _dataConnect - .updateApplicationStatus(id: appId) + await executeProtected(() => _dataConnect + .updateApplicationStatus(id: appId!) .status(newStatus) - .execute(); + .execute()); } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/accept_shift_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/accept_shift_usecase.dart index d3056716..d11ec6e6 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/accept_shift_usecase.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/accept_shift_usecase.dart @@ -1,4 +1,3 @@ -import 'package:krow_domain/krow_domain.dart'; import '../repositories/shifts_repository_interface.dart'; class AcceptShiftUseCase { diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/decline_shift_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/decline_shift_usecase.dart index 7dbbee45..2925ffa0 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/decline_shift_usecase.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/decline_shift_usecase.dart @@ -1,4 +1,3 @@ -import 'package:krow_domain/krow_domain.dart'; import '../repositories/shifts_repository_interface.dart'; class DeclineShiftUseCase { diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index f58339c3..3a8f22c9 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -223,7 +223,7 @@ class _ShiftDetailsPageState extends State { const SizedBox(height: 8), Row( children: [ - Container( + SizedBox( width: 24, height: 24, child: displayShift.logoUrl != null @@ -462,12 +462,12 @@ class _ShiftDetailsPageState extends State { ), ], const SizedBox(height: 20), - if (displayShift!.status != 'confirmed' && - displayShift!.hasApplied != true && - (displayShift!.requiredSlots == null || - displayShift!.filledSlots == null || - displayShift!.filledSlots! < - displayShift!.requiredSlots!)) + if (displayShift.status != 'confirmed' && + displayShift.hasApplied != true && + (displayShift.requiredSlots == null || + displayShift.filledSlots == null || + displayShift.filledSlots! < + displayShift.requiredSlots!)) Row( children: [ Expanded( diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 4e914b84..1b75a48c 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -22,12 +22,6 @@ class _FindShiftsTabState extends State { String _searchQuery = ''; String _jobType = 'all'; - @override - void initState() { - super.initState(); - print('FindShiftsTab init: tab entered, data pending'); - } - Widget _buildFilterTab(String id, String label) { final isSelected = _jobType == id; return GestureDetector( @@ -69,8 +63,9 @@ class _FindShiftsTabState extends State { if (_jobType == 'one-day') { return s.durationDays == null || s.durationDays! <= 1; } - if (_jobType == 'multi-day') + if (_jobType == 'multi-day') { return s.durationDays != null && s.durationDays! > 1; + } return true; }).toList(); @@ -176,21 +171,8 @@ class _FindShiftsTabState extends State { ...filteredJobs.map( (shift) => Padding( padding: const EdgeInsets.only(bottom: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Debug shiftId: ${shift.id}', - style: const TextStyle( - fontSize: 10, - color: Color(0xFF94A3B8), - ), - ), - const SizedBox(height: 4), - MyShiftCard( - shift: shift, - ), - ], + child: MyShiftCard( + shift: shift, ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart index 47fc79f0..02bade2c 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart @@ -13,7 +13,6 @@ import 'domain/usecases/get_shift_details_usecase.dart'; import 'presentation/blocs/shifts/shifts_bloc.dart'; import 'presentation/blocs/shift_details/shift_details_bloc.dart'; import 'presentation/pages/shifts_page.dart'; -import 'presentation/pages/shift_details_page.dart'; class StaffShiftsModule extends Module { @override diff --git a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart index 2738feae..fd3484ea 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart @@ -1,4 +1,4 @@ -library staff_shifts; +library; export 'src/staff_shifts_module.dart'; export 'src/shift_details_module.dart'; diff --git a/docs/CENTRALIZED_ERROR_HANDLING_MASTER.md b/docs/CENTRALIZED_ERROR_HANDLING_MASTER.md index 5d43d7bb..6da5ddbe 100644 --- a/docs/CENTRALIZED_ERROR_HANDLING_MASTER.md +++ b/docs/CENTRALIZED_ERROR_HANDLING_MASTER.md @@ -19,6 +19,11 @@ I've successfully implemented a **production-ready centralized error handling sy - Registered in both Client and Staff apps - Ready for Sentry/Crashlytics +**✅ Data Layer Error Handler** (`data_connect` package) +- `DataErrorHandler` mixin +- Wraps Data Connect calls +- Maps `SocketException` and `FirebaseException` to Domain `AppException` (Network/Server) + ### 2. **BLoC Migrations** (2 Complete) **✅ ClientAuthBloc** - 4 event handlers migrated - Reduced from 173 to 153 lines (-11.6%)