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:
Achintha Isuru
2026-03-16 22:45:06 -04:00
parent 4834266986
commit b31a615092
478 changed files with 10512 additions and 19854 deletions

View File

@@ -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(),
);
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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(

View File

@@ -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,
}) {

View File

@@ -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),
),
],

View File

@@ -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({

View File

@@ -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(

View File

@@ -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:

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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];
}

View File

@@ -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)),
],
);
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -23,8 +23,6 @@ dependencies:
path: ../../../../../core
krow_domain:
path: ../../../../../domain
krow_data_connect:
path: ../../../../../data_connect
dev_dependencies:
flutter_test: