chore: fix 273+ analysis issues and repair corrupted core files
This commit is contained in:
@@ -25,7 +25,7 @@ void main() async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Register global BLoC observer for centralized error logging
|
// Register global BLoC observer for centralized error logging
|
||||||
Bloc.observer = CoreBlocObserver(
|
Bloc.observer = const CoreBlocObserver(
|
||||||
logEvents: true,
|
logEvents: true,
|
||||||
logStateChanges: false, // Set to true for verbose debugging
|
logStateChanges: false, // Set to true for verbose debugging
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ void main() async {
|
|||||||
await const BackgroundTaskService().initialize(backgroundGeofenceDispatcher);
|
await const BackgroundTaskService().initialize(backgroundGeofenceDispatcher);
|
||||||
|
|
||||||
// Register global BLoC observer for centralized error logging
|
// Register global BLoC observer for centralized error logging
|
||||||
Bloc.observer = CoreBlocObserver(
|
Bloc.observer = const CoreBlocObserver(
|
||||||
logEvents: true,
|
logEvents: true,
|
||||||
logStateChanges: false, // Set to true for verbose debugging
|
logStateChanges: false, // Set to true for verbose debugging
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
appId: com.krowwithus.staff
|
appId: com.krowwithus.staff
|
||||||
---
|
---
|
||||||
- launchApp
|
- launchApp
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "(?i).*(Home|Profile|Log In).*"
|
||||||
|
timeout: 10000
|
||||||
- assertVisible: "Profile"
|
- assertVisible: "Profile"
|
||||||
- tapOn: "Profile"
|
- tapOn: "Profile"
|
||||||
- assertVisible: "Personal Info"
|
- assertVisible: "Personal Info"
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
appId: com.krowwithus.staff
|
appId: com.krowwithus.staff
|
||||||
---
|
---
|
||||||
- launchApp
|
- launchApp
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: "(?i).*(Home|Shifts|Welcome back|Log In).*"
|
||||||
|
timeout: 10000
|
||||||
- assertVisible: "Shifts"
|
- assertVisible: "Shifts"
|
||||||
- tapOn: "Shifts"
|
- tapOn: "Shifts"
|
||||||
- assertVisible: "Find Shifts"
|
- assertVisible: "Find Shifts"
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ appId: com.krowwithus.staff
|
|||||||
|
|
||||||
# Case A: shifts are available — list renders
|
# Case A: shifts are available — list renders
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
text: "(?i).*(Available|Apply Now|shift|hours).*"
|
text: "(?i).*(Available|Apply Now|Book Shift|shift|hours|jobs).*"
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
# Case B: no shifts — empty state copy is shown (not a blank screen)
|
# Case B: no shifts — empty state copy is shown (not a blank screen)
|
||||||
- assertVisible:
|
- 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
|
optional: true
|
||||||
|
|
||||||
# Entry assertion: incomplete-profile banner (if applicable)
|
# Entry assertion: incomplete-profile banner (if applicable)
|
||||||
|
|||||||
@@ -29,20 +29,20 @@ appId: com.krowwithus.staff
|
|||||||
visible: "Shifts"
|
visible: "Shifts"
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
|
|
||||||
# If jobs exist, APPLY NOW should be present (optional)
|
# If jobs exist, BOOK SHIFT should be present (optional)
|
||||||
- tapOn:
|
- tapOn:
|
||||||
text: "APPLY NOW"
|
text: "BOOK SHIFT"
|
||||||
optional: true
|
optional: true
|
||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Applying"
|
visible: "Booking order.*"
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
optional: true
|
optional: true
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
text: "Applying"
|
text: "Booking order.*"
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
# Otherwise, empty state may be visible (optional)
|
# Otherwise, empty state may be visible (optional)
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
text: "No shifts found"
|
text: "No jobs available"
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:krow_domain/krow_domain.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 'package:krow_core/src/services/auth/firebase_auth_token_provider.dart';
|
||||||
|
|
||||||
import '../core.dart';
|
import '../core.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 'dart:developer' as developer;
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
/// Global BLoC observer for centralized logging and monitoring.
|
/// A BLoC observer that logs state changes and optionally events.
|
||||||
///
|
|
||||||
/// 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());
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
class CoreBlocObserver extends BlocObserver {
|
class CoreBlocObserver extends BlocObserver {
|
||||||
|
/// Creates a [CoreBlocObserver].
|
||||||
CoreBlocObserver({
|
const CoreBlocObserver({
|
||||||
this.logStateChanges = false,
|
this.logEvents = false,
|
||||||
this.logEvents = true,
|
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;
|
final bool logEvents;
|
||||||
|
|
||||||
@override
|
/// Whether to log BLoC state transitions.
|
||||||
void onCreate(BlocBase bloc) {
|
final bool logStateChanges;
|
||||||
super.onCreate(bloc);
|
|
||||||
developer.log(
|
|
||||||
'Created: ${bloc.runtimeType}',
|
|
||||||
name: 'BlocObserver',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onEvent(Bloc bloc, Object? event) {
|
void onEvent(Bloc<dynamic, dynamic> bloc, Object? event) {
|
||||||
super.onEvent(bloc, event);
|
super.onEvent(bloc, event);
|
||||||
if (logEvents) {
|
if (logEvents) {
|
||||||
developer.log(
|
developer.log(
|
||||||
'Event: ${event.runtimeType}',
|
'onEvent -- ${bloc.runtimeType}: $event',
|
||||||
name: bloc.runtimeType.toString(),
|
name: 'BLOC_EVENT',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onChange(BlocBase bloc, Change change) {
|
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
|
||||||
super.onChange(bloc, change);
|
super.onChange(bloc, change);
|
||||||
if (logStateChanges) {
|
if (logStateChanges) {
|
||||||
developer.log(
|
developer.log(
|
||||||
'State: ${change.currentState.runtimeType}’ ${change.nextState.runtimeType}',
|
'onChange -- ${bloc.runtimeType}: ${change.currentState} -> ${change.nextState}',
|
||||||
name: bloc.runtimeType.toString(),
|
name: 'BLOC_STATE',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
|
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
|
||||||
super.onError(bloc, error, stackTrace);
|
|
||||||
|
|
||||||
// Log error to console
|
|
||||||
developer.log(
|
developer.log(
|
||||||
'ERROR in ${bloc.runtimeType}',
|
'onError -- ${bloc.runtimeType}: $error',
|
||||||
name: 'BlocObserver',
|
name: 'BLOC_ERROR',
|
||||||
error: error,
|
error: error,
|
||||||
stackTrace: stackTrace,
|
stackTrace: stackTrace,
|
||||||
);
|
);
|
||||||
|
super.onError(bloc, error, 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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
Container(
|
Container(
|
||||||
height: 2,
|
height: 2,
|
||||||
width: 40,
|
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),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.6),
|
color: UiColors.black.withValues(alpha: 0.6),
|
||||||
blurRadius: 40,
|
blurRadius: 40,
|
||||||
spreadRadius: 10,
|
spreadRadius: 10,
|
||||||
),
|
),
|
||||||
@@ -241,12 +241,12 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
|||||||
width: 30,
|
width: 30,
|
||||||
height: 30,
|
height: 30,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.mutedForeground.withOpacity(0.3),
|
color: UiColors.mutedForeground.withValues(alpha: 0.3),
|
||||||
shape: BoxShape.circle,
|
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>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.2),
|
color: UiColors.black.withValues(alpha: 0.2),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
spreadRadius: 1,
|
spreadRadius: 1,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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<List<BusinessBankAccount>> 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<double> 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<double>(
|
|
||||||
0.0,
|
|
||||||
(double sum, Invoice item) => sum + item.totalAmount,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Invoice>> 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<List<Invoice>> 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<List<InvoiceItem>> 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<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
|
|
||||||
shiftRoles = result.data.shiftRoles;
|
|
||||||
if (shiftRoles.isEmpty) {
|
|
||||||
return <InvoiceItem>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
|
|
||||||
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<void> approveInvoice({required String id}) async {
|
|
||||||
return _service.run(() async {
|
|
||||||
await _service.connector
|
|
||||||
.updateInvoice(id: id)
|
|
||||||
.status(dc.InvoiceStatus.APPROVED)
|
|
||||||
.execute();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> 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<InvoiceWorker> workers = <InvoiceWorker>[];
|
|
||||||
|
|
||||||
// Try to get workers from denormalized 'roles' field first
|
|
||||||
final List<dynamic> rolesData = invoice.roles is List
|
|
||||||
? invoice.roles
|
|
||||||
: <dynamic>[];
|
|
||||||
if (rolesData.isNotEmpty) {
|
|
||||||
workers = rolesData.map((dynamic r) {
|
|
||||||
final Map<String, dynamic> role = r as Map<String, dynamic>;
|
|
||||||
|
|
||||||
// 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<dynamic> 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<dynamic> roles) {
|
|
||||||
return roles.fold<double>(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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<List<Shift>> 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<List<Shift>> 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 <Shift>[];
|
|
||||||
|
|
||||||
final QueryResult<
|
|
||||||
dc.ListShiftRolesByVendorIdData,
|
|
||||||
dc.ListShiftRolesByVendorIdVariables
|
|
||||||
>
|
|
||||||
response = await _service.connector
|
|
||||||
.listShiftRolesByVendorId(vendorId: vendorId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
final List<dc.ListShiftRolesByVendorIdShiftRoles> 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<String> appliedShiftIds = myAppsResponse.data.applications
|
|
||||||
.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId)
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
final List<Shift> mappedShifts = <Shift>[];
|
|
||||||
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<ShiftSchedule>? 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<List<Shift>> 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 <Shift>[];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Shift?> getShiftDetails({
|
|
||||||
required String shiftId,
|
|
||||||
required String staffId,
|
|
||||||
String? roleId,
|
|
||||||
}) async {
|
|
||||||
return _service.run(() async {
|
|
||||||
if (roleId != null && roleId.isNotEmpty) {
|
|
||||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
|
||||||
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<dc.GetShiftByIdData, dc.GetShiftByIdVariables> 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<void> 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<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
|
|
||||||
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<dc.OrderType> orderTypeEnum =
|
|
||||||
initialShift.order.orderType;
|
|
||||||
final bool isMultiDay =
|
|
||||||
orderTypeEnum is dc.Known<dc.OrderType> &&
|
|
||||||
(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<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
|
||||||
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<String> 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<void> _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<void> acceptShift({required String shiftId, required String staffId}) {
|
|
||||||
return _updateApplicationStatus(
|
|
||||||
shiftId,
|
|
||||||
staffId,
|
|
||||||
dc.ApplicationStatus.CONFIRMED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> declineShift({
|
|
||||||
required String shiftId,
|
|
||||||
required String staffId,
|
|
||||||
}) {
|
|
||||||
return _updateApplicationStatus(
|
|
||||||
shiftId,
|
|
||||||
staffId,
|
|
||||||
dc.ApplicationStatus.REJECTED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Shift>> getCancelledShifts({required String staffId}) async {
|
|
||||||
return _service.run(() async {
|
|
||||||
// Logic would go here to fetch by REJECTED status if needed
|
|
||||||
return <Shift>[];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Shift>> 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<Shift> shifts = <Shift>[];
|
|
||||||
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<Shift> _mapApplicationsToShifts(List<dynamic> 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<void> _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<ShiftSchedule>? _generateSchedules({
|
|
||||||
required String orderType,
|
|
||||||
required DateTime? startDate,
|
|
||||||
required DateTime? endDate,
|
|
||||||
required List<String>? recurringDays,
|
|
||||||
required List<String>? permanentDays,
|
|
||||||
required String startTime,
|
|
||||||
required String endTime,
|
|
||||||
}) {
|
|
||||||
if (orderType != 'RECURRING' && orderType != 'PERMANENT') return null;
|
|
||||||
if (startDate == null || endDate == null) return null;
|
|
||||||
|
|
||||||
final List<String>? daysToInclude = orderType == 'RECURRING'
|
|
||||||
? recurringDays
|
|
||||||
: permanentDays;
|
|
||||||
if (daysToInclude == null || daysToInclude.isEmpty) return null;
|
|
||||||
|
|
||||||
final List<ShiftSchedule> schedules = <ShiftSchedule>[];
|
|
||||||
final Set<int> 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -17,4 +17,4 @@ class Accessibility extends Equatable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[id, name];
|
List<Object?> get props => <Object?>[id, name];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:krow_domain/krow_domain.dart'
|
|||||||
AppException,
|
AppException,
|
||||||
BaseApiService,
|
BaseApiService,
|
||||||
ClientSession,
|
ClientSession,
|
||||||
NetworkException,
|
|
||||||
PasswordMismatchException,
|
PasswordMismatchException,
|
||||||
SignInFailedException,
|
SignInFailedException,
|
||||||
SignUpFailedException,
|
SignUpFailedException,
|
||||||
@@ -111,19 +110,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
} on AppException {
|
} on AppException {
|
||||||
rethrow;
|
rethrow;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Map common Firebase-originated errors from the V2 API response
|
if (e is AppException) rethrow;
|
||||||
// to domain exceptions.
|
|
||||||
|
// Extract error code if available from the API response
|
||||||
final String errorMessage = e.toString();
|
final String errorMessage = e.toString();
|
||||||
if (errorMessage.contains('EMAIL_EXISTS') ||
|
_throwSignUpError('SIGN_UP_ERROR', errorMessage);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.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});
|
const ClientIntroPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ClientIntroPage> createState() => _ClientIntroPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ClientIntroPageState extends State<ClientIntroPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
Future<void>.delayed(const Duration(seconds: 2), () {
|
||||||
|
if (mounted && Modular.to.path == ClientPaths.root) {
|
||||||
|
Modular.to.toClientGetStartedPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -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.
|
/// Shows an error message with a retry button when data fails to load.
|
||||||
class ClientHomeErrorState extends StatelessWidget {
|
class ClientHomeErrorState extends StatelessWidget {
|
||||||
/// The current home state containing error information.
|
|
||||||
final ClientHomeState state;
|
|
||||||
|
|
||||||
/// Creates a [ClientHomeErrorState].
|
/// Creates a [ClientHomeErrorState].
|
||||||
const ClientHomeErrorState({
|
const ClientHomeErrorState({
|
||||||
required this.state,
|
required this.state,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
/// The current home state containing error information.
|
||||||
|
final ClientHomeState state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -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.
|
/// Shows visible dashboard widgets in a vertical scrollable list with dividers.
|
||||||
class ClientHomeNormalModeBody extends StatelessWidget {
|
class ClientHomeNormalModeBody extends StatelessWidget {
|
||||||
/// The current home state.
|
|
||||||
final ClientHomeState state;
|
|
||||||
|
|
||||||
/// Creates a [ClientHomeNormalModeBody].
|
/// Creates a [ClientHomeNormalModeBody].
|
||||||
const ClientHomeNormalModeBody({
|
const ClientHomeNormalModeBody({
|
||||||
required this.state,
|
required this.state,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
/// The current home state.
|
||||||
|
final ClientHomeState state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ class CoverageBloc extends Bloc<CoverageEvent, CoverageState>
|
|||||||
Emitter<CoverageState> emit,
|
Emitter<CoverageState> emit,
|
||||||
) async {
|
) async {
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
emit(CoverageLoading());
|
emit(CoverageLoading());
|
||||||
final CoverageReport report = await _getCoverageReportUseCase.call(
|
final CoverageReport report = await _getCoverageReportUseCase.call(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Base state for the coverage report BLoC.
|
/// Base state for the coverage report BLoC.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DailyOpsBloc extends Bloc<DailyOpsEvent, DailyOpsState>
|
|||||||
Emitter<DailyOpsState> emit,
|
Emitter<DailyOpsState> emit,
|
||||||
) async {
|
) async {
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
emit(DailyOpsLoading());
|
emit(DailyOpsLoading());
|
||||||
final DailyOpsReport report = await _getDailyOpsReportUseCase.call(
|
final DailyOpsReport report = await _getDailyOpsReportUseCase.call(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Base state for the daily operations report BLoC.
|
/// Base state for the daily operations report BLoC.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ForecastBloc extends Bloc<ForecastEvent, ForecastState>
|
|||||||
Emitter<ForecastState> emit,
|
Emitter<ForecastState> emit,
|
||||||
) async {
|
) async {
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
emit(ForecastLoading());
|
emit(ForecastLoading());
|
||||||
final ForecastReport report = await _getForecastReportUseCase.call(
|
final ForecastReport report = await _getForecastReportUseCase.call(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Base state for the forecast report BLoC.
|
/// Base state for the forecast report BLoC.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class NoShowBloc extends Bloc<NoShowEvent, NoShowState>
|
|||||||
Emitter<NoShowState> emit,
|
Emitter<NoShowState> emit,
|
||||||
) async {
|
) async {
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
emit(NoShowLoading());
|
emit(NoShowLoading());
|
||||||
final NoShowReport report = await _getNoShowReportUseCase.call(
|
final NoShowReport report = await _getNoShowReportUseCase.call(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Base state for the no-show report BLoC.
|
/// Base state for the no-show report BLoC.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class PerformanceBloc extends Bloc<PerformanceEvent, PerformanceState>
|
|||||||
Emitter<PerformanceState> emit,
|
Emitter<PerformanceState> emit,
|
||||||
) async {
|
) async {
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
emit(PerformanceLoading());
|
emit(PerformanceLoading());
|
||||||
final PerformanceReport report = await _getPerformanceReportUseCase.call(
|
final PerformanceReport report = await _getPerformanceReportUseCase.call(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Base state for the performance report BLoC.
|
/// Base state for the performance report BLoC.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class SpendBloc extends Bloc<SpendEvent, SpendState>
|
|||||||
Emitter<SpendState> emit,
|
Emitter<SpendState> emit,
|
||||||
) async {
|
) async {
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
emit(SpendLoading());
|
emit(SpendLoading());
|
||||||
final SpendReport report = await _getSpendReportUseCase.call(
|
final SpendReport report = await _getSpendReportUseCase.call(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Base state for the spend report BLoC.
|
/// Base state for the spend report BLoC.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ReportsSummaryBloc
|
|||||||
Emitter<ReportsSummaryState> emit,
|
Emitter<ReportsSummaryState> emit,
|
||||||
) async {
|
) async {
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit.call,
|
||||||
action: () async {
|
action: () async {
|
||||||
emit(ReportsSummaryLoading());
|
emit(ReportsSummaryLoading());
|
||||||
final ReportSummary summary = await _getReportsSummaryUseCase.call(
|
final ReportSummary summary = await _getReportsSummaryUseCase.call(
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.2),
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -95,7 +95,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
|
|||||||
context.t.client_reports.coverage_report
|
context.t.client_reports.coverage_report
|
||||||
.subtitle,
|
.subtitle,
|
||||||
style: UiTypography.body3r.copyWith(
|
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),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -214,7 +214,7 @@ class _CoverageSummaryCard extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Icon(icon, size: 16, color: color),
|
child: Icon(icon, size: 16, color: color),
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.2),
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -126,7 +126,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
|
|||||||
context.t.client_reports.daily_ops_report
|
context.t.client_reports.daily_ops_report
|
||||||
.subtitle,
|
.subtitle,
|
||||||
style: UiTypography.body3r.copyWith(
|
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<DailyOpsReportPage> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.06),
|
color: UiColors.black.withValues(alpha: 0.06),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -390,7 +390,7 @@ class _OpsStatCard extends StatelessWidget {
|
|||||||
vertical: 3,
|
vertical: 3,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.12),
|
color: color.withValues(alpha: 0.12),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -451,7 +451,7 @@ class _ShiftListItem extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.02),
|
color: UiColors.black.withValues(alpha: 0.02),
|
||||||
blurRadius: 2,
|
blurRadius: 2,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -497,7 +497,7 @@ class _ShiftListItem extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(left: 8),
|
margin: const EdgeInsets.only(left: 8),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusColor.withOpacity(0.1),
|
color: statusColor.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.2),
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -141,7 +141,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
|
|||||||
Text(
|
Text(
|
||||||
context.t.client_reports.forecast_report.subtitle,
|
context.t.client_reports.forecast_report.subtitle,
|
||||||
style: UiTypography.body2m.copyWith(
|
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<ForecastReportPage> {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -289,7 +289,7 @@ class _MetricCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -469,7 +469,7 @@ class _ForecastChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
belowBarData: BarAreaData(
|
belowBarData: BarAreaData(
|
||||||
show: true,
|
show: true,
|
||||||
color: UiColors.tagPending.withOpacity(0.5),
|
color: UiColors.tagPending.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.15),
|
color: UiColors.white.withValues(alpha: 0.15),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -97,7 +97,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
|||||||
Text(
|
Text(
|
||||||
context.t.client_reports.no_show_report.subtitle,
|
context.t.client_reports.no_show_report.subtitle,
|
||||||
style: UiTypography.body3r.copyWith(
|
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),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.06),
|
color: UiColors.black.withValues(alpha: 0.06),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -302,7 +302,7 @@ class _WorkerCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 6,
|
blurRadius: 6,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.2),
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -182,7 +182,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
context.t.client_reports.performance_report
|
context.t.client_reports.performance_report
|
||||||
.subtitle,
|
.subtitle,
|
||||||
style: UiTypography.body3r.copyWith(
|
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<PerformanceReportPage> {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -270,7 +270,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -387,7 +387,7 @@ class _KpiRow extends StatelessWidget {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: kpi.iconColor.withOpacity(0.1),
|
color: kpi.iconColor.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(kpi.icon, size: 18, color: kpi.iconColor),
|
child: Icon(kpi.icon, size: 18, color: kpi.iconColor),
|
||||||
|
|||||||
@@ -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:client_reports/src/presentation/blocs/summary/reports_summary_event.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.2),
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -104,7 +104,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
|
|||||||
Text(
|
Text(
|
||||||
context.t.client_reports.spend_report.subtitle,
|
context.t.client_reports.spend_report.subtitle,
|
||||||
style: UiTypography.body3r.copyWith(
|
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<SpendReportPage> {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -358,7 +358,7 @@ class _SpendStatCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.06),
|
color: UiColors.black.withValues(alpha: 0.06),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
@@ -392,7 +392,7 @@ class _SpendStatCard extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: themeColor.withOpacity(0.1),
|
color: themeColor.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -424,7 +424,7 @@ class _SpendByCategoryCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ReportCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: <BoxShadow>[
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withOpacity(0.02),
|
color: UiColors.black.withValues(alpha: 0.02),
|
||||||
blurRadius: 2,
|
blurRadius: 2,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class ReportsHeader extends StatelessWidget {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.2),
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@@ -84,7 +84,7 @@ class ReportsHeader extends StatelessWidget {
|
|||||||
height: 44,
|
height: 44,
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white.withOpacity(0.2),
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
|
|||||||
@@ -176,104 +176,4 @@ class _QuickLinkItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NotificationsSettingsCard extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
|
|
||||||
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: <Widget>[
|
|
||||||
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<ClientSettingsBloc>().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<ClientSettingsBloc>().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<ClientSettingsBloc>().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<bool> onChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class SettingsProfileHeader extends StatelessWidget {
|
|||||||
final String businessName = session?.businessName ?? 'Your Company';
|
final String businessName = session?.businessName ?? 'Your Company';
|
||||||
final String email = session?.email ?? 'client@example.com';
|
final String email = session?.email ?? 'client@example.com';
|
||||||
// V2 session does not include a photo URL; show letter avatar.
|
// 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
|
final String avatarLetter = businessName.trim().isNotEmpty
|
||||||
? businessName.trim()[0].toUpperCase()
|
? businessName.trim()[0].toUpperCase()
|
||||||
: 'C';
|
: 'C';
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.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.
|
/// A simple introductory page that displays the KROW logo and navigates
|
||||||
class IntroPage extends StatelessWidget {
|
/// to the get started page after a short delay.
|
||||||
|
class IntroPage extends StatefulWidget {
|
||||||
const IntroPage({super.key});
|
const IntroPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<IntroPage> createState() => _IntroPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IntroPageState extends State<IntroPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
Timer(const Duration(seconds: 2), () {
|
||||||
|
if (Modular.to.path == StaffPaths.root) {
|
||||||
|
Modular.to.toGetStartedPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -12,3 +31,4 @@ class IntroPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,12 +142,14 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
|||||||
(state.status == AuthStatus.loading &&
|
(state.status == AuthStatus.loading &&
|
||||||
state.verificationId != null);
|
state.verificationId != null);
|
||||||
|
|
||||||
return WillPopScope(
|
return PopScope(
|
||||||
onWillPop: () async {
|
canPop: true,
|
||||||
BlocProvider.of<AuthBloc>(
|
onPopInvokedWithResult: (bool didPop, dynamic result) {
|
||||||
context,
|
if (didPop) {
|
||||||
).add(AuthResetRequested(mode: widget.mode));
|
BlocProvider.of<AuthBloc>(
|
||||||
return true;
|
context,
|
||||||
|
).add(AuthResetRequested(mode: widget.mode));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: UiAppBar(
|
appBar: UiAppBar(
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ class _GetStartedBackgroundState extends State<GetStartedBackground> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Column(
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: UiConstants.space8),
|
const SizedBox(height: UiConstants.space8),
|
||||||
// Logo
|
// Logo
|
||||||
@@ -113,7 +112,6 @@ class _GetStartedBackgroundState extends State<GetStartedBackground> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:pinput/pinput.dart';
|
import 'package:pinput/pinput.dart';
|
||||||
@@ -50,7 +49,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _listenForSmsCode() async {
|
Future<void> _listenForSmsCode() async {
|
||||||
final res = await _smartAuth.getSmsCode();
|
final SmsCodeResult res = await _smartAuth.getSmsCode();
|
||||||
if (res.code != null && mounted) {
|
if (res.code != null && mounted) {
|
||||||
_controller.text = res.code!;
|
_controller.text = res.code!;
|
||||||
_onChanged(_controller.text);
|
_onChanged(_controller.text);
|
||||||
@@ -68,7 +67,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final defaultPinTheme = PinTheme(
|
final PinTheme defaultPinTheme = PinTheme(
|
||||||
width: 45,
|
width: 45,
|
||||||
height: 56,
|
height: 56,
|
||||||
textStyle: UiTypography.headline3m,
|
textStyle: UiTypography.headline3m,
|
||||||
@@ -81,7 +80,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final focusedPinTheme = defaultPinTheme.copyWith(
|
final PinTheme focusedPinTheme = defaultPinTheme.copyWith(
|
||||||
decoration: defaultPinTheme.decoration!.copyWith(
|
decoration: defaultPinTheme.decoration!.copyWith(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
||||||
@@ -90,7 +89,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final submittedPinTheme = defaultPinTheme.copyWith(
|
final PinTheme submittedPinTheme = defaultPinTheme.copyWith(
|
||||||
decoration: defaultPinTheme.decoration!.copyWith(
|
decoration: defaultPinTheme.decoration!.copyWith(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
||||||
@@ -99,7 +98,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final errorPinTheme = defaultPinTheme.copyWith(
|
final PinTheme errorPinTheme = defaultPinTheme.copyWith(
|
||||||
decoration: defaultPinTheme.decoration!.copyWith(
|
decoration: defaultPinTheme.decoration!.copyWith(
|
||||||
border: Border.all(color: UiColors.textError, width: 2),
|
border: Border.all(color: UiColors.textError, width: 2),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ class PendingPaymentCard extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
UiColors.primary.withOpacity(0.08),
|
UiColors.primary.withValues(alpha: 0.08),
|
||||||
UiColors.primary.withOpacity(0.04),
|
UiColors.primary.withValues(alpha: 0.04),
|
||||||
],
|
],
|
||||||
begin: Alignment.centerLeft,
|
begin: Alignment.centerLeft,
|
||||||
end: Alignment.centerRight,
|
end: Alignment.centerRight,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
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(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class DocumentSelectedCard extends StatelessWidget {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.primary.withOpacity(0.1),
|
color: UiColors.primary.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(UiConstants.space2),
|
borderRadius: BorderRadius.circular(UiConstants.space2),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
|
|||||||
@@ -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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class AttireGrid extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? UiColors.primary.withOpacity(0.1)
|
? UiColors.primary.withValues(alpha: 0.1)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
borderRadius: UiConstants.radiusSm,
|
borderRadius: UiConstants.radiusSm,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
@@ -141,7 +141,7 @@ class AttireGrid extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: hasPhoto
|
color: hasPhoto
|
||||||
? UiColors.primary.withOpacity(0.05)
|
? UiColors.primary.withValues(alpha: 0.05)
|
||||||
: UiColors.white,
|
: UiColors.white,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: hasPhoto ? UiColors.primary : UiColors.border,
|
color: hasPhoto ? UiColors.primary : UiColors.border,
|
||||||
|
|||||||
@@ -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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.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 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
|
||||||
import '../blocs/personal_info_event.dart';
|
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
|
||||||
import '../blocs/personal_info_state.dart';
|
import 'package:staff_profile_info/src/presentation/blocs/personal_info_event.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';
|
|
||||||
|
|
||||||
/// The maximum number of preferred locations a staff member can add.
|
/// Page for staff members to manage their preferred work locations.
|
||||||
const int _kMaxLocations = 5;
|
|
||||||
|
|
||||||
/// Uber-style Preferred Locations editing page.
|
|
||||||
///
|
///
|
||||||
/// Allows staff to search for US locations using the Google Places API,
|
/// Allows searching for and adding multiple locations to the profile.
|
||||||
/// add them as chips (max 5), and save back to their profile.
|
|
||||||
class PreferredLocationsPage extends StatefulWidget {
|
class PreferredLocationsPage extends StatefulWidget {
|
||||||
/// Creates a [PreferredLocationsPage].
|
/// Creates a [PreferredLocationsPage].
|
||||||
const PreferredLocationsPage({super.key});
|
const PreferredLocationsPage({super.key});
|
||||||
@@ -30,211 +20,112 @@ class PreferredLocationsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
||||||
late final TextEditingController _searchController;
|
final TextEditingController _searchController = TextEditingController();
|
||||||
late final FocusNode _searchFocusNode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_searchController = TextEditingController();
|
|
||||||
_searchFocusNode = FocusNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
_searchFocusNode.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLocationSelected(Prediction prediction, PersonalInfoBloc bloc) {
|
void _onAddLocation(String location, PersonalInfoBloc bloc) {
|
||||||
final String description = prediction.description ?? '';
|
if (location.trim().isEmpty) return;
|
||||||
if (description.isEmpty) return;
|
bloc.add(PersonalInfoLocationAdded(location: location));
|
||||||
|
|
||||||
bloc.add(PersonalInfoLocationAdded(location: description));
|
|
||||||
|
|
||||||
// Clear search field after selection
|
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
_searchFocusNode.unfocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeLocation(String location, PersonalInfoBloc bloc) {
|
void _onRemoveLocation(String location, PersonalInfoBloc bloc) {
|
||||||
bloc.add(PersonalInfoLocationRemoved(location: location));
|
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<PersonalInfoBloc>();
|
|
||||||
|
|
||||||
return BlocProvider<PersonalInfoBloc>.value(
|
|
||||||
value: bloc,
|
|
||||||
child: BlocConsumer<PersonalInfoBloc, PersonalInfoState>(
|
|
||||||
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<String> 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: <Widget>[
|
|
||||||
// ” 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: <Widget>[
|
|
||||||
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<Color>(
|
|
||||||
UiColors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (isSaving)
|
|
||||||
Container(
|
|
||||||
color: UiColors.black.withValues(alpha: 0.3),
|
|
||||||
child: const Center(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _currentLocations(PersonalInfoState state) {
|
List<String> _currentLocations(PersonalInfoState state) {
|
||||||
final dynamic raw = state.formValues['preferredLocations'];
|
final dynamic raw = state.personalInfo?.preferredLocations;
|
||||||
if (raw is List<String>) return raw;
|
if (raw is List<String>) return raw;
|
||||||
if (raw is List) return raw.map((dynamic e) => e.toString()).toList();
|
if (raw is List) return raw.map((dynamic e) => e.toString()).toList();
|
||||||
return <String>[];
|
return <String>[];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsStaffOnboardingPersonalInfoEn i18n =
|
||||||
|
Translations.of(context).staff.onboarding.personal_info;
|
||||||
|
|
||||||
|
return BlocProvider<PersonalInfoBloc>.value(
|
||||||
|
value: Modular.get<PersonalInfoBloc>(),
|
||||||
|
child: BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
||||||
|
builder: (BuildContext context, PersonalInfoState state) {
|
||||||
|
final PersonalInfoBloc bloc = BlocProvider.of<PersonalInfoBloc>(context);
|
||||||
|
final List<String> locations = _currentLocations(state);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: UiAppBar(
|
||||||
|
title: i18n.preferred_locations.title,
|
||||||
|
showBackButton: true,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
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()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -98,4 +98,4 @@ class PersonalInfoForm extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
apps/mobile/packages/features/staff/shifts/analyze_output.txt
Normal file
BIN
apps/mobile/packages/features/staff/shifts/analyze_output.txt
Normal file
Binary file not shown.
@@ -221,25 +221,15 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
color: UiColors.primary.withValues(alpha: 0.09),
|
color: UiColors.primary.withValues(alpha: 0.09),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: widget.shift.logoUrl != null
|
child: const Center(
|
||||||
? ClipRRect(
|
child: Icon(
|
||||||
borderRadius: BorderRadius.circular(
|
UiIcons.briefcase,
|
||||||
UiConstants.radiusBase,
|
color: UiColors.primary,
|
||||||
),
|
size: UiConstants.iconMd,
|
||||||
child: Image.network(
|
),
|
||||||
widget.shift.logoUrl!,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Center(
|
|
||||||
child: Icon(
|
|
||||||
UiIcons.briefcase,
|
|
||||||
color: UiColors.primary,
|
|
||||||
size: UiConstants.iconMd,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: UiConstants.space3),
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
|
||||||
// Consensed Details
|
// Consensed Details
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.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_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:staff_shifts/src/presentation/widgets/available_order_card.dart';
|
import 'package:staff_shifts/src/presentation/widgets/available_order_card.dart';
|
||||||
@@ -34,124 +32,15 @@ class FindShiftsTab extends StatefulWidget {
|
|||||||
class _FindShiftsTabState extends State<FindShiftsTab> {
|
class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
String _jobType = 'all';
|
String _jobType = 'all';
|
||||||
double? _maxDistance; // miles
|
|
||||||
Position? _currentPosition;
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
|
||||||
|
|
||||||
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _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<void>(
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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.
|
/// Builds a filter tab chip.
|
||||||
Widget _buildFilterTab(String id, String label) {
|
Widget _buildFilterTab(String id, String label) {
|
||||||
final bool isSelected = _jobType == id;
|
final bool isSelected = _jobType == id;
|
||||||
|
|||||||
Reference in New Issue
Block a user