refactor: extract invoice approval and dispute logic into a dedicated ShiftCompletionReviewBloc.
This commit is contained in:
@@ -102,7 +102,7 @@ extension ClientNavigator on IModularNavigator {
|
|||||||
|
|
||||||
/// Navigates to the full list of invoices awaiting approval.
|
/// Navigates to the full list of invoices awaiting approval.
|
||||||
void toAwaitingApproval({Object? arguments}) {
|
void toAwaitingApproval({Object? arguments}) {
|
||||||
pushNamed(ClientPaths.awaitingApproval, arguments: arguments);
|
navigate(ClientPaths.awaitingApproval, arguments: arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the Invoice Ready page.
|
/// Navigates to the Invoice Ready page.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import '../ui_constants.dart';
|
|||||||
|
|
||||||
/// A custom button widget with different variants and icon support.
|
/// A custom button widget with different variants and icon support.
|
||||||
class UiButton extends StatelessWidget {
|
class UiButton extends StatelessWidget {
|
||||||
|
|
||||||
/// Creates a [UiButton] with a custom button builder.
|
/// Creates a [UiButton] with a custom button builder.
|
||||||
const UiButton({
|
const UiButton({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -17,6 +16,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.large,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
|
this.isLoading = false,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
'Either text or child must be provided',
|
'Either text or child must be provided',
|
||||||
@@ -34,6 +34,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.large,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
|
this.isLoading = false,
|
||||||
}) : buttonBuilder = _elevatedButtonBuilder,
|
}) : buttonBuilder = _elevatedButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
@@ -52,6 +53,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.large,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
|
this.isLoading = false,
|
||||||
}) : buttonBuilder = _outlinedButtonBuilder,
|
}) : buttonBuilder = _outlinedButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
@@ -70,6 +72,7 @@ class UiButton extends StatelessWidget {
|
|||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.large,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
|
this.isLoading = false,
|
||||||
}) : buttonBuilder = _textButtonBuilder,
|
}) : buttonBuilder = _textButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
@@ -88,11 +91,13 @@ class UiButton extends StatelessWidget {
|
|||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.size = UiButtonSize.large,
|
this.size = UiButtonSize.large,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
|
this.isLoading = false,
|
||||||
}) : buttonBuilder = _textButtonBuilder,
|
}) : buttonBuilder = _textButtonBuilder,
|
||||||
assert(
|
assert(
|
||||||
text != null || child != null,
|
text != null || child != null,
|
||||||
'Either text or child must be provided',
|
'Either text or child must be provided',
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The text to display on the button.
|
/// The text to display on the button.
|
||||||
final String? text;
|
final String? text;
|
||||||
|
|
||||||
@@ -129,6 +134,9 @@ class UiButton extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
buttonBuilder;
|
buttonBuilder;
|
||||||
|
|
||||||
|
/// Whether to show a loading indicator.
|
||||||
|
final bool isLoading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Builds the button UI.
|
/// Builds the button UI.
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -138,9 +146,9 @@ class UiButton extends StatelessWidget {
|
|||||||
|
|
||||||
final Widget button = buttonBuilder(
|
final Widget button = buttonBuilder(
|
||||||
context,
|
context,
|
||||||
onPressed,
|
isLoading ? null : onPressed,
|
||||||
mergedStyle,
|
mergedStyle,
|
||||||
_buildButtonContent(),
|
isLoading ? _buildLoadingContent() : _buildButtonContent(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fullWidth) {
|
if (fullWidth) {
|
||||||
@@ -150,6 +158,15 @@ class UiButton extends StatelessWidget {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds the loading indicator.
|
||||||
|
Widget _buildLoadingContent() {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the style based on the button size.
|
/// Gets the style based on the button size.
|
||||||
ButtonStyle _getSizeStyle() {
|
ButtonStyle _getSizeStyle() {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'domain/usecases/get_spending_breakdown.dart';
|
|||||||
import 'domain/usecases/approve_invoice.dart';
|
import 'domain/usecases/approve_invoice.dart';
|
||||||
import 'domain/usecases/dispute_invoice.dart';
|
import 'domain/usecases/dispute_invoice.dart';
|
||||||
import 'presentation/blocs/billing_bloc.dart';
|
import 'presentation/blocs/billing_bloc.dart';
|
||||||
|
import 'presentation/blocs/shift_completion_review/shift_completion_review_bloc.dart';
|
||||||
import 'presentation/models/billing_invoice_model.dart';
|
import 'presentation/models/billing_invoice_model.dart';
|
||||||
import 'presentation/pages/billing_page.dart';
|
import 'presentation/pages/billing_page.dart';
|
||||||
import 'presentation/pages/completion_review_page.dart';
|
import 'presentation/pages/completion_review_page.dart';
|
||||||
@@ -44,6 +45,10 @@ class BillingModule extends Module {
|
|||||||
getPendingInvoices: i.get<GetPendingInvoicesUseCase>(),
|
getPendingInvoices: i.get<GetPendingInvoicesUseCase>(),
|
||||||
getInvoiceHistory: i.get<GetInvoiceHistoryUseCase>(),
|
getInvoiceHistory: i.get<GetInvoiceHistoryUseCase>(),
|
||||||
getSpendingBreakdown: i.get<GetSpendingBreakdownUseCase>(),
|
getSpendingBreakdown: i.get<GetSpendingBreakdownUseCase>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i.add<ShiftCompletionReviewBloc>(
|
||||||
|
() => ShiftCompletionReviewBloc(
|
||||||
approveInvoice: i.get<ApproveInvoiceUseCase>(),
|
approveInvoice: i.get<ApproveInvoiceUseCase>(),
|
||||||
disputeInvoice: i.get<DisputeInvoiceUseCase>(),
|
disputeInvoice: i.get<DisputeInvoiceUseCase>(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import '../../domain/usecases/get_invoice_history.dart';
|
|||||||
import '../../domain/usecases/get_pending_invoices.dart';
|
import '../../domain/usecases/get_pending_invoices.dart';
|
||||||
import '../../domain/usecases/get_savings_amount.dart';
|
import '../../domain/usecases/get_savings_amount.dart';
|
||||||
import '../../domain/usecases/get_spending_breakdown.dart';
|
import '../../domain/usecases/get_spending_breakdown.dart';
|
||||||
import '../../domain/usecases/approve_invoice.dart';
|
|
||||||
import '../../domain/usecases/dispute_invoice.dart';
|
|
||||||
import '../models/billing_invoice_model.dart';
|
import '../models/billing_invoice_model.dart';
|
||||||
import '../models/spending_breakdown_model.dart';
|
import '../models/spending_breakdown_model.dart';
|
||||||
import 'billing_event.dart';
|
import 'billing_event.dart';
|
||||||
@@ -26,21 +24,15 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
|||||||
required GetPendingInvoicesUseCase getPendingInvoices,
|
required GetPendingInvoicesUseCase getPendingInvoices,
|
||||||
required GetInvoiceHistoryUseCase getInvoiceHistory,
|
required GetInvoiceHistoryUseCase getInvoiceHistory,
|
||||||
required GetSpendingBreakdownUseCase getSpendingBreakdown,
|
required GetSpendingBreakdownUseCase getSpendingBreakdown,
|
||||||
required ApproveInvoiceUseCase approveInvoice,
|
|
||||||
required DisputeInvoiceUseCase disputeInvoice,
|
|
||||||
}) : _getBankAccounts = getBankAccounts,
|
}) : _getBankAccounts = getBankAccounts,
|
||||||
_getCurrentBillAmount = getCurrentBillAmount,
|
_getCurrentBillAmount = getCurrentBillAmount,
|
||||||
_getSavingsAmount = getSavingsAmount,
|
_getSavingsAmount = getSavingsAmount,
|
||||||
_getPendingInvoices = getPendingInvoices,
|
_getPendingInvoices = getPendingInvoices,
|
||||||
_getInvoiceHistory = getInvoiceHistory,
|
_getInvoiceHistory = getInvoiceHistory,
|
||||||
_getSpendingBreakdown = getSpendingBreakdown,
|
_getSpendingBreakdown = getSpendingBreakdown,
|
||||||
_approveInvoice = approveInvoice,
|
|
||||||
_disputeInvoice = disputeInvoice,
|
|
||||||
super(const BillingState()) {
|
super(const BillingState()) {
|
||||||
on<BillingLoadStarted>(_onLoadStarted);
|
on<BillingLoadStarted>(_onLoadStarted);
|
||||||
on<BillingPeriodChanged>(_onPeriodChanged);
|
on<BillingPeriodChanged>(_onPeriodChanged);
|
||||||
on<BillingInvoiceApproved>(_onInvoiceApproved);
|
|
||||||
on<BillingInvoiceDisputed>(_onInvoiceDisputed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final GetBankAccountsUseCase _getBankAccounts;
|
final GetBankAccountsUseCase _getBankAccounts;
|
||||||
@@ -49,8 +41,6 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
|||||||
final GetPendingInvoicesUseCase _getPendingInvoices;
|
final GetPendingInvoicesUseCase _getPendingInvoices;
|
||||||
final GetInvoiceHistoryUseCase _getInvoiceHistory;
|
final GetInvoiceHistoryUseCase _getInvoiceHistory;
|
||||||
final GetSpendingBreakdownUseCase _getSpendingBreakdown;
|
final GetSpendingBreakdownUseCase _getSpendingBreakdown;
|
||||||
final ApproveInvoiceUseCase _approveInvoice;
|
|
||||||
final DisputeInvoiceUseCase _disputeInvoice;
|
|
||||||
|
|
||||||
Future<void> _onLoadStarted(
|
Future<void> _onLoadStarted(
|
||||||
BillingLoadStarted event,
|
BillingLoadStarted event,
|
||||||
@@ -136,38 +126,6 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onInvoiceApproved(
|
|
||||||
BillingInvoiceApproved event,
|
|
||||||
Emitter<BillingState> emit,
|
|
||||||
) async {
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _approveInvoice.call(event.invoiceId);
|
|
||||||
add(const BillingLoadStarted());
|
|
||||||
},
|
|
||||||
onError: (String errorKey) =>
|
|
||||||
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onInvoiceDisputed(
|
|
||||||
BillingInvoiceDisputed event,
|
|
||||||
Emitter<BillingState> emit,
|
|
||||||
) async {
|
|
||||||
await handleError(
|
|
||||||
emit: emit.call,
|
|
||||||
action: () async {
|
|
||||||
await _disputeInvoice.call(
|
|
||||||
DisputeInvoiceParams(id: event.invoiceId, reason: event.reason),
|
|
||||||
);
|
|
||||||
add(const BillingLoadStarted());
|
|
||||||
},
|
|
||||||
onError: (String errorKey) =>
|
|
||||||
state.copyWith(status: BillingStatus.failure, errorMessage: errorKey),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
BillingInvoice _mapInvoiceToUiModel(Invoice invoice) {
|
BillingInvoice _mapInvoiceToUiModel(Invoice invoice) {
|
||||||
final DateFormat formatter = DateFormat('EEEE, MMMM d');
|
final DateFormat formatter = DateFormat('EEEE, MMMM d');
|
||||||
final String dateLabel = invoice.issueDate == null
|
final String dateLabel = invoice.issueDate == null
|
||||||
|
|||||||
@@ -24,20 +24,3 @@ class BillingPeriodChanged extends BillingEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[period];
|
List<Object?> get props => <Object?>[period];
|
||||||
}
|
}
|
||||||
|
|
||||||
class BillingInvoiceApproved extends BillingEvent {
|
|
||||||
const BillingInvoiceApproved(this.invoiceId);
|
|
||||||
final String invoiceId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[invoiceId];
|
|
||||||
}
|
|
||||||
|
|
||||||
class BillingInvoiceDisputed extends BillingEvent {
|
|
||||||
const BillingInvoiceDisputed(this.invoiceId, this.reason);
|
|
||||||
final String invoiceId;
|
|
||||||
final String reason;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => <Object?>[invoiceId, reason];
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../../../domain/usecases/approve_invoice.dart';
|
||||||
|
import '../../../domain/usecases/dispute_invoice.dart';
|
||||||
|
import 'shift_completion_review_event.dart';
|
||||||
|
import 'shift_completion_review_state.dart';
|
||||||
|
|
||||||
|
class ShiftCompletionReviewBloc
|
||||||
|
extends Bloc<ShiftCompletionReviewEvent, ShiftCompletionReviewState>
|
||||||
|
with BlocErrorHandler<ShiftCompletionReviewState> {
|
||||||
|
ShiftCompletionReviewBloc({
|
||||||
|
required ApproveInvoiceUseCase approveInvoice,
|
||||||
|
required DisputeInvoiceUseCase disputeInvoice,
|
||||||
|
}) : _approveInvoice = approveInvoice,
|
||||||
|
_disputeInvoice = disputeInvoice,
|
||||||
|
super(const ShiftCompletionReviewState()) {
|
||||||
|
on<ShiftCompletionReviewApproved>(_onApproved);
|
||||||
|
on<ShiftCompletionReviewDisputed>(_onDisputed);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ApproveInvoiceUseCase _approveInvoice;
|
||||||
|
final DisputeInvoiceUseCase _disputeInvoice;
|
||||||
|
|
||||||
|
Future<void> _onApproved(
|
||||||
|
ShiftCompletionReviewApproved event,
|
||||||
|
Emitter<ShiftCompletionReviewState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: ShiftCompletionReviewStatus.loading));
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
await _approveInvoice.call(event.invoiceId);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: ShiftCompletionReviewStatus.success,
|
||||||
|
message: 'approved',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: ShiftCompletionReviewStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onDisputed(
|
||||||
|
ShiftCompletionReviewDisputed event,
|
||||||
|
Emitter<ShiftCompletionReviewState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: ShiftCompletionReviewStatus.loading));
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
await _disputeInvoice.call(
|
||||||
|
DisputeInvoiceParams(id: event.invoiceId, reason: event.reason),
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: ShiftCompletionReviewStatus.success,
|
||||||
|
message: 'disputed',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: ShiftCompletionReviewStatus.failure,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Base class for all shift completion review events.
|
||||||
|
abstract class ShiftCompletionReviewEvent extends Equatable {
|
||||||
|
/// Creates a [ShiftCompletionReviewEvent].
|
||||||
|
const ShiftCompletionReviewEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered when an invoice is approved.
|
||||||
|
class ShiftCompletionReviewApproved extends ShiftCompletionReviewEvent {
|
||||||
|
/// Creates a [ShiftCompletionReviewApproved] event.
|
||||||
|
const ShiftCompletionReviewApproved(this.invoiceId);
|
||||||
|
|
||||||
|
/// The ID of the invoice to approve.
|
||||||
|
final String invoiceId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[invoiceId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered when an invoice is disputed.
|
||||||
|
class ShiftCompletionReviewDisputed extends ShiftCompletionReviewEvent {
|
||||||
|
/// Creates a [ShiftCompletionReviewDisputed] event.
|
||||||
|
const ShiftCompletionReviewDisputed(this.invoiceId, this.reason);
|
||||||
|
|
||||||
|
/// The ID of the invoice to dispute.
|
||||||
|
final String invoiceId;
|
||||||
|
|
||||||
|
/// The reason for the dispute.
|
||||||
|
final String reason;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[invoiceId, reason];
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Status of the shift completion review process.
|
||||||
|
enum ShiftCompletionReviewStatus {
|
||||||
|
/// Initial state.
|
||||||
|
initial,
|
||||||
|
|
||||||
|
/// Loading state.
|
||||||
|
loading,
|
||||||
|
|
||||||
|
/// Success state.
|
||||||
|
success,
|
||||||
|
|
||||||
|
/// Failure state.
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the [ShiftCompletionReviewBloc].
|
||||||
|
class ShiftCompletionReviewState extends Equatable {
|
||||||
|
/// Creates a [ShiftCompletionReviewState].
|
||||||
|
const ShiftCompletionReviewState({
|
||||||
|
this.status = ShiftCompletionReviewStatus.initial,
|
||||||
|
this.message,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Current status of the process.
|
||||||
|
final ShiftCompletionReviewStatus status;
|
||||||
|
|
||||||
|
/// Success message (e.g., 'approved' or 'disputed').
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
/// Error message to display if [status] is [ShiftCompletionReviewStatus.failure].
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// Creates a copy of this state with the given fields replaced.
|
||||||
|
ShiftCompletionReviewState copyWith({
|
||||||
|
ShiftCompletionReviewStatus? status,
|
||||||
|
String? message,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return ShiftCompletionReviewState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
message: message ?? this.message,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[status, message, errorMessage];
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ class BillingInvoice extends Equatable {
|
|||||||
required this.workersCount,
|
required this.workersCount,
|
||||||
required this.totalHours,
|
required this.totalHours,
|
||||||
required this.status,
|
required this.status,
|
||||||
this.workers = const [],
|
this.workers = const <BillingWorkerRecord>[],
|
||||||
this.startTime,
|
this.startTime,
|
||||||
this.endTime,
|
this.endTime,
|
||||||
});
|
});
|
||||||
@@ -70,7 +70,7 @@ class BillingWorkerRecord extends Equatable {
|
|||||||
final String? workerAvatarUrl;
|
final String? workerAvatarUrl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => <Object?>[
|
||||||
workerName,
|
workerName,
|
||||||
roleName,
|
roleName,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../models/billing_invoice_model.dart';
|
import '../models/billing_invoice_model.dart';
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ class _ShiftCompletionReviewPageState extends State<ShiftCompletionReviewPage> {
|
|||||||
title: invoice.title,
|
title: invoice.title,
|
||||||
subtitle: invoice.clientName,
|
subtitle: invoice.clientName,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
|
onLeadingPressed: () => Modular.to.toAwaitingApproval(),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class InvoiceReadyView extends StatelessWidget {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: <Widget>[
|
||||||
const Icon(
|
const Icon(
|
||||||
UiIcons.file,
|
UiIcons.file,
|
||||||
size: 64,
|
size: 64,
|
||||||
@@ -81,7 +81,7 @@ class _InvoiceSummaryCard extends StatelessWidget {
|
|||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
boxShadow: [
|
boxShadow: <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.black.withValues(alpha: 0.04),
|
color: UiColors.black.withValues(alpha: 0.04),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
@@ -91,10 +91,10 @@ class _InvoiceSummaryCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10,
|
horizontal: 10,
|
||||||
@@ -124,10 +124,10 @@ class _InvoiceSummaryCard extends StatelessWidget {
|
|||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
'TOTAL AMOUNT',
|
'TOTAL AMOUNT',
|
||||||
style: UiTypography.titleUppercase4m.textSecondary,
|
style: UiTypography.titleUppercase4m.textSecondary,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../blocs/billing_bloc.dart';
|
import '../blocs/billing_bloc.dart';
|
||||||
import '../blocs/billing_state.dart';
|
import '../blocs/billing_state.dart';
|
||||||
@@ -20,6 +21,7 @@ class PendingInvoicesPage extends StatelessWidget {
|
|||||||
appBar: UiAppBar(
|
appBar: UiAppBar(
|
||||||
title: t.client_billing.awaiting_approval,
|
title: t.client_billing.awaiting_approval,
|
||||||
showBackButton: true,
|
showBackButton: true,
|
||||||
|
onLeadingPressed: () => Modular.to.toClientBilling(),
|
||||||
),
|
),
|
||||||
body: _buildBody(context, state),
|
body: _buildBody(context, state),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../../blocs/billing_bloc.dart';
|
import '../../blocs/shift_completion_review/shift_completion_review_bloc.dart';
|
||||||
import '../../blocs/billing_event.dart';
|
import '../../blocs/shift_completion_review/shift_completion_review_event.dart';
|
||||||
|
import '../../blocs/shift_completion_review/shift_completion_review_state.dart';
|
||||||
|
|
||||||
class CompletionReviewActions extends StatelessWidget {
|
class CompletionReviewActions extends StatelessWidget {
|
||||||
const CompletionReviewActions({required this.invoiceId, super.key});
|
const CompletionReviewActions({required this.invoiceId, super.key});
|
||||||
@@ -14,6 +16,33 @@ class CompletionReviewActions extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<ShiftCompletionReviewBloc>.value(
|
||||||
|
value: Modular.get<ShiftCompletionReviewBloc>(),
|
||||||
|
child:
|
||||||
|
BlocConsumer<ShiftCompletionReviewBloc, ShiftCompletionReviewState>(
|
||||||
|
listener: (BuildContext context, ShiftCompletionReviewState state) {
|
||||||
|
if (state.status == ShiftCompletionReviewStatus.success) {
|
||||||
|
final String message = state.message == 'approved'
|
||||||
|
? t.client_billing.approved_success
|
||||||
|
: t.client_billing.flagged_success;
|
||||||
|
final UiSnackbarType type = state.message == 'approved'
|
||||||
|
? UiSnackbarType.success
|
||||||
|
: UiSnackbarType.warning;
|
||||||
|
|
||||||
|
UiSnackbar.show(context, message: message, type: type);
|
||||||
|
Modular.to.toAwaitingApproval();
|
||||||
|
} else if (state.status == ShiftCompletionReviewStatus.failure) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: state.errorMessage ?? t.errors.generic.unknown,
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (BuildContext context, ShiftCompletionReviewState state) {
|
||||||
|
final bool isLoading =
|
||||||
|
state.status == ShiftCompletionReviewStatus.loading;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
spacing: UiConstants.space2,
|
spacing: UiConstants.space2,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -21,7 +50,9 @@ class CompletionReviewActions extends StatelessWidget {
|
|||||||
child: UiButton.secondary(
|
child: UiButton.secondary(
|
||||||
text: t.client_billing.actions.flag_review,
|
text: t.client_billing.actions.flag_review,
|
||||||
leadingIcon: UiIcons.warning,
|
leadingIcon: UiIcons.warning,
|
||||||
onPressed: () => _showFlagDialog(context),
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: () => _showFlagDialog(context, state),
|
||||||
size: UiButtonSize.large,
|
size: UiButtonSize.large,
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: UiColors.destructive,
|
foregroundColor: UiColors.destructive,
|
||||||
@@ -32,29 +63,33 @@ class CompletionReviewActions extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: UiButton.primary(
|
child: UiButton.primary(
|
||||||
text: t.client_billing.actions.approve_pay,
|
text: t.client_billing.actions.approve_pay,
|
||||||
leadingIcon: UiIcons.checkCircle,
|
leadingIcon: isLoading ? null : UiIcons.checkCircle,
|
||||||
onPressed: () {
|
isLoading: isLoading,
|
||||||
Modular.get<BillingBloc>().add(BillingInvoiceApproved(invoiceId));
|
onPressed: isLoading
|
||||||
Modular.to.pop();
|
? null
|
||||||
UiSnackbar.show(
|
: () {
|
||||||
|
BlocProvider.of<ShiftCompletionReviewBloc>(
|
||||||
context,
|
context,
|
||||||
message: t.client_billing.approved_success,
|
).add(ShiftCompletionReviewApproved(invoiceId));
|
||||||
type: UiSnackbarType.success,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
size: UiButtonSize.large,
|
size: UiButtonSize.large,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showFlagDialog(BuildContext context) {
|
void _showFlagDialog(BuildContext context, ShiftCompletionReviewState state) {
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext dialogContext) => AlertDialog(
|
builder: (BuildContext dialogContext) => AlertDialog(
|
||||||
title: Text(t.client_billing.flag_dialog.title),
|
title: Text(t.client_billing.flag_dialog.title),
|
||||||
|
surfaceTintColor: Colors.white,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -70,15 +105,10 @@ class CompletionReviewActions extends StatelessWidget {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (controller.text.isNotEmpty) {
|
if (controller.text.isNotEmpty) {
|
||||||
Modular.get<BillingBloc>().add(
|
BlocProvider.of<ShiftCompletionReviewBloc>(context).add(
|
||||||
BillingInvoiceDisputed(invoiceId, controller.text),
|
ShiftCompletionReviewDisputed(invoiceId, controller.text),
|
||||||
);
|
|
||||||
Modular.to.toClientBilling();
|
|
||||||
UiSnackbar.show(
|
|
||||||
context,
|
|
||||||
message: t.client_billing.flagged_success,
|
|
||||||
type: UiSnackbarType.warning,
|
|
||||||
);
|
);
|
||||||
|
Navigator.pop(dialogContext);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(t.client_billing.flag_dialog.button),
|
child: Text(t.client_billing.flag_dialog.button),
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ class CompletionReviewAmount extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(UiConstants.space6),
|
padding: const EdgeInsets.all(UiConstants.space6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFEFF6FF),
|
color: UiColors.bgPopup,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: const Color(0xFFDBEAFE)),
|
border: Border.all(color: UiColors.border),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|||||||
Reference in New Issue
Block a user