feat: legacy mobile apps created
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
const String _staffPaymentFields = '''
|
||||
id
|
||||
rate
|
||||
assignment {
|
||||
clock_in
|
||||
clock_out
|
||||
start_at
|
||||
end_at
|
||||
break_in
|
||||
break_out
|
||||
position {
|
||||
shift {
|
||||
event {
|
||||
name
|
||||
date
|
||||
business {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
business_skill {
|
||||
skill {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
work_hours
|
||||
amount
|
||||
status
|
||||
paid_at
|
||||
created_at
|
||||
updated_at
|
||||
''';
|
||||
|
||||
const String getWorkSummaryQuerySchema = '''
|
||||
query GetWorkSummary {
|
||||
staff_work_summary {
|
||||
weekly_hours
|
||||
monthly_hours
|
||||
weekly_earnings
|
||||
monthly_earnings
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String getPaymentsQuerySchema = '''
|
||||
query GetStaffPayments (\$status: StaffPaymentStatusInput!, \$first: Int!, \$after: String) {
|
||||
staff_payments(status: \$status, first: \$first, after: \$after) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
$_staffPaymentFields
|
||||
}
|
||||
cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String confirmPaymentMutationSchema = '''
|
||||
mutation ConfirmStaffPayment (\$id: ID!) {
|
||||
confirm_staff_payment(id: \$id) {
|
||||
$_staffPaymentFields
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String declinePaymentMutationSchema = '''
|
||||
mutation DeclineStaffPayment (\$id: ID!, \$reason: String!, \$details: String) {
|
||||
decline_staff_payment(id: \$id, reason: \$reason, details: \$details) {
|
||||
$_staffPaymentFields
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,67 @@
|
||||
class EarningModel {
|
||||
EarningModel({
|
||||
required this.id,
|
||||
required this.rate,
|
||||
required this.businessName,
|
||||
required this.businessAvatar,
|
||||
required this.businessSkill,
|
||||
required this.workHours,
|
||||
required this.amount,
|
||||
required this.status,
|
||||
required this.paidAt,
|
||||
required this.clockInAt,
|
||||
required this.clockOutAt,
|
||||
this.breakIn,
|
||||
this.breakOut,
|
||||
required this.eventName,
|
||||
required this.eventDate,
|
||||
});
|
||||
|
||||
factory EarningModel.fromJson(Map<String, dynamic> json) {
|
||||
final assignment = json['assignment'] as Map<String, dynamic>;
|
||||
final positionData = assignment['position'] as Map<String, dynamic>;
|
||||
final eventData = positionData['shift']['event'] as Map<String, dynamic>;
|
||||
final businessData = eventData['business'] as Map<String, dynamic>;
|
||||
|
||||
return EarningModel(
|
||||
id: json['id'] as String? ?? '',
|
||||
rate: (json['rate'] as num? ?? 0).toDouble(),
|
||||
businessName: businessData['name'] as String? ?? '',
|
||||
businessAvatar: businessData['avatar'] as String? ?? '',
|
||||
businessSkill:
|
||||
positionData['business_skill']['skill']['name'] as String? ?? '',
|
||||
workHours: (json['work_hours'] as num? ?? 0).toDouble(),
|
||||
amount: (json['amount'] as num? ?? 0).toDouble(),
|
||||
status: json['status'] as String? ?? 'failed',
|
||||
paidAt: DateTime.tryParse(
|
||||
json['paid_at'] as String? ?? '',
|
||||
),
|
||||
clockInAt: DateTime.parse(
|
||||
assignment['clock_in'] ?? assignment['start_at'] as String,
|
||||
),
|
||||
clockOutAt: DateTime.parse(
|
||||
assignment['clock_out'] ?? assignment['end_at'] as String,
|
||||
),
|
||||
breakIn: DateTime.tryParse(assignment['break_in'] ?? ''),
|
||||
breakOut: DateTime.tryParse(assignment['break_out'] ?? ''),
|
||||
eventName: eventData['name'] as String? ?? '',
|
||||
eventDate: DateTime.parse(eventData['date'] as String? ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final double rate;
|
||||
final String businessName;
|
||||
final String businessAvatar;
|
||||
final String businessSkill;
|
||||
final double workHours;
|
||||
final double amount;
|
||||
final String status;
|
||||
final DateTime? paidAt;
|
||||
final DateTime clockInAt;
|
||||
final DateTime clockOutAt;
|
||||
final DateTime? breakIn;
|
||||
final DateTime? breakOut;
|
||||
final String eventName;
|
||||
final DateTime eventDate;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
class EarningsSummaryModel {
|
||||
EarningsSummaryModel({
|
||||
required this.totalEarningsByWeek,
|
||||
required this.totalEarningsByMonth,
|
||||
required this.totalWorkedHoursByWeek,
|
||||
required this.totalWorkedHoursByMonth,
|
||||
required this.payoutByWeek,
|
||||
required this.payoutByMonth,
|
||||
required this.startDatePeriod,
|
||||
required this.endDatePeriod,
|
||||
required this.maxEarningInPeriod,
|
||||
required this.minEarningInPeriod,
|
||||
});
|
||||
|
||||
//TODO: Additional fields that are used in the Earnings History screen are for now returning default values.
|
||||
factory EarningsSummaryModel.fromJson(Map<String, dynamic> json) {
|
||||
final time = DateTime.now();
|
||||
return EarningsSummaryModel(
|
||||
totalEarningsByWeek: (json['weekly_earnings'] as num?)?.toDouble() ?? 0,
|
||||
totalEarningsByMonth: (json['monthly_earnings'] as num?)?.toDouble() ?? 0,
|
||||
totalWorkedHoursByWeek: (json['weekly_hours'] as num?)?.toDouble() ?? 0,
|
||||
totalWorkedHoursByMonth: (json['monthly_hours'] as num?)?.toDouble() ?? 0,
|
||||
payoutByWeek: 0,
|
||||
payoutByMonth: 0,
|
||||
startDatePeriod: DateTime(time.year, time.month),
|
||||
endDatePeriod: DateTime(time.year, time.month, 28),
|
||||
maxEarningInPeriod: 0,
|
||||
minEarningInPeriod: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final double totalEarningsByWeek;
|
||||
final double totalEarningsByMonth;
|
||||
final double totalWorkedHoursByWeek;
|
||||
final double totalWorkedHoursByMonth;
|
||||
final double payoutByWeek;
|
||||
final double payoutByMonth;
|
||||
final DateTime? startDatePeriod;
|
||||
final DateTime? endDatePeriod;
|
||||
final int maxEarningInPeriod;
|
||||
final int minEarningInPeriod;
|
||||
|
||||
EarningsSummaryModel copyWith({
|
||||
double? totalEarningsByWeek,
|
||||
double? totalEarningsByMonth,
|
||||
double? totalWorkedHoursByWeek,
|
||||
double? totalWorkedHoursByMonth,
|
||||
double? payoutByWeek,
|
||||
double? payoutByMonth,
|
||||
DateTime? startDatePeriod,
|
||||
DateTime? endDatePeriod,
|
||||
int? maxEarningInPeriod,
|
||||
int? minEarningInPeriod,
|
||||
}) {
|
||||
return EarningsSummaryModel(
|
||||
totalEarningsByWeek: totalEarningsByWeek ?? this.totalEarningsByWeek,
|
||||
totalEarningsByMonth: totalEarningsByMonth ?? this.totalEarningsByMonth,
|
||||
totalWorkedHoursByWeek:
|
||||
totalWorkedHoursByWeek ?? this.totalWorkedHoursByWeek,
|
||||
totalWorkedHoursByMonth:
|
||||
totalWorkedHoursByMonth ?? this.totalWorkedHoursByMonth,
|
||||
payoutByWeek: payoutByWeek ?? this.payoutByWeek,
|
||||
payoutByMonth: payoutByMonth ?? this.payoutByMonth,
|
||||
startDatePeriod: startDatePeriod,
|
||||
endDatePeriod: endDatePeriod,
|
||||
maxEarningInPeriod: maxEarningInPeriod ?? this.maxEarningInPeriod,
|
||||
minEarningInPeriod: minEarningInPeriod ?? this.minEarningInPeriod,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/pagination_wrapper/pagination_wrapper.dart';
|
||||
import 'package:krow/features/earning/data/earning_qgl.dart';
|
||||
import 'package:krow/features/earning/data/models/earning_model.dart';
|
||||
import 'package:krow/features/earning/data/models/earnings_summary_model.dart';
|
||||
|
||||
@injectable
|
||||
class StaffEarningApiProvider {
|
||||
StaffEarningApiProvider({required ApiClient client}) : _client = client;
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Future<EarningsSummaryModel> fetchStaffEarningsSummary() async {
|
||||
final QueryResult result = await _client.query(
|
||||
schema: getWorkSummaryQuerySchema,
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
return EarningsSummaryModel.fromJson(
|
||||
result.data?['staff_work_summary'] as Map<String, dynamic>? ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
Future<PaginationWrapper<EarningModel>> fetchEarnings({
|
||||
required String status,
|
||||
required int limit,
|
||||
String? cursor,
|
||||
}) async {
|
||||
final QueryResult result = await _client.query(
|
||||
schema: getPaymentsQuerySchema,
|
||||
body: {'status': status, 'first': limit, 'after': cursor},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
return PaginationWrapper<EarningModel>.fromJson(
|
||||
result.data?['staff_payments'] ?? {},
|
||||
EarningModel.fromJson,
|
||||
);
|
||||
}
|
||||
|
||||
Future<EarningModel?> _processPaymentMutation({
|
||||
required String schema,
|
||||
required Map<String, dynamic> body,
|
||||
required String mutationName,
|
||||
}) async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: schema,
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (result.hasException) throw Exception(result.exception.toString());
|
||||
|
||||
if (result.data == null) {
|
||||
throw Exception('Payment data is missing on mutation $mutationName');
|
||||
}
|
||||
|
||||
final jsonData = result.data?[mutationName] as Map<String, dynamic>;
|
||||
return EarningModel.fromJson(jsonData);
|
||||
}
|
||||
|
||||
Future<EarningModel?> confirmPayment({required String id}) {
|
||||
return _processPaymentMutation(
|
||||
schema: confirmPaymentMutationSchema,
|
||||
body: {'id': id},
|
||||
mutationName: 'confirm_staff_payment',
|
||||
);
|
||||
}
|
||||
|
||||
Future<EarningModel?> declinePayment({
|
||||
required String id,
|
||||
required String reason,
|
||||
String? details,
|
||||
}) {
|
||||
return _processPaymentMutation(
|
||||
schema: declinePaymentMutationSchema,
|
||||
body: {'id': id, 'reason': reason, 'details': details},
|
||||
mutationName: 'decline_staff_payment',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/features/earning/data/models/earning_model.dart';
|
||||
import 'package:krow/features/earning/data/staff_earning_api_provider.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earnings_batch_entity.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earnings_summary_entity.dart';
|
||||
import 'package:krow/features/earning/domain/staff_earning_repository.dart';
|
||||
|
||||
@Injectable(as: StaffEarningRepository)
|
||||
class StaffEarningRepositoryImpl implements StaffEarningRepository {
|
||||
StaffEarningRepositoryImpl({
|
||||
required StaffEarningApiProvider apiProvider,
|
||||
}) : _apiProvider = apiProvider;
|
||||
|
||||
final StaffEarningApiProvider _apiProvider;
|
||||
|
||||
EarningShiftEntity _convertEarningModel(EarningModel data) {
|
||||
return EarningShiftEntity(
|
||||
id: data.id,
|
||||
status: EarningStatus.fromString(data.status),
|
||||
businessImageUrl: data.businessAvatar,
|
||||
skillName: data.businessSkill,
|
||||
businessName: data.businessName,
|
||||
totalBreakTime: data.breakIn != null
|
||||
? data.breakOut?.difference(data.breakIn!).inSeconds
|
||||
: null,
|
||||
paymentStatus: 1,
|
||||
earned: data.amount,
|
||||
clockIn: data.clockInAt,
|
||||
clockOut: data.clockOutAt,
|
||||
eventName: data.eventName,
|
||||
eventDate: data.eventDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EarningsSummaryEntity> getStaffEarningsData() async {
|
||||
final data = await _apiProvider.fetchStaffEarningsSummary();
|
||||
|
||||
return EarningsSummaryEntity(
|
||||
totalEarningsByWeek: data.totalEarningsByWeek,
|
||||
totalEarningsByMonth: data.totalEarningsByMonth,
|
||||
totalWorkedHoursByWeek: data.totalWorkedHoursByWeek,
|
||||
totalWorkedHoursByMonth: data.totalWorkedHoursByMonth,
|
||||
payoutByWeek: data.payoutByWeek,
|
||||
payoutByMonth: data.payoutByMonth,
|
||||
startDatePeriod: data.startDatePeriod,
|
||||
endDatePeriod: data.endDatePeriod,
|
||||
maxEarningInPeriod: data.maxEarningInPeriod,
|
||||
minEarningInPeriod: data.minEarningInPeriod,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EarningsBatchEntity> getEarningsBatch({
|
||||
required String status,
|
||||
int limit = 10,
|
||||
String? lastEntryCursor,
|
||||
}) async {
|
||||
final paginationInfo = await _apiProvider.fetchEarnings(
|
||||
status: status,
|
||||
limit: limit,
|
||||
cursor: lastEntryCursor,
|
||||
);
|
||||
|
||||
return EarningsBatchEntity(
|
||||
batchStatus: status,
|
||||
hasNextBatch: paginationInfo.pageInfo?.hasNextPage??false,
|
||||
cursor: paginationInfo.pageInfo?.endCursor ??
|
||||
paginationInfo.edges.lastOrNull?.cursor,
|
||||
earnings: paginationInfo.edges.map(
|
||||
(edgeData) {
|
||||
return _convertEarningModel(edgeData.node);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EarningShiftEntity?> confirmStaffEarning({
|
||||
required String earningId,
|
||||
}) async {
|
||||
final result = await _apiProvider.confirmPayment(id: earningId);
|
||||
|
||||
if (result == null) return null;
|
||||
|
||||
return _convertEarningModel(result);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EarningShiftEntity?> disputeStaffEarning({
|
||||
required String id,
|
||||
required String reason,
|
||||
String? details,
|
||||
}) async {
|
||||
final result = await _apiProvider.declinePayment(
|
||||
id: id,
|
||||
reason: reason,
|
||||
details: details,
|
||||
);
|
||||
|
||||
if (result == null) return null;
|
||||
|
||||
return _convertEarningModel(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/enums/pagination_status.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earnings_summary_entity.dart';
|
||||
import 'package:krow/features/earning/domain/staff_earning_repository.dart';
|
||||
|
||||
part 'earnings_data.dart';
|
||||
part 'earnings_event.dart';
|
||||
part 'earnings_state.dart';
|
||||
|
||||
class EarningsBloc extends Bloc<EarningsEvent, EarningsState> {
|
||||
EarningsBloc()
|
||||
: super(const EarningsState.initial()
|
||||
..copyWith(
|
||||
tabs: defaultEarningTabs,
|
||||
)) {
|
||||
on<EarningsInitEvent>(_onInit);
|
||||
on<EarningsSwitchBalancePeriodEvent>(_onSwitchBalancePeriod);
|
||||
on<EarningsTabChangedEvent>(_onTabChanged);
|
||||
on<LoadAdditionalEarnings>(_onLoadAdditionalEarnings);
|
||||
on<ReloadCurrentEarnings>(_onReloadCurrentEarnings);
|
||||
on<ConfirmStaffEarning>(_onConfirmStaffEarning);
|
||||
on<DisputeStaffEarning>(_onDisputeStaffEarning);
|
||||
}
|
||||
|
||||
final _earningRepository = getIt<StaffEarningRepository>();
|
||||
|
||||
Future<void> _onInit(
|
||||
EarningsInitEvent event,
|
||||
Emitter<EarningsState> emit,
|
||||
) async {
|
||||
add(const LoadAdditionalEarnings());
|
||||
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
EarningsSummaryEntity? earningsData;
|
||||
try {
|
||||
earningsData = await _earningRepository.getStaffEarningsData();
|
||||
} catch (except) {
|
||||
log('Error in EarningsBloc, on EarningsInitEvent', error: except);
|
||||
}
|
||||
emit(state.copyWith(earnings: earningsData, status: StateStatus.idle));
|
||||
}
|
||||
|
||||
void _onSwitchBalancePeriod(
|
||||
EarningsSwitchBalancePeriodEvent event,
|
||||
Emitter<EarningsState> emit,
|
||||
) {
|
||||
emit(state.copyWith(balancePeriod: event.period));
|
||||
}
|
||||
|
||||
void _onTabChanged(
|
||||
EarningsTabChangedEvent event,
|
||||
Emitter<EarningsState> emit,
|
||||
) {
|
||||
emit(state.copyWith(tabIndex: event.tabIndex));
|
||||
|
||||
if (state.currentTab.status == PaginationStatus.initial) {
|
||||
add(const LoadAdditionalEarnings());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadAdditionalEarnings(
|
||||
LoadAdditionalEarnings event,
|
||||
Emitter<EarningsState> emit,
|
||||
) async {
|
||||
if (!state.currentTab.status.allowLoad) return;
|
||||
emit(state.updateCurrentTab(status: PaginationStatus.loading));
|
||||
|
||||
final tabIndex = state.tabIndex;
|
||||
try {
|
||||
final earningsBatch = await _earningRepository.getEarningsBatch(
|
||||
status: state.currentTab.loadingKey,
|
||||
lastEntryCursor: state.currentTab.paginationCursor,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWithTab(
|
||||
tabIndex: tabIndex,
|
||||
tab: state.tabs[tabIndex].copyWith(
|
||||
status: earningsBatch.hasNextBatch
|
||||
? PaginationStatus.idle
|
||||
: PaginationStatus.end,
|
||||
items: [...state.tabs[tabIndex].items, ...earningsBatch.earnings],
|
||||
hasMoreItems: earningsBatch.hasNextBatch,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (except) {
|
||||
log(
|
||||
'Error in EarningsBloc, on LoadAdditionalEarnings',
|
||||
error: except,
|
||||
);
|
||||
emit(
|
||||
state.copyWithTab(
|
||||
tabIndex: tabIndex,
|
||||
tab: state.tabs[tabIndex].copyWith(
|
||||
status: PaginationStatus.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.tabs[tabIndex].status == PaginationStatus.loading) {
|
||||
emit(
|
||||
state.copyWithTab(
|
||||
tabIndex: tabIndex,
|
||||
tab: state.tabs[tabIndex].copyWith(
|
||||
status: PaginationStatus.idle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onReloadCurrentEarnings(
|
||||
ReloadCurrentEarnings event,
|
||||
Emitter<EarningsState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWithTab(
|
||||
tab: EarningsTabState.initial(
|
||||
label: state.currentTab.label,
|
||||
loadingKey: state.currentTab.loadingKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
add(const LoadAdditionalEarnings());
|
||||
}
|
||||
|
||||
Future<void> _onEarningAction(
|
||||
Future<EarningShiftEntity?> earningFuture,
|
||||
String eventName,
|
||||
Emitter<EarningsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
|
||||
EarningShiftEntity? earning;
|
||||
try {
|
||||
earning = await earningFuture;
|
||||
} catch (except) {
|
||||
log(
|
||||
'Error in EarningsBloc, on $eventName',
|
||||
error: except,
|
||||
);
|
||||
}
|
||||
|
||||
var tabData = state.currentTab;
|
||||
if (earning != null) {
|
||||
final earningIndex =
|
||||
state.currentTab.items.indexWhere((item) => item.id == earning?.id);
|
||||
|
||||
if (earningIndex >= 0) {
|
||||
tabData = state.currentTab.copyWith(
|
||||
items: List.from(state.currentTab.items)..[earningIndex] = earning,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWithTab(
|
||||
status: StateStatus.idle,
|
||||
tab: tabData,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onConfirmStaffEarning(
|
||||
ConfirmStaffEarning event,
|
||||
Emitter<EarningsState> emit,
|
||||
) {
|
||||
return _onEarningAction(
|
||||
_earningRepository.confirmStaffEarning(
|
||||
earningId: event.earningId,
|
||||
),
|
||||
'ConfirmStaffEarning',
|
||||
emit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDisputeStaffEarning(
|
||||
DisputeStaffEarning event,
|
||||
Emitter<EarningsState> emit,
|
||||
) async {
|
||||
return _onEarningAction(
|
||||
_earningRepository.disputeStaffEarning(
|
||||
id: event.earningId,
|
||||
reason: event.reason,
|
||||
details: event.details,
|
||||
),
|
||||
'DeclineStaffEarning',
|
||||
emit,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
part of 'earnings_bloc.dart';
|
||||
|
||||
enum BalancePeriod {
|
||||
week,
|
||||
month,
|
||||
}
|
||||
|
||||
class EarningsTabState {
|
||||
const EarningsTabState({
|
||||
required this.label,
|
||||
required this.loadingKey,
|
||||
required this.items,
|
||||
this.status = PaginationStatus.idle,
|
||||
this.hasMoreItems = true,
|
||||
this.paginationCursor,
|
||||
});
|
||||
|
||||
const EarningsTabState.initial({
|
||||
required this.label,
|
||||
required this.loadingKey,
|
||||
this.items = const [],
|
||||
this.status = PaginationStatus.initial,
|
||||
this.hasMoreItems = true,
|
||||
this.paginationCursor,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String loadingKey;
|
||||
final List<EarningShiftEntity> items;
|
||||
final PaginationStatus status;
|
||||
final bool hasMoreItems;
|
||||
final String? paginationCursor;
|
||||
|
||||
EarningsTabState copyWith({
|
||||
List<EarningShiftEntity>? items,
|
||||
PaginationStatus? status,
|
||||
bool? hasMoreItems,
|
||||
String? label,
|
||||
String? loadingKey,
|
||||
String? paginationCursor,
|
||||
}) {
|
||||
return EarningsTabState(
|
||||
loadingKey: loadingKey ?? this.loadingKey,
|
||||
items: items ?? this.items,
|
||||
status: status ?? this.status,
|
||||
hasMoreItems: hasMoreItems ?? this.hasMoreItems,
|
||||
label: label ?? this.label,
|
||||
paginationCursor: paginationCursor ?? this.paginationCursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultEarningTabs = [
|
||||
EarningsTabState.initial(
|
||||
label: 'new_earning',
|
||||
loadingKey: 'new' // Confirmed by admin
|
||||
),
|
||||
EarningsTabState.initial(
|
||||
label: 'confirmed',
|
||||
loadingKey: 'confirmed', // Confirmed by staff
|
||||
),
|
||||
EarningsTabState.initial(
|
||||
label: 'disputed',
|
||||
loadingKey: 'disputed', // Declined by staff
|
||||
),
|
||||
EarningsTabState.initial(
|
||||
label: 'sent',
|
||||
loadingKey: 'sent', // Should remain sent
|
||||
),
|
||||
EarningsTabState.initial(
|
||||
label: 'received',
|
||||
loadingKey: 'paid', // Should remain paid
|
||||
),
|
||||
];
|
||||
|
||||
// $of = match ($status) {
|
||||
// 'new' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::new]),
|
||||
// 'confirmed' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::confirmed_by_admin]),
|
||||
// 'pending' => fn(Builder $q) => $q->whereIn('status', [
|
||||
// StaffPaymentStatus::decline_by_staff,
|
||||
// StaffPaymentStatus::confirmed_by_staff,
|
||||
// ]),
|
||||
// 'sent' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::sent]),
|
||||
// 'paid' => fn(Builder $q) => $q->whereIn('status', [StaffPaymentStatus::paid]),
|
||||
// };
|
||||
@@ -0,0 +1,48 @@
|
||||
part of 'earnings_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class EarningsEvent {
|
||||
const EarningsEvent();
|
||||
}
|
||||
|
||||
class EarningsInitEvent extends EarningsEvent {
|
||||
const EarningsInitEvent();
|
||||
}
|
||||
|
||||
class EarningsSwitchBalancePeriodEvent extends EarningsEvent {
|
||||
final BalancePeriod period;
|
||||
|
||||
const EarningsSwitchBalancePeriodEvent({required this.period});
|
||||
}
|
||||
|
||||
class EarningsTabChangedEvent extends EarningsEvent {
|
||||
final int tabIndex;
|
||||
|
||||
const EarningsTabChangedEvent({required this.tabIndex});
|
||||
}
|
||||
|
||||
class LoadAdditionalEarnings extends EarningsEvent {
|
||||
const LoadAdditionalEarnings();
|
||||
}
|
||||
|
||||
class ReloadCurrentEarnings extends EarningsEvent {
|
||||
const ReloadCurrentEarnings();
|
||||
}
|
||||
|
||||
class ConfirmStaffEarning extends EarningsEvent {
|
||||
const ConfirmStaffEarning(this.earningId);
|
||||
|
||||
final String earningId;
|
||||
}
|
||||
|
||||
class DisputeStaffEarning extends EarningsEvent {
|
||||
const DisputeStaffEarning({
|
||||
required this.earningId,
|
||||
required this.reason,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
final String earningId;
|
||||
final String reason;
|
||||
final String details;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
part of 'earnings_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class EarningsState {
|
||||
const EarningsState({
|
||||
required this.status,
|
||||
required this.tabIndex,
|
||||
required this.balancePeriod,
|
||||
required this.earnings,
|
||||
required this.tabs,
|
||||
});
|
||||
|
||||
const EarningsState.initial({
|
||||
this.status = StateStatus.idle,
|
||||
this.tabIndex = 0,
|
||||
this.balancePeriod = BalancePeriod.week,
|
||||
this.earnings = const EarningsSummaryEntity.empty(),
|
||||
this.tabs = defaultEarningTabs,
|
||||
});
|
||||
|
||||
final StateStatus status;
|
||||
final int tabIndex;
|
||||
final BalancePeriod balancePeriod;
|
||||
final EarningsSummaryEntity earnings;
|
||||
final List<EarningsTabState> tabs;
|
||||
|
||||
EarningsTabState get currentTab => tabs[tabIndex];
|
||||
|
||||
double get totalEarnings {
|
||||
return balancePeriod == BalancePeriod.week
|
||||
? earnings.totalEarningsByWeek
|
||||
: earnings.totalEarningsByMonth;
|
||||
}
|
||||
|
||||
double get totalHours {
|
||||
return balancePeriod == BalancePeriod.week
|
||||
? earnings.totalWorkedHoursByWeek
|
||||
: earnings.totalWorkedHoursByMonth;
|
||||
}
|
||||
|
||||
double get totalPayout {
|
||||
return balancePeriod == BalancePeriod.week
|
||||
? earnings.payoutByWeek
|
||||
: earnings.payoutByMonth;
|
||||
}
|
||||
|
||||
EarningsState copyWith({
|
||||
StateStatus? status,
|
||||
int? tabIndex,
|
||||
BalancePeriod? balancePeriod,
|
||||
EarningsSummaryEntity? earnings,
|
||||
List<EarningsTabState>? tabs,
|
||||
}) {
|
||||
return EarningsState(
|
||||
status: status ?? this.status,
|
||||
tabIndex: tabIndex ?? this.tabIndex,
|
||||
balancePeriod: balancePeriod ?? this.balancePeriod,
|
||||
earnings: earnings ?? this.earnings,
|
||||
tabs: tabs ?? this.tabs,
|
||||
);
|
||||
}
|
||||
|
||||
EarningsState copyWithTab({
|
||||
required EarningsTabState tab,
|
||||
int? tabIndex,
|
||||
StateStatus? status,
|
||||
}) {
|
||||
return copyWith(
|
||||
tabs: List.from(tabs)..[tabIndex ?? this.tabIndex] = tab,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
EarningsState updateCurrentTab({
|
||||
List<EarningShiftEntity>? items,
|
||||
PaginationStatus? status,
|
||||
bool? hasMoreItems,
|
||||
String? label,
|
||||
String? paginationCursor,
|
||||
}) {
|
||||
return copyWith(
|
||||
tabs: List.from(tabs)
|
||||
..[tabIndex] = currentTab.copyWith(
|
||||
items: items,
|
||||
status: status,
|
||||
hasMoreItems: hasMoreItems,
|
||||
label: label,
|
||||
paginationCursor: paginationCursor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
class EarningDisputeInfo {
|
||||
String reason = '';
|
||||
String details = '';
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
class EarningShiftEntity {
|
||||
final String id;
|
||||
final EarningStatus status;
|
||||
final String businessImageUrl;
|
||||
final String skillName;
|
||||
final String businessName;
|
||||
final int? totalBreakTime;
|
||||
final int? paymentStatus;
|
||||
final double? earned;
|
||||
final DateTime? clockIn;
|
||||
final DateTime? clockOut;
|
||||
final String eventName;
|
||||
final DateTime eventDate;
|
||||
|
||||
EarningShiftEntity({
|
||||
required this.id,
|
||||
required this.status,
|
||||
required this.businessImageUrl,
|
||||
required this.skillName,
|
||||
required this.businessName,
|
||||
required this.totalBreakTime,
|
||||
required this.paymentStatus,
|
||||
required this.earned,
|
||||
required this.clockIn,
|
||||
required this.clockOut,
|
||||
required this.eventName,
|
||||
required this.eventDate,
|
||||
});
|
||||
|
||||
int get paymentProgressStep {
|
||||
return switch (status) {
|
||||
EarningStatus.pending => 0, // corresponds to Pending Sent
|
||||
EarningStatus.processing => 1, // corresponds to Pending Payment
|
||||
EarningStatus.paid => 2, // corresponds to Payment Received
|
||||
_ => -1, // don't show active step
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum EarningStatus {
|
||||
new_,
|
||||
pending,
|
||||
confirmedByAdmin,
|
||||
confirmedByStaff,
|
||||
declineByStaff,
|
||||
processing,
|
||||
paid,
|
||||
failed,
|
||||
canceled;
|
||||
|
||||
static EarningStatus fromString(value) {
|
||||
return switch (value) {
|
||||
'new' => new_,
|
||||
'confirmed_by_admin' => confirmedByAdmin,
|
||||
'confirmed_by_staff' => confirmedByStaff,
|
||||
'decline_by_staff' => declineByStaff,
|
||||
'pending' => pending,
|
||||
'paid' => paid,
|
||||
'failed' => failed,
|
||||
'canceled' => canceled,
|
||||
_ => canceled,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
|
||||
class EarningsBatchEntity {
|
||||
EarningsBatchEntity({
|
||||
required this.batchStatus,
|
||||
required this.hasNextBatch,
|
||||
required this.earnings,
|
||||
this.cursor,
|
||||
});
|
||||
|
||||
final String batchStatus;
|
||||
final bool hasNextBatch;
|
||||
final List<EarningShiftEntity> earnings;
|
||||
final String? cursor;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
class EarningsSummaryEntity {
|
||||
const EarningsSummaryEntity({
|
||||
required this.totalEarningsByWeek,
|
||||
required this.totalEarningsByMonth,
|
||||
required this.totalWorkedHoursByWeek,
|
||||
required this.totalWorkedHoursByMonth,
|
||||
required this.payoutByWeek,
|
||||
required this.payoutByMonth,
|
||||
required this.startDatePeriod,
|
||||
required this.endDatePeriod,
|
||||
required this.maxEarningInPeriod,
|
||||
required this.minEarningInPeriod,
|
||||
});
|
||||
|
||||
const EarningsSummaryEntity.empty({
|
||||
this.totalEarningsByWeek = 0,
|
||||
this.totalEarningsByMonth = 0,
|
||||
this.totalWorkedHoursByWeek = 0,
|
||||
this.totalWorkedHoursByMonth = 0,
|
||||
this.payoutByWeek = 0,
|
||||
this.payoutByMonth = 0,
|
||||
this.startDatePeriod,
|
||||
this.endDatePeriod,
|
||||
this.maxEarningInPeriod = 0,
|
||||
this.minEarningInPeriod = 0,
|
||||
});
|
||||
|
||||
final double totalEarningsByWeek;
|
||||
final double totalEarningsByMonth;
|
||||
final double totalWorkedHoursByWeek;
|
||||
final double totalWorkedHoursByMonth;
|
||||
final double payoutByWeek;
|
||||
final double payoutByMonth;
|
||||
final DateTime? startDatePeriod;
|
||||
final DateTime? endDatePeriod;
|
||||
final int maxEarningInPeriod;
|
||||
final int minEarningInPeriod;
|
||||
|
||||
EarningsSummaryEntity copyWith({
|
||||
double? totalEarningsByWeek,
|
||||
double? totalEarningsByMonth,
|
||||
double? totalWorkedHoursByWeek,
|
||||
double? totalWorkedHoursByMonth,
|
||||
double? payoutByWeek,
|
||||
double? payoutByMonth,
|
||||
DateTime? startDatePeriod,
|
||||
DateTime? endDatePeriod,
|
||||
int? maxEarningInPeriod,
|
||||
int? minEarningInPeriod,
|
||||
}) {
|
||||
return EarningsSummaryEntity(
|
||||
totalEarningsByWeek: totalEarningsByWeek ?? this.totalEarningsByWeek,
|
||||
totalEarningsByMonth: totalEarningsByMonth ?? this.totalEarningsByMonth,
|
||||
totalWorkedHoursByWeek:
|
||||
totalWorkedHoursByWeek ?? this.totalWorkedHoursByWeek,
|
||||
totalWorkedHoursByMonth:
|
||||
totalWorkedHoursByMonth ?? this.totalWorkedHoursByMonth,
|
||||
payoutByWeek: payoutByWeek ?? this.payoutByWeek,
|
||||
payoutByMonth: payoutByMonth ?? this.payoutByMonth,
|
||||
startDatePeriod: startDatePeriod,
|
||||
endDatePeriod: endDatePeriod,
|
||||
maxEarningInPeriod: maxEarningInPeriod ?? this.maxEarningInPeriod,
|
||||
minEarningInPeriod: minEarningInPeriod ?? this.minEarningInPeriod,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earnings_batch_entity.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earnings_summary_entity.dart';
|
||||
|
||||
abstract interface class StaffEarningRepository {
|
||||
Future<EarningsSummaryEntity> getStaffEarningsData();
|
||||
|
||||
Future<EarningsBatchEntity> getEarningsBatch({
|
||||
required String status,
|
||||
int limit = 10,
|
||||
String? lastEntryCursor,
|
||||
});
|
||||
|
||||
Future<EarningShiftEntity?> confirmStaffEarning({required String earningId});
|
||||
|
||||
Future<EarningShiftEntity?> disputeStaffEarning({
|
||||
required String id,
|
||||
required String reason,
|
||||
String? details,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
|
||||
@RoutePage()
|
||||
class EarningsFlowScreen extends StatelessWidget {
|
||||
const EarningsFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (BuildContext context) =>
|
||||
EarningsBloc()..add(const EarningsInitEvent()),
|
||||
child: const AutoRouter(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:auto_route/annotations.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/earnings_appbar.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/list_item/earning_card_compact_widget.dart';
|
||||
|
||||
import '../widget/earnings_history_header.dart';
|
||||
|
||||
@RoutePage()
|
||||
class EarningsHistoryScreen extends StatelessWidget {
|
||||
const EarningsHistoryScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: BlocBuilder<EarningsBloc, EarningsState>(
|
||||
builder: (context, state) {
|
||||
List<EarningShiftEntity> items = state.currentTab.items;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const EarningsAppBar(
|
||||
leading: true,
|
||||
child: EarningsHistoryHeader(),
|
||||
),
|
||||
const Gap(24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
'your_shifts'.tr(),
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
),
|
||||
_buildListView(items),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ListView _buildListView(List<EarningShiftEntity> items) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return EarningCardCompactWidget(
|
||||
earningEntity: items[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:auto_route/annotations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
import 'package:krow/features/earning/presentation/slivers/bottom_earnings_sliver.dart';
|
||||
import 'package:krow/features/earning/presentation/slivers/earnings_header_sliver.dart';
|
||||
import 'package:krow/features/earning/presentation/slivers/earnings_list_sliver.dart';
|
||||
|
||||
@RoutePage()
|
||||
class EarningsScreen extends StatefulWidget {
|
||||
const EarningsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<EarningsScreen> createState() => _EarningsScreenState();
|
||||
}
|
||||
|
||||
class _EarningsScreenState extends State<EarningsScreen> {
|
||||
final _scrollController = ScrollController();
|
||||
bool allowPagination = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_scrollController.addListener(
|
||||
() {
|
||||
if (allowPagination &&
|
||||
_scrollController.position.userScrollDirection ==
|
||||
ScrollDirection.reverse &&
|
||||
_scrollController.position.extentAfter < 50) {
|
||||
context.read<EarningsBloc>().add(const LoadAdditionalEarnings());
|
||||
allowPagination = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
body: RefreshIndicator(
|
||||
displacement: 80,
|
||||
backgroundColor: AppColors.bgColorLight,
|
||||
color: AppColors.bgColorDark,
|
||||
triggerMode: RefreshIndicatorTriggerMode.anywhere,
|
||||
onRefresh: () {
|
||||
context.read<EarningsBloc>().add(const ReloadCurrentEarnings());
|
||||
return Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: BlocListener<EarningsBloc, EarningsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.currentTab.status != current.currentTab.status,
|
||||
listener: (context, state) {
|
||||
allowPagination = state.currentTab.status.allowLoad;
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
const EarningsHeaderSliver(),
|
||||
const EarningsListSliver(),
|
||||
const BottomEarningsSliver(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/enums/pagination_status.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
|
||||
class BottomEarningsSliver extends StatelessWidget {
|
||||
const BottomEarningsSliver({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverSafeArea(
|
||||
top: false,
|
||||
sliver: BlocBuilder<EarningsBloc, EarningsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.currentTab != current.currentTab ||
|
||||
current.currentTab.status != previous.currentTab.status,
|
||||
builder: (context, state) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 60,
|
||||
child: Center(
|
||||
child: switch (state.currentTab.status) {
|
||||
PaginationStatus.initial ||
|
||||
PaginationStatus.loading =>
|
||||
const CircularProgressIndicator(),
|
||||
PaginationStatus.empty => Text(
|
||||
'no_history_section'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
),
|
||||
PaginationStatus.end => Text(
|
||||
'end_of_payments_history'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(20),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_tabs.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/earnings_appbar.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/earnings_header.dart';
|
||||
|
||||
class EarningsHeaderSliver extends StatelessWidget {
|
||||
const EarningsHeaderSliver({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverList.list(
|
||||
children: [
|
||||
const EarningsAppBar(child: EarningsHeader()),
|
||||
const Gap(24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: Text(
|
||||
'your_shifts'.tr(),
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
BlocBuilder<EarningsBloc, EarningsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.tabs != current.tabs,
|
||||
builder: (context, state) {
|
||||
return KwTabBar(
|
||||
key: const Key('earnings_tab_bar'),
|
||||
forceScroll: true,
|
||||
tabs: [for (final tab in state.tabs) tab.label.tr()],
|
||||
onTap: (int index) {
|
||||
context
|
||||
.read<EarningsBloc>()
|
||||
.add(EarningsTabChangedEvent(tabIndex: index));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/list_item/earning_card_widget.dart';
|
||||
|
||||
class EarningsListSliver extends StatelessWidget {
|
||||
const EarningsListSliver({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
sliver: BlocBuilder<EarningsBloc, EarningsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.tabIndex != current.tabIndex ||
|
||||
previous.currentTab.items != current.currentTab.items,
|
||||
builder: (context, state) {
|
||||
return SliverList.separated(
|
||||
itemCount: state.currentTab.items.length,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
return EarningCardWidget(
|
||||
earningEntity: state.currentTab.items[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
|
||||
class AnimatedSlider extends StatefulWidget {
|
||||
const AnimatedSlider({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedSlider> createState() => _AnimatedSliderState();
|
||||
}
|
||||
|
||||
class _AnimatedSliderState extends State<AnimatedSlider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 200, // Adjust the width as needed
|
||||
height: 36, // Adjust the height as needed
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueGrey.shade400, // Background color
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
BlocBuilder<EarningsBloc, EarningsState>(
|
||||
builder: (context, state) {
|
||||
bool isThisWeekSelected =
|
||||
state.balancePeriod == BalancePeriod.week;
|
||||
|
||||
return AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
alignment: isThisWeekSelected
|
||||
? Alignment.centerLeft
|
||||
: Alignment.centerRight,
|
||||
child: Container(
|
||||
width: 90,
|
||||
margin: const EdgeInsets.all(4),
|
||||
// Half the width of the container
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueGrey.shade900, // Active slider color
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<EarningsBloc>(context)
|
||||
.add(const EarningsSwitchBalancePeriodEvent(
|
||||
period: BalancePeriod.week,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'this_week'.tr(),
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
BlocProvider.of<EarningsBloc>(context)
|
||||
.add(const EarningsSwitchBalancePeriodEvent(
|
||||
period: BalancePeriod.month,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'this_month'.tr(),
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_dispute_info.dart';
|
||||
|
||||
class DisputeFormWidget extends StatelessWidget {
|
||||
const DisputeFormWidget({super.key, required this.disputeInfo});
|
||||
|
||||
final EarningDisputeInfo disputeInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
KwDropdown<String>(
|
||||
title: 'reason'.tr(),
|
||||
hintText: 'select_reason_from_list'.tr(),
|
||||
horizontalPadding: 24,
|
||||
items: [
|
||||
for (final reason in _disputeReasonsData)
|
||||
KwDropDownItem(data: reason, title: reason),
|
||||
],
|
||||
onSelected: (reason) => disputeInfo.reason = reason,
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
title: 'additional_reasons'.tr(),
|
||||
minHeight: 114,
|
||||
controller: TextEditingController(),
|
||||
onChanged: (details) => disputeInfo.details = details,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _disputeReasonsData = [
|
||||
'incorrect_hours'.tr(),
|
||||
'incorrect_charges'.tr(),
|
||||
'other'.tr()
|
||||
];
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class EarningsAppBar extends StatelessWidget {
|
||||
final Widget child;
|
||||
final bool leading;
|
||||
|
||||
const EarningsAppBar({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.leading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
_buildProfileBackground(context),
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Gap(16),
|
||||
_buildAppBar(context),
|
||||
const Gap(24),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileBackground(BuildContext context) {
|
||||
return Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: AppColors.bgColorDark,
|
||||
child: Assets.images.bg.svg(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context) {
|
||||
return Container(
|
||||
height: 48,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (leading) ...[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: Colors.transparent,
|
||||
alignment: Alignment.center,
|
||||
child: Assets.images.appBar.appbarLeading.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
Colors.white,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
Text(
|
||||
'Earnings'.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.white),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignment: Alignment.center,
|
||||
child: Assets.images.appBar.notification.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
Colors.white,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/earnings_charts_widget.dart';
|
||||
|
||||
class EarningsCharts extends StatefulWidget {
|
||||
const EarningsCharts({super.key});
|
||||
|
||||
@override
|
||||
State<EarningsCharts> createState() => _EarningsChartsState();
|
||||
}
|
||||
|
||||
class _EarningsChartsState extends State<EarningsCharts> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryDark
|
||||
.copyWith(color: AppColors.darkBgStroke),
|
||||
height: 218,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'12.01 - 12.31.2024 period:',
|
||||
style: AppTextStyles.captionReg
|
||||
.copyWith(color: AppColors.bgColorLight),
|
||||
),
|
||||
const Gap(8),
|
||||
Text('\$1490',style: AppTextStyles.headingH3.copyWith(color: AppColors.primaryYellow),),
|
||||
const SizedBox(
|
||||
height: 150,
|
||||
child: EarningsChartsWidget(),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
|
||||
class EarningsChartsWidget extends StatefulWidget {
|
||||
const EarningsChartsWidget({super.key});
|
||||
|
||||
@override
|
||||
State<EarningsChartsWidget> createState() => _EarningsChartsWidgetState();
|
||||
}
|
||||
|
||||
class _EarningsChartsWidgetState extends State<EarningsChartsWidget> {
|
||||
var touchedIndex = -1;
|
||||
var values = [300, 99, 300, 9];
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var maxValue = values.reduce((value, element) => value > element ? value : element);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top:8.0),
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.center,
|
||||
maxY: maxValue.toDouble(),
|
||||
minY: 0,
|
||||
groupsSpace: 12,
|
||||
barTouchData: buildBarTouchData(),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: const Border(
|
||||
bottom: BorderSide(
|
||||
color: AppColors.darkBgInactive,
|
||||
width: 1,
|
||||
),
|
||||
top: BorderSide(
|
||||
color: AppColors.darkBgInactive,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
titlesData: _buildTiles(),
|
||||
gridData: buildFlGridData(),
|
||||
barGroups: getData()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FlGridData buildFlGridData() {
|
||||
return FlGridData(
|
||||
show: true,
|
||||
checkToShowHorizontalLine: (value) {
|
||||
return true;
|
||||
},
|
||||
getDrawingHorizontalLine: (value) => const FlLine(
|
||||
color: AppColors.darkBgInactive,
|
||||
dashArray: [4, 4],
|
||||
strokeWidth: 1,
|
||||
),
|
||||
drawVerticalLine: false,
|
||||
);
|
||||
}
|
||||
|
||||
FlTitlesData _buildTiles() {
|
||||
return FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 16,
|
||||
getTitlesWidget: _buildBottomTiles,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
maxIncluded: true,
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
getTitlesWidget: _buildLeftTitles,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLeftTitles(double value, TitleMeta meta) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: value == 0?8.0:0),
|
||||
child: Text(
|
||||
meta.formattedValue,
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.darkBgInactive),
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _buildBottomTiles(double value, TitleMeta meta) {
|
||||
var diap = '1-8 9-15 16-23 24-30'.split(' ');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top:4.0),
|
||||
child: Text(
|
||||
diap[value.toInt()],
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.darkBgInactive),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BarTouchData buildBarTouchData() {
|
||||
return BarTouchData(
|
||||
allowTouchBarBackDraw: true,
|
||||
touchExtraThreshold: const EdgeInsets.only(top:16),
|
||||
touchCallback: (FlTouchEvent event, barTouchResponse) {
|
||||
setState(() {
|
||||
if (!event.isInterestedForInteractions ||
|
||||
barTouchResponse == null ||
|
||||
barTouchResponse.spot == null) {
|
||||
touchedIndex = -1;
|
||||
return;
|
||||
}
|
||||
touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
|
||||
});
|
||||
},
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (group) => AppColors.primaryMint,
|
||||
tooltipRoundedRadius: 24,
|
||||
tooltipPadding:
|
||||
const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 8),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
return BarTooltipItem(
|
||||
'\$${rod.toY.round()}',
|
||||
AppTextStyles.bodyMediumMed,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<BarChartGroupData> getData() {
|
||||
double width = (MediaQuery.of(context).size.width-140)/4;
|
||||
|
||||
|
||||
return List.generate(4, (i)=>
|
||||
BarChartGroupData(
|
||||
x: i,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: values[i].toDouble(),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
color: touchedIndex == i?AppColors.primaryMint:AppColors.primaryYellow,
|
||||
width: width),
|
||||
],
|
||||
),);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/animated_slider.dart';
|
||||
|
||||
class EarningsHeader extends StatelessWidget {
|
||||
const EarningsHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 24,
|
||||
left: 12,
|
||||
right: 12,
|
||||
),
|
||||
decoration: KwBoxDecorations.primaryDark,
|
||||
child: BlocBuilder<EarningsBloc, EarningsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'your_earnings'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
const Gap(12),
|
||||
const AnimatedSlider(),
|
||||
const Gap(24),
|
||||
Text(
|
||||
'\$${state.totalEarnings.toStringAsFixed(2)}',
|
||||
style: AppTextStyles.headingH0
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
const Gap(24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryDark.copyWith(
|
||||
color: AppColors.darkBgStroke,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
'total_worked_hours'.tr(),
|
||||
'${state.totalHours.toInt()}',
|
||||
),
|
||||
// const Gap(12),
|
||||
// _buildInfoRow(
|
||||
// 'Payout:',
|
||||
// '\$${state.totalPayout.toStringAsFixed(2)}',
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
// const Gap(24),
|
||||
// KwButton.accent(
|
||||
// label: 'Earnings History',
|
||||
// onPressed: () {
|
||||
// context.router.push(const EarningsHistoryRoute());
|
||||
// },
|
||||
// )
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildInfoRow(String title, String? value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style:
|
||||
AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
Text(
|
||||
value ?? '',
|
||||
style:
|
||||
AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/earnings_charts_card.dart';
|
||||
|
||||
class EarningsHistoryHeader extends StatelessWidget {
|
||||
const EarningsHistoryHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 24, left: 12, right: 12),
|
||||
decoration: KwBoxDecorations.primaryDark,
|
||||
child: BlocBuilder<EarningsBloc, EarningsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPeriodRow(),
|
||||
const Gap(12),
|
||||
const EarningsCharts(),
|
||||
const Gap(8),
|
||||
_buildMinMaxRow(
|
||||
state.earnings.minEarningInPeriod,
|
||||
state.earnings.maxEarningInPeriod,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
_buildMinMaxRow(int min, int max) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration:
|
||||
KwBoxDecorations.primaryDark.copyWith(color: AppColors.darkBgStroke),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildInfoRow('max_earning'.tr(), '\$$max'),
|
||||
const Gap(12),
|
||||
_buildInfoRow('min_earning'.tr(), '\$$min'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildInfoRow(String title, String? value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style:
|
||||
AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
Text(
|
||||
value ?? '',
|
||||
style:
|
||||
AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildPeriodRow() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'earnings_history'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
AppTextStyles.bodyMediumMed.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
KwPopupMenu(
|
||||
customButtonBuilder: (context, isOpen) {
|
||||
return Row(
|
||||
children: [
|
||||
Assets.images.icons.calendar.svg(
|
||||
width: 12,
|
||||
height: 12,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayWhite, BlendMode.srcIn)),
|
||||
const Gap(2),
|
||||
Text(
|
||||
'period'.tr(),
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
const Gap(12),
|
||||
AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
turns: isOpen ? -0.5 : 0,
|
||||
child: Assets.images.icons.caretDown.svg(
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.darkBgInactive, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
menuItems: [
|
||||
KwPopupMenuItem(title: 'week'.tr(), onTap: () {}),
|
||||
KwPopupMenuItem(title: 'month'.tr(), onTap: () {}),
|
||||
KwPopupMenuItem(title: 'range'.tr(), onTap: () {}),
|
||||
])
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/earning/domain/bloc/earnings_bloc.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_dispute_info.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/dispute_form_widget.dart';
|
||||
|
||||
class EarningActionWidget extends StatefulWidget {
|
||||
const EarningActionWidget({
|
||||
super.key,
|
||||
required this.earningShiftEntity,
|
||||
});
|
||||
|
||||
final EarningShiftEntity earningShiftEntity;
|
||||
|
||||
@override
|
||||
State<EarningActionWidget> createState() => _EarningActionWidgetState();
|
||||
}
|
||||
|
||||
class _EarningActionWidgetState extends State<EarningActionWidget> {
|
||||
CrossFadeState _crossFadeState = CrossFadeState.showFirst;
|
||||
|
||||
Future<void> _onConfirmPaymentPressed() async {
|
||||
final bloc = context.read<EarningsBloc>();
|
||||
|
||||
await KwDialog.show<void>(
|
||||
context: context,
|
||||
icon: Assets.images.icons.moneyRecive,
|
||||
state: KwDialogState.positive,
|
||||
title: 'your_earnings_are_in'.tr(),
|
||||
message: '${'you_earned_for_shift'.tr(args: [
|
||||
widget.earningShiftEntity.earned.toString(),
|
||||
widget.earningShiftEntity.eventName,
|
||||
DateFormat('d MMM yyyy', context.locale.languageCode).format(
|
||||
widget.earningShiftEntity.eventDate,
|
||||
)
|
||||
])}\n\n${'total_earnings_added'.tr(args: [
|
||||
widget.earningShiftEntity.earned.toString()
|
||||
])}',
|
||||
primaryButtonLabel: 'confirm_earning'.tr(),
|
||||
secondaryButtonLabel: 'dispute_contact_support'.tr(),
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Navigator.pop(dialogContext);
|
||||
|
||||
bloc.add(ConfirmStaffEarning(widget.earningShiftEntity.id));
|
||||
setState(() => _crossFadeState = CrossFadeState.showSecond);
|
||||
},
|
||||
onSecondaryButtonPressed: (dialogContext) {
|
||||
Navigator.pop(dialogContext);
|
||||
|
||||
_onDisputePaymentPressed();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDisputePaymentPressed() async {
|
||||
final bloc = context.read<EarningsBloc>();
|
||||
final disputeInfo = EarningDisputeInfo();
|
||||
|
||||
await KwDialog.show<void>(
|
||||
context: context,
|
||||
icon: Assets.images.icons.moneyRecive,
|
||||
state: KwDialogState.negative,
|
||||
title: 'dispute_earnings'.tr(),
|
||||
message: 'dispute_message'.tr(),
|
||||
primaryButtonLabel: 'Submit Dispute',
|
||||
secondaryButtonLabel: 'cancel'.tr(),
|
||||
child: DisputeFormWidget(disputeInfo: disputeInfo),
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Navigator.pop(dialogContext);
|
||||
|
||||
bloc.add(
|
||||
DisputeStaffEarning(
|
||||
earningId: widget.earningShiftEntity.id,
|
||||
reason: disputeInfo.reason,
|
||||
details: disputeInfo.details,
|
||||
),
|
||||
);
|
||||
setState(() => _crossFadeState = CrossFadeState.showSecond);
|
||||
},
|
||||
onSecondaryButtonPressed: (dialogContext) {
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedCrossFade(
|
||||
duration: Durations.medium4,
|
||||
crossFadeState: _crossFadeState,
|
||||
secondChild: const SizedBox.shrink(),
|
||||
firstChild: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, top: 24),
|
||||
child: KwButton.outlinedPrimary(
|
||||
label: 'confirm'.tr(),
|
||||
onPressed: _onConfirmPaymentPressed,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
|
||||
class EarningCardCompactWidget extends StatelessWidget {
|
||||
final EarningShiftEntity earningEntity;
|
||||
|
||||
const EarningCardCompactWidget({
|
||||
super.key,
|
||||
required this.earningEntity,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
height: 72,
|
||||
margin: const EdgeInsets.only(bottom: 12, left: 16, right: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildContent(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: earningEntity.businessImageUrl,
|
||||
fit: BoxFit.cover,
|
||||
height: 48,
|
||||
width: 48,
|
||||
)),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Gap(5),
|
||||
Text(
|
||||
earningEntity.skillName,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
earningEntity.businessName,
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
const Gap(5),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
'\$${earningEntity.earned}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
|
||||
import 'package:krow/core/presentation/widgets/shift_payment_step_widget.dart';
|
||||
import 'package:krow/core/presentation/widgets/shift_total_time_spend_widget.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/list_item/earning_action_widget.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/list_item/shift_item_header_widget.dart';
|
||||
|
||||
class EarningCardWidget extends StatelessWidget {
|
||||
const EarningCardWidget({
|
||||
super.key,
|
||||
required this.earningEntity,
|
||||
});
|
||||
|
||||
final EarningShiftEntity earningEntity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.primaryLight12,
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ShiftItemHeaderWidget(earningEntity),
|
||||
ShiftTotalTimeSpendWidget(
|
||||
startTime: earningEntity.clockIn!,
|
||||
endTime: earningEntity.clockOut!,
|
||||
totalBreakTime: earningEntity.totalBreakTime ?? 0,
|
||||
),
|
||||
ShiftPaymentStepWidget(
|
||||
currentIndex: earningEntity.paymentProgressStep,
|
||||
),
|
||||
if (earningEntity.status == EarningStatus.confirmedByAdmin)
|
||||
EarningActionWidget(earningShiftEntity: earningEntity),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
import 'package:krow/features/earning/presentation/widget/list_item/shift_status_label_widget.dart';
|
||||
|
||||
class ShiftItemHeaderWidget extends StatelessWidget {
|
||||
final EarningShiftEntity viewModel;
|
||||
|
||||
const ShiftItemHeaderWidget(
|
||||
this.viewModel, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: viewModel.businessImageUrl,
|
||||
fit: BoxFit.cover,
|
||||
height: 48,
|
||||
width: 48,
|
||||
),
|
||||
),
|
||||
EarnedShiftStatusLabelWidget(viewModel),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
viewModel.skillName,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
Text(
|
||||
'\$${viewModel.earned}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
viewModel.businessName,
|
||||
style: AppTextStyles.bodySmallReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/features/earning/domain/entities/earning_shift_entity.dart';
|
||||
|
||||
class EarnedShiftStatusLabelWidget extends StatefulWidget {
|
||||
final EarningShiftEntity viewModel;
|
||||
|
||||
const EarnedShiftStatusLabelWidget(this.viewModel, {super.key});
|
||||
|
||||
@override
|
||||
State<EarnedShiftStatusLabelWidget> createState() =>
|
||||
_EarnedShiftStatusLabelWidgetState();
|
||||
}
|
||||
|
||||
class _EarnedShiftStatusLabelWidgetState
|
||||
extends State<EarnedShiftStatusLabelWidget> {
|
||||
Color getColor() {
|
||||
return AppColors.bgColorDark;
|
||||
// switch (widget.viewModel.status) {
|
||||
// case EventShiftRoleStaffStatus.assigned:
|
||||
// return AppColors.primaryBlue;
|
||||
// case EventShiftRoleStaffStatus.confirmed:
|
||||
// return AppColors.statusWarning;
|
||||
// case EventShiftRoleStaffStatus.ongoing:
|
||||
// return AppColors.statusSuccess;
|
||||
// case EventShiftRoleStaffStatus.completed:
|
||||
// return AppColors.bgColorDark;
|
||||
// case EventShiftRoleStaffStatus.canceledByAdmin:
|
||||
// case EventShiftRoleStaffStatus.canceledByBusiness:
|
||||
// case EventShiftRoleStaffStatus.canceledByStaff:
|
||||
// case EventShiftRoleStaffStatus.requestedReplace:
|
||||
// return AppColors.statusError;
|
||||
// }
|
||||
}
|
||||
|
||||
String getText() {
|
||||
return 'completed'.tr();
|
||||
// switch (widget.viewModel.status) {
|
||||
// case EventShiftRoleStaffStatus.assigned:
|
||||
// return _getAssignedAgo();
|
||||
// case EventShiftRoleStaffStatus.confirmed:
|
||||
// return _getStartIn();
|
||||
// case EventShiftRoleStaffStatus.ongoing:
|
||||
// return 'Ongoing';
|
||||
// case EventShiftRoleStaffStatus.completed:
|
||||
// return 'Completed';
|
||||
// case EventShiftRoleStaffStatus.canceledByAdmin:
|
||||
// case EventShiftRoleStaffStatus.canceledByBusiness:
|
||||
// case EventShiftRoleStaffStatus.canceledByStaff:
|
||||
// case EventShiftRoleStaffStatus.requestedReplace:
|
||||
// return 'Canceled';
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 24,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: getColor(),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
getText(),
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.grayWhite, height: 0.7),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user