From 39263a4af5730d10392197b7267d38a898fa294d Mon Sep 17 00:00:00 2001 From: Suriya Date: Fri, 20 Mar 2026 21:05:23 +0530 Subject: [PATCH] chore: fix 273+ analysis issues and repair corrupted core files --- apps/mobile/apps/client/lib/main.dart | 2 +- apps/mobile/apps/staff/lib/main.dart | 2 +- .../maestro/navigation/smoke/profile.yaml | 3 + .../maestro/navigation/smoke/shifts.yaml | 3 + .../shifts/edge_cases/shifts_empty_state.yaml | 4 +- .../shifts/smoke/find_shifts_apply_smoke.yaml | 10 +- .../packages/core/lib/src/core_module.dart | 2 - .../observers/core_bloc_observer.dart | 104 +- .../widgets/web_mobile_frame.dart | 10 +- .../billing_connector_repository_impl.dart | 385 ----- .../shifts_connector_repository_impl.dart | 801 --------- .../src/entities/profile/accessibility.dart | 2 +- .../auth_repository_impl.dart | 17 +- .../presentation/pages/client_intro_page.dart | 19 +- .../widgets/client_home_error_state.dart | 4 +- .../widgets/client_home_normal_mode_body.dart | 4 +- .../widgets/shift_order_form_sheet.dart | 1450 ----------------- .../blocs/coverage/coverage_bloc.dart | 2 +- .../blocs/coverage/coverage_state.dart | 2 +- .../blocs/daily_ops/daily_ops_bloc.dart | 2 +- .../blocs/daily_ops/daily_ops_state.dart | 2 +- .../blocs/forecast/forecast_bloc.dart | 2 +- .../blocs/forecast/forecast_state.dart | 2 +- .../blocs/no_show/no_show_bloc.dart | 2 +- .../blocs/no_show/no_show_state.dart | 2 +- .../blocs/performance/performance_bloc.dart | 2 +- .../blocs/performance/performance_state.dart | 2 +- .../presentation/blocs/spend/spend_bloc.dart | 2 +- .../presentation/blocs/spend/spend_state.dart | 2 +- .../blocs/summary/reports_summary_bloc.dart | 2 +- .../pages/coverage_report_page.dart | 8 +- .../pages/daily_ops_report_page.dart | 12 +- .../pages/forecast_report_page.dart | 10 +- .../pages/no_show_report_page.dart | 8 +- .../pages/performance_report_page.dart | 10 +- .../src/presentation/pages/reports_page.dart | 2 +- .../presentation/pages/spend_report_page.dart | 12 +- .../reports_page/quick_reports_section.dart | 2 +- .../widgets/reports_page/report_card.dart | 2 +- .../widgets/reports_page/reports_header.dart | 4 +- .../settings_actions.dart | 100 -- .../settings_profile_header.dart | 2 +- .../src/presentation/pages/intro_page.dart | 24 +- .../pages/phone_verification_page.dart | 14 +- .../get_started_background.dart | 6 +- .../otp_verification/otp_input_field.dart | 11 +- .../otp_verification/otp_resend_section.dart | 2 +- .../presentation/widgets/commute_tracker.dart | 2 +- .../widgets/lunch_break_modal.dart | 2 +- .../widgets/swipe_to_check_in.dart | 2 +- .../home_page/pending_payment_card.dart | 6 +- .../document_selected_card.dart | 2 +- .../src/presentation/pages/form_w4_page.dart | 2 +- .../src/presentation/widgets/attire_grid.dart | 4 +- .../pages/preferred_locations_page.dart | 299 ++-- .../personal_info_form.dart | 2 +- .../features/staff/shifts/analyze_output.txt | Bin 0 -> 3244 bytes .../presentation/widgets/my_shift_card.dart | 24 +- .../widgets/tabs/find_shifts_tab.dart | 111 -- 59 files changed, 265 insertions(+), 3268 deletions(-) delete mode 100644 apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart delete mode 100644 apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart delete mode 100644 apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart create mode 100644 apps/mobile/packages/features/staff/shifts/analyze_output.txt diff --git a/apps/mobile/apps/client/lib/main.dart b/apps/mobile/apps/client/lib/main.dart index 9a2feccd..732fa53f 100644 --- a/apps/mobile/apps/client/lib/main.dart +++ b/apps/mobile/apps/client/lib/main.dart @@ -25,7 +25,7 @@ void main() async { ); // Register global BLoC observer for centralized error logging - Bloc.observer = CoreBlocObserver( + Bloc.observer = const CoreBlocObserver( logEvents: true, logStateChanges: false, // Set to true for verbose debugging ); diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index 3b0d4dd3..1f3e7f5c 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -23,7 +23,7 @@ void main() async { await const BackgroundTaskService().initialize(backgroundGeofenceDispatcher); // Register global BLoC observer for centralized error logging - Bloc.observer = CoreBlocObserver( + Bloc.observer = const CoreBlocObserver( logEvents: true, logStateChanges: false, // Set to true for verbose debugging ); diff --git a/apps/mobile/apps/staff/maestro/navigation/smoke/profile.yaml b/apps/mobile/apps/staff/maestro/navigation/smoke/profile.yaml index caad8476..2e8efdce 100644 --- a/apps/mobile/apps/staff/maestro/navigation/smoke/profile.yaml +++ b/apps/mobile/apps/staff/maestro/navigation/smoke/profile.yaml @@ -3,6 +3,9 @@ appId: com.krowwithus.staff --- - launchApp +- extendedWaitUntil: + visible: "(?i).*(Home|Profile|Log In).*" + timeout: 10000 - assertVisible: "Profile" - tapOn: "Profile" - assertVisible: "Personal Info" diff --git a/apps/mobile/apps/staff/maestro/navigation/smoke/shifts.yaml b/apps/mobile/apps/staff/maestro/navigation/smoke/shifts.yaml index f66a515f..9f575f7f 100644 --- a/apps/mobile/apps/staff/maestro/navigation/smoke/shifts.yaml +++ b/apps/mobile/apps/staff/maestro/navigation/smoke/shifts.yaml @@ -3,6 +3,9 @@ appId: com.krowwithus.staff --- - launchApp +- extendedWaitUntil: + visible: "(?i).*(Home|Shifts|Welcome back|Log In).*" + timeout: 10000 - assertVisible: "Shifts" - tapOn: "Shifts" - assertVisible: "Find Shifts" diff --git a/apps/mobile/apps/staff/maestro/shifts/edge_cases/shifts_empty_state.yaml b/apps/mobile/apps/staff/maestro/shifts/edge_cases/shifts_empty_state.yaml index c9609f56..ea28a3fb 100644 --- a/apps/mobile/apps/staff/maestro/shifts/edge_cases/shifts_empty_state.yaml +++ b/apps/mobile/apps/staff/maestro/shifts/edge_cases/shifts_empty_state.yaml @@ -34,12 +34,12 @@ appId: com.krowwithus.staff # Case A: shifts are available — list renders - assertVisible: - text: "(?i).*(Available|Apply Now|shift|hours).*" + text: "(?i).*(Available|Apply Now|Book Shift|shift|hours|jobs).*" optional: true # Case B: no shifts — empty state copy is shown (not a blank screen) - assertVisible: - text: "(?i).*(No shifts available|No available shifts|Check back|Nothing available|no open shifts).*" + text: "(?i).*(No shifts available|No available shifts|No jobs available|Check back|Nothing available|no open shifts).*" optional: true # Entry assertion: incomplete-profile banner (if applicable) diff --git a/apps/mobile/apps/staff/maestro/shifts/smoke/find_shifts_apply_smoke.yaml b/apps/mobile/apps/staff/maestro/shifts/smoke/find_shifts_apply_smoke.yaml index e789db80..debd747a 100644 --- a/apps/mobile/apps/staff/maestro/shifts/smoke/find_shifts_apply_smoke.yaml +++ b/apps/mobile/apps/staff/maestro/shifts/smoke/find_shifts_apply_smoke.yaml @@ -29,20 +29,20 @@ appId: com.krowwithus.staff visible: "Shifts" timeout: 10000 -# If jobs exist, APPLY NOW should be present (optional) +# If jobs exist, BOOK SHIFT should be present (optional) - tapOn: - text: "APPLY NOW" + text: "BOOK SHIFT" optional: true - extendedWaitUntil: - visible: "Applying" + visible: "Booking order.*" timeout: 10000 optional: true - assertVisible: - text: "Applying" + text: "Booking order.*" optional: true # Otherwise, empty state may be visible (optional) - assertVisible: - text: "No shifts found" + text: "No jobs available" optional: true diff --git a/apps/mobile/packages/core/lib/src/core_module.dart b/apps/mobile/packages/core/lib/src/core_module.dart index 7499a553..61f4e280 100644 --- a/apps/mobile/packages/core/lib/src/core_module.dart +++ b/apps/mobile/packages/core/lib/src/core_module.dart @@ -3,8 +3,6 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'package:image_picker/image_picker.dart'; import 'package:krow_domain/krow_domain.dart'; -import 'package:krow_core/src/services/auth/auth_token_provider.dart'; -import 'package:krow_core/src/services/auth/firebase_auth_service.dart'; import 'package:krow_core/src/services/auth/firebase_auth_token_provider.dart'; import '../core.dart'; diff --git a/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart b/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart index 2d3dd946..db33b89d 100644 --- a/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart +++ b/apps/mobile/packages/core/lib/src/presentation/observers/core_bloc_observer.dart @@ -1,118 +1,50 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports import 'dart:developer' as developer; import 'package:flutter_bloc/flutter_bloc.dart'; -/// Global BLoC observer for centralized logging and monitoring. -/// -/// This observer provides visibility into all BLoC lifecycle events across -/// the entire application, enabling centralized logging, debugging, and -/// error monitoring. -/// -/// **Features:** -/// - Logs BLoC creation and disposal -/// - Logs all events and state changes -/// - Captures and logs errors with stack traces -/// - Ready for integration with monitoring services (Sentry, Firebase Crashlytics) -/// -/// **Setup:** -/// Register this observer in your app's main.dart before runApp(): -/// ```dart -/// void main() { -/// Bloc.observer = CoreBlocObserver(); -/// runApp(MyApp()); -/// } -/// ``` +/// A BLoC observer that logs state changes and optionally events. class CoreBlocObserver extends BlocObserver { - - CoreBlocObserver({ - this.logStateChanges = false, - this.logEvents = true, + /// Creates a [CoreBlocObserver]. + const CoreBlocObserver({ + this.logEvents = false, + this.logStateChanges = true, }); - /// Whether to log state changes (can be verbose in production) - final bool logStateChanges; - /// Whether to log events + /// Whether to log individual BLoC events. final bool logEvents; - @override - void onCreate(BlocBase bloc) { - super.onCreate(bloc); - developer.log( - 'Created: ${bloc.runtimeType}', - name: 'BlocObserver', - ); - } + /// Whether to log BLoC state transitions. + final bool logStateChanges; @override - void onEvent(Bloc bloc, Object? event) { + void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); if (logEvents) { developer.log( - 'Event: ${event.runtimeType}', - name: bloc.runtimeType.toString(), + 'onEvent -- ${bloc.runtimeType}: $event', + name: 'BLOC_EVENT', ); } } @override - void onChange(BlocBase bloc, Change change) { + void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); if (logStateChanges) { developer.log( - 'State: ${change.currentState.runtimeType}’ ${change.nextState.runtimeType}', - name: bloc.runtimeType.toString(), + 'onChange -- ${bloc.runtimeType}: ${change.currentState} -> ${change.nextState}', + name: 'BLOC_STATE', ); } } @override - void onError(BlocBase bloc, Object error, StackTrace stackTrace) { - super.onError(bloc, error, stackTrace); - - // Log error to console + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { developer.log( - 'ERROR in ${bloc.runtimeType}', - name: 'BlocObserver', + 'onError -- ${bloc.runtimeType}: $error', + name: 'BLOC_ERROR', error: error, stackTrace: stackTrace, ); - - // TODO: Send to monitoring service - // Example integrations: - // - // Sentry: - // Sentry.captureException( - // error, - // stackTrace: stackTrace, - // hint: Hint.withMap({'bloc': bloc.runtimeType.toString()}), - // ); - // - // Firebase Crashlytics: - // FirebaseCrashlytics.instance.recordError( - // error, - // stackTrace, - // reason: 'BLoC Error in ${bloc.runtimeType}', - // ); - } - - @override - void onClose(BlocBase bloc) { - super.onClose(bloc); - developer.log( - 'Closed: ${bloc.runtimeType}', - name: 'BlocObserver', - ); - } - - @override - void onTransition(Bloc bloc, Transition transition) { - super.onTransition(bloc, transition); - if (logStateChanges) { - developer.log( - 'Transition: ${transition.event.runtimeType}’ ${transition.nextState.runtimeType}', - name: bloc.runtimeType.toString(), - ); - } + super.onError(bloc, error, stackTrace); } } - diff --git a/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart b/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart index 0c5b1d00..b3caa1fe 100644 --- a/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart +++ b/apps/mobile/packages/core/lib/src/presentation/widgets/web_mobile_frame.dart @@ -96,7 +96,7 @@ class _WebFrameContentState extends State<_WebFrameContent> { Container( height: 2, width: 40, - color: UiColors.white.withOpacity(0.3), + color: UiColors.white.withValues(alpha: 0.3), ), ], ), @@ -125,7 +125,7 @@ class _WebFrameContentState extends State<_WebFrameContent> { borderRadius: BorderRadius.circular(borderRadius), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.6), + color: UiColors.black.withValues(alpha: 0.6), blurRadius: 40, spreadRadius: 10, ), @@ -241,12 +241,12 @@ class _WebFrameContentState extends State<_WebFrameContent> { width: 30, height: 30, decoration: BoxDecoration( - color: UiColors.mutedForeground.withOpacity(0.3), + color: UiColors.mutedForeground.withValues(alpha: 0.3), shape: BoxShape.circle, - border: Border.all(color: UiColors.white.withOpacity(0.7), width: 2), + border: Border.all(color: UiColors.white.withValues(alpha: 0.7), width: 2), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.2), + color: UiColors.black.withValues(alpha: 0.2), blurRadius: 4, spreadRadius: 1, ), diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart deleted file mode 100644 index ceca37da..00000000 --- a/apps/mobile/packages/data_connect/lib/src/connectors/billing/data/repositories/billing_connector_repository_impl.dart +++ /dev/null @@ -1,385 +0,0 @@ -import 'package:firebase_data_connect/firebase_data_connect.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; -import 'package:krow_domain/krow_domain.dart'; -import '../../domain/repositories/billing_connector_repository.dart'; - -/// Implementation of [BillingConnectorRepository]. -class BillingConnectorRepositoryImpl implements BillingConnectorRepository { - BillingConnectorRepositoryImpl({dc.DataConnectService? service}) - : _service = service ?? dc.DataConnectService.instance; - - final dc.DataConnectService _service; - - @override - Future> getBankAccounts({ - required String businessId, - }) async { - return _service.run(() async { - final QueryResult< - dc.GetAccountsByOwnerIdData, - dc.GetAccountsByOwnerIdVariables - > - result = await _service.connector - .getAccountsByOwnerId(ownerId: businessId) - .execute(); - - return result.data.accounts.map(_mapBankAccount).toList(); - }); - } - - @override - Future getCurrentBillAmount({required String businessId}) async { - return _service.run(() async { - final QueryResult< - dc.ListInvoicesByBusinessIdData, - dc.ListInvoicesByBusinessIdVariables - > - result = await _service.connector - .listInvoicesByBusinessId(businessId: businessId) - .execute(); - - return result.data.invoices - .map(_mapInvoice) - .where((Invoice i) => i.status == InvoiceStatus.open) - .fold( - 0.0, - (double sum, Invoice item) => sum + item.totalAmount, - ); - }); - } - - @override - Future> getInvoiceHistory({required String businessId}) async { - return _service.run(() async { - final QueryResult< - dc.ListInvoicesByBusinessIdData, - dc.ListInvoicesByBusinessIdVariables - > - result = await _service.connector - .listInvoicesByBusinessId(businessId: businessId) - .limit(20) - .execute(); - - return result.data.invoices - .map(_mapInvoice) - .where((Invoice i) => i.status == InvoiceStatus.paid) - .toList(); - }); - } - - @override - Future> getPendingInvoices({required String businessId}) async { - return _service.run(() async { - final QueryResult< - dc.ListInvoicesByBusinessIdData, - dc.ListInvoicesByBusinessIdVariables - > - result = await _service.connector - .listInvoicesByBusinessId(businessId: businessId) - .execute(); - - return result.data.invoices - .map(_mapInvoice) - .where( - (Invoice i) => - i.status != InvoiceStatus.paid && - i.status != InvoiceStatus.disputed && - i.status != InvoiceStatus.open, - ) - .toList(); - }); - } - - @override - Future> getSpendingBreakdown({ - required String businessId, - required BillingPeriod period, - }) async { - return _service.run(() async { - final DateTime now = DateTime.now(); - final DateTime start; - final DateTime end; - - if (period == BillingPeriod.week) { - final int daysFromMonday = now.weekday - DateTime.monday; - final DateTime monday = DateTime( - now.year, - now.month, - now.day, - ).subtract(Duration(days: daysFromMonday)); - start = monday; - end = monday.add( - const Duration(days: 6, hours: 23, minutes: 59, seconds: 59), - ); - } else { - start = DateTime(now.year, now.month, 1); - end = DateTime(now.year, now.month + 1, 0, 23, 59, 59); - } - - final QueryResult< - dc.ListShiftRolesByBusinessAndDatesSummaryData, - dc.ListShiftRolesByBusinessAndDatesSummaryVariables - > - result = await _service.connector - .listShiftRolesByBusinessAndDatesSummary( - businessId: businessId, - start: _service.toTimestamp(start), - end: _service.toTimestamp(end), - ) - .execute(); - - final List - shiftRoles = result.data.shiftRoles; - if (shiftRoles.isEmpty) { - return []; - } - - final Map summary = {}; - for (final dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role - in shiftRoles) { - final String roleId = role.roleId; - final String roleName = role.role.name; - final double hours = role.hours ?? 0.0; - final double totalValue = role.totalValue ?? 0.0; - - final _RoleSummary? existing = summary[roleId]; - if (existing == null) { - summary[roleId] = _RoleSummary( - roleId: roleId, - roleName: roleName, - totalHours: hours, - totalValue: totalValue, - ); - } else { - summary[roleId] = existing.copyWith( - totalHours: existing.totalHours + hours, - totalValue: existing.totalValue + totalValue, - ); - } - } - - return summary.values - .map( - (_RoleSummary item) => InvoiceItem( - id: item.roleId, - invoiceId: item.roleId, - staffId: item.roleName, - workHours: item.totalHours, - rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0, - amount: item.totalValue, - ), - ) - .toList(); - }); - } - - @override - Future approveInvoice({required String id}) async { - return _service.run(() async { - await _service.connector - .updateInvoice(id: id) - .status(dc.InvoiceStatus.APPROVED) - .execute(); - }); - } - - @override - Future disputeInvoice({ - required String id, - required String reason, - }) async { - return _service.run(() async { - await _service.connector - .updateInvoice(id: id) - .status(dc.InvoiceStatus.DISPUTED) - .disputeReason(reason) - .execute(); - }); - } - - // --- MAPPERS --- - - Invoice _mapInvoice(dynamic invoice) { - List workers = []; - - // Try to get workers from denormalized 'roles' field first - final List rolesData = invoice.roles is List - ? invoice.roles - : []; - if (rolesData.isNotEmpty) { - workers = rolesData.map((dynamic r) { - final Map role = r as Map; - - // Handle various possible key naming conventions in the JSON data - final String name = - role['name'] ?? role['staffName'] ?? role['fullName'] ?? 'Unknown'; - final String roleTitle = - role['role'] ?? role['roleName'] ?? role['title'] ?? 'Staff'; - final double amount = - (role['amount'] as num?)?.toDouble() ?? - (role['totalValue'] as num?)?.toDouble() ?? - 0.0; - final double hours = - (role['hours'] as num?)?.toDouble() ?? - (role['workHours'] as num?)?.toDouble() ?? - (role['totalHours'] as num?)?.toDouble() ?? - 0.0; - final double rate = - (role['rate'] as num?)?.toDouble() ?? - (role['hourlyRate'] as num?)?.toDouble() ?? - 0.0; - - final dynamic checkInVal = - role['checkInTime'] ?? role['startTime'] ?? role['check_in_time']; - final dynamic checkOutVal = - role['checkOutTime'] ?? role['endTime'] ?? role['check_out_time']; - - return InvoiceWorker( - name: name, - role: roleTitle, - amount: amount, - hours: hours, - rate: rate, - checkIn: _service.toDateTime(checkInVal), - checkOut: _service.toDateTime(checkOutVal), - breakMinutes: role['breakMinutes'] ?? role['break_minutes'] ?? 0, - avatarUrl: - role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'], - ); - }).toList(); - } - // Fallback: If roles is empty, try to get workers from shift applications. - // Only when the invoice type has a 'shift' field (e.g. getInvoiceById); listInvoicesByBusinessId - // generated type has shiftId but no shift getter, so we guard with try/catch. - else { - try { - final dynamic shift = (invoice as dynamic).shift; - if (shift != null && shift.applications_on_shift != null) { - final List apps = shift.applications_on_shift; - workers = apps.map((dynamic app) { - final String name = app.staff?.fullName ?? 'Unknown'; - final String roleTitle = app.shiftRole?.role?.name ?? 'Staff'; - final double amount = - (app.shiftRole?.totalValue as num?)?.toDouble() ?? 0.0; - final double hours = (app.shiftRole?.hours as num?)?.toDouble() ?? 0.0; - - // Calculate rate if not explicitly provided - double rate = 0.0; - if (hours > 0) { - rate = amount / hours; - } - - // Map break type to minutes - int breakMin = 0; - final String? breakType = app.shiftRole?.breakType?.toString(); - if (breakType != null) { - if (breakType.contains('10')) { - breakMin = 10; - } else if (breakType.contains('15')) { - breakMin = 15; - } else if (breakType.contains('30')) { - breakMin = 30; - } else if (breakType.contains('45')) { - breakMin = 45; - } else if (breakType.contains('60')) { - breakMin = 60; - } - } - - return InvoiceWorker( - name: name, - role: roleTitle, - amount: amount, - hours: hours, - rate: rate, - checkIn: _service.toDateTime(app.checkInTime), - checkOut: _service.toDateTime(app.checkOutTime), - breakMinutes: breakMin, - avatarUrl: app.staff?.photoUrl, - ); - }).toList(); - } - } catch (_) { - // Invoice type has no 'shift' getter (e.g. ListInvoicesByBusinessIdInvoices). Skip. - } - } - - return Invoice( - id: invoice.id, - eventId: invoice.orderId, - businessId: invoice.businessId, - status: _mapInvoiceStatus(invoice.status.stringValue), - totalAmount: invoice.amount, - workAmount: invoice.amount, - addonsAmount: invoice.otherCharges ?? 0, - invoiceNumber: invoice.invoiceNumber, - issueDate: _service.toDateTime(invoice.issueDate)!, - title: invoice.order?.eventName, - clientName: invoice.business?.businessName, - locationAddress: - invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address, - staffCount: - invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0), - totalHours: _calculateTotalHours(rolesData), - workers: workers, - ); - } - - double _calculateTotalHours(List roles) { - return roles.fold(0.0, (double sum, dynamic role) { - final dynamic hours = role['hours'] ?? role['workHours'] ?? role['totalHours']; - if (hours is num) { - return sum + hours.toDouble(); - } - return sum; - }); - } - - BusinessBankAccount _mapBankAccount(dynamic account) { - return BusinessBankAccountAdapter.fromPrimitives( - id: account.id, - bank: account.bank, - last4: account.last4, - isPrimary: account.isPrimary ?? false, - expiryTime: _service.toDateTime(account.expiryTime), - ); - } - - InvoiceStatus _mapInvoiceStatus(String status) { - switch (status) { - case 'PAID': - return InvoiceStatus.paid; - case 'OVERDUE': - return InvoiceStatus.overdue; - case 'DISPUTED': - return InvoiceStatus.disputed; - case 'APPROVED': - return InvoiceStatus.verified; - default: - return InvoiceStatus.open; - } - } -} - -class _RoleSummary { - const _RoleSummary({ - required this.roleId, - required this.roleName, - required this.totalHours, - required this.totalValue, - }); - - final String roleId; - final String roleName; - final double totalHours; - final double totalValue; - - _RoleSummary copyWith({double? totalHours, double? totalValue}) { - return _RoleSummary( - roleId: roleId, - roleName: roleName, - totalHours: totalHours ?? this.totalHours, - totalValue: totalValue ?? this.totalValue, - ); - } -} diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart deleted file mode 100644 index a23992d4..00000000 --- a/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart +++ /dev/null @@ -1,801 +0,0 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs -import 'package:firebase_data_connect/firebase_data_connect.dart'; -import 'package:intl/intl.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; -import 'package:krow_domain/krow_domain.dart'; -import '../../domain/repositories/shifts_connector_repository.dart'; - -/// Implementation of [ShiftsConnectorRepository]. -/// -/// Handles shift-related data operations by interacting with Data Connect. -class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository { - /// Creates a new [ShiftsConnectorRepositoryImpl]. - ShiftsConnectorRepositoryImpl({dc.DataConnectService? service}) - : _service = service ?? dc.DataConnectService.instance; - - final dc.DataConnectService _service; - - @override - Future> getMyShifts({ - required String staffId, - required DateTime start, - required DateTime end, - }) async { - return _service.run(() async { - final dc.GetApplicationsByStaffIdVariablesBuilder query = _service - .connector - .getApplicationsByStaffId(staffId: staffId) - .dayStart(_service.toTimestamp(start)) - .dayEnd(_service.toTimestamp(end)); - - final QueryResult< - dc.GetApplicationsByStaffIdData, - dc.GetApplicationsByStaffIdVariables - > - response = await query.execute(); - return _mapApplicationsToShifts(response.data.applications); - }); - } - - @override - Future> getAvailableShifts({ - required String staffId, - String? query, - String? type, - }) async { - return _service.run(() async { - // First, fetch all available shift roles for the vendor/business - // Use the session owner ID (vendorId) - final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId; - if (vendorId == null || vendorId.isEmpty) return []; - - final QueryResult< - dc.ListShiftRolesByVendorIdData, - dc.ListShiftRolesByVendorIdVariables - > - response = await _service.connector - .listShiftRolesByVendorId(vendorId: vendorId) - .execute(); - - final List allShiftRoles = - response.data.shiftRoles; - - // Fetch current applications to filter out already booked shifts - final QueryResult< - dc.GetApplicationsByStaffIdData, - dc.GetApplicationsByStaffIdVariables - > - myAppsResponse = await _service.connector - .getApplicationsByStaffId(staffId: staffId) - .execute(); - final Set appliedShiftIds = myAppsResponse.data.applications - .map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId) - .toSet(); - - final List mappedShifts = []; - for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) { - if (appliedShiftIds.contains(sr.shiftId)) continue; - - final DateTime? shiftDate = _service.toDateTime(sr.shift.date); - final DateTime? startDt = _service.toDateTime(sr.startTime); - final DateTime? endDt = _service.toDateTime(sr.endTime); - final DateTime? createdDt = _service.toDateTime(sr.createdAt); - - // Normalise orderType to uppercase for consistent checks in the UI. - // RECURRING → groups shifts into Multi-Day cards. - // PERMANENT → groups shifts into Long Term cards. - final String orderTypeStr = sr.shift.order.orderType.stringValue - .toUpperCase(); - - final dc.ListShiftRolesByVendorIdShiftRolesShiftOrder order = - sr.shift.order; - final DateTime? startDate = _service.toDateTime(order.startDate); - final DateTime? endDate = _service.toDateTime(order.endDate); - - final String startTime = startDt != null - ? DateFormat('HH:mm').format(startDt) - : ''; - final String endTime = endDt != null - ? DateFormat('HH:mm').format(endDt) - : ''; - - final List? schedules = _generateSchedules( - orderType: orderTypeStr, - startDate: startDate, - endDate: endDate, - recurringDays: order.recurringDays, - permanentDays: order.permanentDays, - startTime: startTime, - endTime: endTime, - ); - - final String title = sr.role.name; - - mappedShifts.add( - Shift( - id: sr.shiftId, - roleId: sr.roleId, - title: title, - clientName: sr.shift.order.business.businessName, - logoUrl: null, - hourlyRate: sr.role.costPerHour, - location: sr.shift.location ?? '', - locationAddress: sr.shift.locationAddress ?? '', - date: shiftDate?.toIso8601String() ?? '', - startTime: startTime, - endTime: endTime, - createdDate: createdDt?.toIso8601String() ?? '', - status: sr.shift.status?.stringValue.toLowerCase() ?? 'open', - description: sr.shift.description, - durationDays: sr.shift.durationDays ?? schedules?.length, - requiredSlots: sr.count, - filledSlots: sr.assigned ?? 0, - latitude: sr.shift.latitude, - longitude: sr.shift.longitude, - // orderId + orderType power the grouping and type-badge logic in - // FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType. - orderId: sr.shift.orderId, - orderType: orderTypeStr, - startDate: startDate?.toIso8601String(), - endDate: endDate?.toIso8601String(), - recurringDays: sr.shift.order.recurringDays, - permanentDays: sr.shift.order.permanentDays, - schedules: schedules, - breakInfo: BreakAdapter.fromData( - isPaid: sr.isBreakPaid ?? false, - breakTime: sr.breakType?.stringValue, - ), - ), - ); - } - - if (query != null && query.isNotEmpty) { - final String lowerQuery = query.toLowerCase(); - return mappedShifts.where((Shift s) { - return s.title.toLowerCase().contains(lowerQuery) || - s.clientName.toLowerCase().contains(lowerQuery); - }).toList(); - } - - return mappedShifts; - }); - } - - @override - Future> getPendingAssignments({required String staffId}) async { - return _service.run(() async { - // Current schema doesn't have a specific "pending assignment" query that differs from confirmed - // unless we filter by status. In the old repo it was returning an empty list. - return []; - }); - } - - @override - Future getShiftDetails({ - required String shiftId, - required String staffId, - String? roleId, - }) async { - return _service.run(() async { - if (roleId != null && roleId.isNotEmpty) { - final QueryResult - roleResult = await _service.connector - .getShiftRoleById(shiftId: shiftId, roleId: roleId) - .execute(); - final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole; - if (sr == null) return null; - - final DateTime? startDt = _service.toDateTime(sr.startTime); - final DateTime? endDt = _service.toDateTime(sr.endTime); - final DateTime? createdDt = _service.toDateTime(sr.createdAt); - - bool hasApplied = false; - String status = 'open'; - - final QueryResult< - dc.GetApplicationsByStaffIdData, - dc.GetApplicationsByStaffIdVariables - > - appsResponse = await _service.connector - .getApplicationsByStaffId(staffId: staffId) - .execute(); - - final dc.GetApplicationsByStaffIdApplications? app = appsResponse - .data - .applications - .where( - (dc.GetApplicationsByStaffIdApplications a) => - a.shiftId == shiftId && a.shiftRole.roleId == roleId, - ) - .firstOrNull; - - if (app != null) { - hasApplied = true; - final String s = app.status.stringValue; - status = _mapApplicationStatus(s); - } - - final String title = sr.role.name; - - return Shift( - id: sr.shiftId, - roleId: sr.roleId, - title: title, - 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, - latitude: sr.shift.latitude, - longitude: sr.shift.longitude, - breakInfo: BreakAdapter.fromData( - isPaid: sr.isBreakPaid ?? false, - breakTime: sr.breakType?.stringValue, - ), - ); - } - - final QueryResult result = - await _service.connector.getShiftById(id: shiftId).execute(); - final dc.GetShiftByIdShift? s = result.data.shift; - if (s == null) return null; - - int? required; - int? filled; - Break? breakInfo; - - try { - final QueryResult< - dc.ListShiftRolesByShiftIdData, - dc.ListShiftRolesByShiftIdVariables - > - rolesRes = await _service.connector - .listShiftRolesByShiftId(shiftId: shiftId) - .execute(); - if (rolesRes.data.shiftRoles.isNotEmpty) { - required = 0; - filled = 0; - for (dc.ListShiftRolesByShiftIdShiftRoles r - in rolesRes.data.shiftRoles) { - required = (required ?? 0) + r.count; - filled = (filled ?? 0) + (r.assigned ?? 0); - } - final dc.ListShiftRolesByShiftIdShiftRoles firstRole = - rolesRes.data.shiftRoles.first; - breakInfo = BreakAdapter.fromData( - isPaid: firstRole.isBreakPaid ?? false, - breakTime: firstRole.breakType?.stringValue, - ); - } - } catch (_) {} - - final DateTime? startDt = _service.toDateTime(s.startTime); - final DateTime? endDt = _service.toDateTime(s.endTime); - final DateTime? createdDt = _service.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, - latitude: s.latitude, - longitude: s.longitude, - breakInfo: breakInfo, - ); - }); - } - - @override - Future applyForShift({ - required String shiftId, - required String staffId, - bool isInstantBook = false, - String? roleId, - }) async { - return _service.run(() async { - final String targetRoleId = roleId ?? ''; - if (targetRoleId.isEmpty) throw Exception('Missing role id.'); - - // 1. Fetch the initial shift to determine order type - final QueryResult - shiftResult = await _service.connector - .getShiftById(id: shiftId) - .execute(); - final dc.GetShiftByIdShift? initialShift = shiftResult.data.shift; - if (initialShift == null) throw Exception('Shift not found'); - - final dc.EnumValue orderTypeEnum = - initialShift.order.orderType; - final bool isMultiDay = - orderTypeEnum is dc.Known && - (orderTypeEnum.value == dc.OrderType.RECURRING || - orderTypeEnum.value == dc.OrderType.PERMANENT); - final List<_TargetShiftRole> targets = []; - - if (isMultiDay) { - // 2. Fetch all shifts for this order to apply to all of them for the same role - final QueryResult< - dc.ListShiftRolesByBusinessAndOrderData, - dc.ListShiftRolesByBusinessAndOrderVariables - > - allRolesRes = await _service.connector - .listShiftRolesByBusinessAndOrder( - businessId: initialShift.order.businessId, - orderId: initialShift.orderId, - ) - .execute(); - - for (final role in allRolesRes.data.shiftRoles) { - if (role.roleId == targetRoleId) { - targets.add( - _TargetShiftRole( - shiftId: role.shiftId, - roleId: role.roleId, - count: role.count, - assigned: role.assigned ?? 0, - shiftFilled: role.shift.filled ?? 0, - date: _service.toDateTime(role.shift.date), - ), - ); - } - } - } else { - // Single shift application - final QueryResult - roleResult = await _service.connector - .getShiftRoleById(shiftId: shiftId, roleId: targetRoleId) - .execute(); - final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole; - if (role == null) throw Exception('Shift role not found'); - - targets.add( - _TargetShiftRole( - shiftId: shiftId, - roleId: targetRoleId, - count: role.count, - assigned: role.assigned ?? 0, - shiftFilled: initialShift.filled ?? 0, - date: _service.toDateTime(initialShift.date), - ), - ); - } - - if (targets.isEmpty) { - throw Exception('No valid shifts found to apply for.'); - } - - int appliedCount = 0; - final List errors = []; - - for (final target in targets) { - try { - await _applyToSingleShiftRole(target: target, staffId: staffId); - appliedCount++; - } catch (e) { - // For multi-shift apply, we might want to continue even if some fail due to conflicts - if (targets.length == 1) rethrow; - errors.add('Shift on ${target.date}: ${e.toString()}'); - } - } - - if (appliedCount == 0 && targets.length > 1) { - throw Exception('Failed to apply for any shifts: ${errors.join(", ")}'); - } - }); - } - - Future _applyToSingleShiftRole({ - required _TargetShiftRole target, - required String staffId, - }) async { - // Validate daily limit - if (target.date != null) { - final DateTime dayStartUtc = DateTime.utc( - target.date!.year, - target.date!.month, - target.date!.day, - ); - final DateTime dayEndUtc = dayStartUtc - .add(const Duration(days: 1)) - .subtract(const Duration(microseconds: 1)); - - final QueryResult< - dc.VaidateDayStaffApplicationData, - dc.VaidateDayStaffApplicationVariables - > - validationResponse = await _service.connector - .vaidateDayStaffApplication(staffId: staffId) - .dayStart(_service.toTimestamp(dayStartUtc)) - .dayEnd(_service.toTimestamp(dayEndUtc)) - .execute(); - - // if (validationResponse.data.applications.isNotEmpty) { - // throw Exception('The user already has a shift that day.'); - // } - } - - // Check for existing application - final QueryResult< - dc.GetApplicationByStaffShiftAndRoleData, - dc.GetApplicationByStaffShiftAndRoleVariables - > - existingAppRes = await _service.connector - .getApplicationByStaffShiftAndRole( - staffId: staffId, - shiftId: target.shiftId, - roleId: target.roleId, - ) - .execute(); - - if (existingAppRes.data.applications.isNotEmpty) { - throw Exception('Application already exists.'); - } - - if (target.assigned >= target.count) { - throw Exception('This shift is full.'); - } - - String? createdAppId; - try { - final OperationResult< - dc.CreateApplicationData, - dc.CreateApplicationVariables - > - createRes = await _service.connector - .createApplication( - shiftId: target.shiftId, - staffId: staffId, - roleId: target.roleId, - status: dc.ApplicationStatus.CONFIRMED, - origin: dc.ApplicationOrigin.STAFF, - ) - .execute(); - - createdAppId = createRes.data.application_insert.id; - - await _service.connector - .updateShiftRole(shiftId: target.shiftId, roleId: target.roleId) - .assigned(target.assigned + 1) - .execute(); - - await _service.connector - .updateShift(id: target.shiftId) - .filled(target.shiftFilled + 1) - .execute(); - } catch (e) { - // Simple rollback attempt (not guaranteed) - if (createdAppId != null) { - await _service.connector.deleteApplication(id: createdAppId).execute(); - } - rethrow; - } - } - - @override - Future acceptShift({required String shiftId, required String staffId}) { - return _updateApplicationStatus( - shiftId, - staffId, - dc.ApplicationStatus.CONFIRMED, - ); - } - - @override - Future declineShift({ - required String shiftId, - required String staffId, - }) { - return _updateApplicationStatus( - shiftId, - staffId, - dc.ApplicationStatus.REJECTED, - ); - } - - @override - Future> getCancelledShifts({required String staffId}) async { - return _service.run(() async { - // Logic would go here to fetch by REJECTED status if needed - return []; - }); - } - - @override - Future> getHistoryShifts({required String staffId}) async { - return _service.run(() async { - final QueryResult< - dc.ListCompletedApplicationsByStaffIdData, - dc.ListCompletedApplicationsByStaffIdVariables - > - response = await _service.connector - .listCompletedApplicationsByStaffId(staffId: staffId) - .execute(); - - final List shifts = []; - for (final dc.ListCompletedApplicationsByStaffIdApplications app - in response.data.applications) { - 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 = _service.toDateTime(app.shift.date); - final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _service.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: 'completed', // Hardcoded as checked out implies completion - description: app.shift.description, - durationDays: app.shift.durationDays, - requiredSlots: app.shiftRole.count, - filledSlots: app.shiftRole.assigned ?? 0, - hasApplied: true, - latitude: app.shift.latitude, - longitude: app.shift.longitude, - breakInfo: BreakAdapter.fromData( - isPaid: app.shiftRole.isBreakPaid ?? false, - breakTime: app.shiftRole.breakType?.stringValue, - ), - ), - ); - } - return shifts; - }); - } - - // --- PRIVATE HELPERS --- - - List _mapApplicationsToShifts(List apps) { - return apps.map((app) { - 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 = _service.toDateTime(app.shift.date); - final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _service.toDateTime(app.createdAt); - - final bool hasCheckIn = app.checkInTime != null; - final bool hasCheckOut = app.checkOutTime != null; - - String status; - if (hasCheckOut) { - status = 'completed'; - } else if (hasCheckIn) { - status = 'checked_in'; - } else { - status = _mapApplicationStatus(app.status.stringValue); - } - - return 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: status, - description: app.shift.description, - durationDays: app.shift.durationDays, - requiredSlots: app.shiftRole.count, - filledSlots: app.shiftRole.assigned ?? 0, - hasApplied: true, - latitude: app.shift.latitude, - longitude: app.shift.longitude, - breakInfo: BreakAdapter.fromData( - isPaid: app.shiftRole.isBreakPaid ?? false, - breakTime: app.shiftRole.breakType?.stringValue, - ), - ); - }).toList(); - } - - String _mapApplicationStatus(String status) { - switch (status) { - case 'CONFIRMED': - return 'confirmed'; - case 'PENDING': - return 'pending'; - case 'CHECKED_OUT': - return 'completed'; - case 'REJECTED': - return 'cancelled'; - default: - return 'open'; - } - } - - Future _updateApplicationStatus( - String shiftId, - String staffId, - dc.ApplicationStatus newStatus, - ) async { - return _service.run(() async { - // First try to find the application - final QueryResult< - dc.GetApplicationsByStaffIdData, - dc.GetApplicationsByStaffIdVariables - > - appsResponse = await _service.connector - .getApplicationsByStaffId(staffId: staffId) - .execute(); - - final dc.GetApplicationsByStaffIdApplications? app = appsResponse - .data - .applications - .where( - (dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId, - ) - .firstOrNull; - - if (app != null) { - await _service.connector - .updateApplicationStatus(id: app.id) - .status(newStatus) - .execute(); - } else if (newStatus == dc.ApplicationStatus.REJECTED) { - // If declining but no app found, create a rejected application - final QueryResult< - dc.ListShiftRolesByShiftIdData, - dc.ListShiftRolesByShiftIdVariables - > - rolesRes = await _service.connector - .listShiftRolesByShiftId(shiftId: shiftId) - .execute(); - - if (rolesRes.data.shiftRoles.isNotEmpty) { - final dc.ListShiftRolesByShiftIdShiftRoles firstRole = - rolesRes.data.shiftRoles.first; - await _service.connector - .createApplication( - shiftId: shiftId, - staffId: staffId, - roleId: firstRole.id, - status: dc.ApplicationStatus.REJECTED, - origin: dc.ApplicationOrigin.STAFF, - ) - .execute(); - } - } else { - throw Exception("Application not found for shift $shiftId"); - } - }); - } - - /// Generates a list of [ShiftSchedule] for RECURRING or PERMANENT orders. - List? _generateSchedules({ - required String orderType, - required DateTime? startDate, - required DateTime? endDate, - required List? recurringDays, - required List? permanentDays, - required String startTime, - required String endTime, - }) { - if (orderType != 'RECURRING' && orderType != 'PERMANENT') return null; - if (startDate == null || endDate == null) return null; - - final List? daysToInclude = orderType == 'RECURRING' - ? recurringDays - : permanentDays; - if (daysToInclude == null || daysToInclude.isEmpty) return null; - - final List schedules = []; - final Set targetWeekdayIndex = daysToInclude - .map((String day) { - switch (day.toUpperCase()) { - case 'MONDAY': - return DateTime.monday; - case 'TUESDAY': - return DateTime.tuesday; - case 'WEDNESDAY': - return DateTime.wednesday; - case 'THURSDAY': - return DateTime.thursday; - case 'FRIDAY': - return DateTime.friday; - case 'SATURDAY': - return DateTime.saturday; - case 'SUNDAY': - return DateTime.sunday; - default: - return -1; - } - }) - .where((int idx) => idx != -1) - .toSet(); - - DateTime current = startDate; - while (current.isBefore(endDate) || - current.isAtSameMomentAs(endDate) || - // Handle cases where the time component might differ slightly by checking date equality - (current.year == endDate.year && - current.month == endDate.month && - current.day == endDate.day)) { - if (targetWeekdayIndex.contains(current.weekday)) { - schedules.add( - ShiftSchedule( - date: current.toIso8601String(), - startTime: startTime, - endTime: endTime, - ), - ); - } - current = current.add(const Duration(days: 1)); - - // Safety break to prevent infinite loops if dates are messed up - if (schedules.length > 365) break; - } - - return schedules; - } -} - -class _TargetShiftRole { - final String shiftId; - final String roleId; - final int count; - final int assigned; - final int shiftFilled; - final DateTime? date; - - _TargetShiftRole({ - required this.shiftId, - required this.roleId, - required this.count, - required this.assigned, - required this.shiftFilled, - this.date, - }); -} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart b/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart index 263b5550..3ab05605 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart @@ -17,4 +17,4 @@ class Accessibility extends Equatable { @override List get props => [id, name]; -} \ No newline at end of file +} 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 a09a78fc..e8d064f3 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 @@ -9,7 +9,6 @@ import 'package:krow_domain/krow_domain.dart' AppException, BaseApiService, ClientSession, - NetworkException, PasswordMismatchException, SignInFailedException, SignUpFailedException, @@ -111,19 +110,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { } on AppException { rethrow; } catch (e) { - // Map common Firebase-originated errors from the V2 API response - // to domain exceptions. + if (e is AppException) rethrow; + + // Extract error code if available from the API response final String errorMessage = e.toString(); - if (errorMessage.contains('EMAIL_EXISTS') || - errorMessage.contains('email-already-in-use')) { - throw AccountExistsException(technicalMessage: errorMessage); - } else if (errorMessage.contains('WEAK_PASSWORD') || - errorMessage.contains('weak-password')) { - throw WeakPasswordException(technicalMessage: errorMessage); - } else if (errorMessage.contains('network-request-failed')) { - throw NetworkException(technicalMessage: errorMessage); - } - throw SignUpFailedException(technicalMessage: 'Unexpected error: $e'); + _throwSignUpError('SIGN_UP_ERROR', errorMessage); } } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_intro_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_intro_page.dart index 418533fd..1dccec4b 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_intro_page.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_intro_page.dart @@ -1,9 +1,26 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; -class ClientIntroPage extends StatelessWidget { +class ClientIntroPage extends StatefulWidget { const ClientIntroPage({super.key}); + @override + State createState() => _ClientIntroPageState(); +} + +class _ClientIntroPageState extends State { + @override + void initState() { + super.initState(); + Future.delayed(const Duration(seconds: 2), () { + if (mounted && Modular.to.path == ClientPaths.root) { + Modular.to.toClientGetStartedPage(); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_error_state.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_error_state.dart index 91999e01..f684de8c 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_error_state.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_error_state.dart @@ -11,14 +11,14 @@ import 'package:client_home/src/presentation/blocs/client_home_state.dart'; /// /// Shows an error message with a retry button when data fails to load. class ClientHomeErrorState extends StatelessWidget { - /// The current home state containing error information. - final ClientHomeState state; /// Creates a [ClientHomeErrorState]. const ClientHomeErrorState({ required this.state, super.key, }); + /// The current home state containing error information. + final ClientHomeState state; @override Widget build(BuildContext context) { diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_normal_mode_body.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_normal_mode_body.dart index fcec6d84..09901387 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_normal_mode_body.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/client_home_normal_mode_body.dart @@ -8,14 +8,14 @@ import 'package:client_home/src/presentation/widgets/dashboard_widget_builder.da /// /// Shows visible dashboard widgets in a vertical scrollable list with dividers. class ClientHomeNormalModeBody extends StatelessWidget { - /// The current home state. - final ClientHomeState state; /// Creates a [ClientHomeNormalModeBody]. const ClientHomeNormalModeBody({ required this.state, super.key, }); + /// The current home state. + final ClientHomeState state; @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 deleted file mode 100644 index b5d2014e..00000000 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart +++ /dev/null @@ -1,1450 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; - -class _RoleOption { - const _RoleOption({ - required this.id, - required this.name, - required this.costPerHour, - }); - - final String id; - final String name; - final double costPerHour; -} - -class _VendorOption { - const _VendorOption({required this.id, required this.name}); - - final String id; - final String name; -} - -/// A bottom sheet form for creating or reordering shifts. -/// -/// 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 { - - /// Creates a [ShiftOrderFormSheet]. - const ShiftOrderFormSheet({ - super.key, - this.initialData, - 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(); -} - -class _ShiftOrderFormSheetState extends State { - late TextEditingController _dateController; - late TextEditingController _globalLocationController; - late TextEditingController _orderNameController; - - late List> _positions; - - final dc.ExampleConnector _dataConnect = dc.ExampleConnector.instance; - List<_VendorOption> _vendors = const <_VendorOption>[]; - List<_RoleOption> _roles = const <_RoleOption>[]; - String? _selectedVendorId; - List _hubs = const []; - dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub; - bool _showSuccess = false; - Map? _submitData; - bool _isSubmitting = false; - String? _errorMessage; - - @override - void initState() { - super.initState(); - - // Initialize date controller (always today for reorder sheet) - final DateTime today = DateTime.now(); - final String initialDate = today.toIso8601String().split('T')[0]; - _dateController = TextEditingController(text: initialDate); - - // Initialize location controller - _globalLocationController = TextEditingController( - text: widget.initialData?['location'] ?? - widget.initialData?['locationAddress'] ?? - '', - ); - _orderNameController = TextEditingController( - text: widget.initialData?['eventName']?.toString() ?? '', - ); - - // Initialize positions - _positions = >[ - { - 'roleId': widget.initialData?['roleId'] ?? '', - 'roleName': widget.initialData?['title'] ?? widget.initialData?['role'] ?? '', - 'count': widget.initialData?['workersNeeded'] ?? - widget.initialData?['workers_needed'] ?? - 1, - 'start_time': widget.initialData?['startTime'] ?? - widget.initialData?['start_time'] ?? - '09:00', - 'end_time': widget.initialData?['endTime'] ?? - widget.initialData?['end_time'] ?? - '17:00', - 'lunch_break': 'NO_BREAK', - 'location': null, - }, - ]; - - _loadVendors(); - _loadHubs(); - _loadOrderDetails(); - } - - @override - void dispose() { - _dateController.dispose(); - _globalLocationController.dispose(); - _orderNameController.dispose(); - super.dispose(); - } - - void _addPosition() { - setState(() { - _positions.add({ - 'roleId': '', - 'roleName': '', - 'count': 1, - 'start_time': '09:00', - 'end_time': '17:00', - 'lunch_break': 'NO_BREAK', - 'location': null, - }); - }); - } - - void _removePosition(int index) { - if (_positions.length > 1) { - setState(() => _positions.removeAt(index)); - } - } - - void _updatePosition(int index, String key, dynamic value) { - setState(() => _positions[index][key] = value); - } - - double _calculateTotalCost() { - double total = 0; - for (final Map pos in _positions) { - double hours = 8.0; - try { - final List startParts = pos['start_time'].toString().split(':'); - final List endParts = pos['end_time'].toString().split(':'); - final double startH = - int.parse(startParts[0]) + int.parse(startParts[1]) / 60; - final double endH = - int.parse(endParts[0]) + int.parse(endParts[1]) / 60; - hours = endH - startH; - if (hours < 0) hours += 24; - } catch (_) {} - final String roleId = pos['roleId']?.toString() ?? ''; - final double rate = _rateForRole(roleId); - total += hours * rate * (pos['count'] as int); - } - return total; - } - - String _getShiftType() { - final String? type = widget.initialData?['type']?.toString(); - if (type != null && type.isNotEmpty) { - switch (type) { - case 'PERMANENT': - return 'Long Term'; - case 'RECURRING': - return 'Multi-Day'; - case 'RAPID': - return 'Rapid'; - case 'ONE_TIME': - return 'One-Time Order'; - } - } - // Determine shift type based on initial data - final dynamic initialData = widget.initialData; - if (initialData != null) { - if (initialData['permanent'] == true || initialData['duration_months'] != null) { - return 'Long Term'; - } - if (initialData['recurring'] == true || initialData['duration_days'] != null) { - return 'Multi-Day'; - } - } - return 'One-Time Order'; - } - - Future _handleSubmit() async { - if (_isSubmitting) return; - - setState(() { - _isSubmitting = true; - _errorMessage = null; - }); - - try { - await _submitNewOrder(); - } catch (e) { - if (!mounted) return; - setState(() { - _isSubmitting = false; - _errorMessage = 'Failed to create order. Please try again.'; - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(_errorMessage!)), - ); - } - } - - Future _submitNewOrder() async { - final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return; - } - final dc.ListTeamHubsByOwnerIdTeamHubs? selectedHub = _selectedHub; - if (selectedHub == null) { - return; - } - - final DateTime date = DateTime.parse(_dateController.text); - final DateTime dateOnly = DateTime.utc(date.year, date.month, date.day); - final fdc.Timestamp orderTimestamp = _toTimestamp(dateOnly); - final dc.OrderType orderType = - _orderTypeFromValue(widget.initialData?['type']?.toString()); - - final fdc.OperationResult - orderResult = await _dataConnect - .createOrder( - businessId: businessId, - orderType: orderType, - teamHubId: selectedHub.id, - ) - .vendorId(_selectedVendorId) - .eventName(_orderNameController.text) - .status(dc.OrderStatus.POSTED) - .date(orderTimestamp) - .execute(); - - final String orderId = orderResult.data.order_insert.id; - - final int workersNeeded = _positions.fold( - 0, - (int sum, Map pos) => sum + (pos['count'] as int), - ); - final String shiftTitle = - 'Shift 1 ${DateFormat('yyyy-MM-dd').format(date)}'; - final double shiftCost = _calculateTotalCost(); - - final fdc.OperationResult - shiftResult = await _dataConnect - .createShift(title: shiftTitle, orderId: orderId) - .description(_orderNameController.text) - .date(orderTimestamp) - .location(selectedHub.hubName) - .locationAddress(selectedHub.address) - .latitude(selectedHub.latitude) - .longitude(selectedHub.longitude) - .placeId(selectedHub.placeId) - .city(selectedHub.city) - .state(selectedHub.state) - .street(selectedHub.street) - .country(selectedHub.country) - .status(dc.ShiftStatus.OPEN) - .workersNeeded(workersNeeded) - .filled(0) - .durationDays(1) - .cost(shiftCost) - .execute(); - - final String shiftId = shiftResult.data.shift_insert.id; - - for (final Map pos in _positions) { - final String roleId = pos['roleId']?.toString() ?? ''; - if (roleId.isEmpty) { - continue; - } - final DateTime start = _parseTime(date, pos['start_time'].toString()); - final DateTime end = _parseTime(date, pos['end_time'].toString()); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; - final double hours = normalizedEnd.difference(start).inMinutes / 60.0; - final int count = pos['count'] as int; - final double rate = _rateForRole(roleId); - final double totalValue = rate * hours * count; - final String lunchBreak = pos['lunch_break'] as String; - - await _dataConnect - .createShiftRole( - shiftId: shiftId, - roleId: roleId, - count: count, - ) - .startTime(_toTimestamp(start)) - .endTime(_toTimestamp(normalizedEnd)) - .hours(hours) - .breakType(_breakDurationFromValue(lunchBreak)) - .isBreakPaid(_isBreakPaid(lunchBreak)) - .totalValue(totalValue) - .execute(); - } - - await _dataConnect - .updateOrder(id: orderId, teamHubId: selectedHub.id) - .shifts(fdc.AnyValue([shiftId])) - .execute(); - - if (!mounted) return; - setState(() { - _submitData = { - 'orderId': orderId, - 'date': _dateController.text, - }; - _showSuccess = true; - _isSubmitting = false; - }); - } - - Future _loadVendors() async { - try { - final fdc.QueryResult result = - await _dataConnect.listVendors().execute(); - final List<_VendorOption> vendors = result.data.vendors - .map( - (dc.ListVendorsVendors vendor) => - _VendorOption(id: vendor.id, name: vendor.companyName), - ) - .toList(); - if (!mounted) return; - setState(() { - _vendors = vendors; - final String? current = _selectedVendorId; - if (current == null || - !vendors.any((_VendorOption v) => v.id == current)) { - _selectedVendorId = vendors.isNotEmpty ? vendors.first.id : null; - } - }); - if (_selectedVendorId != null) { - await _loadRolesForVendor(_selectedVendorId!); - } - } catch (_) { - if (!mounted) return; - setState(() { - _vendors = const <_VendorOption>[]; - _roles = const <_RoleOption>[]; - }); - } - } - - Future _loadHubs() async { - final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return; - } - - try { - final fdc.QueryResult< - dc.ListTeamHubsByOwnerIdData, - dc.ListTeamHubsByOwnerIdVariables> result = - await _dataConnect.listTeamHubsByOwnerId(ownerId: businessId).execute(); - final List hubs = result.data.teamHubs; - if (!mounted) return; - setState(() { - _hubs = hubs; - _selectedHub = hubs.isNotEmpty ? hubs.first : null; - if (_selectedHub != null) { - _globalLocationController.text = _selectedHub!.address; - } - }); - } catch (_) { - if (!mounted) return; - setState(() { - _hubs = const []; - _selectedHub = null; - }); - } - } - - Future _loadRolesForVendor(String vendorId) async { - try { - final fdc.QueryResult - result = - await _dataConnect.listRolesByVendorId(vendorId: vendorId).execute(); - final List<_RoleOption> roles = result.data.roles - .map( - (dc.ListRolesByVendorIdRoles role) => _RoleOption( - id: role.id, - name: role.name, - costPerHour: role.costPerHour, - ), - ) - .toList(); - if (!mounted) return; - setState(() => _roles = roles); - } catch (_) { - if (!mounted) return; - setState(() => _roles = const <_RoleOption>[]); - } - } - - Future _loadOrderDetails() async { - final String? orderId = widget.initialData?['orderId']?.toString(); - if (orderId == null || orderId.isEmpty) { - return; - } - - final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return; - } - - try { - final fdc.QueryResult< - dc.ListShiftRolesByBusinessAndOrderData, - dc.ListShiftRolesByBusinessAndOrderVariables> result = await _dataConnect - .listShiftRolesByBusinessAndOrder( - businessId: businessId, - orderId: orderId, - ) - .execute(); - - final List shiftRoles = - result.data.shiftRoles; - if (shiftRoles.isEmpty) { - return; - } - - final dc.ListShiftRolesByBusinessAndOrderShiftRolesShift firstShift = - shiftRoles.first.shift; - final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub - teamHub = firstShift.order.teamHub; - await _loadHubsAndSelect( - placeId: teamHub.placeId, - hubName: teamHub.hubName, - address: teamHub.address, - ); - _orderNameController.text = firstShift.order.eventName ?? ''; - - final String? vendorId = firstShift.order.vendorId; - if (mounted) { - setState(() { - _selectedVendorId = vendorId; - }); - } - if (vendorId != null && vendorId.isNotEmpty) { - await _loadRolesForVendor(vendorId); - } - - final List> positions = - shiftRoles.map((dc.ListShiftRolesByBusinessAndOrderShiftRoles role) { - return { - 'roleId': role.roleId, - 'roleName': role.role.name, - 'count': role.count, - 'start_time': _formatTimeForField(role.startTime), - 'end_time': _formatTimeForField(role.endTime), - 'lunch_break': _breakValueFromDuration(role.breakType), - 'location': null, - }; - }).toList(); - - if (!mounted) return; - setState(() { - _positions = positions; - }); - } catch (_) { - // Keep defaults on failure. - } - } - - Future _loadHubsAndSelect({ - String? placeId, - String? hubName, - String? address, - }) async { - final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return; - } - - try { - final fdc.QueryResult< - dc.ListTeamHubsByOwnerIdData, - dc.ListTeamHubsByOwnerIdVariables> result = - await _dataConnect.listTeamHubsByOwnerId(ownerId: businessId).execute(); - final List hubs = result.data.teamHubs; - dc.ListTeamHubsByOwnerIdTeamHubs? selected; - - if (placeId != null && placeId.isNotEmpty) { - for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { - if (hub.placeId == placeId) { - selected = hub; - break; - } - } - } - - if (selected == null && hubName != null && hubName.isNotEmpty) { - for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { - if (hub.hubName == hubName) { - selected = hub; - break; - } - } - } - - if (selected == null && address != null && address.isNotEmpty) { - for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { - if (hub.address == address) { - selected = hub; - break; - } - } - } - - selected ??= hubs.isNotEmpty ? hubs.first : null; - - if (!mounted) return; - setState(() { - _hubs = hubs; - _selectedHub = selected; - if (selected != null) { - _globalLocationController.text = selected.address; - } - }); - } catch (_) { - if (!mounted) return; - setState(() { - _hubs = const []; - _selectedHub = null; - }); - } - } - - String _formatTimeForField(fdc.Timestamp? value) { - if (value == null) return ''; - try { - return DateFormat('HH:mm').format(value.toDateTime()); - } catch (_) { - return ''; - } - } - - String _breakValueFromDuration(dc.EnumValue? breakType) { - final dc.BreakDuration? value = - breakType is dc.Known ? breakType.value : null; - switch (value) { - case dc.BreakDuration.MIN_10: - return 'MIN_10'; - case dc.BreakDuration.MIN_15: - return 'MIN_15'; - case dc.BreakDuration.MIN_30: - return 'MIN_30'; - case dc.BreakDuration.MIN_45: - return 'MIN_45'; - case dc.BreakDuration.MIN_60: - return 'MIN_60'; - case dc.BreakDuration.NO_BREAK: - case null: - return 'NO_BREAK'; - } - } - - dc.BreakDuration _breakDurationFromValue(String value) { - switch (value) { - case 'MIN_10': - return dc.BreakDuration.MIN_10; - case 'MIN_15': - return dc.BreakDuration.MIN_15; - case 'MIN_30': - return dc.BreakDuration.MIN_30; - case 'MIN_45': - return dc.BreakDuration.MIN_45; - case 'MIN_60': - return dc.BreakDuration.MIN_60; - default: - return dc.BreakDuration.NO_BREAK; - } - } - - bool _isBreakPaid(String value) { - return value == 'MIN_10' || value == 'MIN_15'; - } - - dc.OrderType _orderTypeFromValue(String? value) { - switch (value) { - case 'PERMANENT': - return dc.OrderType.PERMANENT; - case 'RECURRING': - return dc.OrderType.RECURRING; - case 'RAPID': - return dc.OrderType.RAPID; - case 'ONE_TIME': - default: - return dc.OrderType.ONE_TIME; - } - } - - _RoleOption? _roleById(String roleId) { - for (final _RoleOption role in _roles) { - if (role.id == roleId) { - return role; - } - } - return null; - } - - double _rateForRole(String roleId) { - return _roleById(roleId)?.costPerHour ?? 0; - } - - DateTime _parseTime(DateTime date, String time) { - DateTime parsed; - try { - parsed = DateFormat.Hm().parse(time); - } catch (_) { - parsed = DateFormat.jm().parse(time); - } - return DateTime( - date.year, - date.month, - date.day, - parsed.hour, - parsed.minute, - ); - } - - fdc.Timestamp _toTimestamp(DateTime date) { - final DateTime utc = date.toUtc(); - final int millis = utc.millisecondsSinceEpoch; - final int seconds = millis ~/ 1000; - final int nanos = (millis % 1000) * 1000000; - return fdc.Timestamp(nanos, seconds); - } - - @override - Widget build(BuildContext context) { - if (_showSuccess) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - return _buildSuccessView( - title: labels.success_title, - message: labels.success_message, - buttonLabel: labels.back_to_orders, - ); - } - - return Container( - height: MediaQuery.of(context).size.height * 0.95, - decoration: const BoxDecoration( - color: UiColors.bgPrimary, - borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), - ), - child: Column( - children: [ - _buildHeader(), - Expanded( - child: ListView( - padding: const EdgeInsets.all(UiConstants.space5), - children: [ - Text( - widget.initialData != null ? 'Edit Your Order' : 'Create New Order', - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - Text( - widget.initialData != null - ? 'Review and adjust the details below' - : 'Fill in the details for your staffing needs', - style: UiTypography.body2r.textSecondary, - ), - const SizedBox(height: UiConstants.space5), - - // Shift Type Badge - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - vertical: UiConstants.space2, - ), - decoration: BoxDecoration( - color: UiColors.primary.withValues(alpha: 0.1), - borderRadius: UiConstants.radiusFull, - border: Border.all( - color: UiColors.primary.withValues(alpha: 0.3), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: UiColors.primary, - ), - ), - const SizedBox(width: UiConstants.space2), - Text( - _getShiftType(), - style: UiTypography.footnote1b.copyWith( - color: UiColors.primary, - ), - ), - ], - ), - ), - const SizedBox(height: UiConstants.space5), - - _buildSectionHeader('VENDOR'), - _buildVendorDropdown(), - const SizedBox(height: UiConstants.space4), - - _buildSectionHeader('ORDER NAME'), - _buildOrderNameField(), - const SizedBox(height: UiConstants.space4), - - _buildSectionHeader('DATE'), - _buildDateField(), - const SizedBox(height: UiConstants.space4), - - _buildSectionHeader('HUB'), - _buildHubField(), - const SizedBox(height: UiConstants.space5), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'POSITIONS', - style: UiTypography.footnote2r.textSecondary, - ), - GestureDetector( - onTap: _addPosition, - child: Row( - children: [ - const Icon( - UiIcons.add, - size: 16, - color: UiColors.primary, - ), - const SizedBox(width: UiConstants.space1), - Text( - 'Add Position', - style: UiTypography.footnote1m.copyWith( - color: UiColors.primary, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - ..._positions.asMap().entries.map((MapEntry> entry) { - return _buildPositionCard(entry.key, entry.value); - }), - - const SizedBox(height: UiConstants.space5), - - // Total Cost Display - Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Estimated Total', - style: UiTypography.body1b.textPrimary, - ), - Text( - '\$${_calculateTotalCost().toStringAsFixed(2)}', - style: UiTypography.headline3m.copyWith( - color: UiColors.primary, - ), - ), - ], - ), - ), - const SizedBox(height: UiConstants.space5), - - UiButton.primary( - text: widget.initialData != null ? 'Update Order' : 'Post Order', - onPressed: (widget.isLoading || _isSubmitting) ? null : _handleSubmit, - ), - SizedBox(height: MediaQuery.of(context).padding.bottom + UiConstants.space5), - ], - ), - ), - ], - ), - ); - } - - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.all(UiConstants.space5), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - UiColors.primary, - UiColors.primary.withValues(alpha: 0.8), - ], - ), - borderRadius: const BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), - ), - child: Row( - children: [ - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: UiColors.white.withValues(alpha: 0.2), - borderRadius: UiConstants.radiusMd, - ), - child: const Icon( - UiIcons.chevronLeft, - color: UiColors.white, - size: 24, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _getShiftType(), - style: UiTypography.headline3m.copyWith(color: UiColors.white), - ), - Text( - 'Configure your staffing needs', - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.8), - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildSectionHeader(String title) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text(title, style: UiTypography.footnote2r.textSecondary), - ); - } - - Widget _buildVendorDropdown() { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - ), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: _selectedVendorId, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - style: UiTypography.body2r.textPrimary, - items: _vendors.map((_VendorOption vendor) { - return DropdownMenuItem( - value: vendor.id, - child: Text(vendor.name), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - _selectedVendorId = newValue; - }); - _loadRolesForVendor(newValue); - } - }, - ), - ), - ); - } - - Widget _buildDateField() { - return GestureDetector( - onTap: () async { - final DateTime? selectedDate = await showDatePicker( - context: context, - initialDate: _dateController.text.isNotEmpty - ? DateTime.parse(_dateController.text) - : DateTime.now().add(const Duration(days: 1)), - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365 * 2)), - ); - if (selectedDate != null) { - setState(() { - _dateController.text = - selectedDate.toIso8601String().split('T')[0]; - }); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - vertical: UiConstants.space3, - ), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: Row( - children: [ - const Icon(UiIcons.calendar, size: 20, color: UiColors.iconSecondary), - const SizedBox(width: UiConstants.space2), - Expanded( - child: Text( - _dateController.text.isNotEmpty - ? DateFormat('EEEE, MMM d, y') - .format(DateTime.parse(_dateController.text)) - : 'Select date', - style: _dateController.text.isNotEmpty - ? UiTypography.body2r.textPrimary - : UiTypography.body2r.textSecondary, - ), - ), - const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - ], - ), - ), - ); - } - - Widget _buildHubField() { - return Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: _selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (dc.ListTeamHubsByOwnerIdTeamHubs? hub) { - if (hub != null) { - setState(() { - _selectedHub = hub; - _globalLocationController.text = hub.address; - }); - } - }, - items: _hubs.map((dc.ListTeamHubsByOwnerIdTeamHubs hub) { - return DropdownMenuItem( - value: hub, - child: Text( - hub.hubName, - style: UiTypography.body2r.textPrimary, - ), - ); - }).toList(), - ), - ), - ); - } - - Widget _buildOrderNameField() { - return Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: TextField( - controller: _orderNameController, - decoration: const InputDecoration( - hintText: 'Order name', - border: InputBorder.none, - ), - style: UiTypography.body2r.textPrimary, - ), - ); - } - - Widget _buildPositionCard(int index, Map pos) { - return Container( - margin: const EdgeInsets.only(bottom: UiConstants.space3), - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'POSITION #${index + 1}', - style: UiTypography.footnote1m.textSecondary, - ), - if (_positions.length > 1) - GestureDetector( - onTap: () => _removePosition(index), - child: Text( - 'Remove', - style: UiTypography.footnote1m.copyWith( - color: UiColors.destructive, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - _buildDropdownField( - hint: 'Select role', - value: pos['roleId'], - items: [ - ..._roles.map((_RoleOption role) => role.id), - if (pos['roleId'] != null && - pos['roleId'].toString().isNotEmpty && - !_roles.any( - (_RoleOption role) => role.id == pos['roleId'].toString(), - )) - pos['roleId'].toString(), - ], - itemBuilder: (dynamic roleId) { - final _RoleOption? role = _roleById(roleId.toString()); - if (role == null) { - final String fallback = pos['roleName']?.toString() ?? ''; - return fallback.isEmpty ? roleId.toString() : fallback; - } - return '${role.name} - \$${role.costPerHour.toStringAsFixed(0)}/hr'; - }, - onChanged: (dynamic val) { - final String roleId = val?.toString() ?? ''; - final _RoleOption? role = _roleById(roleId); - setState(() { - _positions[index]['roleId'] = roleId; - _positions[index]['roleName'] = role?.name ?? ''; - }); - }, - ), - - const SizedBox(height: UiConstants.space3), - - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Lunch Break', - style: UiTypography.footnote2r.textSecondary, - ), - const SizedBox(height: UiConstants.space1), - _buildDropdownField( - hint: 'No Break', - value: pos['lunch_break'], - items: [ - 'NO_BREAK', - 'MIN_10', - 'MIN_15', - 'MIN_30', - 'MIN_45', - 'MIN_60', - ], - itemBuilder: (dynamic value) { - switch (value.toString()) { - case 'MIN_10': - return '10 min (Paid)'; - case 'MIN_15': - return '15 min (Paid)'; - case 'MIN_30': - return '30 min (Unpaid)'; - case 'MIN_45': - return '45 min (Unpaid)'; - case 'MIN_60': - return '60 min (Unpaid)'; - default: - return 'No Break'; - } - }, - onChanged: (dynamic val) => _updatePosition(index, 'lunch_break', val), - ), - ], - ), - ), - ], - ), - - const SizedBox(height: UiConstants.space3), - - Row( - children: [ - Expanded( - child: _buildInlineTimeInput( - label: 'Start', - value: pos['start_time'], - onTap: () async { - final TimeOfDay? time = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (time != null) { - _updatePosition( - index, - 'start_time', - '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}', - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - Expanded( - child: _buildInlineTimeInput( - label: 'End', - value: pos['end_time'], - onTap: () async { - final TimeOfDay? time = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (time != null) { - _updatePosition( - index, - 'end_time', - '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}', - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Workers', - style: UiTypography.footnote2r.textSecondary, - ), - const SizedBox(height: UiConstants.space1), - Container( - height: 40, - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusSm, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - if ((pos['count'] as int) > 1) { - _updatePosition( - index, - 'count', - (pos['count'] as int) - 1, - ); - } - }, - child: const Icon(UiIcons.minus, size: 12), - ), - Text( - '${pos['count']}', - style: UiTypography.body2b.textPrimary, - ), - GestureDetector( - onTap: () => _updatePosition( - index, - 'count', - (pos['count'] as int) + 1, - ), - child: const Icon(UiIcons.add, size: 12), - ), - ], - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), - - if (pos['location'] == null) - GestureDetector( - onTap: () => _updatePosition(index, 'location', ''), - child: Row( - children: [ - const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary), - const SizedBox(width: UiConstants.space1), - Text( - 'Use different location for this position', - style: UiTypography.footnote1m.copyWith( - color: UiColors.primary, - ), - ), - ], - ), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Custom Location', - style: UiTypography.footnote2r.textSecondary, - ), - GestureDetector( - onTap: () => _updatePosition(index, 'location', null), - child: Text( - 'Remove', - style: UiTypography.footnote1m.copyWith( - color: UiColors.destructive, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - vertical: UiConstants.space2, - ), - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusSm, - border: Border.all(color: UiColors.border), - ), - child: TextField( - controller: TextEditingController(text: pos['location']), - decoration: const InputDecoration( - hintText: 'Enter custom location', - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.zero, - ), - style: UiTypography.body2r.textPrimary, - onChanged: (String value) => - _updatePosition(index, 'location', value), - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildDropdownField({ - required String hint, - required dynamic value, - required List items, - required String Function(T) itemBuilder, - required void Function(T?) onChanged, - }) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 48, - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: value.toString().isEmpty ? null : value as T?, - hint: Text(hint, style: UiTypography.body2r.textSecondary), - icon: const Icon(UiIcons.chevronDown, size: 18), - style: UiTypography.body2r.textPrimary, - items: items - .map( - (T item) => DropdownMenuItem( - value: item, - child: Text(itemBuilder(item)), - ), - ) - .toList(), - onChanged: onChanged, - ), - ), - ); - } - - Widget _buildSuccessView({ - required String title, - required String message, - required String buttonLabel, - }) { - return Container( - width: double.infinity, - height: MediaQuery.of(context).size.height * 0.95, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [UiColors.primary, UiColors.buttonPrimaryHover], - ), - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), - child: SafeArea( - child: Center( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 40), - padding: const EdgeInsets.all(UiConstants.space8), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg * 1.5, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 64, - height: 64, - decoration: const BoxDecoration( - color: UiColors.accent, - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - UiIcons.check, - color: UiColors.black, - size: 32, - ), - ), - ), - const SizedBox(height: UiConstants.space6), - Text( - title, - style: UiTypography.headline2m.textPrimary, - textAlign: TextAlign.center, - ), - const SizedBox(height: UiConstants.space3), - Text( - message, - textAlign: TextAlign.center, - style: UiTypography.body2r.textSecondary.copyWith( - height: 1.5, - ), - ), - const SizedBox(height: UiConstants.space8), - SizedBox( - width: double.infinity, - child: UiButton.primary( - text: buttonLabel, - onPressed: () { - widget.onSubmit(_submitData ?? {}); - Navigator.pop(context); - }, - size: UiButtonSize.large, - ), - ), - ], - ), - ), - ), - ), - ); - } - - Widget _buildInlineTimeInput({ - required String label, - required String value, - required VoidCallback onTap, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - GestureDetector( - onTap: onTap, - child: Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space2), - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusSm, - border: Border.all(color: UiColors.border), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(UiIcons.clock, size: 14, color: UiColors.iconSecondary), - const SizedBox(width: UiConstants.space1), - Text( - value.isEmpty ? '--:--' : value, - style: UiTypography.body2r.textPrimary, - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_bloc.dart index 2eb9cc9f..47c6f127 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_bloc.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_bloc.dart @@ -24,7 +24,7 @@ class CoverageBloc extends Bloc Emitter emit, ) async { await handleError( - emit: emit, + emit: emit.call, action: () async { emit(CoverageLoading()); final CoverageReport report = await _getCoverageReportUseCase.call( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_state.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_state.dart index 381ac3e0..ff137eeb 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_state.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/coverage/coverage_state.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; /// Base state for the coverage report BLoC. diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_bloc.dart index 3190999d..d232a7b8 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_bloc.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_bloc.dart @@ -24,7 +24,7 @@ class DailyOpsBloc extends Bloc Emitter emit, ) async { await handleError( - emit: emit, + emit: emit.call, action: () async { emit(DailyOpsLoading()); final DailyOpsReport report = await _getDailyOpsReportUseCase.call( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_state.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_state.dart index 3063b192..8a0b2612 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_state.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/daily_ops/daily_ops_state.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; /// Base state for the daily operations report BLoC. diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_bloc.dart index 83d99323..8dc9e95a 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_bloc.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_bloc.dart @@ -24,7 +24,7 @@ class ForecastBloc extends Bloc Emitter emit, ) async { await handleError( - emit: emit, + emit: emit.call, action: () async { emit(ForecastLoading()); final ForecastReport report = await _getForecastReportUseCase.call( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_state.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_state.dart index 53ac6dbe..f80b6c1c 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_state.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/forecast/forecast_state.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; /// Base state for the forecast report BLoC. diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_bloc.dart index 00092c71..72aace35 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_bloc.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_bloc.dart @@ -24,7 +24,7 @@ class NoShowBloc extends Bloc Emitter emit, ) async { await handleError( - emit: emit, + emit: emit.call, action: () async { emit(NoShowLoading()); final NoShowReport report = await _getNoShowReportUseCase.call( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_state.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_state.dart index c761dea0..916f8ca6 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_state.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/no_show/no_show_state.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; /// Base state for the no-show report BLoC. diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_bloc.dart index cab097a2..6ecf808a 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_bloc.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_bloc.dart @@ -25,7 +25,7 @@ class PerformanceBloc extends Bloc Emitter emit, ) async { await handleError( - emit: emit, + emit: emit.call, action: () async { emit(PerformanceLoading()); final PerformanceReport report = await _getPerformanceReportUseCase.call( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_state.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_state.dart index bc47680b..3779d84d 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_state.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/performance/performance_state.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; /// Base state for the performance report BLoC. diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_bloc.dart index e2efff90..670ed4ad 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_bloc.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_bloc.dart @@ -24,7 +24,7 @@ class SpendBloc extends Bloc Emitter emit, ) async { await handleError( - emit: emit, + emit: emit.call, action: () async { emit(SpendLoading()); final SpendReport report = await _getSpendReportUseCase.call( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_state.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_state.dart index 011f1b66..52b281d4 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_state.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/spend/spend_state.dart @@ -1,4 +1,4 @@ -import 'package:equatable/equatable.dart'; +import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; /// Base state for the spend report BLoC. diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/summary/reports_summary_bloc.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/summary/reports_summary_bloc.dart index 3f46e9b2..9e5188ef 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/summary/reports_summary_bloc.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/blocs/summary/reports_summary_bloc.dart @@ -26,7 +26,7 @@ class ReportsSummaryBloc Emitter emit, ) async { await handleError( - emit: emit, + emit: emit.call, action: () async { emit(ReportsSummaryLoading()); final ReportSummary summary = await _getReportsSummaryUseCase.call( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart index 0f80583c..4dc87f70 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart @@ -71,7 +71,7 @@ class _CoverageReportPageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.2), + color: UiColors.white.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: const Icon( @@ -95,7 +95,7 @@ class _CoverageReportPageState extends State { context.t.client_reports.coverage_report .subtitle, style: UiTypography.body3r.copyWith( - color: UiColors.white.withOpacity(0.7), + color: UiColors.white.withValues(alpha: 0.7), ), ), ], @@ -203,7 +203,7 @@ class _CoverageSummaryCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 10, ), ], @@ -214,7 +214,7 @@ class _CoverageSummaryCard extends StatelessWidget { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(icon, size: 16, color: color), diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart index 7910c1f0..f3c50751 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart @@ -101,7 +101,7 @@ class _DailyOpsReportPageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.2), + color: UiColors.white.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: const Icon( @@ -126,7 +126,7 @@ class _DailyOpsReportPageState extends State { context.t.client_reports.daily_ops_report .subtitle, style: UiTypography.body3r.copyWith( - color: UiColors.white.withOpacity(0.7), + color: UiColors.white.withValues(alpha: 0.7), ), ), ], @@ -155,7 +155,7 @@ class _DailyOpsReportPageState extends State { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.06), + color: UiColors.black.withValues(alpha: 0.06), blurRadius: 4, ), ], @@ -390,7 +390,7 @@ class _OpsStatCard extends StatelessWidget { vertical: 3, ), decoration: BoxDecoration( - color: color.withOpacity(0.12), + color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -451,7 +451,7 @@ class _ShiftListItem extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.02), + color: UiColors.black.withValues(alpha: 0.02), blurRadius: 2, ), ], @@ -497,7 +497,7 @@ class _ShiftListItem extends StatelessWidget { margin: const EdgeInsets.only(left: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), + color: statusColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/forecast_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/forecast_report_page.dart index eef44b28..4aefe21b 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/forecast_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/forecast_report_page.dart @@ -119,7 +119,7 @@ class _ForecastReportPageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.2), + color: UiColors.white.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: const Icon( @@ -141,7 +141,7 @@ class _ForecastReportPageState extends State { Text( context.t.client_reports.forecast_report.subtitle, style: UiTypography.body2m.copyWith( - color: UiColors.white.withOpacity(0.7), + color: UiColors.white.withValues(alpha: 0.7), ), ), ], @@ -213,7 +213,7 @@ class _ForecastReportPageState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 10, ), ], @@ -289,7 +289,7 @@ class _MetricCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 8, ), ], @@ -469,7 +469,7 @@ class _ForecastChart extends StatelessWidget { ), belowBarData: BarAreaData( show: true, - color: UiColors.tagPending.withOpacity(0.5), + color: UiColors.tagPending.withValues(alpha: 0.5), ), ), ], diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart index 2cdd6c52..55cedccd 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart @@ -74,7 +74,7 @@ class _NoShowReportPageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.15), + color: UiColors.white.withValues(alpha: 0.15), shape: BoxShape.circle, ), child: const Icon( @@ -97,7 +97,7 @@ class _NoShowReportPageState extends State { Text( context.t.client_reports.no_show_report.subtitle, style: UiTypography.body3r.copyWith( - color: UiColors.white.withOpacity(0.6), + color: UiColors.white.withValues(alpha: 0.6), ), ), ], @@ -225,7 +225,7 @@ class _SummaryChip extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.06), + color: UiColors.black.withValues(alpha: 0.06), blurRadius: 8, offset: const Offset(0, 2), ), @@ -302,7 +302,7 @@ class _WorkerCard extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 6, ), ], diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart index 1b27bc4c..c2c98bf8 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart @@ -157,7 +157,7 @@ class _PerformanceReportPageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.2), + color: UiColors.white.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: const Icon( @@ -182,7 +182,7 @@ class _PerformanceReportPageState extends State { context.t.client_reports.performance_report .subtitle, style: UiTypography.body3r.copyWith( - color: UiColors.white.withOpacity(0.7), + color: UiColors.white.withValues(alpha: 0.7), ), ), ], @@ -212,7 +212,7 @@ class _PerformanceReportPageState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 10, offset: const Offset(0, 4), ), @@ -270,7 +270,7 @@ class _PerformanceReportPageState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 10, ), ], @@ -387,7 +387,7 @@ class _KpiRow extends StatelessWidget { width: 36, height: 36, decoration: BoxDecoration( - color: kpi.iconColor.withOpacity(0.1), + color: kpi.iconColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(kpi.icon, size: 18, color: kpi.iconColor), diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/reports_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/reports_page.dart index 405666dc..1fb0591c 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/reports_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/reports_page.dart @@ -1,4 +1,4 @@ -import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart'; +import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart'; import 'package:client_reports/src/presentation/blocs/summary/reports_summary_event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart index de02465f..0571df90 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart @@ -81,7 +81,7 @@ class _SpendReportPageState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.2), + color: UiColors.white.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: const Icon( @@ -104,7 +104,7 @@ class _SpendReportPageState extends State { Text( context.t.client_reports.spend_report.subtitle, style: UiTypography.body3r.copyWith( - color: UiColors.white.withOpacity(0.7), + color: UiColors.white.withValues(alpha: 0.7), ), ), ], @@ -164,7 +164,7 @@ class _SpendReportPageState extends State { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 10, offset: const Offset(0, 4), ), @@ -358,7 +358,7 @@ class _SpendStatCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.06), + color: UiColors.black.withValues(alpha: 0.06), blurRadius: 8, offset: const Offset(0, 4), ), @@ -392,7 +392,7 @@ class _SpendStatCard extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: themeColor.withOpacity(0.1), + color: themeColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Text( @@ -424,7 +424,7 @@ class _SpendByCategoryCard extends StatelessWidget { borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.04), + color: UiColors.black.withValues(alpha: 0.04), blurRadius: 10, offset: const Offset(0, 4), ), diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/quick_reports_section.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/quick_reports_section.dart index 200fb4f2..4759efdb 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/quick_reports_section.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/quick_reports_section.dart @@ -1,4 +1,4 @@ -import 'package:client_reports/src/presentation/widgets/reports_page/report_card.dart'; +import 'package:client_reports/src/presentation/widgets/reports_page/report_card.dart'; import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/report_card.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/report_card.dart index 9c2b1cd5..4c42bf97 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/report_card.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/report_card.dart @@ -44,7 +44,7 @@ class ReportCard extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: UiColors.black.withOpacity(0.02), + color: UiColors.black.withValues(alpha: 0.02), blurRadius: 2, ), ], diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/reports_header.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/reports_header.dart index 67c26db2..c1d537f1 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/reports_header.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/reports_header.dart @@ -51,7 +51,7 @@ class ReportsHeader extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.2), + color: UiColors.white.withValues(alpha: 0.2), shape: BoxShape.circle, ), child: const Icon( @@ -84,7 +84,7 @@ class ReportsHeader extends StatelessWidget { height: 44, padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.2), + color: UiColors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: TabBar( diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart index f27cb2db..a92249b4 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart @@ -176,104 +176,4 @@ class _QuickLinkItem extends StatelessWidget { } } -class _NotificationsSettingsCard extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (BuildContext context, ClientSettingsState state) { - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusLg, - side: const BorderSide(color: UiColors.border), - ), - color: UiColors.white, - child: Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.t.client_settings.preferences.title, - style: UiTypography.footnote1b.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - _NotificationToggle( - icon: UiIcons.bell, - title: context.t.client_settings.preferences.push, - value: state.pushEnabled, - onChanged: (bool val) => - ReadContext(context).read().add( - ClientSettingsNotificationToggled( - type: 'push', - isEnabled: val, - ), - ), - ), - _NotificationToggle( - icon: UiIcons.mail, - title: context.t.client_settings.preferences.email, - value: state.emailEnabled, - onChanged: (bool val) => - ReadContext(context).read().add( - ClientSettingsNotificationToggled( - type: 'email', - isEnabled: val, - ), - ), - ), - _NotificationToggle( - icon: UiIcons.phone, - title: context.t.client_settings.preferences.sms, - value: state.smsEnabled, - onChanged: (bool val) => - ReadContext(context).read().add( - ClientSettingsNotificationToggled( - type: 'sms', - isEnabled: val, - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} -class _NotificationToggle extends StatelessWidget { - - const _NotificationToggle({ - required this.icon, - required this.title, - required this.value, - required this.onChanged, - }); - final IconData icon; - final String title; - final bool value; - final ValueChanged onChanged; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon(icon, size: 20, color: UiColors.iconSecondary), - const SizedBox(width: UiConstants.space3), - Text(title, style: UiTypography.footnote1m.textPrimary), - ], - ), - Switch.adaptive( - value: value, - activeColor: UiColors.primary, - onChanged: onChanged, - ), - ], - ); - } -} diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart index f1f27f5b..25f300c6 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart @@ -18,7 +18,7 @@ class SettingsProfileHeader extends StatelessWidget { final String businessName = session?.businessName ?? 'Your Company'; final String email = session?.email ?? 'client@example.com'; // V2 session does not include a photo URL; show letter avatar. - final String? photoUrl = null; + const String? photoUrl = null; final String avatarLetter = businessName.trim().isNotEmpty ? businessName.trim()[0].toUpperCase() : 'C'; diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/intro_page.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/intro_page.dart index 0acc300b..a798c552 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/intro_page.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/intro_page.dart @@ -1,10 +1,29 @@ +import 'dart:async'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_core/core.dart'; -/// A simple introductory page that displays the KROW logo. -class IntroPage extends StatelessWidget { +/// A simple introductory page that displays the KROW logo and navigates +/// to the get started page after a short delay. +class IntroPage extends StatefulWidget { const IntroPage({super.key}); + @override + State createState() => _IntroPageState(); +} + +class _IntroPageState extends State { + @override + void initState() { + super.initState(); + Timer(const Duration(seconds: 2), () { + if (Modular.to.path == StaffPaths.root) { + Modular.to.toGetStartedPage(); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -12,3 +31,4 @@ class IntroPage extends StatelessWidget { ); } } + diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart index 17a0a531..1d5039f0 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/phone_verification_page.dart @@ -142,12 +142,14 @@ class _PhoneVerificationPageState extends State { (state.status == AuthStatus.loading && state.verificationId != null); - return WillPopScope( - onWillPop: () async { - BlocProvider.of( - context, - ).add(AuthResetRequested(mode: widget.mode)); - return true; + return PopScope( + canPop: true, + onPopInvokedWithResult: (bool didPop, dynamic result) { + if (didPop) { + BlocProvider.of( + context, + ).add(AuthResetRequested(mode: widget.mode)); + } }, child: Scaffold( appBar: UiAppBar( diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart index 42b12e15..9b468818 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/get_started_page/get_started_background.dart @@ -13,8 +13,7 @@ class _GetStartedBackgroundState extends State { @override Widget build(BuildContext context) { - return Container( - child: Column( + return Column( children: [ const SizedBox(height: UiConstants.space8), // Logo @@ -113,7 +112,6 @@ class _GetStartedBackgroundState extends State { ], ), ], - ), - ); + ); } } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart index 5c294e02..23ca6bf6 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart @@ -1,6 +1,5 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:core_localization/core_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinput/pinput.dart'; @@ -50,7 +49,7 @@ class _OtpInputFieldState extends State { } Future _listenForSmsCode() async { - final res = await _smartAuth.getSmsCode(); + final SmsCodeResult res = await _smartAuth.getSmsCode(); if (res.code != null && mounted) { _controller.text = res.code!; _onChanged(_controller.text); @@ -68,7 +67,7 @@ class _OtpInputFieldState extends State { @override Widget build(BuildContext context) { - final defaultPinTheme = PinTheme( + final PinTheme defaultPinTheme = PinTheme( width: 45, height: 56, textStyle: UiTypography.headline3m, @@ -81,7 +80,7 @@ class _OtpInputFieldState extends State { ), ); - final focusedPinTheme = defaultPinTheme.copyWith( + final PinTheme focusedPinTheme = defaultPinTheme.copyWith( decoration: defaultPinTheme.decoration!.copyWith( border: Border.all( color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary, @@ -90,7 +89,7 @@ class _OtpInputFieldState extends State { ), ); - final submittedPinTheme = defaultPinTheme.copyWith( + final PinTheme submittedPinTheme = defaultPinTheme.copyWith( decoration: defaultPinTheme.decoration!.copyWith( border: Border.all( color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary, @@ -99,7 +98,7 @@ class _OtpInputFieldState extends State { ), ); - final errorPinTheme = defaultPinTheme.copyWith( + final PinTheme errorPinTheme = defaultPinTheme.copyWith( decoration: defaultPinTheme.decoration!.copyWith( border: Border.all(color: UiColors.textError, width: 2), ), diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_resend_section.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_resend_section.dart index 85f0c887..827da238 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_resend_section.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_resend_section.dart @@ -1,4 +1,4 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports +// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; 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 bc2e2d2f..a9c09638 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 @@ -1,4 +1,4 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports +// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; 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 04f1fde2..5042eda9 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 @@ -1,4 +1,4 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore +// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; 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 4cac15dc..7227bec5 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 @@ -1,4 +1,4 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports +// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart index 77fe1ff1..2478566d 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart @@ -20,14 +20,14 @@ class PendingPaymentCard extends StatelessWidget { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - UiColors.primary.withOpacity(0.08), - UiColors.primary.withOpacity(0.04), + UiColors.primary.withValues(alpha: 0.08), + UiColors.primary.withValues(alpha: 0.04), ], begin: Alignment.centerLeft, end: Alignment.centerRight, ), borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.primary.withOpacity(0.12)), + border: Border.all(color: UiColors.primary.withValues(alpha: 0.12)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_selected_card.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_selected_card.dart index 5a27ce1c..93a6d13e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_selected_card.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/documents/lib/src/presentation/widgets/document_upload/document_selected_card.dart @@ -23,7 +23,7 @@ class DocumentSelectedCard extends StatelessWidget { width: 40, height: 40, decoration: BoxDecoration( - color: UiColors.primary.withOpacity(0.1), + color: UiColors.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(UiConstants.space2), ), child: const Center( diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart index e1a17d49..53c66c01 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart @@ -1,4 +1,4 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore +// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_grid.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_grid.dart index 014ba478..304a960a 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_grid.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_grid.dart @@ -53,7 +53,7 @@ class AttireGrid extends StatelessWidget { return Container( decoration: BoxDecoration( color: isSelected - ? UiColors.primary.withOpacity(0.1) + ? UiColors.primary.withValues(alpha: 0.1) : Colors.transparent, borderRadius: UiConstants.radiusSm, border: Border.all( @@ -141,7 +141,7 @@ class AttireGrid extends StatelessWidget { ), decoration: BoxDecoration( color: hasPhoto - ? UiColors.primary.withOpacity(0.05) + ? UiColors.primary.withValues(alpha: 0.05) : UiColors.white, border: Border.all( color: hasPhoto ? UiColors.primary : UiColors.border, diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart index 3eaa9a0b..0d26d71f 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/pages/preferred_locations_page.dart @@ -1,26 +1,16 @@ -// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:google_places_flutter/model/prediction.dart'; -import 'package:krow_core/core.dart'; -import '../blocs/personal_info_bloc.dart'; -import '../blocs/personal_info_event.dart'; -import '../blocs/personal_info_state.dart'; -import '../widgets/preferred_locations_page/places_search_field.dart'; -import '../widgets/preferred_locations_page/locations_list.dart'; -import '../widgets/preferred_locations_page/empty_locations_state.dart'; +import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart'; +import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart'; +import 'package:staff_profile_info/src/presentation/blocs/personal_info_event.dart'; -/// The maximum number of preferred locations a staff member can add. -const int _kMaxLocations = 5; - -/// Uber-style Preferred Locations editing page. +/// Page for staff members to manage their preferred work locations. /// -/// Allows staff to search for US locations using the Google Places API, -/// add them as chips (max 5), and save back to their profile. +/// Allows searching for and adding multiple locations to the profile. class PreferredLocationsPage extends StatefulWidget { /// Creates a [PreferredLocationsPage]. const PreferredLocationsPage({super.key}); @@ -30,211 +20,112 @@ class PreferredLocationsPage extends StatefulWidget { } class _PreferredLocationsPageState extends State { - late final TextEditingController _searchController; - late final FocusNode _searchFocusNode; - - @override - void initState() { - super.initState(); - _searchController = TextEditingController(); - _searchFocusNode = FocusNode(); - } + final TextEditingController _searchController = TextEditingController(); @override void dispose() { _searchController.dispose(); - _searchFocusNode.dispose(); super.dispose(); } - void _onLocationSelected(Prediction prediction, PersonalInfoBloc bloc) { - final String description = prediction.description ?? ''; - if (description.isEmpty) return; - - bloc.add(PersonalInfoLocationAdded(location: description)); - - // Clear search field after selection + void _onAddLocation(String location, PersonalInfoBloc bloc) { + if (location.trim().isEmpty) return; + bloc.add(PersonalInfoLocationAdded(location: location)); _searchController.clear(); - _searchFocusNode.unfocus(); } - void _removeLocation(String location, PersonalInfoBloc bloc) { + void _onRemoveLocation(String location, PersonalInfoBloc bloc) { bloc.add(PersonalInfoLocationRemoved(location: location)); } - void _save(BuildContext context, PersonalInfoBloc bloc, PersonalInfoState state) { - bloc.add(const PersonalInfoFormSubmitted()); - } - - @override - Widget build(BuildContext context) { - final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info; - // Access the same PersonalInfoBloc singleton managed by the module. - final PersonalInfoBloc bloc = Modular.get(); - - return BlocProvider.value( - value: bloc, - child: BlocConsumer( - listener: (BuildContext context, PersonalInfoState state) { - if (state.status == PersonalInfoStatus.saved) { - UiSnackbar.show( - context, - message: i18n.preferred_locations.save_success, - type: UiSnackbarType.success, - ); - } else if (state.status == PersonalInfoStatus.error) { - UiSnackbar.show( - context, - message: state.errorMessage != null - ? translateErrorKey(state.errorMessage!) - : 'An error occurred', - type: UiSnackbarType.error, - ); - } - }, - builder: (BuildContext context, PersonalInfoState state) { - final List locations = _currentLocations(state); - final bool atMax = locations.length >= _kMaxLocations; - final bool isSaving = state.status == PersonalInfoStatus.saving; - - return Scaffold( - backgroundColor: UiColors.background, - appBar: UiAppBar( - title: i18n.preferred_locations.title, - showBackButton: true, - ), - body: Stack( - children: [ - SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ” Description - Padding( - padding: const EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space5, - UiConstants.space5, - UiConstants.space3, - ), - child: Text( - i18n.preferred_locations.description, - style: UiTypography.body2r.textSecondary, - ), - ), - - // ” Search autocomplete field - Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space5, - ), - child: PlacesSearchField( - controller: _searchController, - focusNode: _searchFocusNode, - hint: i18n.preferred_locations.search_hint, - enabled: !atMax && !isSaving, - onSelected: (Prediction p) => _onLocationSelected(p, bloc), - ), - ), - - // ” "Max reached" banner - if (atMax) - Padding( - padding: const EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space2, - UiConstants.space5, - 0, - ), - child: Row( - children: [ - const Icon( - UiIcons.info, - size: 14, - color: UiColors.textWarning, - ), - const SizedBox(width: UiConstants.space1), - Text( - i18n.preferred_locations.max_reached, - style: UiTypography.footnote1r.textWarning, - ), - ], - ), - ), - - const SizedBox(height: UiConstants.space5), - - // ” Section label - Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space5, - ), - child: Text( - i18n.preferred_locations.added_label, - style: UiTypography.titleUppercase3m.textSecondary, - ), - ), - - const SizedBox(height: UiConstants.space3), - - // Locations list / empty state - Expanded( - child: locations.isEmpty - ? EmptyLocationsState(message: i18n.preferred_locations.empty_state) - : LocationsList( - locations: locations, - isSaving: isSaving, - removeTooltip: i18n.preferred_locations.remove_tooltip, - onRemove: (String loc) => _removeLocation(loc, bloc), - ), - ), - - // Save button - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: UiButton.primary( - text: isSaving ? null : i18n.preferred_locations.save_button, - fullWidth: true, - onPressed: isSaving ? null : () => _save(context, bloc, state), - child: isSaving - ? const SizedBox( - height: UiConstants.iconMd, - width: UiConstants.iconMd, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - UiColors.white, - ), - ), - ) - : null, - ), - ), - ], - ), - ), - if (isSaving) - Container( - color: UiColors.black.withValues(alpha: 0.3), - child: const Center( - child: CircularProgressIndicator( - color: UiColors.primary, - ), - ), - ), - ], - ), - ); - }, - ), - ); - } - List _currentLocations(PersonalInfoState state) { - final dynamic raw = state.formValues['preferredLocations']; + final dynamic raw = state.personalInfo?.preferredLocations; if (raw is List) return raw; if (raw is List) return raw.map((dynamic e) => e.toString()).toList(); return []; } -} + @override + Widget build(BuildContext context) { + final TranslationsStaffOnboardingPersonalInfoEn i18n = + Translations.of(context).staff.onboarding.personal_info; + + return BlocProvider.value( + value: Modular.get(), + child: BlocBuilder( + builder: (BuildContext context, PersonalInfoState state) { + final PersonalInfoBloc bloc = BlocProvider.of(context); + final List locations = _currentLocations(state); + + return Scaffold( + appBar: UiAppBar( + title: i18n.preferred_locations.title, + showBackButton: true, + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + i18n.preferred_locations.description, + style: UiTypography.body2r.textSecondary, + ), + const SizedBox(height: UiConstants.space5), + + // Search field (Mock autocomplete) + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: i18n.preferred_locations.search_hint, + suffixIcon: IconButton( + icon: const Icon(UiIcons.add), + onPressed: () => _onAddLocation(_searchController.text, bloc), + ), + ), + onSubmitted: (String val) => _onAddLocation(val, bloc), + ), + ], + ), + ), + + Expanded( + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5), + itemCount: locations.length, + separatorBuilder: (BuildContext context, int index) => const Divider(), + itemBuilder: (BuildContext context, int index) { + final String loc = locations[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(UiIcons.mapPin, color: UiColors.primary), + title: Text(loc, style: UiTypography.body2m.textPrimary), + trailing: IconButton( + icon: const Icon(UiIcons.close, size: 20), + onPressed: () => _onRemoveLocation(loc, bloc), + ), + ); + }, + ), + ), + + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: UiButton.primary( + text: i18n.save_button, + onPressed: state.status == PersonalInfoStatus.loading + ? null + : () => bloc.add(const PersonalInfoFormSubmitted()), + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart index cd0ad51d..2c37c7da 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/profile_info/lib/src/presentation/widgets/personal_info_page/personal_info_form.dart @@ -98,4 +98,4 @@ class PersonalInfoForm extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/apps/mobile/packages/features/staff/shifts/analyze_output.txt b/apps/mobile/packages/features/staff/shifts/analyze_output.txt new file mode 100644 index 0000000000000000000000000000000000000000..9f7cfe75ea76efe41a47b45432e68423eac31b2e GIT binary patch literal 3244 zcmd^>U279j5QgVk@IUO8Y0;LXNvq^SDE)wf6x4cUB&5lv4JO%;-B{y=KV5y^IlCsi zD%h4vKn}a-oZb0+XXc#w{_W7F*0ZrK?Td|AUs+_SU9%oq#%k9`#Mg%zkL{7^k@VQ+ zjD#n$9i9v9h?%c=XAJ$1tcJA--x7Pre#}0?+7#Ps&_p(XZs@+mtRvPzVwOwG@O9>L zYjy}<3hfo#qJI0+b4isAWBy)R2dcUCnPRW;r(@0697 zyeog3w^(Udjf408V)s@r35Nw9o_ETqb&zlB_=!E|J6Gx}ckVr3xbth3$tk#hjoY7N z)<%A?A#i6{8Sdt7zQ4B*_Kf)rdM9p8Y$qVB8-9juqntPBRxeQ4Aer+2l(>u;OMA^q o5nlOmg09fdpW)TI2a)@D?%0j&6WWf@K4B>O18gWbgFd}~01+H7x&QzG literal 0 HcmV?d00001 diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index 9a719ffb..b7dabda7 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -221,25 +221,15 @@ class _MyShiftCardState extends State { color: UiColors.primary.withValues(alpha: 0.09), ), ), - child: widget.shift.logoUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), - child: Image.network( - widget.shift.logoUrl!, - fit: BoxFit.contain, - ), - ) - : const Center( - child: Icon( - UiIcons.briefcase, - color: UiColors.primary, - size: UiConstants.iconMd, - ), - ), + child: const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: UiConstants.iconMd, + ), ), ), + ), const SizedBox(width: UiConstants.space3), // Consensed Details 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 aeab8db0..1a6fa8e7 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 @@ -2,8 +2,6 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:intl/intl.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:staff_shifts/src/presentation/widgets/available_order_card.dart'; @@ -34,124 +32,15 @@ class FindShiftsTab extends StatefulWidget { class _FindShiftsTabState extends State { String _searchQuery = ''; String _jobType = 'all'; - double? _maxDistance; // miles - Position? _currentPosition; final TextEditingController _searchController = TextEditingController(); - String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt); - @override void dispose() { _searchController.dispose(); super.dispose(); } - Future _initLocation() async { - try { - final LocationPermission permission = await Geolocator.checkPermission(); - if (permission == LocationPermission.always || - permission == LocationPermission.whileInUse) { - final Position pos = await Geolocator.getCurrentPosition(); - if (mounted) { - setState(() => _currentPosition = pos); - } - } - } catch (_) {} - } - - double _calculateDistance(double lat, double lng) { - if (_currentPosition == null) return -1; - final double distMeters = Geolocator.distanceBetween( - _currentPosition!.latitude, - _currentPosition!.longitude, - lat, - lng, - ); - return distMeters / 1609.34; // meters to miles - } - - void _showDistanceFilter() { - showModalBottomSheet( - context: context, - backgroundColor: UiColors.bgPopup, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), - builder: (BuildContext context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setModalState) { - return Container( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.t.staff_shifts.find_shifts.radius_filter_title, - style: UiTypography.headline4m.textPrimary, - ), - const SizedBox(height: 16), - Text( - _maxDistance == null - ? context.t.staff_shifts.find_shifts.unlimited_distance - : context.t.staff_shifts.find_shifts.within_miles( - miles: _maxDistance!.round().toString(), - ), - style: UiTypography.body2m.textSecondary, - ), - Slider( - value: _maxDistance ?? 100, - min: 5, - max: 100, - divisions: 19, - activeColor: UiColors.primary, - onChanged: (double val) { - setModalState(() => _maxDistance = val); - setState(() => _maxDistance = val); - }, - ), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: UiButton.secondary( - text: context.t.staff_shifts.find_shifts.clear, - onPressed: () { - setModalState(() => _maxDistance = null); - setState(() => _maxDistance = null); - Navigator.pop(context); - }, - ), - ), - const SizedBox(width: 12), - Expanded( - child: UiButton.primary( - text: context.t.staff_shifts.find_shifts.apply, - onPressed: () => Navigator.pop(context), - ), - ), - ], - ), - ], - ), - ); - }, - ); - }, - ); - } - - String _formatDate(DateTime date) { - final DateTime now = DateTime.now(); - final DateTime today = DateTime(now.year, now.month, now.day); - final DateTime tomorrow = today.add(const Duration(days: 1)); - final DateTime d = DateTime(date.year, date.month, date.day); - if (d == today) return 'Today'; - if (d == tomorrow) return 'Tomorrow'; - return DateFormat('EEE, MMM d').format(date); - } - /// Builds a filter tab chip. Widget _buildFilterTab(String id, String label) { final bool isSelected = _jobType == id;