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,156 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/entity/event_entity.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_popup_button.dart';
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
import 'package:krow/features/events/domain/events_repository.dart';
import 'package:krow/features/events/presentation/event_details/widgets/event_completed_by_card_widget.dart';
import 'package:krow/features/events/presentation/event_details/widgets/event_info_card_widget.dart';
import 'package:krow/features/events/presentation/event_details/widgets/shift/shift_widget.dart';
import 'package:krow/features/home/presentation/home_screen.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
import '../../domain/blocs/events_list_bloc/events_bloc.dart';
import '../../domain/blocs/events_list_bloc/events_event.dart';
@RoutePage()
class EventDetailsScreen extends StatefulWidget implements AutoRouteWrapper {
final EventEntity event;
final bool isPreview;
const EventDetailsScreen(
{super.key, required this.event, this.isPreview = false});
@override
State<EventDetailsScreen> createState() => _EventDetailsScreenState();
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider(
key: Key(event.id),
create: (context) {
return EventDetailsBloc(event, getIt<EventsRepository>(),isPreview)
..add(EventDetailsInitialEvent());
},
child: this,
);
}
}
class _EventDetailsScreenState extends State<EventDetailsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
titleText: widget.isPreview ? 'Event Preview' : 'Event Details',
),
body: BlocConsumer<EventDetailsBloc, EventDetailsState>(
listenWhen: (previous, current) =>
previous.showErrorPopup != current.showErrorPopup ||
previous.needDeepPop != current.needDeepPop,
listener: (context, state) {
if (state.needDeepPop) {
context.router.popUntilRoot();
homeContext?.maybePop();
return;
}
if (state.showErrorPopup != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.showErrorPopup ?? ''),
));
}
},
builder: (context, state) {
return ModalProgressHUD(
inAsyncCall: state.inLoading,
child: ScrollLayoutHelper(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24),
upperWidget: Column(
children: [
EventInfoCardWidget(
item: state.event,
isPreview: widget.isPreview,
),
EventCompletedByCardWidget(
completedBy: state.event.completedBy,
completedNote: state.event.completedNode,
),
ListView.builder(
shrinkWrap: true,
primary: false,
itemCount: state.shifts.length,
itemBuilder: (context, index) {
return ShiftWidget(
index: index, shiftState: state.shifts[index]);
},
),
],
),
lowerWidget: widget.isPreview
? _buildPreviewButtons()
: const SizedBox.shrink()),
);
},
),
);
}
Widget _buildPreviewButtons() {
return Padding(
padding: const EdgeInsets.only(top: 24),
child: KwPopUpButton(
label: widget.event.status == null ||
widget.event.status == EventStatus.draft
? widget.event.id.isEmpty
? 'Save as Draft'
: 'Update Draft'
: 'Save Event',
popUpPadding: 16,
items: [
KwPopUpButtonItem(
title: 'Publish Event',
onTap: () {
BlocProvider.of<EventDetailsBloc>(context)
.add(DetailsPublishEvent());
},
),
if (widget.event.status == EventStatus.draft ||
widget.event.status == null)
KwPopUpButtonItem(
title: widget.event.id.isEmpty ? 'Save as Draft' : 'Update Draft',
onTap: () {
BlocProvider.of<EventDetailsBloc>(context)
.add(CreateEventPostEvent());
},
),
KwPopUpButtonItem(
title: widget.event.status == null ||
widget.event.status == EventStatus.draft
? 'Edit Event Draft'
: 'Edit Event',
onTap: () {
context.router.maybePop();
},
color: AppColors.primaryBlue),
if (widget.event.status == EventStatus.draft ||
widget.event.status == null)
KwPopUpButtonItem(
title: 'Delete Event Draft',
onTap: () {
BlocProvider.of<EventDetailsBloc>(context)
.add(DetailsDeleteDraftEvent());
BlocProvider.of<EventsBloc>(context)
.add(LoadTabEventEvent(tabIndex: 3, subTabIndex: 0));
},
color: AppColors.statusError),
],
),
);
}
}

View File

@@ -0,0 +1,200 @@
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/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/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
import 'package:krow/features/events/presentation/event_details/widgets/event_qr_popup.dart';
class EventButtonGroupWidget extends StatelessWidget {
final EventEntity item;
final bool isPreview;
const EventButtonGroupWidget(
{super.key, required this.item, required this.isPreview});
@override
Widget build(BuildContext context) {
if (isPreview) return const SizedBox.shrink();
final now = DateTime.now();
final startTime =
item.startDate ?? DateTime.now(); // adjust property name if needed
final hoursUntilStart = startTime.difference(now).inHours;
final showDraftButton = hoursUntilStart >= 24;
switch (item.status) {
case EventStatus.confirmed:
return Column(children: [
_buildActiveButtonGroup(context),
Gap(8),
_buildConfirmedButtonGroup(context)
]);
case EventStatus.active:
return _buildActiveButtonGroup(context);
case EventStatus.finished:
return _buildCompletedButtonGroup(context);
case EventStatus.pending:
case EventStatus.assigned:
return Column(children: [
if (showDraftButton) _buildDraftButtonGroup(context),
if (showDraftButton) Gap(8),
_buildConfirmedButtonGroup(context)
]);
case EventStatus.draft:
return Column(children: [
_buildDraftButtonGroup(context)
]);
case EventStatus.completed:
case EventStatus.closed:
// return _buildClosedButtonGroup();
default:
return const SizedBox.shrink();
}
}
Widget _buildActiveButtonGroup(BuildContext context) {
return Column(
children: [
KwButton.primary(
label: 'QR Code',
onPressed: () {
EventQrPopup.show(context, item);
},
),
const Gap(8),
KwButton.outlinedPrimary(
label: 'Clock Manually',
onPressed: () {
context.router.push(ClockManualRoute(
staffContacts: item.shifts
?.expand(
(s) => s.positions.expand((p) => p.staffContacts))
.toList() ??
[]));
},
),
],
);
}
Widget _buildConfirmedButtonGroup(BuildContext context) {
return KwButton.outlinedPrimary(
label: 'Cancel Event',
onPressed: () {
BlocProvider.of<EventDetailsBloc>(context).add(CancelClientEvent());
},
).copyWith(
color: AppColors.statusError,
textColors: AppColors.statusError,
borderColor: AppColors.statusError);
}
Widget _buildDraftButtonGroup(BuildContext context) {
return Column(
children: [
KwButton.accent(
label: 'Edit Event',
onPressed: () async {
BlocProvider.of<EventDetailsBloc>(context).add(
DisablePollingEvent(),
);
await context.router.push(
CreateEventFlowRoute(children: [
CreateEventRoute(
eventModel: item.dto,
),
]),
);
BlocProvider.of<EventDetailsBloc>(context).add(
RefreshEventDetailsEvent(),
);
BlocProvider.of<EventDetailsBloc>(context).add(
EnablePollingEvent(),
);
},
),
// Gap(8),
// KwButton.outlinedPrimary(
// label: 'Delete Event Draft',
// onPressed: () {
// BlocProvider.of<EventDetailsBloc>(context)
// .add(DetailsDeleteDraftEvent());
// },
// ).copyWith(
// color: AppColors.statusError,
// textColors: AppColors.statusError,
// borderColor: AppColors.statusError),
],
);
}
Widget _buildCompletedButtonGroup(context) {
return Column(
children: [
KwButton.primary(
label: 'Complete Event',
onPressed: () {
_completeEvent(context);
},
),
],
);
}
Widget _buildClosedButtonGroup() {
return Column(
children: [
KwButton.primary(
label: 'View Invoice',
onPressed: () {},
),
],
);
}
void _completeEvent(BuildContext context) async {
var controller = TextEditingController();
var result = await KwDialog.show(
context: context,
icon: Assets.images.icons.navigation.confetti,
title: 'Complete Event',
message: 'Please tell us how did the event went:',
state: KwDialogState.info,
child: KwTextInput(
controller: controller,
maxLength: 300,
showCounter: true,
minHeight: 144,
hintText: 'Enter your note here...',
title: 'Note (optional)',
),
primaryButtonLabel: 'Complete Event',
onPrimaryButtonPressed: (dialogContext) {
BlocProvider.of<EventDetailsBloc>(context)
.add(CompleteEventEvent(comment: controller.text));
Navigator.of(dialogContext).pop(true);
},
secondaryButtonLabel: 'Cancel',
);
if (result) {
await KwDialog.show(
context: context,
icon: Assets.images.icons.navigation.confetti,
title: 'Thanks!',
message:
'Thank you for using our app! We hope youve had an awesome event!',
primaryButtonLabel: 'Close',
);
}
}
}

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/data/models/event/business_member_model.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';
class EventCompletedByCardWidget extends StatelessWidget {
final BusinessMemberModel? completedBy;
final String? completedNote;
const EventCompletedByCardWidget(
{super.key, required this.completedBy, required this.completedNote});
@override
Widget build(BuildContext context) {
if (completedBy == null) return Container();
return Container(
decoration: KwBoxDecorations.white12,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(top: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Completed by',
style:
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
),
const Gap(4),
_contact(),
const Gap(12),
Text(
'Note',
style:
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray),
),
const Gap(2),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
completedNote ?? '',
style: AppTextStyles.bodyMediumMed,
),
],
)
],
),
);
}
Container _contact() {
return Container(
height: 28,
padding: const EdgeInsets.only(left: 2, right: 12, top: 2, bottom: 2),
decoration: BoxDecoration(
color: AppColors.tintBlue,
borderRadius: BorderRadius.circular(14),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 24,
width: 24,
decoration: BoxDecoration(
color: AppColors.blackGray,
borderRadius: BorderRadius.circular(12),
)),
const Gap(8),
Text(
'${completedBy?.firstName} ${completedBy?.lastName}',
style: AppTextStyles.bodyMediumMed,
),
],
),
);
}
}

View File

@@ -0,0 +1,211 @@
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/data/models/event/addon_model.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/events/presentation/event_details/widgets/event_button_group_widget.dart';
class EventInfoCardWidget extends StatelessWidget {
final EventEntity item;
final bool isPreview;
const EventInfoCardWidget(
{required this.item, super.key, this.isPreview = false});
Color getIconColor(EventStatus? status) {
return switch (status) {
EventStatus.active || EventStatus.finished => AppColors.statusSuccess,
EventStatus.pending ||
EventStatus.assigned ||
EventStatus.confirmed =>
AppColors.primaryBlue,
_ => AppColors.statusWarning
};
}
Color getIconBgColor(EventStatus? status) {
switch (status) {
case EventStatus.active:
case EventStatus.finished:
return AppColors.tintGreen;
case EventStatus.pending:
case EventStatus.assigned:
case EventStatus.confirmed:
return AppColors.tintBlue;
default:
return AppColors.tintOrange;
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: KwBoxDecorations.white12,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
margin: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusRow(),
const Gap(24),
Text(item.name, style: AppTextStyles.headingH1),
const Gap(24),
IconRowInfoWidget(
icon: Assets.images.icons.calendar.svg(),
title: 'Date',
value: DateFormat('MM.dd.yyyy')
.format(item.startDate ?? DateTime.now()),
),
const Gap(12),
IconRowInfoWidget(
icon: Assets.images.icons.location.svg(),
title: 'Location',
value: item.hub?.name ?? 'Hub Name',
),
const Gap(12),
ValueListenableBuilder(
valueListenable: item.totalCost,
builder: (context, value, child) {
return IconRowInfoWidget(
icon: Assets.images.icons.dollarSquare.svg(),
title: 'Value',
value: '\$${value.toStringAsFixed(2)}',
);
}),
const Gap(24),
const Divider(
color: AppColors.grayTintStroke,
thickness: 1,
height: 0,
),
_buildAddons(item.addons),
..._buildAdditionalInfo(),
if (!isPreview) const Gap(24),
EventButtonGroupWidget(item: item, isPreview: isPreview),
],
),
);
}
List<Widget> _buildAdditionalInfo() {
return [
const Gap(12),
Text('Additional Information',
style:
AppTextStyles.bodySmallReg.copyWith(color: AppColors.blackGray)),
const Gap(2),
Text(
(item.additionalInfo == null || item.additionalInfo!.trim().isEmpty)
? 'No additional information'
: item.additionalInfo!,
style: AppTextStyles.bodyMediumMed),
];
}
Widget _buildAddons(List<AddonModel>? selectedAddons) {
if (item.addons == null || item.addons!.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
...selectedAddons?.map((addon) {
var textStyle = AppTextStyles.bodyMediumMed.copyWith(height: 1);
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(addon.name ?? '', style: textStyle),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Assets.images.icons.addonInclude.svg(),
const Gap(7),
Text('Included', style: textStyle)
],
)
],
),
);
}) ??
[],
],
);
}
Row _buildStatusRow() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
color: getIconBgColor(item.status),
shape: BoxShape.circle,
),
child: Center(
child: Assets.images.icons.navigation.confetti.svg(
colorFilter:
ColorFilter.mode(getIconColor(item.status), BlendMode.srcIn),
),
),
),
if (!isPreview)
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: item.status == EventStatus.draft
? null
: AppColors.grayWhite),
),
),
))
],
);
}
}
extension on EventStatus {
Color get color {
switch (this) {
case EventStatus.active:
case EventStatus.finished:
return AppColors.statusSuccess;
case EventStatus.pending:
return AppColors.blackGray;
case EventStatus.assigned:
return AppColors.statusWarning;
case EventStatus.confirmed:
return AppColors.primaryBlue;
case EventStatus.completed:
return AppColors.statusSuccess;
case EventStatus.closed:
return AppColors.bgColorDark;
case EventStatus.canceled:
return AppColors.statusError;
case EventStatus.draft:
return AppColors.primaryYolk;
}
}
}

View File

@@ -0,0 +1,152 @@
import 'dart:convert';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:gap/gap.dart';
import 'package:image/image.dart' as img;
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/ui_kit/kw_button.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
class EventQrPopup {
static Future<void> show(BuildContext context, EventEntity item) async {
var qrKey = GlobalKey();
return showDialog<void>(
context: context,
builder: (context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: KwBoxDecorations.white24,
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 24, left: 24, right: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Event QR code',
style: AppTextStyles.headingH3.copyWith(height: 1),
),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Assets.images.icons.x.svg(
width: 16,
height: 16,
colorFilter: const ColorFilter.mode(
AppColors.blackCaptionText,
BlendMode.srcIn,
),
),
),
],
),
),
RepaintBoundary(
key: qrKey,
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
const Gap(8),
Text(
'The QR code below has been successfully generated for the ${item.name}. You can share this code with staff members to enable them to clock in.',
style: AppTextStyles.bodySmallReg
.copyWith(color: AppColors.blackGray),
),
const Gap(24),
QrImageView(
data: jsonEncode({
'type': 'event',
'birth': 'app',
'eventId': item.id
}),
version: QrVersions.auto,
size: MediaQuery.of(context).size.width - 56,
),
const Gap(24),
],
),
),
),
Padding(
padding: const EdgeInsets.only(
bottom: 24, left: 24, right: 24),
child: KwButton.primary(
label: 'Share QR Code',
onPressed: () async {
final params = ShareParams(
text: 'Qr code for ${item.name}',
files: [
XFile.fromData(
await removeAlphaAndReplaceTransparentWithWhite(
qrKey.currentContext!.findRenderObject()
as RenderRepaintBoundary),
name: 'event_qr.png',
mimeType: 'image/png',
)
],
);
await SharePlus.instance.share(params);
},
),
),
],
),
),
),
);
});
}
static Future<Uint8List> removeAlphaAndReplaceTransparentWithWhite(
RenderRepaintBoundary boundary) async {
final image = await boundary.toImage(pixelRatio: 5.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
final Uint8List rgba = byteData!.buffer.asUint8List();
final int length = rgba.lengthInBytes;
final Uint8List rgb = Uint8List(length ~/ 4 * 3);
for (int i = 0, j = 0; i < length; i += 4, j += 3) {
int r = rgba[i];
int g = rgba[i + 1];
int b = rgba[i + 2];
int a = rgba[i + 3];
if (a < 255) {
// Replace transparent pixel with white
r = 255;
g = 255;
b = 255;
}
rgb[j] = r;
rgb[j + 1] = g;
rgb[j + 2] = b;
}
final width = image.width;
final height = image.height;
final img.Image rgbImage = img.Image.fromBytes(
width: width,
height: height,
bytes: rgb.buffer,
numChannels: 3,
format: img.Format.uint8,
);
return Uint8List.fromList(img.encodePng(rgbImage));
}
}

View File

@@ -0,0 +1,93 @@
import 'dart:math';
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/application/routing/routes.gr.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/assigned_staff_item_widget.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/events/domain/blocs/details/event_details_bloc.dart';
class AssignedStaff extends StatelessWidget {
final PositionState roleState;
const AssignedStaff({super.key, required this.roleState});
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildRoleHeader(context),
if (roleState.isStaffExpanded)
AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: roleState.isStaffExpanded ? null : 0,
child: Column(children: [
ListView.builder(
shrinkWrap: true,
primary: false,
itemCount: min(3, roleState.position.staffContacts.length),
itemBuilder: (context, index) {
return AssignedStaffItemWidget(
staffContact: roleState.position.staffContacts[index],
department: roleState.position.department?.name ?? '',
);
}),
KwButton.outlinedPrimary(
label: 'View All',
onPressed: () {
context.pushRoute(AssignedStaffRoute(
staffContacts: roleState.position.staffContacts,
department: roleState.position.department?.name ?? '',
));
}),
const Gap(8),
])),
],
);
}
Widget _buildRoleHeader(context) {
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 16),
child: GestureDetector(
onTap: () {
BlocProvider.of<EventDetailsBloc>(context)
.add(OnAssignedStaffHeaderTapEvent(roleState));
},
child: Container(
color: Colors.transparent,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Assigned Staff', style: AppTextStyles.bodyLargeMed),
Container(
height: 16,
width: 16,
color: Colors.transparent,
child: AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: roleState.isStaffExpanded ? 0.5 : 0,
child: Center(
child: Assets.images.icons.chevronDown.svg(
colorFilter: ColorFilter.mode(
roleState.isStaffExpanded
? AppColors.blackBlack
: AppColors.blackCaptionText,
BlendMode.srcIn,
),
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.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/events/domain/blocs/details/event_details_bloc.dart';
import 'package:krow/features/events/presentation/event_details/widgets/role/asigned_staff.dart';
class RoleWidget extends StatefulWidget {
final PositionState roleState;
const RoleWidget({super.key, required this.roleState});
@override
State<RoleWidget> createState() => _RoleWidgetState();
}
class _RoleWidgetState extends State<RoleWidget> {
@override
Widget build(BuildContext context) {
var eventDate =
widget.roleState.position.parentShift?.parentEvent?.startDate ??
DateTime.now();
return AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.only(left: 12, right: 12),
margin: const EdgeInsets.only(top: 8),
decoration: KwBoxDecorations.primaryLight12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRoleHeader(context),
AnimatedSize(
duration: const Duration(milliseconds: 150),
alignment: Alignment.topCenter,
child: Container(
height: widget.roleState.isExpanded ? null : 0,
decoration: const BoxDecoration(),
child: Column(
children: [
IconRowInfoWidget(
icon: Assets.images.icons.data.svg(),
title: 'Department',
value: widget.roleState.position.department?.name ?? ''),
const Gap(16),
IconRowInfoWidget(
icon: Assets.images.icons.profile2user.svg(),
title: 'Number of Employee for one Role',
value: '${widget.roleState.position.count} persons'),
const Gap(16),
IconRowInfoWidget(
icon: Assets.images.icons.calendar.svg(),
title: 'Start Date & Time',
value: DateFormat('MM.dd.yyyy, hh:mm a').format(
eventDate.copyWith(
hour: widget.roleState.position.startTime.hour,
minute:
widget.roleState.position.startTime.minute))),
const Gap(16),
IconRowInfoWidget(
icon: Assets.images.icons.calendar.svg(),
title: 'End Date & Time',
value: DateFormat('MM.dd.yyyy, hh:mm a').format(
eventDate.copyWith(
hour: widget.roleState.position.endTime.hour,
minute:
widget.roleState.position.endTime.minute))),
const Gap(16),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: IconRowInfoWidget(
icon: Assets.images.icons.dollarSquare.svg(),
title: 'Value',
value: '\$${widget.roleState.position.price.toStringAsFixed(2)}'),
),
if (widget.roleState.position.staffContacts.isNotEmpty) ...[
const Gap(16),
const Divider(
color: AppColors.grayTintStroke,
thickness: 1,
height: 0),
AssignedStaff(
roleState: widget.roleState,
),
]
],
),
),
),
],
),
);
}
Widget _buildRoleHeader(context) {
return GestureDetector(
onTap: () {
BlocProvider.of<EventDetailsBloc>(context)
.add(OnRoleHeaderTapEvent(widget.roleState));
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(top: 24, bottom: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(widget.roleState.position.businessSkill?.skill?.name ?? '',
style: AppTextStyles.headingH3),
Container(
height: 16,
width: 16,
color: Colors.transparent,
child: AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: widget.roleState.isExpanded ? 0.5 : 0,
child: Center(
child: Assets.images.icons.chevronDown.svg(
colorFilter: ColorFilter.mode(
widget.roleState.isExpanded
? AppColors.blackBlack
: AppColors.blackCaptionText,
BlendMode.srcIn,
),
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/data/models/event/business_member_model.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/events/domain/blocs/details/event_details_bloc.dart';
import 'package:krow/features/events/presentation/event_details/widgets/role/role_widget.dart';
class ShiftWidget extends StatefulWidget {
final int index;
final ShiftState shiftState;
const ShiftWidget({super.key, required this.index, required this.shiftState});
@override
State<ShiftWidget> createState() => _ShiftWidgetState();
}
class _ShiftWidgetState extends State<ShiftWidget> {
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.only(left: 12, right: 12),
margin: const EdgeInsets.only(top: 12),
decoration: KwBoxDecorations.white12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildShiftHeader(context),
AnimatedSize(
duration: const Duration(milliseconds: 150),
alignment: Alignment.topCenter,
child: Container(
height: widget.shiftState.isExpanded ? null : 0,
decoration: const BoxDecoration(),
child: Column(
children: [
IconRowInfoWidget(
icon: Assets.images.icons.location.svg(),
title: 'Address',
value: widget
.shiftState.shift.fullAddress?.formattedAddress ??
''),
const Gap(16),
const Divider(
color: AppColors.grayTintStroke, thickness: 1, height: 0),
const Gap(16),
Stack(
children: [
IconRowInfoWidget(
icon: Assets.images.icons.userTag.svg(),
title: 'Shift Contact',
value: 'Manager'),
if (widget.shiftState.shift.managers.isNotEmpty)
Positioned(
bottom: 4,
top: 4,
right: 0,
child: _contact(
widget.shiftState.shift.managers.first))
],
),
const Gap(16),
ListView.builder(
itemCount: widget.shiftState.positions.length,
primary: false,
shrinkWrap: true,
itemBuilder: (_, index) {
return RoleWidget(
roleState: widget.shiftState.positions[index],
);
},
),
const Gap(12),
],
),
),
),
],
),
);
}
Widget _buildShiftHeader(context) {
return GestureDetector(
onTap: () {
BlocProvider.of<EventDetailsBloc>(context)
.add(OnShiftHeaderTapEvent(widget.shiftState));
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.only(top: 24, bottom: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Shift Details #${widget.index + 1}',
style: AppTextStyles.headingH1),
Container(
height: 40,
width: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: AppColors.grayTintStroke,
),
),
child: AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: widget.shiftState.isExpanded ? 0.5 : 0,
child: Center(
child: Assets.images.icons.chevronDown.svg(
colorFilter: ColorFilter.mode(
widget.shiftState.isExpanded
? AppColors.blackBlack
: AppColors.blackCaptionText,
BlendMode.srcIn,
),
),
),
),
),
],
),
),
),
);
}
Container _contact(BusinessMemberModel contact) {
return Container(
height: 28,
padding: const EdgeInsets.only(left: 2, right: 12, top: 2, bottom: 2),
decoration: BoxDecoration(
color: AppColors.tintBlue,
borderRadius: BorderRadius.circular(14),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 24,
width: 24,
decoration: BoxDecoration(
color: AppColors.blackGray,
borderRadius: BorderRadius.circular(12),
)),
const Gap(8),
Text(
'${contact.firstName} ${contact.lastName}',
style: AppTextStyles.bodyMediumMed,
),
],
),
);
}
}

View File

@@ -0,0 +1,173 @@
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';
class EventListItemWidget extends StatelessWidget {
final EventEntity item;
const EventListItemWidget({required this.item, super.key});
Color getIconColor(EventStatus? status) {
switch (status) {
case EventStatus.active:
case EventStatus.finished:
return AppColors.statusSuccess;
case EventStatus.pending:
case EventStatus.assigned:
case EventStatus.confirmed:
return AppColors.primaryBlue;
default:
return AppColors.statusWarning;
}
}
Color getIconBgColor(EventStatus? status) {
switch (status) {
case EventStatus.active:
case EventStatus.finished:
return AppColors.tintGreen;
case EventStatus.pending:
case EventStatus.assigned:
case EventStatus.confirmed:
return AppColors.tintBlue;
default:
return AppColors.tintOrange;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
context.router.push(EventDetailsRoute(event: 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(item.name, style: AppTextStyles.headingH1),
const Gap(4),
Text('BEO-${item.id}',
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray)),
const Gap(12),
IconRowInfoWidget(
icon: Assets.images.icons.calendar.svg(),
title: 'Date',
value: DateFormat('MM.dd.yyyy')
.format(item.startDate ?? DateTime.now()),
),
const Gap(12),
IconRowInfoWidget(
icon: Assets.images.icons.location.svg(),
title: 'Location',
value: item.hub?.name ?? 'Hub Name',
),
const Gap(24),
Container(
height: 34,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: KwBoxDecorations.primaryLight8.copyWith(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Approximate Total Costs',
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray)),
ValueListenableBuilder(
valueListenable: item.totalCost,
builder: (context, value, child) {
return Text(
'\$${value.toStringAsFixed(2)}',
style: AppTextStyles.bodyMediumMed,
);
})
],
),
)
],
),
),
);
}
Row _buildStatusRow() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
color: getIconBgColor(item?.status),
shape: BoxShape.circle,
),
child: Center(
child: Assets.images.icons.navigation.confetti.svg(
colorFilter:
ColorFilter.mode(getIconColor(item.status), 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: item.status == EventStatus.draft
? null
: AppColors.grayWhite),
),
),
))
],
);
}
}
extension on EventStatus {
Color get color {
switch (this) {
case EventStatus.active:
case EventStatus.finished:
return AppColors.statusSuccess;
case EventStatus.pending:
return AppColors.blackGray;
case EventStatus.assigned:
return AppColors.statusWarning;
case EventStatus.confirmed:
return AppColors.primaryBlue;
case EventStatus.completed:
return AppColors.statusSuccess;
case EventStatus.closed:
return AppColors.bgColorDark;
case EventStatus.canceled:
return AppColors.statusError;
case EventStatus.draft:
return AppColors.primaryYolk;
}
}
}

View File

@@ -0,0 +1,207 @@
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/entity/event_entity.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_option_selector.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_tabs.dart';
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_bloc.dart';
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_event.dart';
import 'package:krow/features/events/domain/blocs/events_list_bloc/events_state.dart';
import 'package:krow/features/events/presentation/lists/event_list_item_widget.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
@RoutePage()
class EventsListMainScreen extends StatefulWidget implements AutoRouteWrapper {
const EventsListMainScreen({super.key});
@override
State<EventsListMainScreen> createState() => _EventsListMainScreenState();
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider<EventsBloc>(
create: (context) => EventsBloc()..add(const EventsInitialEvent()),
child: this,
);
}
}
class _EventsListMainScreenState extends State<EventsListMainScreen> {
final tabs = <String, List<String>>{
'Upcoming': ['Pending', 'Assigned', 'Confirmed'],
'Active': ['Ongoing', 'Finished'],
'Past': ['Completed', 'Closed', 'Canceled'],
'Drafts': [],
};
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<EventsBloc>(context).add(
LoadMoreEventEvent(
tabIndex: BlocProvider.of<EventsBloc>(context).state.tabIndex,
subTabIndex:
BlocProvider.of<EventsBloc>(context).state.subTabIndex),
);
}
}
}
@override
Widget build(BuildContext context) {
return BlocConsumer<EventsBloc, EventsState>(
listenWhen: (previous, current) =>
previous.errorMessage != current.errorMessage,
listener: (context, state) {
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.errorMessage!),
));
}
},
builder: (context, state) {
var tabState = state.tabs[state.tabIndex]![state.subTabIndex]!;
List<EventEntity> items = tabState.items;
return Scaffold(
appBar: KwAppBar(
titleText: 'Events',
centerTitle: false,
),
body: ModalProgressHUD(
inAsyncCall: tabState.inLoading && tabState.items.isNotEmpty,
child: ScrollLayoutHelper(
padding: const EdgeInsets.symmetric(vertical: 16),
onRefresh: () async {
BlocProvider.of<EventsBloc>(context).add(LoadTabEventEvent(
tabIndex: state.tabIndex, subTabIndex: state.subTabIndex));
},
controller: _scrollController,
upperWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
KwTabBar(
tabs: tabs.keys.toList(),
flexes: const [7, 5, 5, 5],
onTap: (index) {
BlocProvider.of<EventsBloc>(context)
.add(EventsTabChangedEvent(tabIndex: index));
}),
const Gap(24),
_buildSubTab(state, context),
if (tabState.inLoading && tabState.items.isEmpty)
..._buildListLoading(),
if (!tabState.inLoading && items.isEmpty)
..._emptyListWidget(),
RefreshIndicator(
onRefresh: () async {
BlocProvider.of<EventsBloc>(context).add(
LoadTabEventEvent(
tabIndex: state.tabIndex,
subTabIndex: state.subTabIndex));
},
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: items.length,
itemBuilder: (context, index) {
return EventListItemWidget(
item: items[index],
);
}),
),
],
),
lowerWidget: const SizedBox.shrink(),
),
),
);
},
);
}
Widget _buildSubTab(EventsState state, BuildContext context) {
if (state.tabs[state.tabIndex]!.length > 1) {
return Stack(
children: [
Positioned(
left: 16,
right: 16,
bottom: 25.5,
child: Container(
height: 1,
color: AppColors.blackGray,
)),
Padding(
padding: const EdgeInsets.only(bottom: 24, left: 16, right: 16),
child: KwOptionSelector(
selectedIndex: state.subTabIndex,
onChanged: (index) {
BlocProvider.of<EventsBloc>(context)
.add(EventsSubTabChangedEvent(subTabIndex: index));
},
height: 26,
selectorHeight: 4,
textStyle: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
selectedTextStyle: AppTextStyles.bodyMediumMed,
itemAlign: Alignment.topCenter,
items: tabs[tabs.keys.toList()[state.tabIndex]]!),
),
],
);
} else {
return 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 event',
textAlign: TextAlign.center,
style: AppTextStyles.headingH2,
),
];
}
}