Merge pull request #384 from Oloodi/feature/centralized-data-error-handling

refactor: centralize data connect error handling and stabilize mobile applications
This commit is contained in:
Achintha Isuru
2026-02-06 10:05:21 -05:00
committed by GitHub
150 changed files with 1506 additions and 2547 deletions

View File

@@ -1,4 +1,4 @@
library client_authentication;
library;
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:flutter_modular/flutter_modular.dart';

View File

@@ -21,9 +21,9 @@ import '../../domain/repositories/auth_repository_interface.dart';
///
/// This implementation integrates with Firebase Authentication for user
/// identity management and Krow's Data Connect SDK for storing user profile data.
class AuthRepositoryImpl implements AuthRepositoryInterface {
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
class AuthRepositoryImpl
with dc.DataErrorHandler
implements AuthRepositoryInterface {
/// Creates an [AuthRepositoryImpl] with the real dependencies.
AuthRepositoryImpl({
@@ -31,6 +31,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required dc.ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
@override
Future<domain.User> signInWithEmail({
@@ -221,16 +223,12 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
}
/// Checks if a user with BUSINESS role exists in PostgreSQL.
Future<bool> _checkBusinessUserExists(String firebaseUserId) async {
try {
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await _dataConnect.getUserById(id: firebaseUserId).execute();
final dc.GetUserByIdUser? user = response.data?.user;
return user != null && user.userRole == 'BUSINESS';
} catch (e) {
developer.log('Error checking business user: $e', name: 'AuthRepository');
return false;
}
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user;
return user != null && user.userRole == 'BUSINESS';
}
/// Creates Business and User entities in PostgreSQL for a Firebase user.
@@ -241,38 +239,30 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required void Function(String businessId) onBusinessCreated,
}) async {
// Create Business entity in PostgreSQL
final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables> createBusinessResponse =
await _dataConnect.createBusiness(
await executeProtected(() => _dataConnect.createBusiness(
businessName: companyName,
userId: firebaseUser.uid,
rateGroup: dc.BusinessRateGroup.STANDARD,
status: dc.BusinessStatus.PENDING,
).execute();
).execute());
final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert;
if (businessData == null) {
throw const SignUpFailedException(
technicalMessage: 'Business creation failed in PostgreSQL',
);
}
final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert;
onBusinessCreated(businessData.id);
// Create User entity in PostgreSQL
final OperationResult<dc.CreateUserData, dc.CreateUserVariables> createUserResponse =
await _dataConnect.createUser(
await executeProtected(() => _dataConnect.createUser(
id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
.email(email)
.userRole('BUSINESS')
.execute();
.execute());
final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert;
if (newUserData == null) {
throw const SignUpFailedException(
technicalMessage: 'User profile creation failed in PostgreSQL',
);
}
final dc.CreateUserUserInsert newUserData = createUserResponse.data.user_insert;
return _getUserProfile(
firebaseUserId: firebaseUser.uid,
@@ -323,8 +313,9 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required String? fallbackEmail,
bool requireBusinessRole = false,
}) async {
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response = await _dataConnect.getUserById(id: firebaseUserId).execute();
final dc.GetUserByIdUser? user = response.data?.user;
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user;
if (user == null) {
throw UserNotFoundException(
technicalMessage: 'Firebase UID $firebaseUserId not found in users table',
@@ -351,9 +342,10 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
role: user.role.stringValue,
);
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> businessResponse = await _dataConnect.getBusinessesByUserId(
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> businessResponse =
await executeProtected(() => _dataConnect.getBusinessesByUserId(
userId: firebaseUserId,
).execute();
).execute());
final dc.GetBusinessesByUserIdBusinesses? business = businessResponse.data.businesses.isNotEmpty
? businessResponse.data.businesses.first
: null;

View File

@@ -2,14 +2,14 @@ import 'package:krow_core/core.dart';
/// Arguments for the [SignInWithEmailUseCase].
class SignInWithEmailArguments extends UseCaseArgument {
const SignInWithEmailArguments({required this.email, required this.password});
/// The user's email address.
final String email;
/// The user's password.
final String password;
const SignInWithEmailArguments({required this.email, required this.password});
@override
List<Object?> get props => <Object?>[email, password];
}

View File

@@ -2,10 +2,10 @@ import 'package:krow_core/core.dart';
/// Arguments for the [SignInWithSocialUseCase].
class SignInWithSocialArguments extends UseCaseArgument {
/// The social provider name (e.g. 'google' or 'apple').
final String provider;
const SignInWithSocialArguments({required this.provider});
/// The social provider name (e.g. 'google' or 'apple').
final String provider;
@override
List<Object?> get props => <Object?>[provider];

View File

@@ -2,6 +2,12 @@ import 'package:krow_core/core.dart';
/// Arguments for the [SignUpWithEmailUseCase].
class SignUpWithEmailArguments extends UseCaseArgument {
const SignUpWithEmailArguments({
required this.companyName,
required this.email,
required this.password,
});
/// The name of the company.
final String companyName;
@@ -11,12 +17,6 @@ class SignUpWithEmailArguments extends UseCaseArgument {
/// The user's password.
final String password;
const SignUpWithEmailArguments({
required this.companyName,
required this.email,
required this.password,
});
@override
List<Object?> get props => <Object?>[companyName, email, password];
}

View File

@@ -9,9 +9,9 @@ import '../repositories/auth_repository_interface.dart';
/// via email/password credentials.
class SignInWithEmailUseCase
implements UseCase<SignInWithEmailArguments, User> {
final AuthRepositoryInterface _repository;
const SignInWithEmailUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the sign-in operation.
@override

View File

@@ -6,9 +6,9 @@ import '../repositories/auth_repository_interface.dart';
/// Use case for signing in a client via social providers (Google/Apple).
class SignInWithSocialUseCase
implements UseCase<SignInWithSocialArguments, User> {
final AuthRepositoryInterface _repository;
const SignInWithSocialUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the social sign-in operation.
@override

View File

@@ -6,9 +6,9 @@ import '../repositories/auth_repository_interface.dart';
/// This use case handles the termination of the user's session and
/// clearing of any local authentication tokens.
class SignOutUseCase implements NoInputUseCase<void> {
final AuthRepositoryInterface _repository;
const SignOutUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the sign-out operation.
@override

View File

@@ -9,9 +9,9 @@ import '../repositories/auth_repository_interface.dart';
/// email, password, and company details.
class SignUpWithEmailUseCase
implements UseCase<SignUpWithEmailArguments, User> {
final AuthRepositoryInterface _repository;
const SignUpWithEmailUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the sign-up operation.
@override

View File

@@ -25,10 +25,6 @@ import 'client_auth_state.dart';
/// * Session Termination
class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
with BlocErrorHandler<ClientAuthState> {
final SignInWithEmailUseCase _signInWithEmail;
final SignUpWithEmailUseCase _signUpWithEmail;
final SignInWithSocialUseCase _signInWithSocial;
final SignOutUseCase _signOut;
/// Initializes the BLoC with the required use cases and initial state.
ClientAuthBloc({
@@ -46,6 +42,10 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
on<ClientSocialSignInRequested>(_onSocialSignInRequested);
on<ClientSignOutRequested>(_onSignOutRequested);
}
final SignInWithEmailUseCase _signInWithEmail;
final SignUpWithEmailUseCase _signUpWithEmail;
final SignInWithSocialUseCase _signInWithSocial;
final SignOutUseCase _signOut;
/// Handles the [ClientSignInRequested] event.
Future<void> _onSignInRequested(
@@ -57,12 +57,12 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await handleError(
emit: emit,
action: () async {
final user = await _signInWithEmail(
final User user = await _signInWithEmail(
SignInWithEmailArguments(email: event.email, password: event.password),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
@@ -79,7 +79,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await handleError(
emit: emit,
action: () async {
final user = await _signUpWithEmail(
final User user = await _signUpWithEmail(
SignUpWithEmailArguments(
companyName: event.companyName,
email: event.email,
@@ -88,7 +88,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
@@ -105,12 +105,12 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await handleError(
emit: emit,
action: () async {
final user = await _signInWithSocial(
final User user = await _signInWithSocial(
SignInWithSocialArguments(provider: event.provider),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
@@ -130,7 +130,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await _signOut();
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),

View File

@@ -10,10 +10,10 @@ abstract class ClientAuthEvent extends Equatable {
/// Event dispatched when a user attempts to sign in with email and password.
class ClientSignInRequested extends ClientAuthEvent {
final String email;
final String password;
const ClientSignInRequested({required this.email, required this.password});
final String email;
final String password;
@override
List<Object?> get props => <Object?>[email, password];
@@ -21,15 +21,15 @@ class ClientSignInRequested extends ClientAuthEvent {
/// Event dispatched when a user attempts to create a new business account.
class ClientSignUpRequested extends ClientAuthEvent {
final String companyName;
final String email;
final String password;
const ClientSignUpRequested({
required this.companyName,
required this.email,
required this.password,
});
final String companyName;
final String email;
final String password;
@override
List<Object?> get props => <Object?>[companyName, email, password];
@@ -37,9 +37,9 @@ class ClientSignUpRequested extends ClientAuthEvent {
/// Event dispatched for third-party authentication (Google/Apple).
class ClientSocialSignInRequested extends ClientAuthEvent {
final String provider;
const ClientSocialSignInRequested({required this.provider});
final String provider;
@override
List<Object?> get props => <Object?>[provider];

View File

@@ -21,6 +21,12 @@ enum ClientAuthStatus {
/// Represents the state of the client authentication flow.
class ClientAuthState extends Equatable {
const ClientAuthState({
this.status = ClientAuthStatus.initial,
this.user,
this.errorMessage,
});
/// Current status of the authentication process.
final ClientAuthStatus status;
@@ -30,12 +36,6 @@ class ClientAuthState extends Equatable {
/// Optional error message when status is [ClientAuthStatus.error].
final String? errorMessage;
const ClientAuthState({
this.status = ClientAuthStatus.initial,
this.user,
this.errorMessage,
});
/// Creates a copy of this state with the given fields replaced by the new values.
ClientAuthState copyWith({
ClientAuthStatus? status,

View File

@@ -11,7 +11,6 @@ import '../blocs/client_auth_event.dart';
import '../blocs/client_auth_state.dart';
import '../widgets/client_sign_up_page/client_sign_up_form.dart';
import '../widgets/common/auth_divider.dart';
import '../widgets/common/auth_social_button.dart';
/// Page for client users to sign up for a new account.
///

View File

@@ -7,12 +7,6 @@ import 'package:flutter/material.dart';
/// This widget handles user input for email and password and delegates
/// authentication events to the parent via callbacks.
class ClientSignInForm extends StatefulWidget {
/// Callback when the sign-in button is pressed.
final void Function({required String email, required String password})
onSignIn;
/// Whether the authentication is currently loading.
final bool isLoading;
/// Creates a [ClientSignInForm].
const ClientSignInForm({
@@ -20,6 +14,12 @@ class ClientSignInForm extends StatefulWidget {
required this.onSignIn,
this.isLoading = false,
});
/// Callback when the sign-in button is pressed.
final void Function({required String email, required String password})
onSignIn;
/// Whether the authentication is currently loading.
final bool isLoading;
@override
State<ClientSignInForm> createState() => _ClientSignInFormState();

View File

@@ -7,6 +7,13 @@ import 'package:flutter/material.dart';
/// This widget handles user input for company name, email, and password,
/// and delegates registration events to the parent via callbacks.
class ClientSignUpForm extends StatefulWidget {
/// Creates a [ClientSignUpForm].
const ClientSignUpForm({
super.key,
required this.onSignUp,
this.isLoading = false,
});
/// Callback when the sign-up button is pressed.
final void Function({
required String companyName,
@@ -18,13 +25,6 @@ class ClientSignUpForm extends StatefulWidget {
/// Whether the authentication is currently loading.
final bool isLoading;
/// Creates a [ClientSignUpForm].
const ClientSignUpForm({
super.key,
required this.onSignUp,
this.isLoading = false,
});
@override
State<ClientSignUpForm> createState() => _ClientSignUpFormState();
}

View File

@@ -6,11 +6,11 @@ import 'package:flutter/material.dart';
///
/// Displays a horizontal line with text in the middle (e.g., "Or continue with").
class AuthDivider extends StatelessWidget {
/// The text to display in the center of the divider.
final String text;
/// Creates an [AuthDivider].
const AuthDivider({super.key, required this.text});
/// The text to display in the center of the divider.
final String text;
@override
Widget build(BuildContext context) {

View File

@@ -6,14 +6,6 @@ import 'package:flutter/material.dart';
/// This widget wraps [UiButton.secondary] to provide a consistent look and feel
/// for social sign-in/sign-up buttons (e.g., Google, Apple).
class AuthSocialButton extends StatelessWidget {
/// The localizable text to display on the button (e.g., "Continue with Google").
final String text;
/// The icon representing the social provider.
final IconData icon;
/// Callback to execute when the button is tapped.
final VoidCallback onPressed;
/// Creates an [AuthSocialButton].
///
@@ -24,6 +16,14 @@ class AuthSocialButton extends StatelessWidget {
required this.icon,
required this.onPressed,
});
/// The localizable text to display on the button (e.g., "Continue with Google").
final String text;
/// The icon representing the social provider.
final IconData icon;
/// Callback to execute when the button is tapped.
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {

View File

@@ -3,14 +3,14 @@ import 'package:flutter/material.dart';
/// A widget that displays a section title with a leading icon.
class SectionTitle extends StatelessWidget {
const SectionTitle({super.key, required this.title, required this.subtitle});
/// The title of the section.
final String title;
/// The subtitle of the section.
final String subtitle;
const SectionTitle({super.key, required this.title, required this.subtitle});
@override
Widget build(BuildContext context) {
return Column(

View File

@@ -16,13 +16,11 @@ import 'presentation/pages/billing_page.dart';
class BillingModule extends Module {
@override
void binds(Injector i) {
// Mock repositories (TODO: Replace with real implementations)
i.addSingleton<FinancialRepositoryMock>(FinancialRepositoryMock.new);
// Repositories
i.addSingleton<BillingRepository>(
() => BillingRepositoryImpl(
financialRepository: i.get<FinancialRepositoryMock>(),
dataConnect: ExampleConnector.instance,
),
);

View File

@@ -12,28 +12,34 @@ import '../../domain/repositories/billing_repository.dart';
/// It strictly adheres to the Clean Architecture data layer responsibilities:
/// - No business logic (except necessary data transformation/filtering).
/// - Delegates to data sources.
class BillingRepositoryImpl implements BillingRepository {
class BillingRepositoryImpl
with data_connect.DataErrorHandler
implements BillingRepository {
/// Creates a [BillingRepositoryImpl].
///
/// Requires the [financialRepository] to fetch financial data.
BillingRepositoryImpl({
required data_connect.FinancialRepositoryMock financialRepository,
required data_connect.ExampleConnector dataConnect,
}) : _financialRepository = financialRepository,
_dataConnect = dataConnect;
}) : _dataConnect = dataConnect;
final data_connect.FinancialRepositoryMock _financialRepository;
final data_connect.ExampleConnector _dataConnect;
/// Fetches the current bill amount by aggregating open invoices.
@override
@override
Future<double> getCurrentBillAmount() async {
// In a real app, this might be an aggregate query.
// Simulating fetching invoices and summing up.
final List<Invoice> invoices = await _financialRepository.getInvoices(
'current_business',
);
return invoices
final String? businessId =
data_connect.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
return 0.0;
}
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData, data_connect.ListInvoicesByBusinessIdVariables> result = await executeProtected(() => _dataConnect
.listInvoicesByBusinessId(businessId: businessId)
.execute());
return result.data.invoices
.map(_mapInvoice)
.where((Invoice i) => i.status == InvoiceStatus.open)
.fold<double>(
0.0,
@@ -50,25 +56,32 @@ class BillingRepositoryImpl implements BillingRepository {
return <Invoice>[];
}
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
data_connect.ListInvoicesByBusinessIdVariables> result =
await _dataConnect
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData, data_connect.ListInvoicesByBusinessIdVariables> result = await executeProtected(() => _dataConnect
.listInvoicesByBusinessId(
businessId: businessId,
)
.limit(10)
.execute();
.execute());
return result.data.invoices.map(_mapInvoice).toList();
}
/// Fetches pending invoices (Open or Disputed).
@override
@override
Future<List<Invoice>> getPendingInvoices() async {
final List<Invoice> invoices = await _financialRepository.getInvoices(
'current_business',
);
return invoices
final String? businessId =
data_connect.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
return <Invoice>[];
}
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData, data_connect.ListInvoicesByBusinessIdVariables> result = await executeProtected(() => _dataConnect
.listInvoicesByBusinessId(businessId: businessId)
.execute());
return result.data.invoices
.map(_mapInvoice)
.where(
(Invoice i) =>
i.status == InvoiceStatus.open ||
@@ -111,16 +124,13 @@ class BillingRepositoryImpl implements BillingRepository {
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999);
}
final fdc.QueryResult<
data_connect.ListShiftRolesByBusinessAndDatesSummaryData,
data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables> result =
await _dataConnect
final fdc.QueryResult<data_connect.ListShiftRolesByBusinessAndDatesSummaryData, data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables> result = await executeProtected(() => _dataConnect
.listShiftRolesByBusinessAndDatesSummary(
businessId: businessId,
start: _toTimestamp(start),
end: _toTimestamp(end),
)
.execute();
.execute());
final List<data_connect.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
shiftRoles = result.data.shiftRoles;

View File

@@ -1,6 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/models/billing_period.dart';
import '../../domain/usecases/get_current_bill_amount.dart';
import '../../domain/usecases/get_invoice_history.dart';
import '../../domain/usecases/get_pending_invoices.dart';

View File

@@ -1,37 +0,0 @@
import 'package:client_main/src/presentation/blocs/client_main_cubit.dart';
import 'package:client_main/src/presentation/blocs/client_main_state.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockIModularNavigator extends Mock implements IModularNavigator {}
void main() {
group('ClientMainCubit', () {
late MockIModularNavigator navigator;
setUp(() {
navigator = MockIModularNavigator();
when(() => navigator.path).thenReturn('/home');
when(() => navigator.addListener(any())).thenReturn(null);
// Stub addListener to avoid errors when Cubit adds listener
// Note: addListener might be on Modular directly or via some other mechanic,
// but for this unit test we just want to suppress errors if possible or let the Cubit work.
// Actually Modular.to.addListener calls Modular.navigatorDelegate.addListener if it exists?
// Modular.to.addListener uses the internal RouterDelegate.
// Mocking Modular internals is hard.
// Let's rely on the fact that we mocked navigatorDelegate.
Modular.navigatorDelegate = navigator;
});
test('initial state is correct', () {
final ClientMainCubit cubit = ClientMainCubit();
expect(cubit.state, const ClientMainState(currentIndex: 2));
cubit.close();
});
// Note: Testing actual route changes requires more complex Modular mocking
// or integration tests, but the structure allows it.
});
}

View File

@@ -14,6 +14,7 @@ import '../../domain/repositories/client_create_order_repository_interface.dart'
/// It follows the KROW Clean Architecture by keeping the data layer focused
/// on delegation and data mapping, without business logic.
class ClientCreateOrderRepositoryImpl
with dc.DataErrorHandler
implements ClientCreateOrderRepositoryInterface {
ClientCreateOrderRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth,
@@ -74,22 +75,20 @@ class ClientCreateOrderRepositoryImpl
order.date.day,
);
final fdc.Timestamp orderTimestamp = _toTimestamp(orderDateOnly);
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult = await _dataConnect
.createOrder(
businessId: businessId,
orderType: dc.OrderType.ONE_TIME,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.execute();
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
orderResult = await executeProtected(() => _dataConnect
.createOrder(
businessId: businessId,
orderType: dc.OrderType.ONE_TIME,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.execute());
final String? orderId = orderResult.data?.order_insert.id;
if (orderId == null) {
throw Exception('Order creation failed.');
}
final String orderId = orderResult.data.order_insert.id;
final int workersNeeded = order.positions.fold<int>(
0,
@@ -98,29 +97,27 @@ class ClientCreateOrderRepositoryImpl
final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}';
final double shiftCost = _calculateShiftCost(order);
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult = await _dataConnect
.createShift(title: shiftTitle, orderId: orderId)
.date(orderTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.PENDING)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute();
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
shiftResult = await executeProtected(() => _dataConnect
.createShift(title: shiftTitle, orderId: orderId)
.date(orderTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.PENDING)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute());
final String? shiftId = shiftResult.data?.shift_insert.id;
if (shiftId == null) {
throw Exception('Shift creation failed.');
}
final String shiftId = shiftResult.data.shift_insert.id;
for (final domain.OneTimeOrderPosition position in order.positions) {
final DateTime start = _parseTime(order.date, position.startTime);
@@ -135,7 +132,7 @@ class ClientCreateOrderRepositoryImpl
'CreateOneTimeOrder shiftRole: start=${start.toIso8601String()} end=${normalizedEnd.toIso8601String()}',
);
await _dataConnect
await executeProtected(() => _dataConnect
.createShiftRole(
shiftId: shiftId,
roleId: position.role,
@@ -146,13 +143,13 @@ class ClientCreateOrderRepositoryImpl
.hours(hours)
.breakType(_breakDurationFromValue(position.lunchBreak))
.totalValue(totalValue)
.execute();
.execute());
}
await _dataConnect
await executeProtected(() => _dataConnect
.updateOrder(id: orderId, teamHubId: hub.id)
.shifts(fdc.AnyValue(<String>[shiftId]))
.execute();
.execute());
}
@override

View File

@@ -25,7 +25,6 @@ class ClientHomeModule extends Module {
// Repositories
i.addLazySingleton<HomeRepositoryInterface>(
() => HomeRepositoryImpl(
i.get<HomeRepositoryMock>(),
ExampleConnector.instance,
),
);

View File

@@ -1,5 +1,5 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/home_repository_interface.dart';
@@ -8,17 +8,14 @@ import '../../domain/repositories/home_repository_interface.dart';
/// This implementation resides in the data layer and acts as a bridge between the
/// domain layer and the data source (in this case, a mock from data_connect).
class HomeRepositoryImpl implements HomeRepositoryInterface {
final HomeRepositoryMock _mock;
final ExampleConnector _dataConnect;
/// Creates a [HomeRepositoryImpl].
///
/// Requires a [HomeRepositoryMock] to perform data operations.
HomeRepositoryImpl(this._mock, this._dataConnect);
HomeRepositoryImpl(this._dataConnect);
final dc.ExampleConnector _dataConnect;
@override
Future<HomeDashboardData> getDashboardData() async {
final String? businessId = ClientSessionStore.instance.session?.business?.id;
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
return const HomeDashboardData(
weeklySpending: 0,
@@ -38,8 +35,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
final DateTime weekRangeEnd =
DateTime(monday.year, monday.month, monday.day + 13, 23, 59, 59, 999);
final fdc.QueryResult<
GetCompletedShiftsByBusinessIdData,
GetCompletedShiftsByBusinessIdVariables> completedResult =
dc.GetCompletedShiftsByBusinessIdData,
dc.GetCompletedShiftsByBusinessIdVariables> completedResult =
await _dataConnect
.getCompletedShiftsByBusinessId(
businessId: businessId,
@@ -47,16 +44,13 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
dateTo: _toTimestamp(weekRangeEnd),
)
.execute();
print(
'Home spending: businessId=$businessId dateFrom=${weekRangeStart.toIso8601String()} '
'dateTo=${weekRangeEnd.toIso8601String()} shifts=${completedResult.data.shifts.length}',
);
double weeklySpending = 0.0;
double next7DaysSpending = 0.0;
int weeklyShifts = 0;
int next7DaysScheduled = 0;
for (final GetCompletedShiftsByBusinessIdShifts shift
for (final dc.GetCompletedShiftsByBusinessIdShifts shift
in completedResult.data.shifts) {
final DateTime? shiftDate = shift.date?.toDateTime();
if (shiftDate == null) {
@@ -80,8 +74,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
final DateTime end = DateTime(now.year, now.month, now.day, 23, 59, 59, 999);
final fdc.QueryResult<
ListShiftRolesByBusinessAndDateRangeData,
ListShiftRolesByBusinessAndDateRangeVariables> result =
dc.ListShiftRolesByBusinessAndDateRangeData,
dc.ListShiftRolesByBusinessAndDateRangeVariables> result =
await _dataConnect
.listShiftRolesByBusinessAndDateRange(
businessId: businessId,
@@ -89,18 +83,11 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
end: _toTimestamp(end),
)
.execute();
print(
'Home coverage: businessId=$businessId '
'startLocal=${start.toIso8601String()} '
'endLocal=${end.toIso8601String()} '
'startUtc=${_toTimestamp(start).toJson()} '
'endUtc=${_toTimestamp(end).toJson()} '
'shiftRoles=${result.data.shiftRoles.length}',
);
int totalNeeded = 0;
int totalFilled = 0;
for (final ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
in result.data.shiftRoles) {
totalNeeded += shiftRole.count;
totalFilled += shiftRole.assigned ?? 0;
@@ -118,16 +105,16 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
@override
UserSessionData getUserSessionData() {
final (String businessName, String? photoUrl) = _mock.getUserSession();
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
return UserSessionData(
businessName: businessName,
photoUrl: photoUrl,
businessName: session?.business?.businessName ?? '',
photoUrl: null, // Business photo isn't currently in session
);
}
@override
Future<List<ReorderItem>> getRecentReorders() async {
final String? businessId = ClientSessionStore.instance.session?.business?.id;
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
return const <ReorderItem>[];
}
@@ -138,27 +125,20 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
final fdc.Timestamp endTimestamp = _toTimestamp(now);
final fdc.QueryResult<
ListShiftRolesByBusinessDateRangeCompletedOrdersData,
ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result =
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersData,
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result =
await _dataConnect.listShiftRolesByBusinessDateRangeCompletedOrders(
businessId: businessId,
start: startTimestamp,
end: endTimestamp,
).execute();
print(
'Home reorder: completed shiftRoles=${result.data.shiftRoles.length}',
);
return result.data.shiftRoles.map((
ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole,
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole,
) {
print(
'Home reorder item: orderId=${shiftRole.shift.order.id} '
'shiftId=${shiftRole.shiftId} roleId=${shiftRole.roleId} '
'orderType=${shiftRole.shift.order.orderType.stringValue} '
'hours=${shiftRole.hours} count=${shiftRole.count}',
);
final String location =
shiftRole.shift.location ??
shiftRole.shift.locationAddress ??

View File

@@ -2,17 +2,17 @@ import 'package:krow_domain/krow_domain.dart';
/// User session data for the home page.
class UserSessionData {
/// The business name of the logged-in user.
final String businessName;
/// The photo URL of the logged-in user (optional).
final String? photoUrl;
/// Creates a [UserSessionData].
const UserSessionData({
required this.businessName,
this.photoUrl,
});
/// The business name of the logged-in user.
final String businessName;
/// The photo URL of the logged-in user (optional).
final String? photoUrl;
}
/// Interface for the Client Home repository.

View File

@@ -7,10 +7,10 @@ import '../repositories/home_repository_interface.dart';
/// This use case coordinates with the [HomeRepositoryInterface] to retrieve
/// the [HomeDashboardData] required for the dashboard display.
class GetDashboardDataUseCase implements NoInputUseCase<HomeDashboardData> {
final HomeRepositoryInterface _repository;
/// Creates a [GetDashboardDataUseCase].
GetDashboardDataUseCase(this._repository);
final HomeRepositoryInterface _repository;
@override
Future<HomeDashboardData> call() {

View File

@@ -4,10 +4,10 @@ import '../repositories/home_repository_interface.dart';
/// Use case to fetch recent completed shift roles for reorder suggestions.
class GetRecentReordersUseCase implements NoInputUseCase<List<ReorderItem>> {
final HomeRepositoryInterface _repository;
/// Creates a [GetRecentReordersUseCase].
GetRecentReordersUseCase(this._repository);
final HomeRepositoryInterface _repository;
@override
Future<List<ReorderItem>> call() {

View File

@@ -4,10 +4,10 @@ import '../repositories/home_repository_interface.dart';
///
/// Returns the user's business name and photo URL for display in the header.
class GetUserSessionDataUseCase {
final HomeRepositoryInterface _repository;
/// Creates a [GetUserSessionDataUseCase].
GetUserSessionDataUseCase(this._repository);
final HomeRepositoryInterface _repository;
/// Executes the use case to get session data.
UserSessionData call() {

View File

@@ -9,9 +9,6 @@ import 'client_home_state.dart';
/// BLoC responsible for managing the state and business logic of the client home dashboard.
class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
final GetDashboardDataUseCase _getDashboardDataUseCase;
final GetRecentReordersUseCase _getRecentReordersUseCase;
final GetUserSessionDataUseCase _getUserSessionDataUseCase;
ClientHomeBloc({
required GetDashboardDataUseCase getDashboardDataUseCase,
@@ -29,6 +26,9 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
add(ClientHomeStarted());
}
final GetDashboardDataUseCase _getDashboardDataUseCase;
final GetRecentReordersUseCase _getRecentReordersUseCase;
final GetUserSessionDataUseCase _getUserSessionDataUseCase;
Future<void> _onStarted(
ClientHomeStarted event,
@@ -83,7 +83,7 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState> {
Emitter<ClientHomeState> emit,
) {
final List<String> newList = List<String>.from(state.widgetOrder);
int oldIndex = event.oldIndex;
final int oldIndex = event.oldIndex;
int newIndex = event.newIndex;
if (oldIndex < newIndex) {

View File

@@ -12,17 +12,17 @@ class ClientHomeStarted extends ClientHomeEvent {}
class ClientHomeEditModeToggled extends ClientHomeEvent {}
class ClientHomeWidgetVisibilityToggled extends ClientHomeEvent {
final String widgetId;
const ClientHomeWidgetVisibilityToggled(this.widgetId);
final String widgetId;
@override
List<Object?> get props => <Object?>[widgetId];
}
class ClientHomeWidgetReordered extends ClientHomeEvent {
const ClientHomeWidgetReordered(this.oldIndex, this.newIndex);
final int oldIndex;
final int newIndex;
const ClientHomeWidgetReordered(this.oldIndex, this.newIndex);
@override
List<Object?> get props => <Object?>[oldIndex, newIndex];

View File

@@ -6,15 +6,6 @@ enum ClientHomeStatus { initial, loading, success, error }
/// Represents the state of the client home dashboard.
class ClientHomeState extends Equatable {
final ClientHomeStatus status;
final List<String> widgetOrder;
final Map<String, bool> widgetVisibility;
final bool isEditMode;
final String? errorMessage;
final HomeDashboardData dashboardData;
final List<ReorderItem> reorderItems;
final String businessName;
final String? photoUrl;
const ClientHomeState({
this.status = ClientHomeStatus.initial,
@@ -46,6 +37,15 @@ class ClientHomeState extends Equatable {
this.businessName = 'Your Company',
this.photoUrl,
});
final ClientHomeStatus status;
final List<String> widgetOrder;
final Map<String, bool> widgetVisibility;
final bool isEditMode;
final String? errorMessage;
final HomeDashboardData dashboardData;
final List<ReorderItem> reorderItems;
final String businessName;
final String? photoUrl;
ClientHomeState copyWith({
ClientHomeStatus? status,

View File

@@ -4,14 +4,6 @@ import 'package:flutter/material.dart';
/// A widget that displays quick actions for the client.
class ActionsWidget extends StatelessWidget {
/// Callback when RAPID is pressed.
final VoidCallback onRapidPressed;
/// Callback when Create Order is pressed.
final VoidCallback onCreateOrderPressed;
/// Optional subtitle for the section.
final String? subtitle;
/// Creates an [ActionsWidget].
const ActionsWidget({
@@ -20,6 +12,14 @@ class ActionsWidget extends StatelessWidget {
required this.onCreateOrderPressed,
this.subtitle,
});
/// Callback when RAPID is pressed.
final VoidCallback onRapidPressed;
/// Callback when Create Order is pressed.
final VoidCallback onCreateOrderPressed;
/// Optional subtitle for the section.
final String? subtitle;
@override
Widget build(BuildContext context) {
@@ -69,16 +69,6 @@ class ActionsWidget extends StatelessWidget {
}
class _ActionCard extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color color;
final Color borderColor;
final Color iconBgColor;
final Color iconColor;
final Color textColor;
final Color subtitleColor;
final VoidCallback onTap;
const _ActionCard({
required this.title,
@@ -92,6 +82,16 @@ class _ActionCard extends StatelessWidget {
required this.subtitleColor,
required this.onTap,
});
final String title;
final String subtitle;
final IconData icon;
final Color color;
final Color borderColor;
final Color iconBgColor;
final Color iconColor;
final Color textColor;
final Color subtitleColor;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {

View File

@@ -10,14 +10,14 @@ import '../blocs/client_home_state.dart';
/// Shows instructions for reordering widgets and provides a reset button
/// to restore the default layout.
class ClientHomeEditBanner extends StatelessWidget {
/// The internationalization object for localized strings.
final dynamic i18n;
/// Creates a [ClientHomeEditBanner].
const ClientHomeEditBanner({
required this.i18n,
super.key,
});
/// The internationalization object for localized strings.
final dynamic i18n;
@override
Widget build(BuildContext context) {

View File

@@ -13,14 +13,14 @@ import 'header_icon_button.dart';
/// Displays the user's business name, avatar, and action buttons
/// (edit mode, notifications, settings).
class ClientHomeHeader extends StatelessWidget {
/// The internationalization object for localized strings.
final dynamic i18n;
/// Creates a [ClientHomeHeader].
const ClientHomeHeader({
required this.i18n,
super.key,
});
/// The internationalization object for localized strings.
final dynamic i18n;
@override
Widget build(BuildContext context) {

View File

@@ -3,11 +3,6 @@ import 'package:flutter/material.dart';
/// A dashboard widget that displays today's coverage status.
class CoverageDashboard extends StatelessWidget {
/// The list of shifts for today.
final List<dynamic> shifts;
/// The list of applications for today's shifts.
final List<dynamic> applications;
/// Creates a [CoverageDashboard].
const CoverageDashboard({
@@ -15,6 +10,11 @@ class CoverageDashboard extends StatelessWidget {
required this.shifts,
required this.applications,
});
/// The list of shifts for today.
final List<dynamic> shifts;
/// The list of applications for today's shifts.
final List<dynamic> applications;
@override
Widget build(BuildContext context) {
@@ -145,12 +145,6 @@ class CoverageDashboard extends StatelessWidget {
}
class _StatusCard extends StatelessWidget {
final String label;
final String value;
final IconData icon;
final bool isWarning;
final bool isError;
final bool isInfo;
const _StatusCard({
required this.label,
@@ -160,6 +154,12 @@ class _StatusCard extends StatelessWidget {
this.isError = false,
this.isInfo = false,
});
final String label;
final String value;
final IconData icon;
final bool isWarning;
final bool isError;
final bool isInfo;
@override
Widget build(BuildContext context) {

View File

@@ -3,6 +3,15 @@ import 'package:flutter/material.dart';
/// A widget that displays the daily coverage metrics.
class CoverageWidget extends StatelessWidget {
/// Creates a [CoverageWidget].
const CoverageWidget({
super.key,
this.totalNeeded = 0,
this.totalConfirmed = 0,
this.coveragePercent = 0,
this.subtitle,
});
/// The total number of shifts needed.
final int totalNeeded;
@@ -15,15 +24,6 @@ class CoverageWidget extends StatelessWidget {
/// Optional subtitle for the section.
final String? subtitle;
/// Creates a [CoverageWidget].
const CoverageWidget({
super.key,
this.totalNeeded = 0,
this.totalConfirmed = 0,
this.coveragePercent = 0,
this.subtitle,
});
@override
Widget build(BuildContext context) {
Color backgroundColor;
@@ -114,11 +114,6 @@ class CoverageWidget extends StatelessWidget {
}
class _MetricCard extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String label;
final String value;
final Color? valueColor;
const _MetricCard({
required this.icon,
@@ -127,6 +122,11 @@ class _MetricCard extends StatelessWidget {
required this.value,
this.valueColor,
});
final IconData icon;
final Color iconColor;
final String label;
final String value;
final Color? valueColor;
@override
Widget build(BuildContext context) {

View File

@@ -16,14 +16,6 @@ import 'client_home_sheets.dart';
/// This widget encapsulates the logic for rendering different dashboard
/// widgets based on their unique identifiers and current state.
class DashboardWidgetBuilder extends StatelessWidget {
/// The unique identifier for the widget to build.
final String id;
/// The current dashboard state.
final ClientHomeState state;
/// Whether the widget is in edit mode.
final bool isEditMode;
/// Creates a [DashboardWidgetBuilder].
const DashboardWidgetBuilder({
@@ -32,6 +24,14 @@ class DashboardWidgetBuilder extends StatelessWidget {
required this.isEditMode,
super.key,
});
/// The unique identifier for the widget to build.
final String id;
/// The current dashboard state.
final ClientHomeState state;
/// Whether the widget is in edit mode.
final bool isEditMode;
@override
Widget build(BuildContext context) {

View File

@@ -9,6 +9,15 @@ import '../blocs/client_home_event.dart';
/// Displays drag handles, visibility toggles, and wraps the actual widget
/// content with appropriate styling for the edit state.
class DraggableWidgetWrapper extends StatelessWidget {
/// Creates a [DraggableWidgetWrapper].
const DraggableWidgetWrapper({
required this.id,
required this.title,
required this.child,
required this.isVisible,
super.key,
});
/// The unique identifier for this widget.
final String id;
@@ -21,15 +30,6 @@ class DraggableWidgetWrapper extends StatelessWidget {
/// Whether this widget is currently visible.
final bool isVisible;
/// Creates a [DraggableWidgetWrapper].
const DraggableWidgetWrapper({
required this.id,
required this.title,
required this.child,
required this.isVisible,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(

View File

@@ -6,6 +6,15 @@ import 'package:flutter/material.dart';
/// Supports an optional badge for notification counts and an active state
/// for toggled actions.
class HeaderIconButton extends StatelessWidget {
/// Creates a [HeaderIconButton].
const HeaderIconButton({
required this.icon,
this.badgeText,
this.isActive = false,
required this.onTap,
super.key,
});
/// The icon to display.
final IconData icon;
@@ -18,15 +27,6 @@ class HeaderIconButton extends StatelessWidget {
/// Callback invoked when the button is tapped.
final VoidCallback onTap;
/// Creates a [HeaderIconButton].
const HeaderIconButton({
required this.icon,
this.badgeText,
this.isActive = false,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return GestureDetector(

View File

@@ -7,11 +7,6 @@ import 'coverage_dashboard.dart';
/// A widget that displays live activity information.
class LiveActivityWidget extends StatefulWidget {
/// Callback when "View all" is pressed.
final VoidCallback onViewAllPressed;
/// Optional subtitle for the section.
final String? subtitle;
/// Creates a [LiveActivityWidget].
const LiveActivityWidget({
@@ -19,6 +14,11 @@ class LiveActivityWidget extends StatefulWidget {
required this.onViewAllPressed,
this.subtitle
});
/// Callback when "View all" is pressed.
final VoidCallback onViewAllPressed;
/// Optional subtitle for the section.
final String? subtitle;
@override
State<LiveActivityWidget> createState() => _LiveActivityWidgetState();
@@ -178,6 +178,16 @@ class _LiveActivityWidgetState extends State<LiveActivityWidget> {
}
class _LiveActivityData {
factory _LiveActivityData.empty() {
return const _LiveActivityData(
totalNeeded: 0,
totalAssigned: 0,
totalCost: 0,
checkedInCount: 0,
lateCount: 0,
);
}
const _LiveActivityData({
required this.totalNeeded,
required this.totalAssigned,
@@ -191,14 +201,4 @@ class _LiveActivityData {
final double totalCost;
final int checkedInCount;
final int lateCount;
factory _LiveActivityData.empty() {
return const _LiveActivityData(
totalNeeded: 0,
totalAssigned: 0,
totalCost: 0,
checkedInCount: 0,
lateCount: 0,
);
}
}

View File

@@ -5,14 +5,6 @@ import 'package:krow_domain/krow_domain.dart';
/// A widget that allows clients to reorder recent shifts.
class ReorderWidget extends StatelessWidget {
/// Recent completed orders for reorder.
final List<ReorderItem> orders;
/// Callback when a reorder button is pressed.
final Function(Map<String, dynamic> shiftData) onReorderPressed;
/// Optional subtitle for the section.
final String? subtitle;
/// Creates a [ReorderWidget].
const ReorderWidget({
@@ -21,6 +13,14 @@ class ReorderWidget extends StatelessWidget {
required this.onReorderPressed,
this.subtitle,
});
/// Recent completed orders for reorder.
final List<ReorderItem> orders;
/// Callback when a reorder button is pressed.
final Function(Map<String, dynamic> shiftData) onReorderPressed;
/// Optional subtitle for the section.
final String? subtitle;
@override
Widget build(BuildContext context) {
@@ -177,11 +177,6 @@ class ReorderWidget extends StatelessWidget {
}
class _Badge extends StatelessWidget {
final IconData icon;
final String text;
final Color color;
final Color bg;
final Color textColor;
const _Badge({
required this.icon,
@@ -190,6 +185,11 @@ class _Badge extends StatelessWidget {
required this.bg,
required this.textColor,
});
final IconData icon;
final String text;
final Color color;
final Color bg;
final Color textColor;
@override
Widget build(BuildContext context) {

View File

@@ -29,14 +29,6 @@ class _VendorOption {
/// This widget provides a comprehensive form matching the design patterns
/// used in view_order_card.dart for consistency across the app.
class ShiftOrderFormSheet extends StatefulWidget {
/// Initial data for the form (e.g. from a reorder action).
final Map<String, dynamic>? initialData;
/// Callback when the form is submitted.
final Function(Map<String, dynamic> data) onSubmit;
/// Whether the submission is loading.
final bool isLoading;
/// Creates a [ShiftOrderFormSheet].
const ShiftOrderFormSheet({
@@ -45,6 +37,14 @@ class ShiftOrderFormSheet extends StatefulWidget {
required this.onSubmit,
this.isLoading = false,
});
/// Initial data for the form (e.g. from a reorder action).
final Map<String, dynamic>? initialData;
/// Callback when the form is submitted.
final Function(Map<String, dynamic> data) onSubmit;
/// Whether the submission is loading.
final bool isLoading;
@override
State<ShiftOrderFormSheet> createState() => _ShiftOrderFormSheetState();
@@ -222,10 +222,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
.date(orderTimestamp)
.execute();
final String? orderId = orderResult.data?.order_insert.id;
if (orderId == null) {
return;
}
final String orderId = orderResult.data.order_insert.id;
final int workersNeeded = _positions.fold<int>(
0,
@@ -255,10 +252,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
.cost(shiftCost)
.execute();
final String? shiftId = shiftResult.data?.shift_insert.id;
if (shiftId == null) {
return;
}
final String shiftId = shiftResult.data.shift_insert.id;
for (final Map<String, dynamic> pos in _positions) {
final String roleId = pos['roleId']?.toString() ?? '';
@@ -415,12 +409,12 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShift firstShift =
shiftRoles.first.shift;
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub?
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub
teamHub = firstShift.order.teamHub;
await _loadHubsAndSelect(
placeId: teamHub?.placeId,
hubName: teamHub?.hubName,
address: teamHub?.address,
placeId: teamHub.placeId,
hubName: teamHub.hubName,
address: teamHub.address,
);
_orderNameController.text = firstShift.order.eventName ?? '';

View File

@@ -4,6 +4,16 @@ import 'package:flutter/material.dart';
/// A widget that displays spending insights for the client.
class SpendingWidget extends StatelessWidget {
/// Creates a [SpendingWidget].
const SpendingWidget({
super.key,
required this.weeklySpending,
required this.next7DaysSpending,
required this.weeklyShifts,
required this.next7DaysScheduled,
this.subtitle,
});
/// The spending this week.
final double weeklySpending;
@@ -19,16 +29,6 @@ class SpendingWidget extends StatelessWidget {
/// Optional subtitle for the section.
final String? subtitle;
/// Creates a [SpendingWidget].
const SpendingWidget({
super.key,
required this.weeklySpending,
required this.next7DaysSpending,
required this.weeklyShifts,
required this.next7DaysScheduled,
this.subtitle,
});
@override
Widget build(BuildContext context) {
final TranslationsClientHomeEn i18n = t.client_home;

View File

@@ -23,7 +23,11 @@ dependencies:
path: ../../../core_localization
krow_domain: ^0.0.1
krow_data_connect: ^0.0.1
krow_core:
path: ../../../core
firebase_data_connect: any
intl: any
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -1,4 +1,4 @@
library client_hubs;
library;
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';

View File

@@ -14,10 +14,11 @@ import 'package:krow_domain/krow_domain.dart'
NotAuthenticatedException;
import '../../domain/repositories/hub_repository_interface.dart';
import '../../util/hubs_constants.dart';
/// Implementation of [HubRepositoryInterface] backed by Data Connect.
class HubRepositoryImpl implements HubRepositoryInterface {
class HubRepositoryImpl
with dc.DataErrorHandler
implements HubRepositoryInterface {
HubRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth,
required dc.ExampleConnector dataConnect,
@@ -57,27 +58,23 @@ class HubRepositoryImpl implements HubRepositoryInterface {
final String? countryValue = country ?? placeAddress?.country;
final String? zipCodeValue = zipCode ?? placeAddress?.zipCode;
final OperationResult<dc.CreateTeamHubData, dc.CreateTeamHubVariables> result = await _dataConnect
.createTeamHub(
teamId: teamId,
hubName: name,
address: address,
)
.placeId(placeId)
.latitude(latitude)
.longitude(longitude)
.city(cityValue?.isNotEmpty == true ? cityValue : '')
.state(stateValue)
.street(streetValue)
.country(countryValue)
.zipCode(zipCodeValue)
.execute();
final String? createdId = result.data?.teamHub_insert.id;
if (createdId == null) {
throw HubCreationFailedException(
technicalMessage: 'teamHub_insert returned null for hub: $name',
);
}
final OperationResult<dc.CreateTeamHubData, dc.CreateTeamHubVariables>
result = await executeProtected(() => _dataConnect
.createTeamHub(
teamId: teamId,
hubName: name,
address: address,
)
.placeId(placeId)
.latitude(latitude)
.longitude(longitude)
.city(cityValue?.isNotEmpty == true ? cityValue : '')
.state(stateValue)
.street(streetValue)
.country(countryValue)
.zipCode(zipCodeValue)
.execute());
final String createdId = result.data.teamHub_insert.id;
final List<domain.Hub> hubs = await _fetchHubsForTeam(
teamId: teamId,
@@ -111,14 +108,14 @@ class HubRepositoryImpl implements HubRepositoryInterface {
);
}
final QueryResult<
dc.ListOrdersByBusinessAndTeamHubData,
dc.ListOrdersByBusinessAndTeamHubVariables> result = await _dataConnect
.listOrdersByBusinessAndTeamHub(
businessId: businessId,
teamHubId: id,
)
.execute();
final QueryResult<dc.ListOrdersByBusinessAndTeamHubData,
dc.ListOrdersByBusinessAndTeamHubVariables> result =
await executeProtected(() => _dataConnect
.listOrdersByBusinessAndTeamHub(
businessId: businessId,
teamHubId: id,
)
.execute());
if (result.data.orders.isNotEmpty) {
throw HubHasOrdersException(
@@ -126,7 +123,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
);
}
await _dataConnect.deleteTeamHub(id: id).execute();
await executeProtected(() => _dataConnect.deleteTeamHub(id: id).execute());
}
@override
@@ -169,9 +166,11 @@ class HubRepositoryImpl implements HubRepositoryInterface {
);
}
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> result = await _dataConnect.getBusinessesByUserId(
userId: user.uid,
).execute();
final QueryResult<dc.GetBusinessesByUserIdData,
dc.GetBusinessesByUserIdVariables> result =
await executeProtected(() => _dataConnect.getBusinessesByUserId(
userId: user.uid,
).execute());
if (result.data.businesses.isEmpty) {
await _firebaseAuth.signOut();
throw BusinessNotFoundException(
@@ -203,9 +202,10 @@ class HubRepositoryImpl implements HubRepositoryInterface {
Future<String> _getOrCreateTeamId(
dc.GetBusinessesByUserIdBusinesses business,
) async {
final QueryResult<dc.GetTeamsByOwnerIdData, dc.GetTeamsByOwnerIdVariables> teamsResult = await _dataConnect.getTeamsByOwnerId(
ownerId: business.id,
).execute();
final QueryResult<dc.GetTeamsByOwnerIdData, dc.GetTeamsByOwnerIdVariables>
teamsResult = await executeProtected(() => _dataConnect.getTeamsByOwnerId(
ownerId: business.id,
).execute());
if (teamsResult.data.teams.isNotEmpty) {
return teamsResult.data.teams.first.id;
}
@@ -220,13 +220,10 @@ class HubRepositoryImpl implements HubRepositoryInterface {
createTeamBuilder.email(business.email);
}
final OperationResult<dc.CreateTeamData, dc.CreateTeamVariables> createTeamResult = await createTeamBuilder.execute();
final String? teamId = createTeamResult.data?.team_insert.id;
if (teamId == null) {
throw HubCreationFailedException(
technicalMessage: 'Team creation failed for business ${business.id}',
);
}
final OperationResult<dc.CreateTeamData, dc.CreateTeamVariables>
createTeamResult =
await executeProtected(() => createTeamBuilder.execute());
final String teamId = createTeamResult.data.team_insert.id;
return teamId;
}
@@ -235,9 +232,11 @@ class HubRepositoryImpl implements HubRepositoryInterface {
required String teamId,
required String businessId,
}) async {
final QueryResult<dc.GetTeamHubsByTeamIdData, dc.GetTeamHubsByTeamIdVariables> hubsResult = await _dataConnect.getTeamHubsByTeamId(
teamId: teamId,
).execute();
final QueryResult<dc.GetTeamHubsByTeamIdData,
dc.GetTeamHubsByTeamIdVariables> hubsResult =
await executeProtected(() => _dataConnect.getTeamHubsByTeamId(
teamId: teamId,
).execute());
return hubsResult.data.teamHubs
.map(
@@ -318,13 +317,13 @@ class HubRepositoryImpl implements HubRepositoryInterface {
}
}
final String? streetValue = <String?>[streetNumber, route]
.where((String? value) => value != null && value!.isNotEmpty)
final String streetValue = <String?>[streetNumber, route]
.where((String? value) => value != null && value.isNotEmpty)
.join(' ')
.trim();
return _PlaceAddress(
street: streetValue?.isEmpty == true ? null : streetValue,
street: streetValue.isEmpty == true ? null : streetValue,
city: city,
state: state,
country: country,

View File

@@ -4,17 +4,17 @@ import 'package:krow_core/core.dart';
///
/// Encapsulates the hub ID and the NFC tag ID to be assigned.
class AssignNfcTagArguments extends UseCaseArgument {
/// Creates an [AssignNfcTagArguments] instance.
///
/// Both [hubId] and [nfcTagId] are required.
const AssignNfcTagArguments({required this.hubId, required this.nfcTagId});
/// The unique identifier of the hub.
final String hubId;
/// The unique identifier of the NFC tag.
final String nfcTagId;
/// Creates an [AssignNfcTagArguments] instance.
///
/// Both [hubId] and [nfcTagId] are required.
const AssignNfcTagArguments({required this.hubId, required this.nfcTagId});
@override
List<Object?> get props => <Object?>[hubId, nfcTagId];
}

View File

@@ -4,20 +4,6 @@ import 'package:krow_core/core.dart';
///
/// Encapsulates the name and address of the hub to be created.
class CreateHubArguments extends UseCaseArgument {
/// The name of the hub.
final String name;
/// The physical address of the hub.
final String address;
final String? placeId;
final double? latitude;
final double? longitude;
final String? city;
final String? state;
final String? street;
final String? country;
final String? zipCode;
/// Creates a [CreateHubArguments] instance.
///
@@ -34,6 +20,20 @@ class CreateHubArguments extends UseCaseArgument {
this.country,
this.zipCode,
});
/// The name of the hub.
final String name;
/// The physical address of the hub.
final String address;
final String? placeId;
final double? latitude;
final double? longitude;
final String? city;
final String? state;
final String? street;
final String? country;
final String? zipCode;
@override
List<Object?> get props => <Object?>[

View File

@@ -4,13 +4,13 @@ import 'package:krow_core/core.dart';
///
/// Encapsulates the hub ID of the hub to be deleted.
class DeleteHubArguments extends UseCaseArgument {
/// The unique identifier of the hub to delete.
final String hubId;
/// Creates a [DeleteHubArguments] instance.
///
/// The [hubId] is required.
const DeleteHubArguments({required this.hubId});
/// The unique identifier of the hub to delete.
final String hubId;
@override
List<Object?> get props => <Object?>[hubId];

View File

@@ -7,12 +7,12 @@ import '../repositories/hub_repository_interface.dart';
/// This use case handles the association of a physical NFC tag with a specific
/// hub by calling the [HubRepositoryInterface].
class AssignNfcTagUseCase implements UseCase<AssignNfcTagArguments, void> {
final HubRepositoryInterface _repository;
/// Creates an [AssignNfcTagUseCase].
///
/// Requires a [HubRepositoryInterface] to interact with the backend.
AssignNfcTagUseCase(this._repository);
final HubRepositoryInterface _repository;
@override
Future<void> call(AssignNfcTagArguments arguments) {

View File

@@ -9,12 +9,12 @@ import '../repositories/hub_repository_interface.dart';
/// [HubRepositoryInterface]. It requires [CreateHubArguments] which includes
/// the name and address of the hub.
class CreateHubUseCase implements UseCase<CreateHubArguments, Hub> {
final HubRepositoryInterface _repository;
/// Creates a [CreateHubUseCase].
///
/// Requires a [HubRepositoryInterface] to perform the actual creation.
CreateHubUseCase(this._repository);
final HubRepositoryInterface _repository;
@override
Future<Hub> call(CreateHubArguments arguments) {

View File

@@ -6,12 +6,12 @@ import '../repositories/hub_repository_interface.dart';
///
/// This use case removes a hub from the system via the [HubRepositoryInterface].
class DeleteHubUseCase implements UseCase<DeleteHubArguments, void> {
final HubRepositoryInterface _repository;
/// Creates a [DeleteHubUseCase].
///
/// Requires a [HubRepositoryInterface] to perform the deletion.
DeleteHubUseCase(this._repository);
final HubRepositoryInterface _repository;
@override
Future<void> call(DeleteHubArguments arguments) {

View File

@@ -7,12 +7,12 @@ import '../repositories/hub_repository_interface.dart';
/// This use case retrieves all hubs associated with the current client
/// by interacting with the [HubRepositoryInterface].
class GetHubsUseCase implements NoInputUseCase<List<Hub>> {
final HubRepositoryInterface _repository;
/// Creates a [GetHubsUseCase].
///
/// Requires a [HubRepositoryInterface] to fetch the data.
GetHubsUseCase(this._repository);
final HubRepositoryInterface _repository;
@override
Future<List<Hub>> call() {

View File

@@ -19,10 +19,6 @@ import 'client_hubs_state.dart';
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
with BlocErrorHandler<ClientHubsState>
implements Disposable {
final GetHubsUseCase _getHubsUseCase;
final CreateHubUseCase _createHubUseCase;
final DeleteHubUseCase _deleteHubUseCase;
final AssignNfcTagUseCase _assignNfcTagUseCase;
ClientHubsBloc({
required GetHubsUseCase getHubsUseCase,
@@ -42,6 +38,10 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
on<ClientHubsAddDialogToggled>(_onAddDialogToggled);
on<ClientHubsIdentifyDialogToggled>(_onIdentifyDialogToggled);
}
final GetHubsUseCase _getHubsUseCase;
final CreateHubUseCase _createHubUseCase;
final DeleteHubUseCase _deleteHubUseCase;
final AssignNfcTagUseCase _assignNfcTagUseCase;
void _onAddDialogToggled(
ClientHubsAddDialogToggled event,
@@ -70,10 +70,10 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
await handleError(
emit: emit,
action: () async {
final hubs = await _getHubsUseCase();
final List<Hub> hubs = await _getHubsUseCase();
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientHubsStatus.failure,
errorMessage: errorKey,
),
@@ -103,7 +103,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
zipCode: event.zipCode,
),
);
final hubs = await _getHubsUseCase();
final List<Hub> hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
@@ -113,7 +113,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
),
);
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),
@@ -130,7 +130,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
emit: emit,
action: () async {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final hubs = await _getHubsUseCase();
final List<Hub> hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
@@ -139,7 +139,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
),
);
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),
@@ -158,7 +158,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
await _assignNfcTagUseCase(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
);
final hubs = await _getHubsUseCase();
final List<Hub> hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
@@ -168,7 +168,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
),
);
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),

View File

@@ -16,16 +16,6 @@ class ClientHubsFetched extends ClientHubsEvent {
/// Event triggered to add a new hub.
class ClientHubsAddRequested extends ClientHubsEvent {
final String name;
final String address;
final String? placeId;
final double? latitude;
final double? longitude;
final String? city;
final String? state;
final String? street;
final String? country;
final String? zipCode;
const ClientHubsAddRequested({
required this.name,
@@ -39,6 +29,16 @@ class ClientHubsAddRequested extends ClientHubsEvent {
this.country,
this.zipCode,
});
final String name;
final String address;
final String? placeId;
final double? latitude;
final double? longitude;
final String? city;
final String? state;
final String? street;
final String? country;
final String? zipCode;
@override
List<Object?> get props => <Object?>[
@@ -57,9 +57,9 @@ class ClientHubsAddRequested extends ClientHubsEvent {
/// Event triggered to delete a hub.
class ClientHubsDeleteRequested extends ClientHubsEvent {
final String hubId;
const ClientHubsDeleteRequested(this.hubId);
final String hubId;
@override
List<Object?> get props => <Object?>[hubId];
@@ -67,13 +67,13 @@ class ClientHubsDeleteRequested extends ClientHubsEvent {
/// Event triggered to assign an NFC tag to a hub.
class ClientHubsNfcTagAssignRequested extends ClientHubsEvent {
final String hubId;
final String nfcTagId;
const ClientHubsNfcTagAssignRequested({
required this.hubId,
required this.nfcTagId,
});
final String hubId;
final String nfcTagId;
@override
List<Object?> get props => <Object?>[hubId, nfcTagId];
@@ -86,9 +86,9 @@ class ClientHubsMessageCleared extends ClientHubsEvent {
/// Event triggered to toggle the visibility of the "Add Hub" dialog.
class ClientHubsAddDialogToggled extends ClientHubsEvent {
final bool visible;
const ClientHubsAddDialogToggled({required this.visible});
final bool visible;
@override
List<Object?> get props => <Object?>[visible];
@@ -96,9 +96,9 @@ class ClientHubsAddDialogToggled extends ClientHubsEvent {
/// Event triggered to toggle the visibility of the "Identify NFC" dialog.
class ClientHubsIdentifyDialogToggled extends ClientHubsEvent {
final Hub? hub;
const ClientHubsIdentifyDialogToggled({this.hub});
final Hub? hub;
@override
List<Object?> get props => <Object?>[hub];

View File

@@ -14,6 +14,15 @@ enum ClientHubsStatus {
/// State class for the ClientHubs BLoC.
class ClientHubsState extends Equatable {
const ClientHubsState({
this.status = ClientHubsStatus.initial,
this.hubs = const <Hub>[],
this.errorMessage,
this.successMessage,
this.showAddHubDialog = false,
this.hubToIdentify,
});
final ClientHubsStatus status;
final List<Hub> hubs;
final String? errorMessage;
@@ -26,15 +35,6 @@ class ClientHubsState extends Equatable {
/// If null, the identification dialog is closed.
final Hub? hubToIdentify;
const ClientHubsState({
this.status = ClientHubsStatus.initial,
this.hubs = const <Hub>[],
this.errorMessage,
this.successMessage,
this.showAddHubDialog = false,
this.hubToIdentify,
});
ClientHubsState copyWith({
ClientHubsStatus? status,
List<Hub>? hubs,

View File

@@ -7,6 +7,13 @@ import 'hub_address_autocomplete.dart';
/// A dialog for adding a new hub.
class AddHubDialog extends StatefulWidget {
/// Creates an [AddHubDialog].
const AddHubDialog({
required this.onCreate,
required this.onCancel,
super.key,
});
/// Callback when the "Create Hub" button is pressed.
final void Function(
String name,
@@ -19,13 +26,6 @@ class AddHubDialog extends StatefulWidget {
/// Callback when the dialog is cancelled.
final VoidCallback onCancel;
/// Creates an [AddHubDialog].
const AddHubDialog({
required this.onCreate,
required this.onCancel,
super.key,
});
@override
State<AddHubDialog> createState() => _AddHubDialogState();
}

View File

@@ -39,7 +39,7 @@ class HubAddressAutocomplete extends StatelessWidget {
);
onSelected?.call(prediction);
},
itemBuilder: (_, _, Prediction prediction) {
itemBuilder: (BuildContext context, int index, Prediction prediction) {
return Padding(
padding: const EdgeInsets.all(UiConstants.space2),
child: Row(

View File

@@ -5,14 +5,6 @@ import 'package:core_localization/core_localization.dart';
/// A card displaying information about a single hub.
class HubCard extends StatelessWidget {
/// The hub to display.
final Hub hub;
/// Callback when the NFC button is pressed.
final VoidCallback onNfcPressed;
/// Callback when the delete button is pressed.
final VoidCallback onDeletePressed;
/// Creates a [HubCard].
const HubCard({
@@ -21,6 +13,14 @@ class HubCard extends StatelessWidget {
required this.onDeletePressed,
super.key,
});
/// The hub to display.
final Hub hub;
/// Callback when the NFC button is pressed.
final VoidCallback onNfcPressed;
/// Callback when the delete button is pressed.
final VoidCallback onDeletePressed;
@override
Widget build(BuildContext context) {

View File

@@ -4,11 +4,11 @@ import 'package:core_localization/core_localization.dart';
/// Widget displayed when there are no hubs.
class HubEmptyState extends StatelessWidget {
/// Callback when the add button is pressed.
final VoidCallback onAddPressed;
/// Creates a [HubEmptyState].
const HubEmptyState({required this.onAddPressed, super.key});
/// Callback when the add button is pressed.
final VoidCallback onAddPressed;
@override
Widget build(BuildContext context) {

View File

@@ -5,14 +5,6 @@ import 'package:core_localization/core_localization.dart';
/// A dialog for identifying and assigning an NFC tag to a hub.
class IdentifyNfcDialog extends StatefulWidget {
/// The hub to assign the tag to.
final Hub hub;
/// Callback when a tag is assigned.
final Function(String nfcTagId) onAssign;
/// Callback when the dialog is cancelled.
final VoidCallback onCancel;
/// Creates an [IdentifyNfcDialog].
const IdentifyNfcDialog({
@@ -21,6 +13,14 @@ class IdentifyNfcDialog extends StatefulWidget {
required this.onCancel,
super.key,
});
/// The hub to assign the tag to.
final Hub hub;
/// Callback when a tag is assigned.
final Function(String nfcTagId) onAssign;
/// Callback when the dialog is cancelled.
final VoidCallback onCancel;
@override
State<IdentifyNfcDialog> createState() => _IdentifyNfcDialogState();

View File

@@ -6,7 +6,9 @@ import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/i_view_orders_repository.dart';
/// Implementation of [IViewOrdersRepository] using Data Connect.
class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
class ViewOrdersRepositoryImpl
with dc.DataErrorHandler
implements IViewOrdersRepository {
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
@@ -29,13 +31,15 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
final fdc.Timestamp startTimestamp = _toTimestamp(_startOfDay(start));
final fdc.Timestamp endTimestamp = _toTimestamp(_endOfDay(end));
final fdc.QueryResult<dc.ListShiftRolesByBusinessAndDateRangeData, dc.ListShiftRolesByBusinessAndDateRangeVariables> result = await _dataConnect
.listShiftRolesByBusinessAndDateRange(
businessId: businessId,
start: startTimestamp,
end: endTimestamp,
)
.execute();
final fdc.QueryResult<dc.ListShiftRolesByBusinessAndDateRangeData,
dc.ListShiftRolesByBusinessAndDateRangeVariables> result =
await executeProtected(() => _dataConnect
.listShiftRolesByBusinessAndDateRange(
businessId: businessId,
start: startTimestamp,
end: endTimestamp,
)
.execute());
print(
'ViewOrders range start=${start.toIso8601String()} end=${end.toIso8601String()} shiftRoles=${result.data.shiftRoles.length}',
);
@@ -101,13 +105,15 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
final fdc.Timestamp dayStart = _toTimestamp(_startOfDay(day));
final fdc.Timestamp dayEnd = _toTimestamp(_endOfDay(day));
final fdc.QueryResult<dc.ListAcceptedApplicationsByBusinessForDayData, dc.ListAcceptedApplicationsByBusinessForDayVariables> result = await _dataConnect
.listAcceptedApplicationsByBusinessForDay(
businessId: businessId,
dayStart: dayStart,
dayEnd: dayEnd,
)
.execute();
final fdc.QueryResult<dc.ListAcceptedApplicationsByBusinessForDayData,
dc.ListAcceptedApplicationsByBusinessForDayVariables> result =
await executeProtected(() => _dataConnect
.listAcceptedApplicationsByBusinessForDay(
businessId: businessId,
dayStart: dayStart,
dayEnd: dayEnd,
)
.execute());
print(
'ViewOrders day=${day.toIso8601String()} applications=${result.data.applications.length}',

View File

@@ -1,82 +0,0 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:view_orders/src/presentation/blocs/view_orders_cubit.dart';
import 'package:view_orders/src/presentation/blocs/view_orders_state.dart';
import 'package:view_orders/src/domain/usecases/get_orders_use_case.dart';
import 'package:view_orders/src/domain/usecases/get_accepted_applications_for_day_use_case.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:view_orders/src/domain/arguments/orders_range_arguments.dart';
import 'package:view_orders/src/domain/arguments/orders_day_arguments.dart';
class MockGetOrdersUseCase extends Mock implements GetOrdersUseCase {}
class MockGetAcceptedAppsUseCase extends Mock implements GetAcceptedApplicationsForDayUseCase {}
void main() {
group('ViewOrdersCubit', () {
late GetOrdersUseCase getOrdersUseCase;
late GetAcceptedApplicationsForDayUseCase getAcceptedAppsUseCase;
setUp(() {
getOrdersUseCase = MockGetOrdersUseCase();
getAcceptedAppsUseCase = MockGetAcceptedAppsUseCase();
registerFallbackValue(OrdersRangeArguments(start: DateTime.now(), end: DateTime.now()));
registerFallbackValue(OrdersDayArguments(day: DateTime.now()));
});
test('initial state is correct', () {
final cubit = ViewOrdersCubit(
getOrdersUseCase: getOrdersUseCase,
getAcceptedAppsUseCase: getAcceptedAppsUseCase,
);
expect(cubit.state.status, ViewOrdersStatus.initial);
cubit.close();
});
blocTest<ViewOrdersCubit, ViewOrdersState>(
'calculates upNextCount based on ALL loaded orders, not just the selected day',
build: () {
final mockOrders = [
// Order 1: Today (Matches selected date)
OrderItem(
id: '1', orderId: '1', title: 'Order 1', clientName: 'Client',
status: 'OPEN', date: '2026-02-04', startTime: '09:00', endTime: '17:00',
location: 'Loc', locationAddress: 'Addr', filled: 0, workersNeeded: 1,
hourlyRate: 20, hours: 8, totalValue: 160
),
// Order 2: Tomorrow (Different date)
OrderItem(
id: '2', orderId: '2', title: 'Order 2', clientName: 'Client',
status: 'OPEN', date: '2026-02-05', startTime: '09:00', endTime: '17:00',
location: 'Loc', locationAddress: 'Addr', filled: 0, workersNeeded: 1,
hourlyRate: 20, hours: 8, totalValue: 160
),
];
when(() => getOrdersUseCase(any())).thenAnswer((_) async => mockOrders);
when(() => getAcceptedAppsUseCase(any())).thenAnswer((_) async => {});
return ViewOrdersCubit(
getOrdersUseCase: getOrdersUseCase,
getAcceptedAppsUseCase: getAcceptedAppsUseCase,
);
},
act: (cubit) async {
// Wait for init to trigger load
await Future.delayed(const Duration(milliseconds: 100));
// Select 'Today' (2026-02-04 matches Order 1)
cubit.selectDate(DateTime(2026, 02, 04));
},
verify: (cubit) {
// Assert:
// 1. filteredOrders should only have 1 order (the one for the selected date)
expect(cubit.state.filteredOrders.length, 1, reason: 'Should only show orders for selected filtered date');
expect(cubit.state.filteredOrders.first.id, '1');
// 2. upNextCount should have 2 orders (Total for the loaded week)
expect(cubit.state.upNextCount, 2, reason: 'Up Next count should include ALL orders in the week range');
},
);
});
}

View File

@@ -10,7 +10,9 @@ import '../../domain/ui_entities/auth_mode.dart';
import '../../domain/repositories/auth_repository_interface.dart';
/// Implementation of [AuthRepositoryInterface].
class AuthRepositoryImpl implements AuthRepositoryInterface {
class AuthRepositoryImpl
with DataErrorHandler
implements AuthRepositoryInterface {
AuthRepositoryImpl({
required this.firebaseAuth,
required this.dataConnect,
@@ -112,31 +114,35 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
}
final QueryResult<GetUserByIdData, GetUserByIdVariables> response =
await dataConnect.getUserById(
id: firebaseUser.uid,
).execute();
await executeProtected(() => dataConnect
.getUserById(
id: firebaseUser.uid,
)
.execute());
final GetUserByIdUser? user = response.data.user;
GetStaffByUserIdStaffs? staffRecord;
if (mode == AuthMode.signup) {
if (user == null) {
await dataConnect
await executeProtected(() => dataConnect
.createUser(
id: firebaseUser.uid,
role: UserBaseRole.USER,
)
.userRole('STAFF')
.execute();
.execute());
} else {
if (user.userRole != 'STAFF') {
await firebaseAuth.signOut();
throw Exception('User is not authorized for this app.');
}
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
staffResponse = await dataConnect.getStaffByUserId(
userId: firebaseUser.uid,
).execute();
staffResponse = await executeProtected(() => dataConnect
.getStaffByUserId(
userId: firebaseUser.uid,
)
.execute());
if (staffResponse.data.staffs.isNotEmpty) {
await firebaseAuth.signOut();
throw Exception(
@@ -155,9 +161,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
}
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
staffResponse = await dataConnect.getStaffByUserId(
userId: firebaseUser.uid,
).execute();
staffResponse = await executeProtected(() => dataConnect
.getStaffByUserId(
userId: firebaseUser.uid,
)
.execute());
if (staffResponse.data.staffs.isEmpty) {
await firebaseAuth.signOut();
throw Exception(

View File

@@ -1,22 +1,24 @@
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/src/session/staff_session_store.dart';
import '../../domain/repositories/clock_in_repository_interface.dart';
/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
final dc.ExampleConnector _dataConnect;
final Map<String, String> _shiftToApplicationId = {};
String? _activeApplicationId;
class ClockInRepositoryImpl
with dc.DataErrorHandler
implements ClockInRepositoryInterface {
ClockInRepositoryImpl({
required dc.ExampleConnector dataConnect,
}) : _dataConnect = dataConnect;
final dc.ExampleConnector _dataConnect;
final Map<String, String> _shiftToApplicationId = <String, String>{};
String? _activeApplicationId;
Future<String> _getStaffId() async {
final StaffSession? session = StaffSessionStore.instance.session;
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
final String? staffId = session?.staff?.id;
if (staffId != null && staffId.isNotEmpty) {
return staffId;
@@ -24,7 +26,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
throw Exception('Staff session not found');
}
/// Helper to convert Data Connect Timestamp to DateTime
/// Helper to convert Data Connect fdc.Timestamp to DateTime
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
DateTime? dt;
@@ -34,7 +36,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
dt = DateTime.tryParse(t);
} else {
try {
if (t is Timestamp) {
if (t is fdc.Timestamp) {
dt = t.toDateTime();
}
} catch (_) {}
@@ -46,9 +48,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
} catch (_) {}
try {
if (dt == null) {
dt = DateTime.tryParse(t.toString());
}
dt ??= DateTime.tryParse(t.toString());
} catch (_) {}
}
@@ -58,13 +58,13 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
return null;
}
/// Helper to create Timestamp from DateTime
Timestamp _fromDateTime(DateTime d) {
// Assuming Timestamp.fromJson takes an ISO string
return Timestamp.fromJson(d.toUtc().toIso8601String());
/// Helper to create fdc.Timestamp from DateTime
fdc.Timestamp _fromDateTime(DateTime d) {
// Assuming fdc.Timestamp.fromJson takes an ISO string
return fdc.Timestamp.fromJson(d.toUtc().toIso8601String());
}
({Timestamp start, Timestamp end}) _utcDayRange(DateTime localDay) {
({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) {
final DateTime dayStartUtc = DateTime.utc(
localDay.year,
localDay.month,
@@ -91,22 +91,24 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
String staffId,
) async {
final DateTime now = DateTime.now();
final range = _utcDayRange(now);
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables>
result = await _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.dayStart(range.start)
.dayEnd(range.end)
.execute();
final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now);
final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables> result = await executeProtected(
() => _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.dayStart(range.start)
.dayEnd(range.end)
.execute(),
);
final apps = result.data.applications;
if (apps.isEmpty) return const [];
final List<dc.GetApplicationsByStaffIdApplications> apps = result.data.applications;
if (apps.isEmpty) return const <dc.GetApplicationsByStaffIdApplications>[];
_shiftToApplicationId
..clear()
..addEntries(apps.map((app) => MapEntry(app.shiftId, app.id)));
..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) => MapEntry(app.shiftId, app.id)));
apps.sort((a, b) {
apps.sort((dc.GetApplicationsByStaffIdApplications a, dc.GetApplicationsByStaffIdApplications b) {
final DateTime? aTime =
_toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date);
final DateTime? bTime =
@@ -122,28 +124,17 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
return apps;
}
dc.GetApplicationsByStaffIdApplications? _getActiveApplication(
List<dc.GetApplicationsByStaffIdApplications> apps,
) {
try {
return apps.firstWhere((app) {
final status = app.status.stringValue;
return status == 'CHECKED_IN' || status == 'LATE';
});
} catch (_) {
return null;
}
}
@override
Future<List<Shift>> getTodaysShifts() async {
final String staffId = await _getStaffId();
final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
if (apps.isEmpty) return const [];
if (apps.isEmpty) return const <Shift>[];
final List<Shift> shifts = [];
for (final app in apps) {
final List<Shift> shifts = <Shift>[];
for (final dc.GetApplicationsByStaffIdApplications app in apps) {
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
@@ -189,7 +180,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
}
dc.GetApplicationsByStaffIdApplications? activeApp;
for (final app in apps) {
for (final dc.GetApplicationsByStaffIdApplications app in apps) {
if (app.checkInTime != null && app.checkOutTime == null) {
if (activeApp == null) {
activeApp = app;
@@ -209,7 +200,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
}
_activeApplicationId = activeApp.id;
print('Active check-in appId=$_activeApplicationId');
return AttendanceStatus(
isCheckedIn: true,
checkInTime: _toDateTime(activeApp.checkInTime),
@@ -227,39 +218,22 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
dc.GetApplicationsByStaffIdApplications? app;
if (cachedAppId != null) {
try {
final apps = await _getTodaysApplications(staffId);
app = apps.firstWhere((a) => a.id == cachedAppId);
final List<dc.GetApplicationsByStaffIdApplications> apps = await _getTodaysApplications(staffId);
app = apps.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.id == cachedAppId);
} catch (_) {}
}
app ??= (await _getTodaysApplications(staffId))
.firstWhere((a) => a.shiftId == shiftId);
.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId);
final Timestamp checkInTs = _fromDateTime(DateTime.now());
print(
'ClockIn request: appId=${app.id} shiftId=$shiftId '
'checkInTime=${checkInTs.toJson()}',
);
try {
await _dataConnect
.updateApplicationStatus(
id: app.id,
)
.checkInTime(checkInTs)
.execute();
_activeApplicationId = app.id;
} catch (e) {
print('ClockIn updateApplicationStatus error: $e');
print('ClockIn error type: ${e.runtimeType}');
try {
final dynamic err = e;
final dynamic details =
err.details ?? err.response ?? err.data ?? err.message;
if (details != null) {
print('ClockIn error details: $details');
}
} catch (_) {}
rethrow;
}
final fdc.Timestamp checkInTs = _fromDateTime(DateTime.now());
await executeProtected(() => _dataConnect
.updateApplicationStatus(
id: app!.id,
)
.checkInTime(checkInTs)
.execute());
_activeApplicationId = app.id;
return getAttendanceStatus();
}
@@ -270,25 +244,18 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
int? breakTimeMinutes,
String? applicationId,
}) async {
final String staffId = await _getStaffId();
await _getStaffId(); // Validate session
print(
'ClockOut request: applicationId=$applicationId '
'activeApplicationId=$_activeApplicationId',
);
final String? targetAppId = applicationId ?? _activeApplicationId;
if (targetAppId == null || targetAppId.isEmpty) {
throw Exception('No active application id for checkout');
}
final appResult = await _dataConnect
final fdc.QueryResult<dc.GetApplicationByIdData, dc.GetApplicationByIdVariables> appResult = await executeProtected(() => _dataConnect
.getApplicationById(id: targetAppId)
.execute();
final app = appResult.data.application;
print(
'ClockOut getApplicationById: id=${app?.id} '
'checkIn=${app?.checkInTime?.toJson()} '
'checkOut=${app?.checkOutTime?.toJson()}',
);
.execute());
final dc.GetApplicationByIdApplication? app = appResult.data.application;
if (app == null) {
throw Exception('Application not found for checkout');
}
@@ -296,12 +263,12 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
throw Exception('No active shift found to clock out');
}
await _dataConnect
await executeProtected(() => _dataConnect
.updateApplicationStatus(
id: targetAppId,
)
.checkOutTime(_fromDateTime(DateTime.now()))
.execute();
.execute());
return getAttendanceStatus();
}

View File

@@ -2,18 +2,18 @@ import 'package:krow_core/core.dart';
/// Represents the arguments required for the [ClockInUseCase].
class ClockInArguments extends UseCaseArgument {
/// The ID of the shift to clock in to.
final String shiftId;
/// Optional notes provided by the user during clock-in.
final String? notes;
/// Creates a [ClockInArguments] instance.
const ClockInArguments({
required this.shiftId,
this.notes,
});
/// The ID of the shift to clock in to.
final String shiftId;
/// Optional notes provided by the user during clock-in.
final String? notes;
@override
List<Object?> get props => [shiftId, notes];
List<Object?> get props => <Object?>[shiftId, notes];
}

View File

@@ -2,6 +2,13 @@ import 'package:krow_core/core.dart';
/// Represents the arguments required for the [ClockOutUseCase].
class ClockOutArguments extends UseCaseArgument {
/// Creates a [ClockOutArguments] instance.
const ClockOutArguments({
this.notes,
this.breakTimeMinutes,
this.applicationId,
});
/// Optional notes provided by the user during clock-out.
final String? notes;
@@ -11,13 +18,6 @@ class ClockOutArguments extends UseCaseArgument {
/// Optional application id for checkout.
final String? applicationId;
/// Creates a [ClockOutArguments] instance.
const ClockOutArguments({
this.notes,
this.breakTimeMinutes,
this.applicationId,
});
@override
List<Object?> get props => [notes, breakTimeMinutes, applicationId];
List<Object?> get props => <Object?>[notes, breakTimeMinutes, applicationId];
}

View File

@@ -5,9 +5,9 @@ import '../arguments/clock_in_arguments.dart';
/// Use case for clocking in a user.
class ClockInUseCase implements UseCase<ClockInArguments, AttendanceStatus> {
final ClockInRepositoryInterface _repository;
ClockInUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<AttendanceStatus> call(ClockInArguments arguments) {

View File

@@ -5,9 +5,9 @@ import '../arguments/clock_out_arguments.dart';
/// Use case for clocking out a user.
class ClockOutUseCase implements UseCase<ClockOutArguments, AttendanceStatus> {
final ClockInRepositoryInterface _repository;
ClockOutUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<AttendanceStatus> call(ClockOutArguments arguments) {

View File

@@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart';
/// Use case for getting the current attendance status (check-in/out times).
class GetAttendanceStatusUseCase implements NoInputUseCase<AttendanceStatus> {
final ClockInRepositoryInterface _repository;
GetAttendanceStatusUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<AttendanceStatus> call() {

View File

@@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart';
/// Use case for retrieving the user's scheduled shifts for today.
class GetTodaysShiftUseCase implements NoInputUseCase<List<Shift>> {
final ClockInRepositoryInterface _repository;
GetTodaysShiftUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<List<Shift>> call() {

View File

@@ -11,13 +11,6 @@ import 'clock_in_event.dart';
import 'clock_in_state.dart';
class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
final GetTodaysShiftUseCase _getTodaysShift;
final GetAttendanceStatusUseCase _getAttendanceStatus;
final ClockInUseCase _clockIn;
final ClockOutUseCase _clockOut;
// Mock Venue Location (e.g., Grand Hotel, NYC)
static const double allowedRadiusMeters = 500;
ClockInBloc({
required GetTodaysShiftUseCase getTodaysShift,
@@ -41,6 +34,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
add(ClockInPageLoaded());
}
final GetTodaysShiftUseCase _getTodaysShift;
final GetAttendanceStatusUseCase _getAttendanceStatus;
final ClockInUseCase _clockIn;
final ClockOutUseCase _clockOut;
// Mock Venue Location (e.g., Grand Hotel, NYC)
static const double allowedRadiusMeters = 500;
Future<void> _onLoaded(
ClockInPageLoaded event,
@@ -48,8 +48,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
) async {
emit(state.copyWith(status: ClockInStatus.loading));
try {
final shifts = await _getTodaysShift();
final status = await _getAttendanceStatus();
final List<Shift> shifts = await _getTodaysShift();
final AttendanceStatus status = await _getAttendanceStatus();
// Check permissions silently on load? Maybe better to wait for user interaction or specific event
// However, if shift exists, we might want to check permission state
@@ -58,7 +58,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
if (status.activeShiftId != null) {
try {
selectedShift =
shifts.firstWhere((s) => s.id == status.activeShiftId);
shifts.firstWhere((Shift s) => s.id == status.activeShiftId);
} catch (_) {}
}
selectedShift ??= shifts.last;
@@ -93,7 +93,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
permission = await Geolocator.requestPermission();
}
final hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
final bool hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
emit(state.copyWith(hasLocationConsent: hasConsent));
@@ -105,9 +105,9 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
}
}
void _startLocationUpdates() async {
Future<void> _startLocationUpdates() async {
try {
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
double distance = 0;
bool isVerified = false; // Require location match by default if shift has location
@@ -195,7 +195,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
emit(state.copyWith(status: ClockInStatus.actionInProgress));
try {
final newStatus = await _clockIn(
final AttendanceStatus newStatus = await _clockIn(
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
);
emit(state.copyWith(
@@ -216,7 +216,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
) async {
emit(state.copyWith(status: ClockInStatus.actionInProgress));
try {
final newStatus = await _clockOut(
final AttendanceStatus newStatus = await _clockOut(
ClockOutArguments(
notes: event.notes,
breakTimeMinutes: 0, // Should be passed from event if supported

View File

@@ -6,75 +6,75 @@ abstract class ClockInEvent extends Equatable {
const ClockInEvent();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
class ClockInPageLoaded extends ClockInEvent {}
class ShiftSelected extends ClockInEvent {
final Shift shift;
const ShiftSelected(this.shift);
final Shift shift;
@override
List<Object?> get props => [shift];
List<Object?> get props => <Object?>[shift];
}
class DateSelected extends ClockInEvent {
final DateTime date;
const DateSelected(this.date);
final DateTime date;
@override
List<Object?> get props => [date];
List<Object?> get props => <Object?>[date];
}
class CheckInRequested extends ClockInEvent {
const CheckInRequested({required this.shiftId, this.notes});
final String shiftId;
final String? notes;
const CheckInRequested({required this.shiftId, this.notes});
@override
List<Object?> get props => [shiftId, notes];
List<Object?> get props => <Object?>[shiftId, notes];
}
class CheckOutRequested extends ClockInEvent {
const CheckOutRequested({this.notes, this.breakTimeMinutes});
final String? notes;
final int? breakTimeMinutes;
const CheckOutRequested({this.notes, this.breakTimeMinutes});
@override
List<Object?> get props => [notes, breakTimeMinutes];
List<Object?> get props => <Object?>[notes, breakTimeMinutes];
}
class CheckInModeChanged extends ClockInEvent {
final String mode;
const CheckInModeChanged(this.mode);
final String mode;
@override
List<Object?> get props => [mode];
List<Object?> get props => <Object?>[mode];
}
class CommuteModeToggled extends ClockInEvent {
final bool isEnabled;
const CommuteModeToggled(this.isEnabled);
final bool isEnabled;
@override
List<Object?> get props => [isEnabled];
List<Object?> get props => <Object?>[isEnabled];
}
class RequestLocationPermission extends ClockInEvent {}
class LocationUpdated extends ClockInEvent {
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
final Position position;
final double distance;
final bool isVerified;
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
@override
List<Object?> get props => [position, distance, isVerified];
List<Object?> get props => <Object?>[position, distance, isVerified];
}

View File

@@ -6,6 +6,22 @@ import 'package:geolocator/geolocator.dart';
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
class ClockInState extends Equatable {
const ClockInState({
this.status = ClockInStatus.initial,
this.todayShifts = const <Shift>[],
this.selectedShift,
this.attendance = const AttendanceStatus(),
required this.selectedDate,
this.checkInMode = 'swipe',
this.errorMessage,
this.currentLocation,
this.distanceFromVenue,
this.isLocationVerified = false,
this.isCommuteModeOn = false,
this.hasLocationConsent = false,
this.etaMinutes,
});
final ClockInStatus status;
final List<Shift> todayShifts;
final Shift? selectedShift;
@@ -21,22 +37,6 @@ class ClockInState extends Equatable {
final bool hasLocationConsent;
final int? etaMinutes;
const ClockInState({
this.status = ClockInStatus.initial,
this.todayShifts = const [],
this.selectedShift,
this.attendance = const AttendanceStatus(),
required this.selectedDate,
this.checkInMode = 'swipe',
this.errorMessage,
this.currentLocation,
this.distanceFromVenue,
this.isLocationVerified = false,
this.isCommuteModeOn = false,
this.hasLocationConsent = false,
this.etaMinutes,
});
ClockInState copyWith({
ClockInStatus? status,
List<Shift>? todayShifts,
@@ -70,7 +70,7 @@ class ClockInState extends Equatable {
}
@override
List<Object?> get props => [
List<Object?> get props => <Object?>[
status,
todayShifts,
selectedShift,

View File

@@ -1,17 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
// --- State ---
class ClockInState extends Equatable {
final bool isLoading;
final bool isLocationVerified;
final String? error;
final Position? currentLocation;
final double? distanceFromVenue;
final bool isClockedIn;
final DateTime? clockInTime;
const ClockInState({
this.isLoading = false,
@@ -22,6 +14,13 @@ class ClockInState extends Equatable {
this.isClockedIn = false,
this.clockInTime,
});
final bool isLoading;
final bool isLocationVerified;
final String? error;
final Position? currentLocation;
final double? distanceFromVenue;
final bool isClockedIn;
final DateTime? clockInTime;
ClockInState copyWith({
bool? isLoading,
@@ -44,7 +43,7 @@ class ClockInState extends Equatable {
}
@override
List<Object?> get props => [
List<Object?> get props => <Object?>[
isLoading,
isLocationVerified,
error,
@@ -56,13 +55,13 @@ class ClockInState extends Equatable {
}
// --- Cubit ---
class ClockInCubit extends Cubit<ClockInState> {
class ClockInCubit extends Cubit<ClockInState> { // 500m radius
ClockInCubit() : super(const ClockInState());
// Mock Venue Location (e.g., Grand Hotel, NYC)
static const double venueLat = 40.7128;
static const double venueLng = -74.0060;
static const double allowedRadiusMeters = 500; // 500m radius
ClockInCubit() : super(const ClockInState());
static const double allowedRadiusMeters = 500;
Future<void> checkLocationPermission() async {
emit(state.copyWith(isLoading: true, error: null));
@@ -95,18 +94,18 @@ class ClockInCubit extends Cubit<ClockInState> {
Future<void> _getCurrentLocation() async {
try {
final position = await Geolocator.getCurrentPosition(
final Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final distance = Geolocator.distanceBetween(
final double distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
venueLat,
venueLng,
);
final isWithinRadius = distance <= allowedRadiusMeters;
final bool isWithinRadius = distance <= allowedRadiusMeters;
emit(state.copyWith(
isLoading: false,

View File

@@ -36,7 +36,7 @@ class _ClockInPageState extends State<ClockInPage> {
return BlocProvider<ClockInBloc>.value(
value: _bloc,
child: BlocConsumer<ClockInBloc, ClockInState>(
listener: (context, state) {
listener: (BuildContext context, ClockInState state) {
if (state.status == ClockInStatus.failure &&
state.errorMessage != null) {
ScaffoldMessenger.of(
@@ -44,7 +44,7 @@ class _ClockInPageState extends State<ClockInPage> {
).showSnackBar(SnackBar(content: Text(state.errorMessage!)));
}
},
builder: (context, state) {
builder: (BuildContext context, ClockInState state) {
if (state.status == ClockInStatus.loading &&
state.todayShifts.isEmpty) {
return const Scaffold(
@@ -52,23 +52,23 @@ class _ClockInPageState extends State<ClockInPage> {
);
}
final todayShifts = state.todayShifts;
final selectedShift = state.selectedShift;
final activeShiftId = state.attendance.activeShiftId;
final List<Shift> todayShifts = state.todayShifts;
final Shift? selectedShift = state.selectedShift;
final String? activeShiftId = state.attendance.activeShiftId;
final bool isActiveSelected =
selectedShift != null && selectedShift.id == activeShiftId;
final checkInTime =
final DateTime? checkInTime =
isActiveSelected ? state.attendance.checkInTime : null;
final checkOutTime =
final DateTime? checkOutTime =
isActiveSelected ? state.attendance.checkOutTime : null;
final isCheckedIn =
final bool isCheckedIn =
state.attendance.isCheckedIn && isActiveSelected;
// Format times for display
final checkInStr = checkInTime != null
final String checkInStr = checkInTime != null
? DateFormat('h:mm a').format(checkInTime)
: '--:-- --';
final checkOutStr = checkOutTime != null
final String checkOutStr = checkOutTime != null
? DateFormat('h:mm a').format(checkOutTime)
: '--:-- --';
@@ -94,7 +94,7 @@ class _ClockInPageState extends State<ClockInPage> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
// Commute Tracker (shows before date selector when applicable)
if (selectedShift != null)
CommuteTracker(
@@ -103,15 +103,15 @@ class _ClockInPageState extends State<ClockInPage> {
isCommuteModeOn: state.isCommuteModeOn,
distanceMeters: state.distanceFromVenue,
etaMinutes: state.etaMinutes,
onCommuteToggled: (value) {
onCommuteToggled: (bool value) {
_bloc.add(CommuteModeToggled(value));
},
),
// Date Selector
DateSelector(
selectedDate: state.selectedDate,
onSelect: (date) => _bloc.add(DateSelected(date)),
shiftDates: [
onSelect: (DateTime date) => _bloc.add(DateSelected(date)),
shiftDates: <String>[
DateFormat('yyyy-MM-dd').format(DateTime.now()),
],
),
@@ -136,7 +136,7 @@ class _ClockInPageState extends State<ClockInPage> {
Column(
children: todayShifts
.map(
(shift) => GestureDetector(
(Shift shift) => GestureDetector(
onTap: () =>
_bloc.add(ShiftSelected(shift)),
child: Container(
@@ -162,12 +162,12 @@ class _ClockInPageState extends State<ClockInPage> {
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
shift.id ==
selectedShift?.id
@@ -208,7 +208,7 @@ class _ClockInPageState extends State<ClockInPage> {
Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
children: <Widget>[
Text(
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
style: const TextStyle(
@@ -236,7 +236,7 @@ class _ClockInPageState extends State<ClockInPage> {
),
// Swipe To Check In / Checked Out State / No Shift State
if (selectedShift != null && checkOutTime == null) ...[
if (selectedShift != null && checkOutTime == null) ...<Widget>[
if (!isCheckedIn &&
!_isCheckInAllowed(selectedShift))
Container(
@@ -247,7 +247,7 @@ class _ClockInPageState extends State<ClockInPage> {
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
children: <Widget>[
const Icon(
LucideIcons.clock,
size: 48,
@@ -296,7 +296,7 @@ class _ClockInPageState extends State<ClockInPage> {
onCheckOut: () {
showDialog(
context: context,
builder: (context) => LunchBreakDialog(
builder: (BuildContext context) => LunchBreakDialog(
onComplete: () {
Navigator.of(
context,
@@ -308,7 +308,7 @@ class _ClockInPageState extends State<ClockInPage> {
},
),
] else if (selectedShift != null &&
checkOutTime != null) ...[
checkOutTime != null) ...<Widget>[
// Shift Completed State
Container(
padding: const EdgeInsets.all(24),
@@ -320,7 +320,7 @@ class _ClockInPageState extends State<ClockInPage> {
), // emerald-200
),
child: Column(
children: [
children: <Widget>[
Container(
width: 48,
height: 48,
@@ -354,7 +354,7 @@ class _ClockInPageState extends State<ClockInPage> {
],
),
),
] else ...[
] else ...<Widget>[
// No Shift State
Container(
width: double.infinity,
@@ -364,8 +364,8 @@ class _ClockInPageState extends State<ClockInPage> {
borderRadius: BorderRadius.circular(16),
),
child: const Column(
children: [
const Text(
children: <Widget>[
Text(
"No confirmed shifts for today",
style: TextStyle(
fontSize: 16,
@@ -374,8 +374,8 @@ class _ClockInPageState extends State<ClockInPage> {
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
const Text(
SizedBox(height: 4),
Text(
"Accept a shift to clock in",
style: TextStyle(
fontSize: 14,
@@ -389,7 +389,7 @@ class _ClockInPageState extends State<ClockInPage> {
],
// Checked In Banner
if (isCheckedIn && checkInTime != null) ...[
if (isCheckedIn && checkInTime != null) ...<Widget>[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
@@ -403,11 +403,11 @@ class _ClockInPageState extends State<ClockInPage> {
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
children: <Widget>[
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
children: <Widget>[
const Text(
"Checked in at",
style: TextStyle(
@@ -468,7 +468,7 @@ class _ClockInPageState extends State<ClockInPage> {
String value,
String currentMode,
) {
final isSelected = currentMode == value;
final bool isSelected = currentMode == value;
return Expanded(
child: GestureDetector(
onTap: () => _bloc.add(CheckInModeChanged(value)),
@@ -478,18 +478,18 @@ class _ClockInPageState extends State<ClockInPage> {
color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(8),
boxShadow: isSelected
? [
? <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
]
: [],
: <BoxShadow>[],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Icon(
icon,
size: 16,
@@ -520,12 +520,12 @@ class _ClockInPageState extends State<ClockInPage> {
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setState) {
builder: (BuildContext context, setState) {
return AlertDialog(
title: Text(scanned ? 'Tag Scanned!' : 'Scan NFC Tag'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
Container(
width: 96,
height: 96,
@@ -559,7 +559,7 @@ class _ClockInPageState extends State<ClockInPage> {
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
),
if (!scanned) ...[
if (!scanned) ...<Widget>[
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
@@ -620,14 +620,14 @@ class _ClockInPageState extends State<ClockInPage> {
if (timeStr.isEmpty) return '';
try {
// Try parsing as ISO string first (which contains date)
final dt = DateTime.parse(timeStr);
final DateTime dt = DateTime.parse(timeStr);
return DateFormat('h:mm a').format(dt);
} catch (_) {
// Fallback for strict "HH:mm" or "HH:mm:ss" strings
try {
final parts = timeStr.split(':');
final List<String> parts = timeStr.split(':');
if (parts.length >= 2) {
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
final DateTime dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
return DateFormat('h:mm a').format(dt);
}
return timeStr;
@@ -638,12 +638,11 @@ class _ClockInPageState extends State<ClockInPage> {
}
bool _isCheckInAllowed(Shift shift) {
if (shift == null) return false;
try {
// Parse shift date (e.g. 2024-01-31T09:00:00)
// The Shift entity has 'date' which is the start DateTime string
final shiftStart = DateTime.parse(shift.startTime);
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
final DateTime shiftStart = DateTime.parse(shift.startTime);
final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateTime.now().isAfter(windowStart);
} catch (e) {
// Fallback: If parsing fails, allow check in to avoid blocking.
@@ -652,10 +651,9 @@ class _ClockInPageState extends State<ClockInPage> {
}
String _getCheckInAvailabilityTime(Shift shift) {
if (shift == null) return '';
try {
final shiftStart = DateTime.parse(shift.startTime.trim());
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
final DateTime shiftStart = DateTime.parse(shift.startTime.trim());
final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateFormat('h:mm a').format(windowStart);
} catch (e) {
return 'soon';

View File

@@ -4,11 +4,6 @@ import 'package:lucide_icons/lucide_icons.dart';
enum AttendanceType { checkin, checkout, breaks, days }
class AttendanceCard extends StatelessWidget {
final AttendanceType type;
final String title;
final String value;
final String subtitle;
final String? scheduledTime;
const AttendanceCard({
super.key,
@@ -18,10 +13,15 @@ class AttendanceCard extends StatelessWidget {
required this.subtitle,
this.scheduledTime,
});
final AttendanceType type;
final String title;
final String value;
final String subtitle;
final String? scheduledTime;
@override
Widget build(BuildContext context) {
final styles = _getStyles(type);
final _AttendanceStyle styles = _getStyles(type);
return Container(
padding: const EdgeInsets.all(12),
@@ -29,7 +29,7 @@ class AttendanceCard extends StatelessWidget {
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -40,7 +40,7 @@ class AttendanceCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
Container(
width: 32,
height: 32,
@@ -72,7 +72,7 @@ class AttendanceCard extends StatelessWidget {
),
),
),
if (scheduledTime != null) ...[
if (scheduledTime != null) ...<Widget>[
const SizedBox(height: 2),
Text(
"Scheduled: $scheduledTime",
@@ -123,13 +123,13 @@ class AttendanceCard extends StatelessWidget {
}
class _AttendanceStyle {
final IconData icon;
final Color bgColor;
final Color iconColor;
_AttendanceStyle({
required this.icon,
required this.bgColor,
required this.iconColor,
});
final IconData icon;
final Color bgColor;
final Color iconColor;
}

View File

@@ -12,13 +12,6 @@ enum CommuteMode {
}
class CommuteTracker extends StatefulWidget {
final Shift? shift;
final Function(CommuteMode)? onModeChange;
final ValueChanged<bool>? onCommuteToggled;
final bool hasLocationConsent;
final bool isCommuteModeOn;
final double? distanceMeters;
final int? etaMinutes;
const CommuteTracker({
super.key,
@@ -30,6 +23,13 @@ class CommuteTracker extends StatefulWidget {
this.distanceMeters,
this.etaMinutes,
});
final Shift? shift;
final Function(CommuteMode)? onModeChange;
final ValueChanged<bool>? onCommuteToggled;
final bool hasLocationConsent;
final bool isCommuteModeOn;
final double? distanceMeters;
final int? etaMinutes;
@override
State<CommuteTracker> createState() => _CommuteTrackerState();
@@ -65,7 +65,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
if (widget.shift == null) return CommuteMode.lockedNoShift;
// For demo purposes, check if we're within 24 hours of shift
final now = DateTime.now();
final DateTime now = DateTime.now();
DateTime shiftStart;
try {
// Try parsing startTime as full datetime first
@@ -81,8 +81,8 @@ class _CommuteTrackerState extends State<CommuteTracker> {
);
}
}
final hoursUntilShift = shiftStart.difference(now).inHours;
final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
final int hoursUntilShift = shiftStart.difference(now).inHours;
final bool inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
if (_localIsCommuteOn) {
// Check if arrived (mock: if distance < 200m)
@@ -102,7 +102,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
}
String _formatDistance(double meters) {
final miles = meters / 1609.34;
final double miles = meters / 1609.34;
return miles < 0.1
? '${meters.round()} m'
: '${miles.toStringAsFixed(1)} mi';
@@ -110,7 +110,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
int _getMinutesUntilShift() {
if (widget.shift == null) return 0;
final now = DateTime.now();
final DateTime now = DateTime.now();
DateTime shiftStart;
try {
// Try parsing startTime as full datetime first
@@ -131,7 +131,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
@override
Widget build(BuildContext context) {
final mode = _getAppMode();
final CommuteMode mode = _getAppMode();
// Notify parent of mode change
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -164,13 +164,13 @@ class _CommuteTrackerState extends State<CommuteTracker> {
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
Color(0xFFEFF6FF), // blue-50
Color(0xFFECFEFF), // cyan-50
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -180,10 +180,10 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Container(
width: 32,
height: 32,
@@ -198,11 +198,11 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
),
const SizedBox(width: 12),
Expanded(
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
children: <Widget>[
Text(
'Enable Commute Tracking?',
style: TextStyle(
fontSize: 14,
@@ -210,7 +210,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
color: Color(0xFF0F172A), // slate-900
),
),
const SizedBox(height: 4),
SizedBox(height: 4),
Text(
'Share location 1hr before shift so your manager can see you\'re on the way.',
style: TextStyle(
@@ -225,7 +225,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
const SizedBox(height: 12),
Row(
children: [
children: <Widget>[
Expanded(
child: OutlinedButton(
onPressed: () {
@@ -268,7 +268,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -277,7 +277,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
],
),
child: Row(
children: [
children: <Widget>[
Container(
width: 32,
height: 32,
@@ -295,9 +295,9 @@ class _CommuteTrackerState extends State<CommuteTracker> {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Row(
children: [
children: <Widget>[
const Text(
'On My Way',
style: TextStyle(
@@ -308,7 +308,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
const SizedBox(width: 8),
Row(
children: [
children: <Widget>[
const Icon(
LucideIcons.clock,
size: 12,
@@ -338,11 +338,11 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
Switch(
value: _localIsCommuteOn,
onChanged: (value) {
onChanged: (bool value) {
setState(() => _localIsCommuteOn = value);
widget.onCommuteToggled?.call(value);
},
activeColor: AppColors.krowBlue,
activeThumbColor: AppColors.krowBlue,
),
],
),
@@ -356,7 +356,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
Color(0xFF2563EB), // blue-600
Color(0xFF0891B2), // cyan-600
],
@@ -364,19 +364,19 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
child: SafeArea(
child: Column(
children: [
children: <Widget>[
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
TweenAnimationBuilder(
tween: Tween<double>(begin: 1.0, end: 1.1),
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
builder: (context, double scale, child) {
builder: (BuildContext context, double scale, Widget? child) {
return Transform.scale(
scale: scale,
child: Container(
@@ -418,7 +418,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
if (widget.distanceMeters != null) ...[
if (widget.distanceMeters != null) ...<Widget>[
Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 300),
@@ -431,7 +431,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
),
child: Column(
children: [
children: <Widget>[
Text(
'Distance to Site',
style: TextStyle(
@@ -451,7 +451,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
],
),
),
if (widget.etaMinutes != null) ...[
if (widget.etaMinutes != null) ...<Widget>[
const SizedBox(height: 12),
Container(
width: double.infinity,
@@ -465,7 +465,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
),
child: Column(
children: [
children: <Widget>[
Text(
'Estimated Arrival',
style: TextStyle(
@@ -530,13 +530,13 @@ class _CommuteTrackerState extends State<CommuteTracker> {
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
Color(0xFFECFDF5), // emerald-50
Color(0xFFD1FAE5), // green-50
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
@@ -545,7 +545,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
],
),
child: Column(
children: [
children: <Widget>[
Container(
width: 64,
height: 64,

View File

@@ -2,21 +2,21 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class DateSelector extends StatelessWidget {
final DateTime selectedDate;
final ValueChanged<DateTime> onSelect;
final List<String> shiftDates;
const DateSelector({
super.key,
required this.selectedDate,
required this.onSelect,
this.shiftDates = const [],
this.shiftDates = const <String>[],
});
final DateTime selectedDate;
final ValueChanged<DateTime> onSelect;
final List<String> shiftDates;
@override
Widget build(BuildContext context) {
final today = DateTime.now();
final dates = List.generate(7, (index) {
final DateTime today = DateTime.now();
final List<DateTime> dates = List.generate(7, (int index) {
return today.add(Duration(days: index - 3));
});
@@ -24,10 +24,10 @@ class DateSelector extends StatelessWidget {
height: 80,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: dates.map((date) {
final isSelected = _isSameDay(date, selectedDate);
final isToday = _isSameDay(date, today);
final hasShift = shiftDates.contains(_formatDateIso(date));
children: dates.map((DateTime date) {
final bool isSelected = _isSameDay(date, selectedDate);
final bool isToday = _isSameDay(date, today);
final bool hasShift = shiftDates.contains(_formatDateIso(date));
return Expanded(
child: GestureDetector(
@@ -39,18 +39,18 @@ class DateSelector extends StatelessWidget {
color: isSelected ? const Color(0xFF0032A0) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: isSelected
? [
? <BoxShadow>[
BoxShadow(
color: const Color(0xFF0032A0).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: [],
: <BoxShadow>[],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Text(
DateFormat('d').format(date),
style: TextStyle(

View File

@@ -3,14 +3,14 @@ import 'package:design_system/design_system.dart';
import 'package:lucide_icons/lucide_icons.dart';
class LocationMapPlaceholder extends StatelessWidget {
final bool isVerified;
final double? distance;
const LocationMapPlaceholder({
super.key,
required this.isVerified,
this.distance,
});
final bool isVerified;
final double? distance;
@override
Widget build(BuildContext context) {
@@ -31,12 +31,12 @@ class LocationMapPlaceholder extends StatelessWidget {
),
),
child: Stack(
children: [
children: <Widget>[
// Fallback UI if image fails (which it will without key)
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Icon(LucideIcons.mapPin, size: 48, color: UiColors.iconSecondary),
SizedBox(height: 8),
Text('Map View (GPS)', style: TextStyle(color: UiColors.textSecondary)),
@@ -54,7 +54,7 @@ class LocationMapPlaceholder extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
@@ -63,7 +63,7 @@ class LocationMapPlaceholder extends StatelessWidget {
],
),
child: Row(
children: [
children: <Widget>[
Icon(
isVerified ? LucideIcons.checkCircle : LucideIcons.alertCircle,
color: isVerified ? UiColors.textSuccess : UiColors.destructive,
@@ -73,7 +73,7 @@ class LocationMapPlaceholder extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
isVerified ? 'Location Verified' : 'Location Check',
style: UiTypography.body1b.copyWith(color: UiColors.textPrimary),

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class LunchBreakDialog extends StatefulWidget {
final VoidCallback onComplete;
const LunchBreakDialog({super.key, required this.onComplete});
final VoidCallback onComplete;
@override
State<LunchBreakDialog> createState() => _LunchBreakDialogState();
@@ -23,7 +23,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
String _additionalNotes = '';
final List<String> _timeOptions = _generateTimeOptions();
final List<String> _noLunchReasons = [
final List<String> _noLunchReasons = <String>[
'Unpredictable Workflows',
'Poor Time Management',
'Lack of coverage or short Staff',
@@ -32,12 +32,12 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
];
static List<String> _generateTimeOptions() {
List<String> options = [];
final List<String> options = <String>[];
for (int h = 0; h < 24; h++) {
for (int m = 0; m < 60; m += 15) {
final hour = h % 12 == 0 ? 12 : h % 12;
final ampm = h < 12 ? 'am' : 'pm';
final timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm';
final int hour = h % 12 == 0 ? 12 : h % 12;
final String ampm = h < 12 ? 'am' : 'pm';
final String timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm';
options.add(timeStr);
}
}
@@ -78,7 +78,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
Container(
width: 80,
height: 80,
@@ -104,7 +104,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
),
const SizedBox(height: 24),
Row(
children: [
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () {
@@ -171,7 +171,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
const Text(
"When did you take lunch?",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
@@ -179,13 +179,13 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
const SizedBox(height: 24),
// Mock Inputs
Row(
children: [
children: <Widget>[
Expanded(
child: DropdownButtonFormField<String>(
isExpanded: true,
value: _breakStart,
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (v) => setState(() => _breakStart = v),
initialValue: _breakStart,
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (String? v) => setState(() => _breakStart = v),
decoration: const InputDecoration(
labelText: 'Start',
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
@@ -196,9 +196,9 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
Expanded(
child: DropdownButtonFormField<String>(
isExpanded: true,
value: _breakEnd,
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (v) => setState(() => _breakEnd = v),
initialValue: _breakEnd,
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (String? v) => setState(() => _breakEnd = v),
decoration: const InputDecoration(
labelText: 'End',
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
@@ -230,17 +230,17 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
children: <Widget>[
const Text(
"Why didn't you take lunch?",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
..._noLunchReasons.map((reason) => RadioListTile<String>(
..._noLunchReasons.map((String reason) => RadioListTile<String>(
title: Text(reason),
value: reason,
groupValue: _noLunchReason,
onChanged: (val) => setState(() => _noLunchReason = val),
onChanged: (String? val) => setState(() => _noLunchReason = val),
)),
const SizedBox(height: 24),
@@ -264,14 +264,14 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
const Text(
"Additional Notes",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextField(
onChanged: (v) => _additionalNotes = v,
onChanged: (String v) => _additionalNotes = v,
decoration: const InputDecoration(
hintText: 'Add any details...',
border: OutlineInputBorder(),
@@ -300,7 +300,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
const Icon(LucideIcons.checkCircle, size: 64, color: Colors.green),
const SizedBox(height: 24),
const Text(

View File

@@ -2,11 +2,6 @@ import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class SwipeToCheckIn extends StatefulWidget {
final VoidCallback? onCheckIn;
final VoidCallback? onCheckOut;
final bool isLoading;
final String mode; // 'swipe' or 'nfc'
final bool isCheckedIn;
const SwipeToCheckIn({
super.key,
@@ -16,6 +11,11 @@ class SwipeToCheckIn extends StatefulWidget {
this.mode = 'swipe',
this.isCheckedIn = false,
});
final VoidCallback? onCheckIn;
final VoidCallback? onCheckOut;
final bool isLoading;
final String mode; // 'swipe' or 'nfc'
final bool isCheckedIn;
@override
State<SwipeToCheckIn> createState() => _SwipeToCheckInState();
@@ -50,7 +50,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
void _onDragEnd(DragEndDetails details, double maxWidth) {
if (_isComplete || widget.isLoading) return;
final threshold = (maxWidth - _handleSize - 8) * 0.8;
final double threshold = (maxWidth - _handleSize - 8) * 0.8;
if (_dragValue > threshold) {
setState(() {
_dragValue = maxWidth - _handleSize - 8;
@@ -72,7 +72,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
@override
Widget build(BuildContext context) {
final baseColor = widget.isCheckedIn
final Color baseColor = widget.isCheckedIn
? const Color(0xFF10B981)
: const Color(0xFF0032A0);
@@ -94,7 +94,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
decoration: BoxDecoration(
color: baseColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: baseColor.withOpacity(0.4),
blurRadius: 25,
@@ -105,7 +105,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
const Icon(LucideIcons.wifi, color: Colors.white),
const SizedBox(width: 12),
Text(
@@ -127,19 +127,19 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
}
return LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final maxDrag = maxWidth - _handleSize - 8;
builder: (BuildContext context, BoxConstraints constraints) {
final double maxWidth = constraints.maxWidth;
final double maxDrag = maxWidth - _handleSize - 8;
// Calculate background color based on drag
final progress = _dragValue / maxDrag;
final startColor = widget.isCheckedIn
final double progress = _dragValue / maxDrag;
final Color startColor = widget.isCheckedIn
? const Color(0xFF10B981)
: const Color(0xFF0032A0);
final endColor = widget.isCheckedIn
final Color endColor = widget.isCheckedIn
? const Color(0xFF0032A0)
: const Color(0xFF10B981);
final currentColor =
final Color currentColor =
Color.lerp(startColor, endColor, progress) ?? startColor;
return Container(
@@ -147,7 +147,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
decoration: BoxDecoration(
color: currentColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
@@ -156,7 +156,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
],
),
child: Stack(
children: [
children: <Widget>[
Center(
child: Opacity(
opacity: 1.0 - progress,
@@ -187,15 +187,15 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
left: 4 + _dragValue,
top: 4,
child: GestureDetector(
onHorizontalDragUpdate: (d) => _onDragUpdate(d, maxWidth),
onHorizontalDragEnd: (d) => _onDragEnd(d, maxWidth),
onHorizontalDragUpdate: (DragUpdateDetails d) => _onDragUpdate(d, maxWidth),
onHorizontalDragEnd: (DragEndDetails d) => _onDragEnd(d, maxWidth),
child: Container(
width: _handleSize,
height: _handleSize,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 2,

View File

@@ -1,4 +1,4 @@
library staff_clock_in;
library;
export 'src/staff_clock_in_module.dart';
export 'src/presentation/pages/clock_in_page.dart';

View File

@@ -11,7 +11,9 @@ extension TimestampExt on Timestamp {
}
}
class HomeRepositoryImpl implements HomeRepository {
class HomeRepositoryImpl
with DataErrorHandler
implements HomeRepository {
HomeRepositoryImpl();
String get _currentStaffId {
@@ -31,33 +33,32 @@ class HomeRepositoryImpl implements HomeRepository {
}
Future<List<Shift>> _getShiftsForDate(DateTime date) async {
try {
final staffId = _currentStaffId;
// Create start and end timestamps for the target date
final DateTime start = DateTime(date.year, date.month, date.day);
final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
final response = await ExampleConnector.instance
.getApplicationsByStaffId(staffId: staffId)
.dayStart(_toTimestamp(start))
.dayEnd(_toTimestamp(end))
.execute();
// Filter for ACCEPTED applications (same logic as shifts_repository_impl)
final apps = response.data.applications.where(
(app) => (app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED) || (app.status is Known && (app.status as Known).value == ApplicationStatus.CONFIRMED)
);
final List<Shift> shifts = [];
for (final app in apps) {
shifts.add(_mapApplicationToShift(app));
}
return shifts;
} catch (e) {
return [];
final staffId = _currentStaffId;
// Create start and end timestamps for the target date
final DateTime start = DateTime(date.year, date.month, date.day);
final DateTime end =
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
final response = await executeProtected(() => ExampleConnector.instance
.getApplicationsByStaffId(staffId: staffId)
.dayStart(_toTimestamp(start))
.dayEnd(_toTimestamp(end))
.execute());
// Filter for ACCEPTED applications (same logic as shifts_repository_impl)
final apps = response.data.applications.where((app) =>
(app.status is Known &&
(app.status as Known).value == ApplicationStatus.ACCEPTED) ||
(app.status is Known &&
(app.status as Known).value == ApplicationStatus.CONFIRMED));
final List<Shift> shifts = [];
for (final app in apps) {
shifts.add(_mapApplicationToShift(app));
}
return shifts;
}
Timestamp _toTimestamp(DateTime dateTime) {
@@ -69,27 +70,24 @@ class HomeRepositoryImpl implements HomeRepository {
@override
Future<List<Shift>> getRecommendedShifts() async {
try {
// Logic: List ALL open shifts (simple recommendation engine)
// Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED.
final response = await ExampleConnector.instance.listShifts().execute();
// Logic: List ALL open shifts (simple recommendation engine)
// Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED.
final response = await executeProtected(() => ExampleConnector.instance.listShifts().execute());
return response.data.shifts
.where((s) {
final isOpen = s.status is Known && (s.status as Known).value == ShiftStatus.OPEN;
if (!isOpen) return false;
return response.data.shifts
.where((s) {
final isOpen =
s.status is Known && (s.status as Known).value == ShiftStatus.OPEN;
if (!isOpen) return false;
final start = s.startTime?.toDate();
if (start == null) return false;
final start = s.startTime?.toDate();
if (start == null) return false;
return start.isAfter(DateTime.now());
})
.take(10)
.map((s) => _mapConnectorShiftToDomain(s))
.toList();
} catch (e) {
return [];
}
return start.isAfter(DateTime.now());
})
.take(10)
.map((s) => _mapConnectorShiftToDomain(s))
.toList();
}
@override

View File

@@ -1,22 +1,22 @@
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_data_connect/src/session/staff_session_store.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:krow_core/core.dart';
import '../../domain/repositories/payments_repository.dart';
class PaymentsRepositoryImpl implements PaymentsRepository {
final dc.ExampleConnector _dataConnect;
final FirebaseAuth _auth = FirebaseAuth.instance;
PaymentsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance;
final dc.ExampleConnector _dataConnect;
final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance;
String? _cachedStaffId;
Future<String> _getStaffId() async {
// 1. Check Session Store
final StaffSession? session = StaffSessionStore.instance.session;
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
if (session?.staff?.id != null) {
return session!.staff!.id;
}
@@ -25,13 +25,13 @@ class PaymentsRepositoryImpl implements PaymentsRepository {
if (_cachedStaffId != null) return _cachedStaffId!;
// 3. Fetch from Data Connect using Firebase UID
final user = _auth.currentUser;
final firebase_auth.User? user = _auth.currentUser;
if (user == null) {
throw Exception('User is not authenticated');
}
try {
final response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (response.data.staffs.isNotEmpty) {
_cachedStaffId = response.data.staffs.first.id;
return _cachedStaffId!;
@@ -66,9 +66,7 @@ class PaymentsRepositoryImpl implements PaymentsRepository {
} catch (_) {}
try {
if (dt == null) {
dt = DateTime.tryParse(t.toString());
}
dt ??= DateTime.tryParse(t.toString());
} catch (_) {}
}

View File

@@ -2,11 +2,11 @@ import 'package:krow_core/core.dart';
/// Arguments for getting payment history.
class GetPaymentHistoryArguments extends UseCaseArgument {
const GetPaymentHistoryArguments(this.period);
/// The period to filter by (e.g., "monthly", "weekly").
final String period;
const GetPaymentHistoryArguments(this.period);
@override
List<Object?> get props => [period];
List<Object?> get props => <Object?>[period];
}

View File

@@ -7,10 +7,10 @@ import '../repositories/payments_repository.dart';
///
/// This use case delegates the data retrieval to [PaymentsRepository].
class GetPaymentHistoryUseCase extends UseCase<GetPaymentHistoryArguments, List<StaffPayment>> {
final PaymentsRepository repository;
/// Creates a [GetPaymentHistoryUseCase].
GetPaymentHistoryUseCase(this.repository);
final PaymentsRepository repository;
@override
Future<List<StaffPayment>> call(GetPaymentHistoryArguments arguments) async {

View File

@@ -4,10 +4,10 @@ import '../repositories/payments_repository.dart';
/// Use case to retrieve payment summary information.
class GetPaymentSummaryUseCase extends NoInputUseCase<PaymentSummary> {
final PaymentsRepository repository;
/// Creates a [GetPaymentSummaryUseCase].
GetPaymentSummaryUseCase(this.repository);
final PaymentsRepository repository;
@override
Future<PaymentSummary> call() async {

View File

@@ -1,6 +1,6 @@
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'domain/repositories/payments_repository.dart';
import 'domain/usecases/get_payment_summary_usecase.dart';
import 'domain/usecases/get_payment_history_usecase.dart';
@@ -26,7 +26,7 @@ class StaffPaymentsModule extends Module {
void routes(RouteManager r) {
r.child(
StaffPaths.childRoute(StaffPaths.payments, StaffPaths.payments),
child: (context) => const PaymentsPage(),
child: (BuildContext context) => const PaymentsPage(),
);
}
}

View File

@@ -7,8 +7,6 @@ import 'payments_event.dart';
import 'payments_state.dart';
class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
final GetPaymentSummaryUseCase getPaymentSummary;
final GetPaymentHistoryUseCase getPaymentHistory;
PaymentsBloc({
required this.getPaymentSummary,
@@ -17,6 +15,8 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
on<LoadPaymentsEvent>(_onLoadPayments);
on<ChangePeriodEvent>(_onChangePeriod);
}
final GetPaymentSummaryUseCase getPaymentSummary;
final GetPaymentHistoryUseCase getPaymentHistory;
Future<void> _onLoadPayments(
LoadPaymentsEvent event,

View File

@@ -1,20 +1,19 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
abstract class PaymentsEvent extends Equatable {
const PaymentsEvent();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
class LoadPaymentsEvent extends PaymentsEvent {}
class ChangePeriodEvent extends PaymentsEvent {
final String period;
const ChangePeriodEvent(this.period);
final String period;
@override
List<Object?> get props => [period];
List<Object?> get props => <Object?>[period];
}

View File

@@ -5,7 +5,7 @@ abstract class PaymentsState extends Equatable {
const PaymentsState();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
class PaymentsInitial extends PaymentsState {}
@@ -13,15 +13,15 @@ class PaymentsInitial extends PaymentsState {}
class PaymentsLoading extends PaymentsState {}
class PaymentsLoaded extends PaymentsState {
final PaymentSummary summary;
final List<StaffPayment> history;
final String activePeriod;
const PaymentsLoaded({
required this.summary,
required this.history,
this.activePeriod = 'week',
});
final PaymentSummary summary;
final List<StaffPayment> history;
final String activePeriod;
PaymentsLoaded copyWith({
PaymentSummary? summary,
@@ -36,14 +36,14 @@ class PaymentsLoaded extends PaymentsState {
}
@override
List<Object?> get props => [summary, history, activePeriod];
List<Object?> get props => <Object?>[summary, history, activePeriod];
}
class PaymentsError extends PaymentsState {
final String message;
const PaymentsError(this.message);
final String message;
@override
List<Object?> get props => [message];
List<Object?> get props => <Object?>[message];
}

View File

@@ -1,10 +1,6 @@
import 'package:equatable/equatable.dart';
class PaymentStats extends Equatable {
final double weeklyEarnings;
final double monthlyEarnings;
final double pendingEarnings;
final double totalEarnings;
const PaymentStats({
this.weeklyEarnings = 0.0,
@@ -12,9 +8,13 @@ class PaymentStats extends Equatable {
this.pendingEarnings = 0.0,
this.totalEarnings = 0.0,
});
final double weeklyEarnings;
final double monthlyEarnings;
final double pendingEarnings;
final double totalEarnings;
@override
List<Object?> get props => [
List<Object?> get props => <Object?>[
weeklyEarnings,
monthlyEarnings,
pendingEarnings,

View File

@@ -177,7 +177,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
// Recent Payments
if (state.history.isNotEmpty) Column(
children: [
children: <Widget>[
const Text(
"Recent Payments",
style: TextStyle(

View File

@@ -4,21 +4,21 @@ import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
class EarningsGraph extends StatelessWidget {
final List<StaffPayment> payments;
final String period;
const EarningsGraph({
super.key,
required this.payments,
required this.period,
});
final List<StaffPayment> payments;
final String period;
@override
Widget build(BuildContext context) {
// Basic data processing for the graph
// We'll aggregate payments by date
final validPayments = payments.where((p) => p.paidAt != null).toList()
..sort((a, b) => a.paidAt!.compareTo(b.paidAt!));
final List<StaffPayment> validPayments = payments.where((StaffPayment p) => p.paidAt != null).toList()
..sort((StaffPayment a, StaffPayment b) => a.paidAt!.compareTo(b.paidAt!));
// If no data, show empty state or simple placeholder
if (validPayments.isEmpty) {
@@ -32,9 +32,9 @@ class EarningsGraph extends StatelessWidget {
);
}
final spots = _generateSpots(validPayments);
final maxX = spots.isNotEmpty ? spots.last.x : 0.0;
final maxY = spots.isNotEmpty ? spots.map((s) => s.y).reduce((a, b) => a > b ? a : b) : 0.0;
final List<FlSpot> spots = _generateSpots(validPayments);
final double maxX = spots.isNotEmpty ? spots.last.x : 0.0;
final double maxY = spots.isNotEmpty ? spots.map((FlSpot s) => s.y).reduce((double a, double b) => a > b ? a : b) : 0.0;
return Container(
height: 220,
@@ -42,7 +42,7 @@ class EarningsGraph extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 4),
@@ -52,7 +52,7 @@ class EarningsGraph extends StatelessWidget {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
const Text(
"Earnings Trend",
style: TextStyle(
@@ -70,10 +70,10 @@ class EarningsGraph extends StatelessWidget {
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
getTitlesWidget: (double value, TitleMeta meta) {
// Simple logic to show a few dates
if (value % 2 != 0) return const SizedBox();
final index = value.toInt();
final int index = value.toInt();
if (index >= 0 && index < validPayments.length) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
@@ -92,7 +92,7 @@ class EarningsGraph extends StatelessWidget {
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
lineBarsData: [
lineBarsData: <LineChartBarData>[
LineChartBarData(
spots: spots,
isCurved: true,
@@ -121,7 +121,7 @@ class EarningsGraph extends StatelessWidget {
List<FlSpot> _generateSpots(List<StaffPayment> data) {
// Generate spots based on index in the list for simplicity in this demo
// Real implementation would map to actual dates on X-axis
return List.generate(data.length, (index) {
return List.generate(data.length, (int index) {
return FlSpot(index.toDouble(), data[index].amount);
});
}

View File

@@ -2,15 +2,6 @@ import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class PaymentHistoryItem extends StatelessWidget {
final double amount;
final String title;
final String location;
final String address;
final String date;
final String workedTime;
final int hours;
final double rate;
final String status;
const PaymentHistoryItem({
super.key,
@@ -24,6 +15,15 @@ class PaymentHistoryItem extends StatelessWidget {
required this.rate,
required this.status,
});
final double amount;
final String title;
final String location;
final String address;
final String date;
final String workedTime;
final int hours;
final double rate;
final String status;
@override
Widget build(BuildContext context) {
@@ -32,7 +32,7 @@ class PaymentHistoryItem extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -42,10 +42,10 @@ class PaymentHistoryItem extends StatelessWidget {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
// Status Badge
Row(
children: [
children: <Widget>[
Container(
width: 6,
height: 6,
@@ -70,7 +70,7 @@ class PaymentHistoryItem extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
// Icon
Container(
width: 44,
@@ -90,15 +90,15 @@ class PaymentHistoryItem extends StatelessWidget {
// Content
Expanded(
child: Column(
children: [
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
title,
style: const TextStyle(
@@ -119,7 +119,7 @@ class PaymentHistoryItem extends StatelessWidget {
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
children: <Widget>[
Text(
"\$${amount.toStringAsFixed(0)}",
style: const TextStyle(
@@ -143,7 +143,7 @@ class PaymentHistoryItem extends StatelessWidget {
// Date and Time
Row(
children: [
children: <Widget>[
const Icon(
LucideIcons.calendar,
size: 12,
@@ -177,7 +177,7 @@ class PaymentHistoryItem extends StatelessWidget {
// Address
Row(
children: [
children: <Widget>[
const Icon(
LucideIcons.mapPin,
size: 12,

View File

@@ -1,11 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class PaymentStatsCard extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String label;
final String amount;
const PaymentStatsCard({
super.key,
@@ -14,6 +9,10 @@ class PaymentStatsCard extends StatelessWidget {
required this.label,
required this.amount,
});
final IconData icon;
final Color iconColor;
final String label;
final String amount;
@override
Widget build(BuildContext context) {
@@ -22,7 +21,7 @@ class PaymentStatsCard extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -32,9 +31,9 @@ class PaymentStatsCard extends StatelessWidget {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Row(
children: [
children: <Widget>[
Icon(icon, size: 16, color: iconColor),
const SizedBox(width: 8),
Text(

View File

@@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class PendingPayCard extends StatelessWidget {
final double amount;
final VoidCallback onCashOut;
const PendingPayCard({
super.key,
required this.amount,
required this.onCashOut,
});
final double amount;
final VoidCallback onCashOut;
@override
Widget build(BuildContext context) {
@@ -17,12 +17,12 @@ class PendingPayCard extends StatelessWidget {
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50
colors: <Color>[Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -32,9 +32,9 @@ class PendingPayCard extends StatelessWidget {
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
children: <Widget>[
Row(
children: [
children: <Widget>[
Container(
width: 40,
height: 40,
@@ -51,7 +51,7 @@ class PendingPayCard extends StatelessWidget {
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
const Text(
"Pending",
style: TextStyle(

View File

@@ -11,7 +11,8 @@ environment:
dependencies:
flutter:
sdk: flutter
firebase_data_connect:
firebase_data_connect: ^0.2.2+2
firebase_auth: ^6.1.4
flutter_modular: ^6.3.2
lucide_icons: ^0.257.0
intl: ^0.20.0
@@ -29,6 +30,8 @@ dependencies:
krow_data_connect:
path: ../../../data_connect
flutter_bloc: any
equatable: any
dev_dependencies:
flutter_test:
sdk: flutter

Some files were not shown because too many files have changed in this diff Show More