feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

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

View File

@@ -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 theres an issue with this invoice, please select a reason below and provide any additional details. Well 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();
},
),
],
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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