feat: Migrate staff profile features from Data Connect to V2 REST API
- Removed data_connect package from mobile pubspec.yaml. - Added documentation for V2 profile migration status and QA findings. - Implemented new session management with ClientSessionStore and StaffSessionStore. - Created V2SessionService for handling user sessions via the V2 API. - Developed use cases for cancelling late worker assignments and submitting worker reviews. - Added arguments and use cases for payment chart retrieval and profile completion checks. - Implemented repository interfaces and their implementations for staff main and profile features. - Ensured proper error handling and validation in use cases.
This commit is contained in:
@@ -1,83 +1,34 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/bank_account_repository.dart';
|
||||
|
||||
/// Implementation of [BankAccountRepository] that integrates with Data Connect.
|
||||
import 'package:staff_bank_account/src/domain/repositories/bank_account_repository.dart';
|
||||
|
||||
/// Implementation of [BankAccountRepository] using the V2 API.
|
||||
///
|
||||
/// Replaces the previous Firebase Data Connect implementation.
|
||||
class BankAccountRepositoryImpl implements BankAccountRepository {
|
||||
/// Creates a [BankAccountRepositoryImpl].
|
||||
BankAccountRepositoryImpl({
|
||||
DataConnectService? service,
|
||||
}) : _service = service ?? DataConnectService.instance;
|
||||
BankAccountRepositoryImpl({required BaseApiService apiService})
|
||||
: _api = apiService;
|
||||
|
||||
/// The Data Connect service.
|
||||
final DataConnectService _service;
|
||||
final BaseApiService _api;
|
||||
|
||||
@override
|
||||
Future<List<StaffBankAccount>> getAccounts() async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
|
||||
result = await _service.connector
|
||||
.getAccountsByOwnerId(ownerId: staffId)
|
||||
.execute();
|
||||
|
||||
return result.data.accounts.map((GetAccountsByOwnerIdAccounts account) {
|
||||
return BankAccountAdapter.fromPrimitives(
|
||||
id: account.id,
|
||||
userId: account.ownerId,
|
||||
bankName: account.bank,
|
||||
accountNumber: account.accountNumber,
|
||||
last4: account.last4,
|
||||
sortCode: account.routeNumber,
|
||||
type: account.type is Known<AccountType>
|
||||
? (account.type as Known<AccountType>).value.name
|
||||
: null,
|
||||
isPrimary: account.isPrimary,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
Future<List<BankAccount>> getAccounts() async {
|
||||
final ApiResponse response =
|
||||
await _api.get(V2ApiEndpoints.staffBankAccounts);
|
||||
final List<dynamic> items = response.data['accounts'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
BankAccount.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addAccount(StaffBankAccount account) async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
|
||||
existingAccounts = await _service.connector
|
||||
.getAccountsByOwnerId(ownerId: staffId)
|
||||
.execute();
|
||||
final bool hasAccounts = existingAccounts.data.accounts.isNotEmpty;
|
||||
final bool isPrimary = !hasAccounts;
|
||||
|
||||
await _service.connector
|
||||
.createAccount(
|
||||
bank: account.bankName,
|
||||
type: AccountType.values
|
||||
.byName(BankAccountAdapter.typeToString(account.type)),
|
||||
last4: _safeLast4(account.last4, account.accountNumber),
|
||||
ownerId: staffId,
|
||||
)
|
||||
.isPrimary(isPrimary)
|
||||
.accountNumber(account.accountNumber)
|
||||
.routeNumber(account.sortCode)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
/// Ensures we have a last4 value, either from input or derived from account number.
|
||||
String _safeLast4(String? last4, String accountNumber) {
|
||||
if (last4 != null && last4.isNotEmpty) {
|
||||
return last4;
|
||||
}
|
||||
if (accountNumber.isEmpty) {
|
||||
return '0000';
|
||||
}
|
||||
if (accountNumber.length < 4) {
|
||||
return accountNumber.padLeft(4, '0');
|
||||
}
|
||||
return accountNumber.substring(accountNumber.length - 4);
|
||||
Future<void> addAccount(BankAccount account) async {
|
||||
await _api.post(
|
||||
V2ApiEndpoints.staffBankAccounts,
|
||||
data: account.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
class AddBankAccountParams extends UseCaseArgument with EquatableMixin {
|
||||
|
||||
const AddBankAccountParams({required this.account});
|
||||
final StaffBankAccount account;
|
||||
final BankAccount account;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[account];
|
||||
|
||||
|
||||
@override
|
||||
bool? get stringify => true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for managing bank accounts.
|
||||
///
|
||||
/// Uses [BankAccount] from the V2 domain layer.
|
||||
abstract class BankAccountRepository {
|
||||
/// Fetches the list of bank accounts for the current user.
|
||||
Future<List<StaffBankAccount>> getAccounts();
|
||||
/// Fetches the list of bank accounts for the current staff member.
|
||||
Future<List<BankAccount>> getAccounts();
|
||||
|
||||
/// adds a new bank account.
|
||||
Future<void> addAccount(StaffBankAccount account);
|
||||
/// Adds a new bank account.
|
||||
Future<void> addAccount(BankAccount account);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/bank_account_repository.dart';
|
||||
|
||||
/// Use case to fetch bank accounts.
|
||||
class GetBankAccountsUseCase implements NoInputUseCase<List<StaffBankAccount>> {
|
||||
class GetBankAccountsUseCase implements NoInputUseCase<List<BankAccount>> {
|
||||
|
||||
GetBankAccountsUseCase(this._repository);
|
||||
final BankAccountRepository _repository;
|
||||
|
||||
@override
|
||||
Future<List<StaffBankAccount>> call() {
|
||||
Future<List<BankAccount>> call() {
|
||||
return _repository.getAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class BankAccountCubit extends Cubit<BankAccountState>
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final List<StaffBankAccount> accounts = await _getBankAccountsUseCase();
|
||||
final List<BankAccount> accounts = await _getBankAccountsUseCase();
|
||||
emit(
|
||||
state.copyWith(status: BankAccountStatus.loaded, accounts: accounts),
|
||||
);
|
||||
@@ -48,19 +48,17 @@ class BankAccountCubit extends Cubit<BankAccountState>
|
||||
emit(state.copyWith(status: BankAccountStatus.loading));
|
||||
|
||||
// Create domain entity
|
||||
final StaffBankAccount newAccount = StaffBankAccount(
|
||||
id: '', // Generated by server usually
|
||||
userId: '', // Handled by Repo/Auth
|
||||
final BankAccount newAccount = BankAccount(
|
||||
accountId: '', // Generated by server
|
||||
bankName: bankName,
|
||||
accountNumber: accountNumber.length > 4
|
||||
providerReference: routingNumber,
|
||||
last4: accountNumber.length > 4
|
||||
? accountNumber.substring(accountNumber.length - 4)
|
||||
: accountNumber,
|
||||
accountName: '',
|
||||
sortCode: routingNumber,
|
||||
type: type == 'CHECKING'
|
||||
? StaffBankAccountType.checking
|
||||
: StaffBankAccountType.savings,
|
||||
isPrimary: false,
|
||||
accountType: type == 'CHECKING'
|
||||
? AccountType.checking
|
||||
: AccountType.savings,
|
||||
);
|
||||
|
||||
await handleError(
|
||||
|
||||
@@ -7,18 +7,18 @@ class BankAccountState extends Equatable {
|
||||
|
||||
const BankAccountState({
|
||||
this.status = BankAccountStatus.initial,
|
||||
this.accounts = const <StaffBankAccount>[],
|
||||
this.accounts = const <BankAccount>[],
|
||||
this.errorMessage,
|
||||
this.showForm = false,
|
||||
});
|
||||
final BankAccountStatus status;
|
||||
final List<StaffBankAccount> accounts;
|
||||
final List<BankAccount> accounts;
|
||||
final String? errorMessage;
|
||||
final bool showForm;
|
||||
|
||||
BankAccountState copyWith({
|
||||
BankAccountStatus? status,
|
||||
List<StaffBankAccount>? accounts,
|
||||
List<BankAccount>? accounts,
|
||||
String? errorMessage,
|
||||
bool? showForm,
|
||||
}) {
|
||||
|
||||
@@ -90,7 +90,7 @@ class BankAccountPage extends StatelessWidget {
|
||||
] else ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
...state.accounts.map<Widget>(
|
||||
(StaffBankAccount account) =>
|
||||
(BankAccount account) =>
|
||||
AccountCard(account: account, strings: strings),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
class AccountCard extends StatelessWidget {
|
||||
final StaffBankAccount account;
|
||||
final BankAccount account;
|
||||
final dynamic strings;
|
||||
|
||||
const AccountCard({
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:staff_bank_account/src/data/repositories/bank_account_repository_impl.dart';
|
||||
import 'package:staff_bank_account/src/domain/repositories/bank_account_repository.dart';
|
||||
import 'package:staff_bank_account/src/domain/usecases/add_bank_account_usecase.dart';
|
||||
import 'package:staff_bank_account/src/domain/usecases/get_bank_accounts_usecase.dart';
|
||||
import 'package:staff_bank_account/src/presentation/blocs/bank_account_cubit.dart';
|
||||
import 'package:staff_bank_account/src/presentation/pages/bank_account_page.dart';
|
||||
|
||||
import 'domain/repositories/bank_account_repository.dart';
|
||||
import 'domain/usecases/add_bank_account_usecase.dart';
|
||||
import 'domain/usecases/get_bank_accounts_usecase.dart';
|
||||
import 'presentation/blocs/bank_account_cubit.dart';
|
||||
import 'presentation/pages/bank_account_page.dart';
|
||||
|
||||
/// Module for the Staff Bank Account feature.
|
||||
///
|
||||
/// Uses the V2 REST API via [BaseApiService] for backend access.
|
||||
class StaffBankAccountModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[DataConnectModule()];
|
||||
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<BankAccountRepository>(BankAccountRepositoryImpl.new);
|
||||
|
||||
i.addLazySingleton<BankAccountRepository>(
|
||||
() => BankAccountRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.addLazySingleton<GetBankAccountsUseCase>(GetBankAccountsUseCase.new);
|
||||
i.addLazySingleton<AddBankAccountUseCase>(AddBankAccountUseCase.new);
|
||||
|
||||
|
||||
// Blocs
|
||||
i.add<BankAccountCubit>(
|
||||
() => BankAccountCubit(
|
||||
|
||||
@@ -15,9 +15,7 @@ dependencies:
|
||||
bloc: ^8.1.0
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
firebase_auth: ^6.1.4
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
|
||||
|
||||
# Architecture Packages
|
||||
design_system:
|
||||
path: ../../../../../design_system
|
||||
@@ -27,8 +25,6 @@ dependencies:
|
||||
path: ../../../../../core
|
||||
krow_domain:
|
||||
path: ../../../../../domain
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -1,74 +1,31 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:krow_domain/src/adapters/financial/time_card_adapter.dart';
|
||||
import '../../domain/repositories/time_card_repository.dart';
|
||||
|
||||
/// Implementation of [TimeCardRepository] using Firebase Data Connect.
|
||||
import 'package:staff_time_card/src/domain/repositories/time_card_repository.dart';
|
||||
|
||||
/// Implementation of [TimeCardRepository] using the V2 API.
|
||||
///
|
||||
/// Replaces the previous Firebase Data Connect implementation.
|
||||
class TimeCardRepositoryImpl implements TimeCardRepository {
|
||||
|
||||
/// Creates a [TimeCardRepositoryImpl].
|
||||
TimeCardRepositoryImpl({dc.DataConnectService? service})
|
||||
: _service = service ?? dc.DataConnectService.instance;
|
||||
final dc.DataConnectService _service;
|
||||
TimeCardRepositoryImpl({required BaseApiService apiService})
|
||||
: _api = apiService;
|
||||
|
||||
final BaseApiService _api;
|
||||
|
||||
@override
|
||||
Future<List<TimeCard>> getTimeCards(DateTime month) async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
// Fetch applications. Limit can be adjusted, assuming 100 is safe for now.
|
||||
final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables> result =
|
||||
await _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.limit(100)
|
||||
.execute();
|
||||
|
||||
return result.data.applications
|
||||
.where((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
|
||||
if (shiftDate == null) return false;
|
||||
return shiftDate.year == month.year &&
|
||||
shiftDate.month == month.month;
|
||||
})
|
||||
.map((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime shiftDate = _service.toDateTime(app.shift.date)!;
|
||||
final String startTime = _formatTime(app.checkInTime) ??
|
||||
_formatTime(app.shift.startTime) ??
|
||||
'';
|
||||
final String endTime = _formatTime(app.checkOutTime) ??
|
||||
_formatTime(app.shift.endTime) ??
|
||||
'';
|
||||
|
||||
// Prefer shiftRole values for pay/hours
|
||||
final double hours = app.shiftRole.hours ?? 0.0;
|
||||
final double rate = app.shiftRole.role.costPerHour;
|
||||
final double pay = app.shiftRole.totalValue ?? 0.0;
|
||||
|
||||
return TimeCardAdapter.fromPrimitives(
|
||||
id: app.id,
|
||||
shiftTitle: app.shift.title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
date: shiftDate,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
totalHours: hours,
|
||||
hourlyRate: rate,
|
||||
totalPay: pay,
|
||||
status: app.status.stringValue,
|
||||
location: app.shift.location,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
String? _formatTime(fdc.Timestamp? timestamp) {
|
||||
if (timestamp == null) return null;
|
||||
final DateTime? dt = _service.toDateTime(timestamp);
|
||||
if (dt == null) return null;
|
||||
return DateFormat('HH:mm').format(dt);
|
||||
Future<List<TimeCardEntry>> getTimeCards(DateTime month) async {
|
||||
final ApiResponse response = await _api.get(
|
||||
V2ApiEndpoints.staffTimeCard,
|
||||
params: <String, dynamic>{
|
||||
'year': month.year,
|
||||
'month': month.month,
|
||||
},
|
||||
);
|
||||
final List<dynamic> items = response.data['entries'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
TimeCardEntry.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for accessing time card data.
|
||||
///
|
||||
/// This repository handles fetching time cards and related financial data
|
||||
/// for the staff member.
|
||||
/// Uses [TimeCardEntry] from the V2 domain layer.
|
||||
abstract class TimeCardRepository {
|
||||
/// Retrieves a list of [TimeCard]s for a specific month.
|
||||
/// Retrieves a list of [TimeCardEntry]s for a specific month.
|
||||
///
|
||||
/// [month] is a [DateTime] representing the month to filter by.
|
||||
Future<List<TimeCard>> getTimeCards(DateTime month);
|
||||
Future<List<TimeCardEntry>> getTimeCards(DateTime month);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../arguments/get_time_cards_arguments.dart';
|
||||
import '../repositories/time_card_repository.dart';
|
||||
|
||||
/// UseCase to retrieve time cards for a given month.
|
||||
class GetTimeCardsUseCase extends UseCase<GetTimeCardsArguments, List<TimeCard>> {
|
||||
import 'package:staff_time_card/src/domain/arguments/get_time_cards_arguments.dart';
|
||||
import 'package:staff_time_card/src/domain/repositories/time_card_repository.dart';
|
||||
|
||||
/// UseCase to retrieve time card entries for a given month.
|
||||
///
|
||||
/// Uses [TimeCardEntry] from the V2 domain layer.
|
||||
class GetTimeCardsUseCase
|
||||
extends UseCase<GetTimeCardsArguments, List<TimeCardEntry>> {
|
||||
/// Creates a [GetTimeCardsUseCase].
|
||||
GetTimeCardsUseCase(this.repository);
|
||||
|
||||
/// The time card repository.
|
||||
final TimeCardRepository repository;
|
||||
|
||||
/// Executes the use case.
|
||||
///
|
||||
/// Returns a list of [TimeCard]s for the specified month in [arguments].
|
||||
@override
|
||||
Future<List<TimeCard>> call(GetTimeCardsArguments arguments) {
|
||||
Future<List<TimeCardEntry>> call(GetTimeCardsArguments arguments) {
|
||||
return repository.getTimeCards(arguments.month);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,25 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/arguments/get_time_cards_arguments.dart';
|
||||
import '../../domain/usecases/get_time_cards_usecase.dart';
|
||||
|
||||
import 'package:staff_time_card/src/domain/arguments/get_time_cards_arguments.dart';
|
||||
import 'package:staff_time_card/src/domain/usecases/get_time_cards_usecase.dart';
|
||||
|
||||
part 'time_card_event.dart';
|
||||
part 'time_card_state.dart';
|
||||
|
||||
/// BLoC to manage Time Card state.
|
||||
///
|
||||
/// Uses V2 API [TimeCardEntry] entities.
|
||||
class TimeCardBloc extends Bloc<TimeCardEvent, TimeCardState>
|
||||
with BlocErrorHandler<TimeCardState> {
|
||||
|
||||
/// Creates a [TimeCardBloc].
|
||||
TimeCardBloc({required this.getTimeCards}) : super(TimeCardInitial()) {
|
||||
on<LoadTimeCards>(_onLoadTimeCards);
|
||||
on<ChangeMonth>(_onChangeMonth);
|
||||
}
|
||||
|
||||
/// The use case for fetching time card entries.
|
||||
final GetTimeCardsUseCase getTimeCards;
|
||||
|
||||
/// Handles fetching time cards for the requested month.
|
||||
@@ -27,17 +32,17 @@ class TimeCardBloc extends Bloc<TimeCardEvent, TimeCardState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final List<TimeCard> cards = await getTimeCards(
|
||||
final List<TimeCardEntry> cards = await getTimeCards(
|
||||
GetTimeCardsArguments(event.month),
|
||||
);
|
||||
|
||||
final double totalHours = cards.fold(
|
||||
0.0,
|
||||
(double sum, TimeCard t) => sum + t.totalHours,
|
||||
(double sum, TimeCardEntry t) => sum + t.minutesWorked / 60.0,
|
||||
);
|
||||
final double totalEarnings = cards.fold(
|
||||
0.0,
|
||||
(double sum, TimeCard t) => sum + t.totalPay,
|
||||
(double sum, TimeCardEntry t) => sum + t.totalPayCents / 100.0,
|
||||
);
|
||||
|
||||
emit(
|
||||
@@ -53,6 +58,7 @@ class TimeCardBloc extends Bloc<TimeCardEvent, TimeCardState>
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles changing the selected month.
|
||||
Future<void> _onChangeMonth(
|
||||
ChangeMonth event,
|
||||
Emitter<TimeCardState> emit,
|
||||
@@ -60,4 +66,3 @@ class TimeCardBloc extends Bloc<TimeCardEvent, TimeCardState>
|
||||
add(LoadTimeCards(event.month));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,54 @@
|
||||
part of 'time_card_bloc.dart';
|
||||
|
||||
/// Base class for time card states.
|
||||
abstract class TimeCardState extends Equatable {
|
||||
/// Creates a [TimeCardState].
|
||||
const TimeCardState();
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Initial state before any data is loaded.
|
||||
class TimeCardInitial extends TimeCardState {}
|
||||
class TimeCardLoading extends TimeCardState {}
|
||||
class TimeCardLoaded extends TimeCardState {
|
||||
|
||||
/// Loading state while data is being fetched.
|
||||
class TimeCardLoading extends TimeCardState {}
|
||||
|
||||
/// Loaded state with time card entries and computed totals.
|
||||
class TimeCardLoaded extends TimeCardState {
|
||||
/// Creates a [TimeCardLoaded].
|
||||
const TimeCardLoaded({
|
||||
required this.timeCards,
|
||||
required this.selectedMonth,
|
||||
required this.totalHours,
|
||||
required this.totalEarnings,
|
||||
});
|
||||
final List<TimeCard> timeCards;
|
||||
|
||||
/// The list of time card entries for the selected month.
|
||||
final List<TimeCardEntry> timeCards;
|
||||
|
||||
/// The currently selected month.
|
||||
final DateTime selectedMonth;
|
||||
|
||||
/// Total hours worked in the selected month.
|
||||
final double totalHours;
|
||||
|
||||
/// Total earnings in the selected month (in dollars).
|
||||
final double totalEarnings;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[timeCards, selectedMonth, totalHours, totalEarnings];
|
||||
List<Object?> get props =>
|
||||
<Object?>[timeCards, selectedMonth, totalHours, totalEarnings];
|
||||
}
|
||||
|
||||
/// Error state when loading fails.
|
||||
class TimeCardError extends TimeCardState {
|
||||
/// Creates a [TimeCardError].
|
||||
const TimeCardError(this.message);
|
||||
|
||||
/// The error message.
|
||||
final String message;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[message];
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'timesheet_card.dart';
|
||||
class ShiftHistoryList extends StatelessWidget {
|
||||
|
||||
const ShiftHistoryList({super.key, required this.timesheets});
|
||||
final List<TimeCard> timesheets;
|
||||
final List<TimeCardEntry> timesheets;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -39,7 +39,7 @@ class ShiftHistoryList extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
else
|
||||
...timesheets.map((TimeCard ts) => TimesheetCard(timesheet: ts)),
|
||||
...timesheets.map((TimeCardEntry ts) => TimesheetCard(timesheet: ts)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,39 +8,16 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
class TimesheetCard extends StatelessWidget {
|
||||
|
||||
const TimesheetCard({super.key, required this.timesheet});
|
||||
final TimeCard timesheet;
|
||||
final TimeCardEntry timesheet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TimeCardStatus status = timesheet.status;
|
||||
Color statusBg;
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
|
||||
switch (status) {
|
||||
case TimeCardStatus.approved:
|
||||
statusBg = UiColors.tagSuccess;
|
||||
statusColor = UiColors.textSuccess;
|
||||
statusText = t.staff_time_card.status.approved;
|
||||
break;
|
||||
case TimeCardStatus.disputed:
|
||||
statusBg = UiColors.destructive.withValues(alpha: 0.12);
|
||||
statusColor = UiColors.destructive;
|
||||
statusText = t.staff_time_card.status.disputed;
|
||||
break;
|
||||
case TimeCardStatus.paid:
|
||||
statusBg = UiColors.primary.withValues(alpha: 0.12);
|
||||
statusColor = UiColors.primary;
|
||||
statusText = t.staff_time_card.status.paid;
|
||||
break;
|
||||
case TimeCardStatus.pending:
|
||||
statusBg = UiColors.tagPending;
|
||||
statusColor = UiColors.textWarning;
|
||||
statusText = t.staff_time_card.status.pending;
|
||||
break;
|
||||
}
|
||||
|
||||
final String dateStr = DateFormat('EEE, MMM d').format(timesheet.date);
|
||||
final double totalHours = timesheet.minutesWorked / 60.0;
|
||||
final double totalPay = timesheet.totalPayCents / 100.0;
|
||||
final double hourlyRate = timesheet.hourlyRateCents != null
|
||||
? timesheet.hourlyRateCents! / 100.0
|
||||
: 0.0;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
@@ -56,33 +33,20 @@ class TimesheetCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
timesheet.shiftTitle,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
timesheet.clientName,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusBg,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: UiTypography.titleUppercase4b.copyWith(
|
||||
color: statusColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
timesheet.shiftName,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
if (timesheet.location != null)
|
||||
Text(
|
||||
timesheet.location!,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -93,10 +57,11 @@ class TimesheetCard extends StatelessWidget {
|
||||
runSpacing: UiConstants.space1,
|
||||
children: <Widget>[
|
||||
_IconText(icon: UiIcons.calendar, text: dateStr),
|
||||
_IconText(
|
||||
icon: UiIcons.clock,
|
||||
text: '${_formatTime(timesheet.startTime)} - ${_formatTime(timesheet.endTime)}',
|
||||
),
|
||||
if (timesheet.clockInAt != null && timesheet.clockOutAt != null)
|
||||
_IconText(
|
||||
icon: UiIcons.clock,
|
||||
text: '${DateFormat('h:mm a').format(timesheet.clockInAt!)} - ${DateFormat('h:mm a').format(timesheet.clockOutAt!)}',
|
||||
),
|
||||
if (timesheet.location != null)
|
||||
_IconText(icon: UiIcons.mapPin, text: timesheet.location!),
|
||||
],
|
||||
@@ -111,11 +76,11 @@ class TimesheetCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${timesheet.totalHours.toStringAsFixed(1)} ${t.staff_time_card.hours} @ \$${timesheet.hourlyRate.toStringAsFixed(2)}${t.staff_time_card.per_hr}',
|
||||
'${totalHours.toStringAsFixed(1)} ${t.staff_time_card.hours} @ \$${hourlyRate.toStringAsFixed(2)}${t.staff_time_card.per_hr}',
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
'\$${timesheet.totalPay.toStringAsFixed(2)}',
|
||||
'\$${totalPay.toStringAsFixed(2)}',
|
||||
style: UiTypography.title2b.primary,
|
||||
),
|
||||
],
|
||||
@@ -125,21 +90,6 @@ class TimesheetCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to safely format time strings like "HH:mm"
|
||||
String _formatTime(String t) {
|
||||
if (t.isEmpty) return '--:--';
|
||||
try {
|
||||
final List<String> parts = t.split(':');
|
||||
if (parts.length >= 2) {
|
||||
final DateTime dt = DateTime(2000, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
|
||||
return DateFormat('h:mm a').format(dt);
|
||||
}
|
||||
return t;
|
||||
} catch (_) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _IconText extends StatelessWidget {
|
||||
|
||||
@@ -3,28 +3,31 @@ library;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'data/repositories_impl/time_card_repository_impl.dart';
|
||||
import 'domain/repositories/time_card_repository.dart';
|
||||
import 'domain/usecases/get_time_cards_usecase.dart';
|
||||
import 'presentation/blocs/time_card_bloc.dart';
|
||||
import 'presentation/pages/time_card_page.dart';
|
||||
import 'package:staff_time_card/src/data/repositories_impl/time_card_repository_impl.dart';
|
||||
import 'package:staff_time_card/src/domain/repositories/time_card_repository.dart';
|
||||
import 'package:staff_time_card/src/domain/usecases/get_time_cards_usecase.dart';
|
||||
import 'package:staff_time_card/src/presentation/blocs/time_card_bloc.dart';
|
||||
import 'package:staff_time_card/src/presentation/pages/time_card_page.dart';
|
||||
|
||||
export 'presentation/pages/time_card_page.dart';
|
||||
export 'package:staff_time_card/src/presentation/pages/time_card_page.dart';
|
||||
|
||||
/// Module for the Staff Time Card feature.
|
||||
///
|
||||
/// This module configures dependency injection for accessing time card data,
|
||||
/// including the repositories, use cases, and BLoCs.
|
||||
/// Uses the V2 REST API via [BaseApiService] for backend access.
|
||||
class StaffTimeCardModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[DataConnectModule()];
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<TimeCardRepository>(TimeCardRepositoryImpl.new);
|
||||
i.addLazySingleton<TimeCardRepository>(
|
||||
() => TimeCardRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// UseCases
|
||||
i.add<GetTimeCardsUseCase>(GetTimeCardsUseCase.new);
|
||||
|
||||
@@ -23,8 +23,6 @@ dependencies:
|
||||
path: ../../../../../core
|
||||
krow_domain:
|
||||
path: ../../../../../domain
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user