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,98 +1,76 @@
|
||||
import 'package:firebase_data_connect/src/core/ref.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
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/payments_repository.dart';
|
||||
import 'package:staff_payments/src/domain/repositories/payments_repository.dart';
|
||||
|
||||
class PaymentsRepositoryImpl
|
||||
implements PaymentsRepository {
|
||||
/// V2 REST API implementation of [PaymentsRepository].
|
||||
///
|
||||
/// Calls the staff payments endpoints via [BaseApiService].
|
||||
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
/// Creates a [PaymentsRepositoryImpl] with the given [apiService].
|
||||
PaymentsRepositoryImpl({required BaseApiService apiService})
|
||||
: _apiService = apiService;
|
||||
|
||||
PaymentsRepositoryImpl() : _service = DataConnectService.instance;
|
||||
final DataConnectService _service;
|
||||
/// The API service used for HTTP requests.
|
||||
final BaseApiService _apiService;
|
||||
|
||||
@override
|
||||
Future<PaymentSummary> getPaymentSummary() async {
|
||||
return _service.run(() async {
|
||||
final String currentStaffId = await _service.getStaffId();
|
||||
|
||||
// Fetch recent payments with a limit
|
||||
final QueryResult<dc.ListRecentPaymentsByStaffIdData, dc.ListRecentPaymentsByStaffIdVariables> response = await _service.connector.listRecentPaymentsByStaffId(
|
||||
staffId: currentStaffId,
|
||||
).limit(100).execute();
|
||||
|
||||
final List<dc.ListRecentPaymentsByStaffIdRecentPayments> payments = response.data.recentPayments;
|
||||
|
||||
double weekly = 0;
|
||||
double monthly = 0;
|
||||
double pending = 0;
|
||||
double total = 0;
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime startOfWeek = now.subtract(const Duration(days: 7));
|
||||
final DateTime startOfMonth = DateTime(now.year, now.month, 1);
|
||||
|
||||
for (final dc.ListRecentPaymentsByStaffIdRecentPayments p in payments) {
|
||||
final DateTime? date = _service.toDateTime(p.invoice.issueDate) ?? _service.toDateTime(p.createdAt);
|
||||
final double amount = p.invoice.amount;
|
||||
final String? status = p.status?.stringValue;
|
||||
|
||||
if (status == 'PENDING') {
|
||||
pending += amount;
|
||||
} else if (status == 'PAID') {
|
||||
total += amount;
|
||||
if (date != null) {
|
||||
if (date.isAfter(startOfWeek)) weekly += amount;
|
||||
if (date.isAfter(startOfMonth)) monthly += amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PaymentSummary(
|
||||
weeklyEarnings: weekly,
|
||||
monthlyEarnings: monthly,
|
||||
pendingEarnings: pending,
|
||||
totalEarnings: total,
|
||||
);
|
||||
});
|
||||
Future<PaymentSummary> getPaymentSummary({
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
}) async {
|
||||
final Map<String, dynamic> params = <String, dynamic>{
|
||||
if (startDate != null) 'startDate': startDate,
|
||||
if (endDate != null) 'endDate': endDate,
|
||||
};
|
||||
final ApiResponse response = await _apiService.get(
|
||||
V2ApiEndpoints.staffPaymentsSummary,
|
||||
params: params.isEmpty ? null : params,
|
||||
);
|
||||
return PaymentSummary.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StaffPayment>> getPaymentHistory(String period) async {
|
||||
return _service.run(() async {
|
||||
final String currentStaffId = await _service.getStaffId();
|
||||
|
||||
final QueryResult<dc.ListRecentPaymentsByStaffIdData, dc.ListRecentPaymentsByStaffIdVariables> response = await _service.connector
|
||||
.listRecentPaymentsByStaffId(staffId: currentStaffId)
|
||||
.execute();
|
||||
Future<List<PaymentRecord>> getPaymentHistory({
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
}) async {
|
||||
final Map<String, dynamic> params = <String, dynamic>{
|
||||
if (startDate != null) 'startDate': startDate,
|
||||
if (endDate != null) 'endDate': endDate,
|
||||
};
|
||||
final ApiResponse response = await _apiService.get(
|
||||
V2ApiEndpoints.staffPaymentsHistory,
|
||||
params: params.isEmpty ? null : params,
|
||||
);
|
||||
final Map<String, dynamic> body = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = body['items'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
PaymentRecord.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return response.data.recentPayments.map((dc.ListRecentPaymentsByStaffIdRecentPayments payment) {
|
||||
// Extract shift details from nested application structure
|
||||
final String? shiftTitle = payment.application.shiftRole.shift.title;
|
||||
final String? locationAddress = payment.application.shiftRole.shift.locationAddress;
|
||||
final double? hoursWorked = payment.application.shiftRole.hours;
|
||||
final double? hourlyRate = payment.application.shiftRole.role.costPerHour;
|
||||
// Extract hub details from order
|
||||
final String? locationHub = payment.invoice.order.teamHub.hubName;
|
||||
final String? hubAddress = payment.invoice.order.teamHub.address;
|
||||
final String? shiftLocation = locationAddress ?? hubAddress;
|
||||
|
||||
return StaffPayment(
|
||||
id: payment.id,
|
||||
staffId: payment.staffId,
|
||||
assignmentId: payment.applicationId,
|
||||
amount: payment.invoice.amount,
|
||||
status: PaymentAdapter.toPaymentStatus(payment.status?.stringValue ?? 'UNKNOWN'),
|
||||
paidAt: _service.toDateTime(payment.invoice.issueDate),
|
||||
shiftTitle: shiftTitle,
|
||||
shiftLocation: locationHub,
|
||||
locationAddress: shiftLocation,
|
||||
hoursWorked: hoursWorked,
|
||||
hourlyRate: hourlyRate,
|
||||
workedTime: payment.workedTime,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
@override
|
||||
Future<List<PaymentChartPoint>> getPaymentChart({
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
String bucket = 'day',
|
||||
}) async {
|
||||
final Map<String, dynamic> params = <String, dynamic>{
|
||||
'bucket': bucket,
|
||||
if (startDate != null) 'startDate': startDate,
|
||||
if (endDate != null) 'endDate': endDate,
|
||||
};
|
||||
final ApiResponse response = await _apiService.get(
|
||||
V2ApiEndpoints.staffPaymentsChart,
|
||||
params: params,
|
||||
);
|
||||
final Map<String, dynamic> body = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = body['items'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
PaymentChartPoint.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// Arguments for retrieving payment chart data.
|
||||
class GetPaymentChartArguments extends UseCaseArgument {
|
||||
/// Creates [GetPaymentChartArguments] with the [bucket] granularity.
|
||||
const GetPaymentChartArguments({
|
||||
this.bucket = 'day',
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
});
|
||||
|
||||
/// Time bucket granularity: `day`, `week`, or `month`.
|
||||
final String bucket;
|
||||
|
||||
/// ISO-8601 start date for the range filter.
|
||||
final String? startDate;
|
||||
|
||||
/// ISO-8601 end date for the range filter.
|
||||
final String? endDate;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[bucket, startDate, endDate];
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// Arguments for getting payment history.
|
||||
/// Arguments for retrieving payment history.
|
||||
class GetPaymentHistoryArguments extends UseCaseArgument {
|
||||
/// Creates [GetPaymentHistoryArguments] with optional date range.
|
||||
const GetPaymentHistoryArguments({
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
});
|
||||
|
||||
const GetPaymentHistoryArguments(this.period);
|
||||
/// The period to filter by (e.g., "monthly", "weekly").
|
||||
final String period;
|
||||
/// ISO-8601 start date for the range filter.
|
||||
final String? startDate;
|
||||
|
||||
/// ISO-8601 end date for the range filter.
|
||||
final String? endDate;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[period];
|
||||
List<Object?> get props => <Object?>[startDate, endDate];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for Payments feature.
|
||||
/// Repository interface for the staff payments feature.
|
||||
///
|
||||
/// Defines the contract for data access related to staff payments.
|
||||
/// Implementations of this interface should reside in the data layer.
|
||||
/// Implementations live in the data layer and call the V2 REST API.
|
||||
abstract class PaymentsRepository {
|
||||
/// Fetches the payment summary (earnings).
|
||||
Future<PaymentSummary> getPaymentSummary();
|
||||
/// Fetches the aggregated payment summary for the given date range.
|
||||
Future<PaymentSummary> getPaymentSummary({
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
});
|
||||
|
||||
/// Fetches the payment history for a specific period.
|
||||
Future<List<StaffPayment>> getPaymentHistory(String period);
|
||||
/// Fetches payment history records for the given date range.
|
||||
Future<List<PaymentRecord>> getPaymentHistory({
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
});
|
||||
|
||||
/// Fetches aggregated chart data points for the given date range and bucket.
|
||||
Future<List<PaymentChartPoint>> getPaymentChart({
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
String bucket,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:staff_payments/src/domain/arguments/get_payment_chart_arguments.dart';
|
||||
import 'package:staff_payments/src/domain/repositories/payments_repository.dart';
|
||||
|
||||
/// Retrieves aggregated chart data for the current staff member's payments.
|
||||
class GetPaymentChartUseCase
|
||||
extends UseCase<GetPaymentChartArguments, List<PaymentChartPoint>> {
|
||||
/// Creates a [GetPaymentChartUseCase].
|
||||
GetPaymentChartUseCase(this._repository);
|
||||
|
||||
/// The payments repository.
|
||||
final PaymentsRepository _repository;
|
||||
|
||||
@override
|
||||
Future<List<PaymentChartPoint>> call(
|
||||
GetPaymentChartArguments arguments,
|
||||
) async {
|
||||
return _repository.getPaymentChart(
|
||||
startDate: arguments.startDate,
|
||||
endDate: arguments.endDate,
|
||||
bucket: arguments.bucket,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,25 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../arguments/get_payment_history_arguments.dart';
|
||||
import '../repositories/payments_repository.dart';
|
||||
|
||||
/// Use case to retrieve payment history filtered by a period.
|
||||
///
|
||||
/// This use case delegates the data retrieval to [PaymentsRepository].
|
||||
class GetPaymentHistoryUseCase extends UseCase<GetPaymentHistoryArguments, List<StaffPayment>> {
|
||||
import 'package:staff_payments/src/domain/arguments/get_payment_history_arguments.dart';
|
||||
import 'package:staff_payments/src/domain/repositories/payments_repository.dart';
|
||||
|
||||
/// Retrieves payment history records for the current staff member.
|
||||
class GetPaymentHistoryUseCase
|
||||
extends UseCase<GetPaymentHistoryArguments, List<PaymentRecord>> {
|
||||
/// Creates a [GetPaymentHistoryUseCase].
|
||||
GetPaymentHistoryUseCase(this.repository);
|
||||
final PaymentsRepository repository;
|
||||
GetPaymentHistoryUseCase(this._repository);
|
||||
|
||||
/// The payments repository.
|
||||
final PaymentsRepository _repository;
|
||||
|
||||
@override
|
||||
Future<List<StaffPayment>> call(GetPaymentHistoryArguments arguments) async {
|
||||
return await repository.getPaymentHistory(arguments.period);
|
||||
Future<List<PaymentRecord>> call(
|
||||
GetPaymentHistoryArguments arguments,
|
||||
) async {
|
||||
return _repository.getPaymentHistory(
|
||||
startDate: arguments.startDate,
|
||||
endDate: arguments.endDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/payments_repository.dart';
|
||||
|
||||
/// Use case to retrieve payment summary information.
|
||||
import 'package:staff_payments/src/domain/repositories/payments_repository.dart';
|
||||
|
||||
/// Retrieves the aggregated payment summary for the current staff member.
|
||||
class GetPaymentSummaryUseCase extends NoInputUseCase<PaymentSummary> {
|
||||
|
||||
/// Creates a [GetPaymentSummaryUseCase].
|
||||
GetPaymentSummaryUseCase(this.repository);
|
||||
final PaymentsRepository repository;
|
||||
GetPaymentSummaryUseCase(this._repository);
|
||||
|
||||
/// The payments repository.
|
||||
final PaymentsRepository _repository;
|
||||
|
||||
@override
|
||||
Future<PaymentSummary> call() async {
|
||||
return await repository.getPaymentSummary();
|
||||
return _repository.getPaymentSummary();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,50 @@
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'domain/repositories/payments_repository.dart';
|
||||
import 'domain/usecases/get_payment_summary_usecase.dart';
|
||||
import 'domain/usecases/get_payment_history_usecase.dart';
|
||||
import 'data/repositories/payments_repository_impl.dart';
|
||||
import 'presentation/blocs/payments/payments_bloc.dart';
|
||||
import 'presentation/pages/payments_page.dart';
|
||||
import 'presentation/pages/early_pay_page.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:staff_payments/src/data/repositories/payments_repository_impl.dart';
|
||||
import 'package:staff_payments/src/domain/repositories/payments_repository.dart';
|
||||
import 'package:staff_payments/src/domain/usecases/get_payment_chart_usecase.dart';
|
||||
import 'package:staff_payments/src/domain/usecases/get_payment_history_usecase.dart';
|
||||
import 'package:staff_payments/src/domain/usecases/get_payment_summary_usecase.dart';
|
||||
import 'package:staff_payments/src/presentation/blocs/payments/payments_bloc.dart';
|
||||
import 'package:staff_payments/src/presentation/pages/early_pay_page.dart';
|
||||
import 'package:staff_payments/src/presentation/pages/payments_page.dart';
|
||||
|
||||
/// Module for the staff payments feature.
|
||||
class StaffPaymentsModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.add<PaymentsRepository>(PaymentsRepositoryImpl.new);
|
||||
i.add<PaymentsRepository>(
|
||||
() => PaymentsRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.add(GetPaymentSummaryUseCase.new);
|
||||
i.add(GetPaymentHistoryUseCase.new);
|
||||
i.add<GetPaymentSummaryUseCase>(
|
||||
() => GetPaymentSummaryUseCase(i.get<PaymentsRepository>()),
|
||||
);
|
||||
i.add<GetPaymentHistoryUseCase>(
|
||||
() => GetPaymentHistoryUseCase(i.get<PaymentsRepository>()),
|
||||
);
|
||||
i.add<GetPaymentChartUseCase>(
|
||||
() => GetPaymentChartUseCase(i.get<PaymentsRepository>()),
|
||||
);
|
||||
|
||||
// Blocs
|
||||
i.add(PaymentsBloc.new);
|
||||
i.add<PaymentsBloc>(
|
||||
() => PaymentsBloc(
|
||||
getPaymentSummary: i.get<GetPaymentSummaryUseCase>(),
|
||||
getPaymentHistory: i.get<GetPaymentHistoryUseCase>(),
|
||||
getPaymentChart: i.get<GetPaymentChartUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../../domain/arguments/get_payment_history_arguments.dart';
|
||||
import '../../../domain/usecases/get_payment_history_usecase.dart';
|
||||
import '../../../domain/usecases/get_payment_summary_usecase.dart';
|
||||
import 'payments_event.dart';
|
||||
import 'payments_state.dart';
|
||||
|
||||
import 'package:staff_payments/src/domain/arguments/get_payment_chart_arguments.dart';
|
||||
import 'package:staff_payments/src/domain/arguments/get_payment_history_arguments.dart';
|
||||
import 'package:staff_payments/src/domain/usecases/get_payment_chart_usecase.dart';
|
||||
import 'package:staff_payments/src/domain/usecases/get_payment_history_usecase.dart';
|
||||
import 'package:staff_payments/src/domain/usecases/get_payment_summary_usecase.dart';
|
||||
import 'package:staff_payments/src/presentation/blocs/payments/payments_event.dart';
|
||||
import 'package:staff_payments/src/presentation/blocs/payments/payments_state.dart';
|
||||
|
||||
/// BLoC that manages loading and displaying staff payment data.
|
||||
class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState>
|
||||
with BlocErrorHandler<PaymentsState> {
|
||||
/// Creates a [PaymentsBloc] injecting the required use cases.
|
||||
PaymentsBloc({
|
||||
required this.getPaymentSummary,
|
||||
required this.getPaymentHistory,
|
||||
required this.getPaymentChart,
|
||||
}) : super(PaymentsInitial()) {
|
||||
on<LoadPaymentsEvent>(_onLoadPayments);
|
||||
on<ChangePeriodEvent>(_onChangePeriod);
|
||||
}
|
||||
|
||||
/// Use case for fetching the earnings summary.
|
||||
final GetPaymentSummaryUseCase getPaymentSummary;
|
||||
|
||||
/// Use case for fetching payment history records.
|
||||
final GetPaymentHistoryUseCase getPaymentHistory;
|
||||
|
||||
/// Use case for fetching chart data points.
|
||||
final GetPaymentChartUseCase getPaymentChart;
|
||||
|
||||
/// Handles the initial load of all payment data.
|
||||
Future<void> _onLoadPayments(
|
||||
LoadPaymentsEvent event,
|
||||
Emitter<PaymentsState> emit,
|
||||
@@ -27,15 +41,28 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final PaymentSummary currentSummary = await getPaymentSummary();
|
||||
|
||||
final List<StaffPayment> history = await getPaymentHistory(
|
||||
const GetPaymentHistoryArguments('week'),
|
||||
);
|
||||
final _DateRange range = _dateRangeFor('week');
|
||||
final List<Object> results = await Future.wait(<Future<Object>>[
|
||||
getPaymentSummary(),
|
||||
getPaymentHistory(
|
||||
GetPaymentHistoryArguments(
|
||||
startDate: range.start,
|
||||
endDate: range.end,
|
||||
),
|
||||
),
|
||||
getPaymentChart(
|
||||
GetPaymentChartArguments(
|
||||
startDate: range.start,
|
||||
endDate: range.end,
|
||||
bucket: 'day',
|
||||
),
|
||||
),
|
||||
]);
|
||||
emit(
|
||||
PaymentsLoaded(
|
||||
summary: currentSummary,
|
||||
history: history,
|
||||
summary: results[0] as PaymentSummary,
|
||||
history: results[1] as List<PaymentRecord>,
|
||||
chartPoints: results[2] as List<PaymentChartPoint>,
|
||||
activePeriod: 'week',
|
||||
),
|
||||
);
|
||||
@@ -44,6 +71,7 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState>
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles switching the active period tab.
|
||||
Future<void> _onChangePeriod(
|
||||
ChangePeriodEvent event,
|
||||
Emitter<PaymentsState> emit,
|
||||
@@ -53,12 +81,27 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final List<StaffPayment> newHistory = await getPaymentHistory(
|
||||
GetPaymentHistoryArguments(event.period),
|
||||
);
|
||||
final _DateRange range = _dateRangeFor(event.period);
|
||||
final String bucket = _bucketFor(event.period);
|
||||
final List<Object> results = await Future.wait(<Future<Object>>[
|
||||
getPaymentHistory(
|
||||
GetPaymentHistoryArguments(
|
||||
startDate: range.start,
|
||||
endDate: range.end,
|
||||
),
|
||||
),
|
||||
getPaymentChart(
|
||||
GetPaymentChartArguments(
|
||||
startDate: range.start,
|
||||
endDate: range.end,
|
||||
bucket: bucket,
|
||||
),
|
||||
),
|
||||
]);
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
history: newHistory,
|
||||
history: results[0] as List<PaymentRecord>,
|
||||
chartPoints: results[1] as List<PaymentChartPoint>,
|
||||
activePeriod: event.period,
|
||||
),
|
||||
);
|
||||
@@ -67,5 +110,46 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes start and end ISO-8601 date strings for a given period.
|
||||
static _DateRange _dateRangeFor(String period) {
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime end = now;
|
||||
late final DateTime start;
|
||||
switch (period) {
|
||||
case 'week':
|
||||
start = now.subtract(const Duration(days: 7));
|
||||
case 'month':
|
||||
start = DateTime(now.year, now.month - 1, now.day);
|
||||
case 'year':
|
||||
start = DateTime(now.year - 1, now.month, now.day);
|
||||
default:
|
||||
start = now.subtract(const Duration(days: 7));
|
||||
}
|
||||
return _DateRange(
|
||||
start: start.toIso8601String(),
|
||||
end: end.toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Maps a period identifier to the chart bucket granularity.
|
||||
static String _bucketFor(String period) {
|
||||
switch (period) {
|
||||
case 'week':
|
||||
return 'day';
|
||||
case 'month':
|
||||
return 'week';
|
||||
case 'year':
|
||||
return 'month';
|
||||
default:
|
||||
return 'day';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper for holding a date range pair.
|
||||
class _DateRange {
|
||||
const _DateRange({required this.start, required this.end});
|
||||
final String start;
|
||||
final String end;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Base event for the payments feature.
|
||||
abstract class PaymentsEvent extends Equatable {
|
||||
/// Creates a [PaymentsEvent].
|
||||
const PaymentsEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Triggered on initial load to fetch summary, history, and chart data.
|
||||
class LoadPaymentsEvent extends PaymentsEvent {}
|
||||
|
||||
/// Triggered when the user switches the period tab (week, month, year).
|
||||
class ChangePeriodEvent extends PaymentsEvent {
|
||||
|
||||
/// Creates a [ChangePeriodEvent] for the given [period].
|
||||
const ChangePeriodEvent(this.period);
|
||||
|
||||
/// The selected period identifier.
|
||||
final String period;
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,47 +1,69 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the payments feature.
|
||||
abstract class PaymentsState extends Equatable {
|
||||
/// Creates a [PaymentsState].
|
||||
const PaymentsState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Initial state before any data has been requested.
|
||||
class PaymentsInitial extends PaymentsState {}
|
||||
|
||||
/// Data is being loaded from the backend.
|
||||
class PaymentsLoading extends PaymentsState {}
|
||||
|
||||
/// Data loaded successfully.
|
||||
class PaymentsLoaded extends PaymentsState {
|
||||
|
||||
/// Creates a [PaymentsLoaded] state.
|
||||
const PaymentsLoaded({
|
||||
required this.summary,
|
||||
required this.history,
|
||||
required this.chartPoints,
|
||||
this.activePeriod = 'week',
|
||||
});
|
||||
|
||||
/// Aggregated payment summary.
|
||||
final PaymentSummary summary;
|
||||
final List<StaffPayment> history;
|
||||
|
||||
/// List of individual payment records.
|
||||
final List<PaymentRecord> history;
|
||||
|
||||
/// Chart data points for the earnings trend graph.
|
||||
final List<PaymentChartPoint> chartPoints;
|
||||
|
||||
/// Currently selected period tab (week, month, year).
|
||||
final String activePeriod;
|
||||
|
||||
/// Creates a copy with optional overrides.
|
||||
PaymentsLoaded copyWith({
|
||||
PaymentSummary? summary,
|
||||
List<StaffPayment>? history,
|
||||
List<PaymentRecord>? history,
|
||||
List<PaymentChartPoint>? chartPoints,
|
||||
String? activePeriod,
|
||||
}) {
|
||||
return PaymentsLoaded(
|
||||
summary: summary ?? this.summary,
|
||||
history: history ?? this.history,
|
||||
chartPoints: chartPoints ?? this.chartPoints,
|
||||
activePeriod: activePeriod ?? this.activePeriod,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[summary, history, activePeriod];
|
||||
List<Object?> get props =>
|
||||
<Object?>[summary, history, chartPoints, activePeriod];
|
||||
}
|
||||
|
||||
/// An error occurred while loading payments data.
|
||||
class PaymentsError extends PaymentsState {
|
||||
|
||||
/// Creates a [PaymentsError] with the given [message].
|
||||
const PaymentsError(this.message);
|
||||
|
||||
/// The error key or message.
|
||||
final String message;
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,15 +5,18 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import '../blocs/payments/payments_bloc.dart';
|
||||
import '../blocs/payments/payments_event.dart';
|
||||
import '../blocs/payments/payments_state.dart';
|
||||
import '../widgets/payments_page_skeleton.dart';
|
||||
import '../widgets/payment_stats_card.dart';
|
||||
import '../widgets/payment_history_item.dart';
|
||||
import '../widgets/earnings_graph.dart';
|
||||
|
||||
import 'package:staff_payments/src/presentation/blocs/payments/payments_bloc.dart';
|
||||
import 'package:staff_payments/src/presentation/blocs/payments/payments_event.dart';
|
||||
import 'package:staff_payments/src/presentation/blocs/payments/payments_state.dart';
|
||||
import 'package:staff_payments/src/presentation/widgets/payments_page_skeleton.dart';
|
||||
import 'package:staff_payments/src/presentation/widgets/payment_stats_card.dart';
|
||||
import 'package:staff_payments/src/presentation/widgets/payment_history_item.dart';
|
||||
import 'package:staff_payments/src/presentation/widgets/earnings_graph.dart';
|
||||
|
||||
/// Main page for the staff payments feature.
|
||||
class PaymentsPage extends StatefulWidget {
|
||||
/// Creates a [PaymentsPage].
|
||||
const PaymentsPage({super.key});
|
||||
|
||||
@override
|
||||
@@ -38,12 +41,11 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
backgroundColor: UiColors.background,
|
||||
body: BlocConsumer<PaymentsBloc, PaymentsState>(
|
||||
listener: (BuildContext context, PaymentsState state) {
|
||||
// Error is already shown on the page itself (lines 53-63), no need for snackbar
|
||||
// Error is rendered inline, no snackbar needed.
|
||||
},
|
||||
builder: (BuildContext context, PaymentsState state) {
|
||||
if (state is PaymentsLoading) {
|
||||
return const PaymentsPageSkeleton();
|
||||
|
||||
} else if (state is PaymentsError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
@@ -51,7 +53,8 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
child: Text(
|
||||
translateErrorKey(state.message),
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.textSecondary),
|
||||
style: UiTypography.body2r
|
||||
.copyWith(color: UiColors.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -65,7 +68,10 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the loaded content layout.
|
||||
Widget _buildContent(BuildContext context, PaymentsLoaded state) {
|
||||
final String totalFormatted =
|
||||
_formatCents(state.summary.totalEarningsCents);
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
@@ -91,7 +97,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Earnings",
|
||||
'Earnings',
|
||||
style: UiTypography.displayMb.white,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
@@ -101,14 +107,14 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Total Earnings",
|
||||
'Total Earnings',
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.accent,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
"\$${state.summary.totalEarnings.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')}",
|
||||
totalFormatted,
|
||||
style: UiTypography.displayL.white,
|
||||
),
|
||||
],
|
||||
@@ -121,13 +127,14 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
padding: const EdgeInsets.all(UiConstants.space1),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
_buildTab("Week", 'week', state.activePeriod),
|
||||
_buildTab("Month", 'month', state.activePeriod),
|
||||
_buildTab("Year", 'year', state.activePeriod),
|
||||
_buildTab('Week', 'week', state.activePeriod),
|
||||
_buildTab('Month', 'month', state.activePeriod),
|
||||
_buildTab('Year', 'year', state.activePeriod),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -139,16 +146,18 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -UiConstants.space4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// Earnings Graph
|
||||
EarningsGraph(
|
||||
payments: state.history,
|
||||
chartPoints: state.chartPoints,
|
||||
period: state.activePeriod,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// Quick Stats
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@@ -156,8 +165,8 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
child: PaymentStatsCard(
|
||||
icon: UiIcons.chart,
|
||||
iconColor: UiColors.success,
|
||||
label: "This Week",
|
||||
amount: "\$${state.summary.weeklyEarnings}",
|
||||
label: 'Total Earnings',
|
||||
amount: totalFormatted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
@@ -165,8 +174,14 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
child: PaymentStatsCard(
|
||||
icon: UiIcons.calendar,
|
||||
iconColor: UiColors.primary,
|
||||
label: "This Month",
|
||||
amount: "\$${state.summary.monthlyEarnings.toStringAsFixed(0)}",
|
||||
label: '${state.history.length} Payments',
|
||||
amount: _formatCents(
|
||||
state.history.fold<int>(
|
||||
0,
|
||||
(int sum, PaymentRecord r) =>
|
||||
sum + r.amountCents,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -179,28 +194,26 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Recent Payments",
|
||||
'Recent Payments',
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Column(
|
||||
children: state.history.map((StaffPayment payment) {
|
||||
children:
|
||||
state.history.map((PaymentRecord payment) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: UiConstants.space2),
|
||||
child: PaymentHistoryItem(
|
||||
amount: payment.amount,
|
||||
title: payment.shiftTitle ?? "Shift Payment",
|
||||
location: payment.shiftLocation ?? "Varies",
|
||||
address: payment.locationAddress ?? payment.id,
|
||||
date: payment.paidAt != null
|
||||
? DateFormat('E, MMM d')
|
||||
.format(payment.paidAt!)
|
||||
: 'Pending',
|
||||
workedTime: payment.workedTime ?? "Completed",
|
||||
hours: (payment.hoursWorked ?? 0).toInt(),
|
||||
rate: payment.hourlyRate ?? 0.0,
|
||||
status: payment.status.name.toUpperCase(),
|
||||
amountCents: payment.amountCents,
|
||||
title: payment.shiftName ?? 'Shift Payment',
|
||||
location: payment.location ?? 'Varies',
|
||||
date:
|
||||
DateFormat('E, MMM d').format(payment.date),
|
||||
minutesWorked: payment.minutesWorked ?? 0,
|
||||
hourlyRateCents:
|
||||
payment.hourlyRateCents ?? 0,
|
||||
status: payment.status,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -218,16 +231,19 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a period tab widget.
|
||||
Widget _buildTab(String label, String value, String activePeriod) {
|
||||
final bool isSelected = activePeriod == value;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _bloc.add(ChangePeriodEvent(value)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space2),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: UiConstants.space2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? UiColors.white : UiColors.transparent,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusMdValue),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
@@ -241,5 +257,14 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an amount in cents to a dollar string (e.g. `$1,234.56`).
|
||||
static String _formatCents(int cents) {
|
||||
final double dollars = cents / 100;
|
||||
final NumberFormat formatter = NumberFormat.currency(
|
||||
symbol: r'$',
|
||||
decimalDigits: 2,
|
||||
);
|
||||
return formatter.format(dollars);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,24 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Displays an earnings trend line chart from backend chart data.
|
||||
class EarningsGraph extends StatelessWidget {
|
||||
|
||||
/// Creates an [EarningsGraph].
|
||||
const EarningsGraph({
|
||||
super.key,
|
||||
required this.payments,
|
||||
required this.chartPoints,
|
||||
required this.period,
|
||||
});
|
||||
final List<StaffPayment> payments;
|
||||
|
||||
/// Pre-aggregated chart data points from the V2 API.
|
||||
final List<PaymentChartPoint> chartPoints;
|
||||
|
||||
/// The currently selected period (week, month, year).
|
||||
final String period;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Basic data processing for the graph
|
||||
// We'll aggregate payments by date
|
||||
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) {
|
||||
if (chartPoints.isEmpty) {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
@@ -31,15 +30,23 @@ class EarningsGraph extends StatelessWidget {
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No sufficient data for graph",
|
||||
'No sufficient data for graph',
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final List<FlSpot> spots = _generateSpots(validPayments);
|
||||
final double maxY = spots.isNotEmpty ? spots.map((FlSpot s) => s.y).reduce((double a, double b) => a > b ? a : b) : 0.0;
|
||||
final List<PaymentChartPoint> sorted = List<PaymentChartPoint>.of(chartPoints)
|
||||
..sort((PaymentChartPoint a, PaymentChartPoint b) =>
|
||||
a.bucket.compareTo(b.bucket));
|
||||
|
||||
final List<FlSpot> spots = _generateSpots(sorted);
|
||||
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,
|
||||
@@ -59,7 +66,7 @@ class EarningsGraph extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Earnings Trend",
|
||||
'Earnings Trend',
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
@@ -71,26 +78,31 @@ class EarningsGraph extends StatelessWidget {
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
// Simple logic to show a few dates
|
||||
if (value % 2 != 0) return const SizedBox();
|
||||
final int index = value.toInt();
|
||||
if (index >= 0 && index < validPayments.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
DateFormat('d').format(validPayments[index].paidAt!),
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
getTitlesWidget:
|
||||
(double value, TitleMeta meta) {
|
||||
if (value % 2 != 0) return const SizedBox();
|
||||
final int index = value.toInt();
|
||||
if (index >= 0 && index < sorted.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
_formatBucketLabel(
|
||||
sorted[index].bucket, period),
|
||||
style:
|
||||
UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: <LineChartBarData>[
|
||||
@@ -119,20 +131,32 @@ class EarningsGraph extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<FlSpot> _generateSpots(List<StaffPayment> data) {
|
||||
if (data.isEmpty) return [];
|
||||
|
||||
// If only one data point, add a dummy point at the start to create a horizontal line
|
||||
/// Converts chart points to [FlSpot] values (dollars).
|
||||
List<FlSpot> _generateSpots(List<PaymentChartPoint> data) {
|
||||
if (data.isEmpty) return <FlSpot>[];
|
||||
|
||||
if (data.length == 1) {
|
||||
return [
|
||||
FlSpot(0, data[0].amount),
|
||||
FlSpot(1, data[0].amount),
|
||||
final double dollars = data[0].amountCents / 100;
|
||||
return <FlSpot>[
|
||||
FlSpot(0, dollars),
|
||||
FlSpot(1, dollars),
|
||||
];
|
||||
}
|
||||
|
||||
// Generate spots based on index in the list for simplicity in this demo
|
||||
return List<FlSpot>.generate(data.length, (int index) {
|
||||
return FlSpot(index.toDouble(), data[index].amount);
|
||||
return FlSpot(index.toDouble(), data[index].amountCents / 100);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns a short label for a chart bucket date.
|
||||
String _formatBucketLabel(DateTime bucket, String period) {
|
||||
switch (period) {
|
||||
case 'year':
|
||||
return DateFormat('MMM').format(bucket);
|
||||
case 'month':
|
||||
return DateFormat('d').format(bucket);
|
||||
default:
|
||||
return DateFormat('d').format(bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Displays a single payment record in the history list.
|
||||
class PaymentHistoryItem extends StatelessWidget {
|
||||
|
||||
/// Creates a [PaymentHistoryItem].
|
||||
const PaymentHistoryItem({
|
||||
super.key,
|
||||
required this.amount,
|
||||
required this.amountCents,
|
||||
required this.title,
|
||||
required this.location,
|
||||
required this.address,
|
||||
required this.date,
|
||||
required this.workedTime,
|
||||
required this.hours,
|
||||
required this.rate,
|
||||
required this.minutesWorked,
|
||||
required this.hourlyRateCents,
|
||||
required this.status,
|
||||
});
|
||||
final double amount;
|
||||
|
||||
/// Payment amount in cents.
|
||||
final int amountCents;
|
||||
|
||||
/// Shift or payment title.
|
||||
final String title;
|
||||
|
||||
/// Location / hub name.
|
||||
final String location;
|
||||
final String address;
|
||||
|
||||
/// Formatted date string.
|
||||
final String date;
|
||||
final String workedTime;
|
||||
final int hours;
|
||||
final double rate;
|
||||
final String status;
|
||||
|
||||
/// Total minutes worked.
|
||||
final int minutesWorked;
|
||||
|
||||
/// Hourly rate in cents.
|
||||
final int hourlyRateCents;
|
||||
|
||||
/// Payment processing status.
|
||||
final PaymentStatus status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String dollarAmount = _centsToDollars(amountCents);
|
||||
final String rateDisplay = _centsToDollars(hourlyRateCents);
|
||||
final int hours = minutesWorked ~/ 60;
|
||||
final int mins = minutesWorked % 60;
|
||||
final String timeDisplay =
|
||||
mins > 0 ? '${hours}h ${mins}m' : '${hours}h';
|
||||
final Color statusColor = _statusColor(status);
|
||||
final String statusLabel = status.value;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -43,16 +64,16 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"PAID",
|
||||
statusLabel,
|
||||
style: UiTypography.titleUppercase4b.copyWith(
|
||||
color: UiColors.primary,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -68,7 +89,8 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.secondary,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.dollar,
|
||||
@@ -90,10 +112,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body2m,
|
||||
),
|
||||
Text(title, style: UiTypography.body2m),
|
||||
Text(
|
||||
location,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
@@ -105,12 +124,13 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"\$${amount.toStringAsFixed(0)}",
|
||||
dollarAmount,
|
||||
style: UiTypography.headline4b,
|
||||
),
|
||||
Text(
|
||||
"\$${rate.toStringAsFixed(0)}/hr · ${hours}h",
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
'$rateDisplay/hr \u00B7 $timeDisplay',
|
||||
style:
|
||||
UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -118,7 +138,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
|
||||
// Date and Time
|
||||
// Date
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
@@ -139,32 +159,11 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
workedTime,
|
||||
timeDisplay,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
|
||||
// Address
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.mapPin,
|
||||
size: 12,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
address,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -174,4 +173,26 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts cents to a formatted dollar string.
|
||||
static String _centsToDollars(int cents) {
|
||||
final double dollars = cents / 100;
|
||||
return '\$${dollars.toStringAsFixed(2)}';
|
||||
}
|
||||
|
||||
/// Returns a colour for the given payment status.
|
||||
static Color _statusColor(PaymentStatus status) {
|
||||
switch (status) {
|
||||
case PaymentStatus.paid:
|
||||
return UiColors.primary;
|
||||
case PaymentStatus.pending:
|
||||
return UiColors.textWarning;
|
||||
case PaymentStatus.processing:
|
||||
return UiColors.primary;
|
||||
case PaymentStatus.failed:
|
||||
return UiColors.error;
|
||||
case PaymentStatus.unknown:
|
||||
return UiColors.mutedForeground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,9 @@ dependencies:
|
||||
path: ../../../domain
|
||||
krow_core:
|
||||
path: ../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
flutter_modular: ^6.3.2
|
||||
intl: ^0.20.0
|
||||
fl_chart: ^0.66.0
|
||||
|
||||
Reference in New Issue
Block a user