feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
|
||||
class DisputeInfoDialog extends StatefulWidget {
|
||||
final String reason;
|
||||
final String details;
|
||||
final String? supportNote;
|
||||
|
||||
const DisputeInfoDialog(
|
||||
{super.key,
|
||||
required this.reason,
|
||||
required this.details,
|
||||
required this.supportNote});
|
||||
|
||||
static Future<Map<String, dynamic>?> showCustomDialog(BuildContext context,
|
||||
{required String reason,
|
||||
required String details,
|
||||
required String? supportNote}) async {
|
||||
return await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => DisputeInfoDialog(
|
||||
reason: reason, details: details, supportNote: supportNote),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<DisputeInfoDialog> createState() => _DisputeInfoDialogState();
|
||||
}
|
||||
|
||||
class _DisputeInfoDialogState extends State<DisputeInfoDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
decoration: KwBoxDecorations.white24,
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Dispute Details',
|
||||
style: AppTextStyles.headingH3,
|
||||
)
|
||||
],
|
||||
),
|
||||
Gap(24),
|
||||
Text(
|
||||
'The reason why the invoice is being disputed:',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
widget.reason,
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Divider(
|
||||
color: AppColors.grayTintStroke,
|
||||
height: 24,
|
||||
),
|
||||
Text('Status',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Gap(2),
|
||||
Text('Under Review',
|
||||
style: AppTextStyles.bodyMediumMed
|
||||
.copyWith(color: AppColors.primaryBlue)),
|
||||
Gap(12),
|
||||
Text(
|
||||
'Additional Details',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
widget.details,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
if (widget.supportNote?.isNotEmpty ?? false) ...[
|
||||
const Gap(12),
|
||||
Text(
|
||||
'Support Note',
|
||||
style: AppTextStyles.bodySmallReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
widget.supportNote ?? '',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
],
|
||||
const Gap(24),
|
||||
KwButton.primary(
|
||||
label: 'Back to Invoice',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:auto_route/auto_route.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/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_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/dialogs/invoice_reason_dropdown.dart';
|
||||
|
||||
class ShiftDeclineDialog extends StatefulWidget {
|
||||
const ShiftDeclineDialog({super.key});
|
||||
|
||||
static Future<Map<String, dynamic>?> showCustomDialog(
|
||||
BuildContext context) async {
|
||||
return await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (context) => const ShiftDeclineDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<ShiftDeclineDialog> createState() => _ShiftDeclineDialogState();
|
||||
}
|
||||
|
||||
class _ShiftDeclineDialogState extends State<ShiftDeclineDialog> {
|
||||
String selectedReason = '';
|
||||
final _textEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Center(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 56),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.tintRed,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.receiptSearch.svg(
|
||||
width: 32,
|
||||
height: 32,
|
||||
colorFilter: ColorFilter.mode(
|
||||
AppColors.statusError, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
Gap(32),
|
||||
Text(
|
||||
'Dispute Invoice',
|
||||
style: AppTextStyles.headingH1.copyWith(height: 1),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'If there’s an issue with this invoice, please select a reason below and provide any additional details. We’ll review your dispute and get back to you as soon as possible.',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
InvoiceReasonDropdown(
|
||||
selectedReason: selectedReason,
|
||||
onReasonSelected: (String reason) {
|
||||
setState(() {
|
||||
selectedReason = reason;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
controller: _textEditingController,
|
||||
minHeight: 144,
|
||||
maxLength: 300,
|
||||
showCounter: true,
|
||||
radius: 12,
|
||||
title: 'Additional reasons',
|
||||
hintText: 'Enter your main text here...',
|
||||
onChanged: (String value) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const Gap(24),
|
||||
_buttonGroup(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buttonGroup(
|
||||
BuildContext context,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
KwButton.primary(
|
||||
disabled: selectedReason.isEmpty || (_textEditingController.text.isEmpty),
|
||||
label: 'Submit Request',
|
||||
onPressed: () {
|
||||
context.pop({
|
||||
'reason': selectedReason,
|
||||
'comment': _textEditingController.text,
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Cancel',
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_popup_menu.dart';
|
||||
|
||||
const reasons = [
|
||||
'Hours didn’t Match',
|
||||
'Calculation Issue',
|
||||
'Other (Please specify)',
|
||||
];
|
||||
|
||||
class InvoiceReasonDropdown extends StatelessWidget {
|
||||
final String? selectedReason;
|
||||
final Function(String reason) onReasonSelected;
|
||||
|
||||
const InvoiceReasonDropdown(
|
||||
{super.key,
|
||||
required this.selectedReason,
|
||||
required this.onReasonSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: buildReasonInput(context, selectedReason),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildReasonInput(BuildContext context, String? selectedReason) {
|
||||
return [
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
'Select reason',
|
||||
style:
|
||||
AppTextStyles.bodyTinyReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
KwPopupMenu(
|
||||
horizontalPadding: 40,
|
||||
fit: KwPopupMenuFit.expand,
|
||||
customButtonBuilder: (context, isOpened) {
|
||||
return _buildMenuButton(isOpened, selectedReason);
|
||||
},
|
||||
menuItems: [
|
||||
...reasons
|
||||
.map((e) => _buildMenuItem(context, e, selectedReason ?? ''))
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
Container _buildMenuButton(bool isOpened, String? selectedReason) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isOpened ? AppColors.bgColorDark : AppColors.grayTintStroke,
|
||||
width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedReason ?? 'Select reason from a list',
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: selectedReason == null
|
||||
? AppColors.blackGray
|
||||
: AppColors.blackBlack),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
KwPopupMenuItem _buildMenuItem(
|
||||
BuildContext context, String reason, String selectedReason) {
|
||||
return KwPopupMenuItem(
|
||||
title: reason,
|
||||
onTap: () {
|
||||
onReasonSelected(reason);
|
||||
},
|
||||
icon: Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason != reason ? null : AppColors.bgColorDark,
|
||||
shape: BoxShape.circle,
|
||||
border: selectedReason == reason
|
||||
? null
|
||||
: Border.all(color: AppColors.grayTintStroke, width: 1),
|
||||
),
|
||||
child: selectedReason == reason
|
||||
? Center(
|
||||
child: Assets.images.icons.receiptSearch.svg(
|
||||
height: 10,
|
||||
width: 10,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.grayWhite, BlendMode.srcIn),
|
||||
))
|
||||
: null,
|
||||
),
|
||||
textStyle: AppTextStyles.bodySmallMed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:krow/core/entity/staff_contact_entity.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/widgets/staff_position_details_widget.dart';
|
||||
|
||||
import '../../../../../../core/presentation/styles/theme.dart';
|
||||
|
||||
class StaffInvoiceContactInfoPopup {
|
||||
static Future<void> show(
|
||||
BuildContext context,
|
||||
StaffContact staff, {
|
||||
String? date,
|
||||
required double hours,
|
||||
required double subtotal,
|
||||
}) async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Center(
|
||||
child: _StaffPopupWidget(
|
||||
staff,
|
||||
date: date,
|
||||
hours: hours,
|
||||
subtotal: subtotal,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _StaffPopupWidget extends StatelessWidget {
|
||||
final StaffContact staff;
|
||||
final String? date;
|
||||
|
||||
final double hours;
|
||||
|
||||
final double subtotal;
|
||||
|
||||
const _StaffPopupWidget(this.staff,
|
||||
{this.date, required this.hours, required this.subtotal});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
// margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: KwBoxDecorations.white24,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _ongoingBtn(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _ongoingBtn(BuildContext context) {
|
||||
return [
|
||||
const Gap(32),
|
||||
StaffPositionAvatar(
|
||||
imageUrl: staff.photoUrl,
|
||||
userName: '${staff.firstName} ${staff.lastName}',
|
||||
status: staff.status,
|
||||
),
|
||||
const Gap(16),
|
||||
StaffContactsWidget(staff: staff),
|
||||
StaffPositionDetailsWidget(
|
||||
staff: staff, date: date, hours: hours, subtotal: subtotal),
|
||||
const Gap(12),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class StaffPositionDetailsWidget extends StatelessWidget {
|
||||
final StaffContact staff;
|
||||
final String? date;
|
||||
|
||||
final double hours;
|
||||
|
||||
final double subtotal;
|
||||
|
||||
const StaffPositionDetailsWidget({
|
||||
super.key,
|
||||
required this.staff,
|
||||
this.date,
|
||||
required this.hours,
|
||||
required this.subtotal,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: KwBoxDecorations.primaryLight8,
|
||||
child: Column(
|
||||
children: [
|
||||
_textRow('Date',
|
||||
DateFormat('MM.dd.yyyy').format(DateTime.parse(date ?? ''))),
|
||||
_textRow(
|
||||
'Start Time',
|
||||
DateFormat('hh:mm a').format(
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.startAt))),
|
||||
_textRow(
|
||||
'End Time',
|
||||
DateFormat('hh:mm a').format(
|
||||
DateFormat('yyyy-MM-dd hh:mm:ss').parse(staff.endAt))),
|
||||
if (staff.breakIn.isNotEmpty && staff.breakOut.isNotEmpty)
|
||||
_textRow(
|
||||
'Break',
|
||||
DateTime.parse(staff.breakIn)
|
||||
.difference(DateTime.parse(staff.breakOut))
|
||||
.inMinutes
|
||||
.toString() +
|
||||
' minutes'),
|
||||
_textRow('Rate (\$/h)', '\$${staff.rate.toStringAsFixed(2)}/h'),
|
||||
_textRow('Hours', '${hours.toStringAsFixed(2)}'),
|
||||
_textRow('Subtotal', '\$${subtotal.toStringAsFixed(2)}'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textRow(String title, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppTextStyles.bodySmallReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.bodySmallMed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:auto_route/auto_route.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/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_decline_model.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoice_details_bloc/invoice_details_bloc.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/dialogs/dispute_info_dialog.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/dialogs/invoice_dispute_dialog.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_details_widget.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_from_to_widget.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_info_card.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoice_details/widgets/invoice_total_widget.dart';
|
||||
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
|
||||
|
||||
@RoutePage()
|
||||
class InvoiceDetailsScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
final InvoiceListEntity invoice;
|
||||
|
||||
const InvoiceDetailsScreen({required this.invoice, super.key});
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => InvoiceDetailsBloc(invoice.invoiceModel!),
|
||||
child: this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Invoice Details',
|
||||
),
|
||||
body: BlocConsumer<InvoiceDetailsBloc, InvoiceDetailsState>(
|
||||
listener: (context, state) {
|
||||
if (state.showErrorPopup != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(state.showErrorPopup ?? ''),
|
||||
));
|
||||
} else if (state.success) {
|
||||
context.router.maybePop();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return ModalProgressHUD(
|
||||
inAsyncCall: state.inLoading,
|
||||
child: ScrollLayoutHelper(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24),
|
||||
upperWidget: Column(
|
||||
children: [
|
||||
InvoiceInfoCardWidget(item: state.invoiceModel??invoice.invoiceModel!),
|
||||
Gap(12),
|
||||
InvoiceFromToWidget(item: state.invoiceModel??invoice.invoiceModel!),
|
||||
Gap(12),
|
||||
InvoiceDetailsWidget(invoice: state.invoiceModel??invoice.invoiceModel!),
|
||||
Gap(12),
|
||||
InvoiceTotalWidget(),
|
||||
Gap(24),
|
||||
],
|
||||
),
|
||||
lowerWidget: _buildButtons(context, state),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildButtons(BuildContext context, InvoiceDetailsState state) {
|
||||
return Column(
|
||||
children: [
|
||||
if (invoice.invoiceModel?.status == InvoiceStatus.open) ...[
|
||||
KwButton.primary(
|
||||
label: 'Approve Invoice',
|
||||
onPressed: () {
|
||||
context.read<InvoiceDetailsBloc>().add(InvoiceApproveEvent());
|
||||
},
|
||||
),
|
||||
Gap(8),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Dispute Invoice',
|
||||
onPressed: () async {
|
||||
var result = await ShiftDeclineDialog.showCustomDialog(context);
|
||||
print(result);
|
||||
if (result != null) {
|
||||
context.read<InvoiceDetailsBloc>().add(InvoiceDisputeEvent(
|
||||
reason: result['reason'], comment: result['comment']));
|
||||
KwDialog.show(
|
||||
icon: Assets.images.icons.receiptSearch,
|
||||
state: KwDialogState.negative,
|
||||
context: context,
|
||||
title: 'Request is \nUnder Review',
|
||||
message:
|
||||
'Thank you! Your request for invoice issue, it is now under review. You will be notified of the outcome shortly.',
|
||||
primaryButtonLabel: 'Back to Event',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
dialogContext.pop();
|
||||
},
|
||||
secondaryButtonLabel: 'Cancel',
|
||||
onSecondaryButtonPressed: (dialogContext) {
|
||||
dialogContext.pop();
|
||||
context.router.maybePop();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
).copyWith(
|
||||
color: AppColors.statusError, borderColor: AppColors.statusError),
|
||||
],
|
||||
if (invoice.invoiceModel?.status == InvoiceStatus.disputed) ...[
|
||||
_buildViewDispute(context, state.invoiceModel?.dispute),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_buildViewDispute(BuildContext context, InvoiceDeclineModel? dispute) {
|
||||
return KwButton.primary(
|
||||
label: 'View Dispute',
|
||||
onPressed: () {
|
||||
DisputeInfoDialog.showCustomDialog(
|
||||
context,
|
||||
reason: dispute?.reason ?? '',
|
||||
details: dispute?.details ?? '',
|
||||
supportNote: dispute?.supportNote,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/models/shift/event_shift_position_model.dart';
|
||||
import 'package:krow/core/entity/staff_contact_entity.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/features/invoice/data/models/invoice_item_model.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
|
||||
import '../../../../../../core/data/models/staff/pivot.dart';
|
||||
import '../../../../../../core/entity/position_entity.dart';
|
||||
import '../dialogs/staff_contact_info_popup.dart';
|
||||
|
||||
class InvoiceDetailsWidget extends StatefulWidget {
|
||||
final InvoiceModel invoice;
|
||||
|
||||
const InvoiceDetailsWidget({super.key, required this.invoice});
|
||||
|
||||
@override
|
||||
State<InvoiceDetailsWidget> createState() => _InvoiceDetailsWidgetState();
|
||||
}
|
||||
|
||||
class _InvoiceDetailsWidgetState extends State<InvoiceDetailsWidget> {
|
||||
List<ExpandableController> controllers = [];
|
||||
|
||||
ExpandableController showMoreController = ExpandableController();
|
||||
|
||||
Map<EventShiftPositionModel, List<InvoiceItemModel>> staffByPosition = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
staffByPosition = groupByPosition(widget.invoice.items ?? []);
|
||||
controllers =
|
||||
List.generate(staffByPosition.length, (_) => ExpandableController());
|
||||
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Invoice details',
|
||||
style: AppTextStyles.headingH3,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildTable(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTable() {
|
||||
final columnWidths = {
|
||||
0: const FlexColumnWidth(),
|
||||
1: const IntrinsicColumnWidth(),
|
||||
2: const IntrinsicColumnWidth(),
|
||||
3: const IntrinsicColumnWidth(),
|
||||
};
|
||||
|
||||
final rows = <TableRow>[];
|
||||
|
||||
_buildTableHeader(rows);
|
||||
|
||||
var maxVisibleRows = staffByPosition.length;
|
||||
|
||||
for (int index = 0; index < maxVisibleRows; index++) {
|
||||
final position = staffByPosition.keys.elementAt(index);
|
||||
final group = staffByPosition[position]!;
|
||||
final controller = controllers[index];
|
||||
|
||||
final roleDuration =
|
||||
group.fold(0.0, (sum, item) => sum + (item.workHours ?? 0));
|
||||
final subTotal =
|
||||
group.fold(0.0, (sum, item) => sum + (item.totalAmount ?? 0));
|
||||
|
||||
_buildRoleRows(rows, controller, position, roleDuration, subTotal,
|
||||
isLastRow: index == maxVisibleRows - 1);
|
||||
|
||||
_buildStaffsRows(controller, group, rows, position);
|
||||
}
|
||||
|
||||
return AnimatedSize(
|
||||
alignment: Alignment.topCenter,
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
child: Table(
|
||||
columnWidths: columnWidths,
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: rows,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _buildTableHeader(List<TableRow> rows) {
|
||||
return rows.add(
|
||||
TableRow(
|
||||
children: buildRowCells(
|
||||
[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, top: 12, bottom: 12),
|
||||
child: Text('Role', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8, right: 16, top: 12, bottom: 12),
|
||||
child: Text('Rate (\$/h)', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8, right: 16, top: 12, bottom: 12),
|
||||
child: Text('Hours', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8, right: 8, top: 12, bottom: 12),
|
||||
child: Text('Subtotal', style: AppTextStyles.bodyMediumMed),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _buildRoleRows(List<TableRow> rows, ExpandableController controller,
|
||||
EventShiftPositionModel position, double roleDuration, double subTotal,
|
||||
{required bool isLastRow}) {
|
||||
return rows.add(
|
||||
TableRow(
|
||||
children: buildRowCells(
|
||||
isLastRow: isLastRow && !controller.expanded,
|
||||
[
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(left: 6, top: 16, bottom: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedRotation(
|
||||
duration: Duration(milliseconds: 300),
|
||||
turns: controller.expanded ? -0.5 : 0,
|
||||
child: Assets.images.icons.chevronDown
|
||||
.svg(width: 12, height: 12),
|
||||
),
|
||||
Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
position.businessSkill.skill?.name ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 8, top: 16, bottom: 16),
|
||||
child: Text(
|
||||
'\$${position.rate.toStringAsFixed(2)}/h',
|
||||
style: AppTextStyles.bodyMediumReg,
|
||||
),
|
||||
),
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 8, top: 16, bottom: 16),
|
||||
child: Text(roleDuration.toStringAsFixed(1),
|
||||
style: AppTextStyles.bodyMediumReg),
|
||||
),
|
||||
expandableCell(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 8, top: 16, bottom: 16),
|
||||
child: Text('\$${subTotal.toStringAsFixed(2)}',
|
||||
style: AppTextStyles.bodyMediumReg),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _buildStaffsRows(
|
||||
ExpandableController controller,
|
||||
List<InvoiceItemModel> group,
|
||||
List<TableRow> rows,
|
||||
EventShiftPositionModel position) {
|
||||
if (controller.expanded) {
|
||||
for (int i = 0; i < group.length; i++) {
|
||||
final user = group[i];
|
||||
rows.add(
|
||||
TableRow(
|
||||
children: buildRowCells(isLastRow: i == group.length - 1, [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
StaffInvoiceContactInfoPopup.show(
|
||||
context,
|
||||
makeStaffContact(
|
||||
user,
|
||||
position,
|
||||
),
|
||||
date: widget.invoice.event?.date,
|
||||
hours: user.workHours ?? 0.0,
|
||||
subtotal: user.totalAmount ?? 0.0);
|
||||
},
|
||||
child: buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0),
|
||||
child: Text(
|
||||
'${user.staff?.firstName ?? ''} ${user.staff?.lastName ?? ''}',
|
||||
style: AppTextStyles.bodyTinyReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Text('\$${(user.rate?.toStringAsFixed(2)) ?? 0}/h',
|
||||
style: AppTextStyles.bodyTinyReg),
|
||||
),
|
||||
buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Text('${user.workHours?.toStringAsFixed(1) ?? 0}',
|
||||
style: AppTextStyles.bodyTinyReg),
|
||||
),
|
||||
buildAnimatedStaffCell(
|
||||
visible: controller.expanded,
|
||||
child: Text('\$${user.totalAmount ?? 0}',
|
||||
style: AppTextStyles.bodyTinyReg),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> buildRowCells(List<Widget> cells, {bool isLastRow = false}) {
|
||||
return List.generate(cells.length, (i) {
|
||||
final isLastColumn = i == cells.length - 1;
|
||||
return buildGridCell(
|
||||
child: cells[i],
|
||||
showRight: !isLastColumn,
|
||||
showBottom: !isLastRow,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget expandableCell(
|
||||
{required Widget child,
|
||||
required ExpandableController controller,
|
||||
required EdgeInsets padding}) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
controller.expanded = !controller.expanded;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent, padding: padding, child: child));
|
||||
}
|
||||
|
||||
Widget buildGridCell({
|
||||
required Widget child,
|
||||
bool showRight = true,
|
||||
bool showBottom = true,
|
||||
EdgeInsets padding = const EdgeInsets.all(8),
|
||||
}) {
|
||||
return Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: showRight
|
||||
? BorderSide(color: Colors.grey.shade300, width: 1)
|
||||
: BorderSide.none,
|
||||
bottom: showBottom
|
||||
? BorderSide(color: Colors.grey.shade300, width: 1)
|
||||
: BorderSide.none,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAnimatedStaffCell({
|
||||
required bool visible,
|
||||
required Widget child,
|
||||
Duration duration = const Duration(milliseconds: 300),
|
||||
Curve curve = Curves.easeInOut,
|
||||
}) {
|
||||
return AnimatedContainer(
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
color: Colors.transparent,
|
||||
height: visible ? null : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 6, top: 10, bottom: 10),
|
||||
child: AnimatedOpacity(
|
||||
duration: duration,
|
||||
opacity: visible ? 1 : 0,
|
||||
child: visible ? child : IgnorePointer(child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<EventShiftPositionModel, List<InvoiceItemModel>> groupByPosition(
|
||||
List<InvoiceItemModel> items,
|
||||
) {
|
||||
|
||||
final Map<String, EventShiftPositionModel> positionCache = {};
|
||||
final Map<EventShiftPositionModel, List<InvoiceItemModel>> grouped = {};
|
||||
|
||||
for (final item in items) {
|
||||
final position = item.position;
|
||||
|
||||
if (position == null) continue;
|
||||
|
||||
final id = position.businessSkill.id ?? '';
|
||||
|
||||
final existingPosition = positionCache[id];
|
||||
|
||||
final key = existingPosition ??
|
||||
() {
|
||||
positionCache[id] = position;
|
||||
return position;
|
||||
}();
|
||||
|
||||
grouped.putIfAbsent(key, () => []);
|
||||
grouped[key]!.add(item);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
StaffContact makeStaffContact(
|
||||
InvoiceItemModel user, EventShiftPositionModel position) {
|
||||
var staff = user.staff!;
|
||||
var pivot = user.position!.staff!
|
||||
.firstWhere(
|
||||
(element) => element.id == user.staff?.id,
|
||||
)
|
||||
.pivot;
|
||||
|
||||
return StaffContact(
|
||||
id: staff.id ?? '',
|
||||
photoUrl: staff.avatar ?? '',
|
||||
firstName: staff.firstName ?? '',
|
||||
lastName: staff.lastName ?? '',
|
||||
phoneNumber: staff.phone ?? '',
|
||||
email: staff.email ?? '',
|
||||
rate: position.businessSkill.price ?? 0,
|
||||
status: pivot?.status ?? PivotStatus.assigned,
|
||||
startAt: pivot?.clockIn ?? '',
|
||||
endAt: pivot?.clockOut ?? '',
|
||||
breakIn: pivot?.breakIn ?? '',
|
||||
breakOut: pivot?.breakOut ?? '',
|
||||
parentPosition: PositionEntity.fromDto(
|
||||
position,
|
||||
DateTime.tryParse(widget.invoice.event?.date ?? '') ??
|
||||
DateTime.now()),
|
||||
skillName: position.businessSkill.skill?.name ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:expandable/expandable.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/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoice_details_bloc/invoice_details_bloc.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoiceFromToWidget extends StatefulWidget {
|
||||
final InvoiceModel item;
|
||||
|
||||
const InvoiceFromToWidget({super.key, required this.item});
|
||||
|
||||
@override
|
||||
State<InvoiceFromToWidget> createState() => _InvoiceFromToWidgetState();
|
||||
}
|
||||
|
||||
class _InvoiceFromToWidgetState extends State<InvoiceFromToWidget> {
|
||||
ExpandableController? _expandableFromController;
|
||||
ExpandableController? _expandableToController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_expandableFromController = ExpandableController(initialExpanded: true);
|
||||
_expandableToController = ExpandableController(initialExpanded: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_expandableFromController?.dispose();
|
||||
_expandableToController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<InvoiceDetailsBloc, InvoiceDetailsState>(
|
||||
builder: (context, state) {
|
||||
return ExpandableTheme(
|
||||
data: ExpandableThemeData(
|
||||
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
||||
iconPadding: const EdgeInsets.only(left: 12),
|
||||
),
|
||||
child: Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Gap(12),
|
||||
ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableFromController,
|
||||
expanded: _fromInfo(),
|
||||
header: _buildHeader(
|
||||
context, _expandableFromController, 'From: Legendary'),
|
||||
),
|
||||
ExpandablePanel(
|
||||
collapsed: Container(),
|
||||
controller: _expandableToController,
|
||||
expanded: _toInfo(state.invoiceModel!),
|
||||
header: _buildHeader(
|
||||
context, _expandableToController, 'To: ${state.invoiceModel?.business?.name}'),
|
||||
),
|
||||
Gap(12),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, ExpandableController? controller,
|
||||
String title,) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller?.toggle();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
title,
|
||||
style: AppTextStyles.headingH3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fromInfo() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Gap(4),
|
||||
infoElement('Address', '848 E Gish Rd, Suite 1 San Jose, CA 95112'),
|
||||
Gap(12),
|
||||
infoElement('Phone Number', '4088360180'),
|
||||
Gap(12),
|
||||
infoElement('Email', 'orders@legendaryeventstaff.com'),
|
||||
Gap(20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toInfo(InvoiceModel invoice) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Gap(4),
|
||||
infoElement('Hub', invoice.event?.hub?.name ?? ''),
|
||||
Gap(12),
|
||||
infoElement('Address', invoice.business?.registration ?? ''),
|
||||
Gap(12),
|
||||
infoElement('Full Name', '${invoice.business?.contact?.firstName??''} ${invoice.business?.contact?.lastName??''}' ),
|
||||
Gap(12),
|
||||
infoElement('Phone Number', invoice.business?.contact?.phone ?? ''),
|
||||
Gap(12),
|
||||
infoElement('Email', invoice.business?.contact?.email ?? ''),
|
||||
Gap(12),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget infoElement(String title, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style:
|
||||
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
|
||||
),
|
||||
Gap(2),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/entity/event_entity.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/icon_row_info_widget.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoiceInfoCardWidget extends StatelessWidget {
|
||||
final InvoiceModel item;
|
||||
|
||||
const InvoiceInfoCardWidget({
|
||||
required this.item,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildStatusRow(),
|
||||
const Gap(24),
|
||||
Text('INV-${item.id}', style: AppTextStyles.headingH1),
|
||||
Gap(4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if(item.event == null) return;
|
||||
context.router.push(EventDetailsRoute(event: EventEntity.fromEventDto(item.event!)));
|
||||
},
|
||||
child: Text(item.event!.name,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray,decoration: TextDecoration.underline)),
|
||||
),
|
||||
const Gap(24),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'Date',
|
||||
value: DateFormat('MM.dd.yyyy').format(DateTime.parse(item.event?.date ?? '')),
|
||||
),
|
||||
const Gap(24),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'Due Date',
|
||||
value: DateFormat('MM.dd.yyyy').format(DateTime.parse(item.dueAt ?? '')),
|
||||
),
|
||||
if (item.event?.purchaseOrder != null) ...[
|
||||
const Gap(24),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.calendar.svg(),
|
||||
title: 'PO Number',
|
||||
value: item.event!.purchaseOrder!,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildStatusRow() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.tintGreen,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.moneySend.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusSuccess, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: item.status.color,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Text(
|
||||
item.status.name.capitalize(),
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on InvoiceStatus {
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case InvoiceStatus.open:
|
||||
return AppColors.primaryBlue;
|
||||
case InvoiceStatus.disputed:
|
||||
return AppColors.statusWarning;
|
||||
case InvoiceStatus.resolved:
|
||||
return AppColors.statusSuccess;
|
||||
case InvoiceStatus.paid:
|
||||
return AppColors.blackGray;
|
||||
case InvoiceStatus.overdue:
|
||||
return AppColors.statusError;
|
||||
case InvoiceStatus.verified:
|
||||
return AppColors.bgColorDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoice_details_bloc/invoice_details_bloc.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoiceTotalWidget extends StatelessWidget {
|
||||
|
||||
const InvoiceTotalWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<InvoiceDetailsBloc, InvoiceDetailsState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Total', style: AppTextStyles.headingH3),
|
||||
Gap(24),
|
||||
Container(
|
||||
decoration: KwBoxDecorations.primaryLight8,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
totalInfoRow('Sub Total', '\$${state.invoiceModel?.workAmount?.toStringAsFixed(2)??'0'}'),
|
||||
Gap(8),
|
||||
totalInfoRow('Other Charges', '\$${state.invoiceModel?.addonsAmount?.toStringAsFixed(2)??'0'}'),
|
||||
Gap(8),
|
||||
totalInfoRow('Grand Total', '\$${state.invoiceModel?.total?.toStringAsFixed(2)??'0'}'),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget totalInfoRow(String title, String value) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Gap(20),
|
||||
Text(value, style: AppTextStyles.bodyMediumMed)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/common/str_extensions.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.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/icon_row_info_widget.dart';
|
||||
import 'package:krow/features/invoice/data/models/invoice_model.dart';
|
||||
import 'package:krow/features/invoice/domain/invoice_entity.dart';
|
||||
|
||||
class InvoiceListItemWidget extends StatelessWidget {
|
||||
final InvoiceListEntity item;
|
||||
|
||||
const InvoiceListItemWidget({required this.item, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.router.push(InvoiceDetailsRoute(
|
||||
invoice: item,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
decoration: KwBoxDecorations.white12,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||
margin: const EdgeInsets.only(bottom: 12, left: 16, right: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildStatusRow(),
|
||||
const Gap(12),
|
||||
Text('INV-${item.id}', style: AppTextStyles.headingH1),
|
||||
const Gap(4),
|
||||
Text(item.eventName,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
const Gap(12),
|
||||
IconRowInfoWidget(
|
||||
icon: Assets.images.icons.profile2user.svg(),
|
||||
title: 'Count',
|
||||
value: '${item.count} persons',
|
||||
),
|
||||
const Gap(24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: KwBoxDecorations.primaryLight8.copyWith(),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Value',
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.blackGray)),
|
||||
Text(
|
||||
'\$${item.value}',
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildStatusRow() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.tintGreen,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Assets.images.icons.moneySend.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.statusSuccess, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: item.status.color,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: Text(
|
||||
item.status.name.capitalize(),
|
||||
style: AppTextStyles.bodySmallMed
|
||||
.copyWith(color: AppColors.grayWhite),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on InvoiceStatus {
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case InvoiceStatus.open:
|
||||
return AppColors.primaryBlue;
|
||||
case InvoiceStatus.disputed:
|
||||
return AppColors.statusWarning;
|
||||
case InvoiceStatus.resolved:
|
||||
return AppColors.statusSuccess;
|
||||
case InvoiceStatus.paid:
|
||||
return AppColors.blackGray;
|
||||
case InvoiceStatus.overdue:
|
||||
return AppColors.statusError;
|
||||
case InvoiceStatus.verified:
|
||||
return AppColors.bgColorDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'package:auto_route/auto_route.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_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_tabs.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoices_list_bloc/invoice_bloc.dart';
|
||||
import 'package:krow/features/invoice/domain/blocs/invoices_list_bloc/invoice_state.dart';
|
||||
import 'package:krow/features/invoice/presentation/screens/invoices_list/invoice_list_item.dart';
|
||||
|
||||
import '../../../domain/blocs/invoices_list_bloc/invoice_event.dart';
|
||||
import '../../../domain/invoice_entity.dart';
|
||||
|
||||
@RoutePage()
|
||||
class InvoicesListMainScreen extends StatefulWidget {
|
||||
const InvoicesListMainScreen({super.key});
|
||||
|
||||
@override
|
||||
State<InvoicesListMainScreen> createState() => _InvoicesListMainScreenState();
|
||||
}
|
||||
|
||||
class _InvoicesListMainScreenState extends State<InvoicesListMainScreen> {
|
||||
final List<String> tabs = const [
|
||||
'All',
|
||||
'Open',
|
||||
'Disputed',
|
||||
'Resolved',
|
||||
'Paid',
|
||||
'Overdue',
|
||||
'Verified',
|
||||
];
|
||||
|
||||
late ScrollController _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.atEdge) {
|
||||
if (_scrollController.position.pixels != 0) {
|
||||
BlocProvider.of<InvoiceBloc>(context).add(
|
||||
LoadMoreInvoiceEvent(
|
||||
status: BlocProvider.of<InvoiceBloc>(context).state.tabIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<InvoiceBloc, InvoicesState>(
|
||||
builder: (context, state) {
|
||||
List<InvoiceListEntity> items = state.tabs[state.tabIndex]!.items;
|
||||
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Invoices',
|
||||
centerTitle: false,
|
||||
),
|
||||
body: ScrollLayoutHelper(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
onRefresh: () async {
|
||||
BlocProvider.of<InvoiceBloc>(context)
|
||||
.add(LoadTabInvoiceEvent(status: state.tabIndex));
|
||||
},
|
||||
controller: _scrollController,
|
||||
upperWidget: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
KwTabBar(
|
||||
tabs: tabs,
|
||||
onTap: (index) {
|
||||
BlocProvider.of<InvoiceBloc>(context)
|
||||
.add(InvoiceTabChangedEvent(tabIndex: index));
|
||||
}),
|
||||
const Gap(16),
|
||||
if (state.tabs[state.tabIndex]!.inLoading &&
|
||||
state.tabs[state.tabIndex]!.items.isEmpty)
|
||||
..._buildListLoading(),
|
||||
if (!state.tabs[state.tabIndex]!.inLoading && items.isEmpty)
|
||||
..._emptyListWidget(),
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
BlocProvider.of<InvoiceBloc>(context)
|
||||
.add(LoadTabInvoiceEvent(status: state.tabIndex));
|
||||
},
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
return InvoiceListItemWidget(item: items[index]);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
lowerWidget: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildListLoading() {
|
||||
return [
|
||||
const Gap(116),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _emptyListWidget() {
|
||||
return [
|
||||
const Gap(100),
|
||||
Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
child: Center(child: Assets.images.icons.xCircle.svg()),
|
||||
),
|
||||
const Gap(24),
|
||||
const Text(
|
||||
'You currently have no Invoices',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppTextStyles.headingH2,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user